@jxsuite/studio 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/data/elements-meta.json +5 -15
- package/data/html-meta.json +51 -6
- package/dist/chunks/{2020-n6tfwnda.js → 2020-g79n01v9.js} +14 -14
- package/dist/chunks/action-group-sp6tna8v.js +118 -0
- package/dist/chunks/adapter-nj7tnjfn.js +13 -0
- package/dist/chunks/cf-account-picker-fyf63pmy.js +495 -0
- package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
- package/dist/chunks/color-area-v17df7b8.js +154 -0
- package/dist/chunks/color-field-anwybnkn.js +208 -0
- package/dist/chunks/color-slider-44ez96fc.js +79 -0
- package/dist/chunks/color-xgg3kfw7.js +35 -0
- package/dist/chunks/combobox-5e7jev23.js +236 -0
- package/dist/chunks/components-2w1f35qs.js +18 -0
- package/dist/chunks/content-source-3wqeas45.js +46 -0
- package/dist/chunks/convert-to-repeater-d0cgnb6d.js +639 -0
- package/dist/chunks/dialog-s890b918.js +85 -0
- package/dist/chunks/editor-3qhbg51x.js +35 -0
- package/dist/chunks/entry-commands-d6g50j7s.js +42 -0
- package/dist/chunks/extension-sections-fy7q4fe9.js +651 -0
- package/dist/chunks/file-ops-3btj6st0.js +47 -0
- package/dist/chunks/files-ef8fgs3w.js +72 -0
- package/dist/chunks/format-host-3c33nwr8.js +40 -0
- package/dist/chunks/fs-events-13tj6sex.js +18 -0
- package/dist/chunks/git-panel-4wp2rn9b.js +49 -0
- package/dist/chunks/icons-yy10wk9r.js +402 -0
- package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
- package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
- package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
- package/dist/chunks/iframe-host-jgesd924.js +115 -0
- package/dist/chunks/{index-wcnt3r9g.js → index-mdmyx6s2.js} +11 -11
- package/dist/chunks/index-n4j7qxyr.js +109 -0
- package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
- package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
- package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
- package/dist/chunks/layers-1nq5evsh.js +29 -0
- package/dist/chunks/layers-panel-ar2gh90g.js +40 -0
- package/dist/chunks/listbox-6v2ac08j.js +44 -0
- package/dist/chunks/lit-html-m244w9nh.js +17 -0
- package/dist/chunks/media-picker-2qbetq73.js +33 -0
- package/dist/chunks/menu-q99sy9rf.js +285 -0
- package/dist/chunks/menu-rf7n72mf.js +14 -0
- package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
- package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
- package/dist/chunks/{monaco-setup-vx9bhp1d.js → monaco-setup-8r62fj6y.js} +27 -37
- package/dist/chunks/notify-628t0anh.js +21 -0
- package/dist/chunks/number-field-5cndyft8.js +72 -0
- package/dist/chunks/parse-0zx14y15.js +10 -0
- package/dist/chunks/platform-ts9cg1a4.js +15 -0
- package/dist/chunks/popover-47z3ytva.js +25 -0
- package/dist/chunks/publish-panel-98f7sr3g.js +27 -0
- package/dist/chunks/quick-search-srzz83k2.js +45 -0
- package/dist/chunks/references-n553f0e1.js +26 -0
- package/dist/chunks/right-panel-rrgx2dn3.js +45 -0
- package/dist/chunks/section-registry-5pq32t0q.js +25 -0
- package/dist/chunks/select-2r16ez41.js +49 -0
- package/dist/chunks/split-g7nm1rgb.js +191 -0
- package/dist/chunks/studio-0hqbpbd0.js +62 -0
- package/dist/chunks/studio-18avnhjp.js +2430 -0
- package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
- package/dist/chunks/{studio-msd847zn.js → studio-25hkdrkw.js} +357 -86
- package/dist/chunks/studio-26d7myd1.js +258 -0
- package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
- package/dist/chunks/studio-3gxv60tn.js +422 -0
- package/dist/chunks/{studio-fbf92ckf.js → studio-3hxkd92t.js} +275 -163
- package/dist/chunks/studio-4m0haseg.js +185 -0
- package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
- package/dist/chunks/{studio-1wagb550.js → studio-5523bpzt.js} +11 -11
- package/dist/chunks/{studio-x4qkdat5.js → studio-57mf66w2.js} +1749 -2481
- package/dist/chunks/studio-5jy81t3s.js +17463 -0
- package/dist/chunks/studio-5rt7sqa0.js +1168 -0
- package/dist/chunks/studio-5te5tqng.js +81 -0
- package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
- package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
- package/dist/chunks/studio-6xa21ej9.js +28 -0
- package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
- package/dist/chunks/{studio-ahn3pdgx.js → studio-79y5sz51.js} +52 -38
- package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
- package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
- package/dist/chunks/studio-7xptrg0j.js +954 -0
- package/dist/chunks/{studio-cft9r4r7.js → studio-81390n6f.js} +22 -22
- package/dist/chunks/{studio-q43a5y9h.js → studio-8jn5yhyn.js} +16 -16
- package/dist/chunks/studio-8qtrpzr3.js +839 -0
- package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
- package/dist/chunks/studio-9xxz1q69.js +1356 -0
- package/dist/chunks/studio-akbww0j5.js +13615 -0
- package/dist/chunks/{studio-x813zp48.js → studio-arfpe389.js} +30 -30
- package/dist/chunks/{studio-trxrabqf.js → studio-az3zd49t.js} +475 -283
- package/dist/chunks/studio-cpvb2ed1.js +1171 -0
- package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
- package/dist/chunks/{studio-bgxt93ga.js → studio-d055c37e.js} +13 -13
- package/dist/chunks/studio-d1ja2x9r.js +362 -0
- package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
- package/dist/chunks/{studio-3bqpr0zh.js → studio-em9nw8eh.js} +299 -110
- package/dist/chunks/studio-erdatzvp.js +1190 -0
- package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
- package/dist/chunks/studio-fabxk6r9.js +2811 -0
- package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
- package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
- package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
- package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
- package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
- package/dist/chunks/studio-pm6hgy7r.js +4763 -0
- package/dist/chunks/studio-q0a3yk4n.js +33 -0
- package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
- package/dist/chunks/studio-qahxvnan.js +111 -0
- package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
- package/dist/chunks/{studio-2b946z26.js → studio-rkstz940.js} +3658 -6134
- package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
- package/dist/chunks/studio-rpyah2ew.js +2805 -0
- package/dist/chunks/studio-rs76k67r.js +12 -0
- package/dist/chunks/{studio-jkftxyvz.js → studio-s6z853aq.js} +5858 -2152
- package/dist/chunks/{studio-6yw4y3my.js → studio-sgdc5pfc.js} +8 -8
- package/dist/chunks/studio-tb3ev01j.js +763 -0
- package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
- package/dist/chunks/studio-vhv5tgxm.js +51 -0
- package/dist/chunks/{studio-qjy2z9bv.js → studio-vz68yn9e.js} +32 -137
- package/dist/chunks/studio-wqpngpsk.js +174 -0
- package/dist/chunks/{studio-b55y6ewx.js → studio-x3rty6tr.js} +58 -58
- package/dist/chunks/studio-xsykpvsh.js +8448 -0
- package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
- package/dist/chunks/studio-yxa81f45.js +12 -0
- package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
- package/dist/chunks/tabs-wbp1cdws.js +156 -0
- package/dist/chunks/textfield-abk6qyee.js +73 -0
- package/dist/chunks/toast-780pvfbv.js +27 -0
- package/dist/chunks/toast-host-3sqv46sn.js +116 -0
- package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
- package/dist/chunks/tooltip-eptb614w.js +369 -0
- package/dist/chunks/tree-a73p0pqm.js +367 -0
- package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
- package/dist/chunks/workspace-wrew8zk4.js +69 -0
- package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
- package/dist/iframe-entry.js +1780 -667
- package/dist/studio.js +52819 -36480
- package/dist/workers/editor.worker.js +1040 -1085
- package/dist/workers/json.worker.js +1946 -1991
- package/dist/workers/ts.worker.js +4960 -5005
- package/index.html +1 -0
- package/package.json +18 -52
- package/src/about/about-modal.ts +72 -123
- package/src/browse/library-commands.ts +20 -49
- package/src/browse/library-layouts.ts +87 -218
- package/src/browse/library-pane.ts +406 -438
- package/src/canvas/canvas-helpers.ts +0 -49
- package/src/canvas/canvas-live-render.ts +25 -2
- package/src/canvas/canvas-render.ts +666 -231
- package/src/canvas/canvas-surface.ts +0 -1
- package/src/canvas/canvas-utils.ts +310 -117
- package/src/canvas/dialog-path.ts +76 -0
- package/src/canvas/dialog-state.ts +67 -0
- package/src/canvas/diff-marks.ts +252 -0
- package/src/canvas/diff-toolbar.ts +349 -0
- package/src/canvas/diff-view.ts +123 -0
- package/src/canvas/edit-width-drag.ts +187 -0
- package/src/canvas/edit-width.ts +150 -0
- package/src/canvas/iframe-entry.ts +125 -8
- package/src/canvas/iframe-host.ts +271 -15
- package/src/canvas/iframe-interaction.ts +133 -13
- package/src/canvas/iframe-patch.ts +7 -1
- package/src/canvas/iframe-position.ts +5 -6
- package/src/canvas/iframe-protocol.ts +106 -5
- package/src/canvas/iframe-render.ts +373 -5
- package/src/canvas/iframe-slash.ts +2 -2
- package/src/canvas/iframe-subtree.ts +5 -8
- package/src/canvas/path-mapping.ts +25 -3
- package/src/canvas/popover-path.ts +106 -0
- package/src/canvas/popover-state.ts +155 -0
- package/src/canvas/surface-registry.ts +21 -10
- package/src/collab/presence-chips.ts +51 -73
- package/src/commands/app-commands.ts +10 -0
- package/src/commands/budget.ts +12 -0
- package/src/commands/command-args.ts +369 -6
- package/src/commands/defaults.ts +44 -14
- package/src/commands/live-context.ts +11 -5
- package/src/commands/reference.ts +12 -2
- package/src/commands/registry.ts +165 -8
- package/src/content/entry-commands.ts +16 -23
- package/src/content/entry-editor.ts +144 -101
- package/src/editor/canvas-slash-bridge.ts +2 -2
- package/src/editor/context-menu.ts +49 -200
- package/src/editor/convert-to-component.ts +42 -82
- package/src/editor/convert-to-repeater.ts +132 -173
- package/src/editor/inline-edit.ts +11 -0
- package/src/editor/shortcuts.ts +65 -26
- package/src/editor/slash-menu.ts +150 -157
- package/src/files/file-ops.ts +117 -11
- package/src/files/files.ts +943 -660
- package/src/files/json-layout.ts +474 -0
- package/src/files/serialize-document.ts +8 -1
- package/src/format/convert-file.ts +16 -10
- package/src/format/format-host.ts +50 -1
- package/src/github/github-auth.ts +71 -29
- package/src/github/github-publish.ts +57 -65
- package/src/grid/cell-editors.ts +46 -4
- package/src/grid/cell-popovers.ts +65 -62
- package/src/grid/grid-open.ts +86 -64
- package/src/grid/grid-panel.ts +390 -469
- package/src/grid/redirects-grid.ts +40 -44
- package/src/hosting/layout.ts +2 -0
- package/src/i18n/i18n-commands.ts +127 -62
- package/src/media/media-pane.ts +83 -133
- package/src/new-project/add-repo-modal.ts +125 -176
- package/src/new-project/import-tab.ts +181 -252
- package/src/new-project/location-fields.ts +93 -160
- package/src/new-project/new-project-modal.ts +376 -367
- package/src/packages/jxsuite-update.ts +76 -18
- package/src/panels/activity-panel.ts +137 -104
- package/src/panels/ai-chat/chat-markdown.ts +15 -13
- package/src/panels/ai-chat/chat-view.ts +307 -472
- package/src/panels/ai-chat/composer.ts +175 -163
- package/src/panels/ai-chat/sessions-view.ts +31 -62
- package/src/panels/ai-panel.ts +210 -185
- package/src/panels/block-action-bar.ts +437 -643
- package/src/panels/bottom-dock.ts +112 -47
- package/src/panels/chat-panel.ts +19 -14
- package/src/panels/data-explorer.ts +141 -102
- package/src/panels/data-grid.ts +183 -150
- package/src/panels/dnd.ts +75 -20
- package/src/panels/editors.ts +26 -19
- package/src/panels/elements-panel.ts +243 -186
- package/src/panels/empty-state.ts +26 -42
- package/src/panels/events-panel.ts +950 -729
- package/src/panels/formula-workspace.ts +434 -207
- package/src/panels/frontmatter-fields.ts +185 -153
- package/src/panels/frontmatter-panel.ts +411 -187
- package/src/panels/git-diff-open.ts +148 -0
- package/src/panels/git-panel.ts +679 -605
- package/src/panels/head-panel.ts +617 -435
- package/src/panels/i18n-panel.ts +153 -133
- package/src/panels/imports-panel.ts +388 -414
- package/src/panels/jump-bar.ts +174 -147
- package/src/panels/layers-panel.ts +785 -671
- package/src/panels/left-panel.ts +166 -94
- package/src/panels/navigator-panels.ts +1 -1
- package/src/panels/pane-context.ts +529 -677
- package/src/panels/pane-grid.ts +265 -241
- package/src/panels/panel-registry.ts +15 -4
- package/src/panels/problems-panel.ts +177 -99
- package/src/panels/properties-panel.ts +1525 -966
- package/src/panels/provenance.ts +31 -113
- package/src/panels/quick-search.ts +217 -172
- package/src/panels/right-panel.ts +135 -169
- package/src/panels/seo-modal.ts +346 -167
- package/src/panels/settings-menu.ts +109 -646
- package/src/panels/settings-pane.ts +102 -86
- package/src/panels/signals-panel.ts +1318 -1172
- package/src/panels/statement-editor.ts +556 -428
- package/src/panels/style-panel.ts +1367 -630
- package/src/panels/style-utils.ts +36 -32
- package/src/panels/stylebook-doc.ts +30 -73
- package/src/panels/stylebook-layers-panel.ts +202 -109
- package/src/panels/stylebook-panel.ts +124 -88
- package/src/panels/tab-strip.ts +385 -287
- package/src/platforms/cloud.ts +106 -16
- package/src/platforms/devserver.ts +13 -0
- package/src/publish/deploy-checklist.ts +124 -78
- package/src/publish/publish-commands.ts +4 -12
- package/src/publish/publish-panel.ts +134 -328
- package/src/services/a11y-report.ts +100 -43
- package/src/services/ai-command-tools.ts +468 -0
- package/src/services/ai-session-store.ts +6 -0
- package/src/services/ai-system-prompt.ts +110 -18
- package/src/services/ai-tools.ts +28 -143
- package/src/services/ai-write-report.ts +174 -0
- package/src/services/document-assistant.ts +50 -2
- package/src/services/idle.ts +16 -14
- package/src/services/live-surfaces.ts +237 -0
- package/src/services/model-uri.ts +75 -0
- package/src/services/notify.ts +63 -6
- package/src/services/popover-report.ts +325 -0
- package/src/services/surface-registry.ts +56 -0
- package/src/services/tool-executor.ts +6 -0
- package/src/settings/contexts-section.ts +301 -299
- package/src/settings/contributed-section.ts +361 -222
- package/src/settings/css-vars-editor.ts +320 -325
- package/src/settings/defs-editor.ts +572 -535
- package/src/settings/dependencies-editor.ts +79 -107
- package/src/settings/extension-commands.ts +379 -0
- package/src/settings/extension-rows.ts +113 -0
- package/src/settings/extensions-section.ts +287 -0
- package/src/settings/general-settings.ts +130 -181
- package/src/settings/head-editor.ts +10 -305
- package/src/settings/locales-section.ts +146 -196
- package/src/settings/preferences-accounts.ts +1 -1
- package/src/settings/preferences-dialog.ts +350 -330
- package/src/settings/project-sections.ts +108 -181
- package/src/settings/schema-field-ui.ts +11 -399
- package/src/settings/section-registry.ts +8 -1
- package/src/settings/settings-document.ts +31 -29
- package/src/shell/tree.ts +42 -87
- package/src/shell.ts +48 -36
- package/src/studio.ts +90 -67
- package/src/style/token-ref.ts +0 -39
- package/src/surfaces/about.json +263 -0
- package/src/surfaces/about.ts +163 -0
- package/src/surfaces/add-repo.json +486 -0
- package/src/surfaces/add-repo.ts +236 -0
- package/src/surfaces/ai-chat.json +2185 -0
- package/src/surfaces/ai-chat.ts +549 -0
- package/src/surfaces/ai-credentials-form.json +332 -0
- package/src/surfaces/ai-credentials-form.ts +151 -0
- package/src/surfaces/ai-managed-connect.json +108 -0
- package/src/surfaces/ai-managed-connect.ts +123 -0
- package/src/surfaces/ai-model-picker.json +114 -0
- package/src/surfaces/ai-model-picker.ts +139 -0
- package/src/surfaces/block-action-bar.json +640 -0
- package/src/surfaces/block-action-bar.ts +368 -0
- package/src/surfaces/bottom-dock.json +154 -0
- package/src/surfaces/bottom-dock.ts +125 -0
- package/src/surfaces/canvas-stage.json +444 -0
- package/src/surfaces/canvas-stage.ts +253 -0
- package/src/surfaces/cf-account-picker.json +300 -0
- package/src/surfaces/cf-account-picker.ts +214 -0
- package/src/surfaces/commandbar.json +984 -0
- package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
- package/src/surfaces/convert-repeater.json +309 -0
- package/src/surfaces/convert-repeater.ts +276 -0
- package/src/surfaces/data-actions.json +157 -0
- package/src/surfaces/data-actions.ts +115 -0
- package/src/surfaces/dialog.json +289 -0
- package/src/surfaces/dialog.ts +343 -0
- package/src/surfaces/diff-toolbar.json +266 -0
- package/src/surfaces/diff-toolbar.ts +139 -0
- package/src/surfaces/doc-header.json +739 -0
- package/src/surfaces/doc-header.ts +223 -0
- package/src/surfaces/empty-state.json +152 -0
- package/src/surfaces/empty-state.ts +173 -0
- package/src/surfaces/entry-editor.json +281 -0
- package/src/surfaces/entry-editor.ts +191 -0
- package/src/surfaces/expression-editor.json +977 -0
- package/src/surfaces/expression-editor.ts +266 -0
- package/src/surfaces/files-panel.json +592 -0
- package/src/surfaces/files-panel.ts +301 -0
- package/src/surfaces/formula-palette.json +364 -0
- package/src/surfaces/formula-palette.ts +340 -0
- package/src/surfaces/git-panel.json +1822 -0
- package/src/surfaces/git-panel.ts +272 -0
- package/src/surfaces/github-auth.json +112 -0
- package/src/surfaces/github-auth.ts +139 -0
- package/src/surfaces/github-publish.json +206 -0
- package/src/surfaces/github-publish.ts +169 -0
- package/src/surfaces/grid-cell.json +218 -0
- package/src/surfaces/grid-cell.ts +120 -0
- package/src/surfaces/grid-open.json +147 -0
- package/src/surfaces/grid-open.ts +87 -0
- package/src/surfaces/grid-panel.json +706 -0
- package/src/surfaces/grid-panel.ts +206 -0
- package/src/surfaces/grid-replace.json +179 -0
- package/src/surfaces/grid-replace.ts +89 -0
- package/src/surfaces/grid-views.json +446 -0
- package/src/surfaces/grid-views.ts +172 -0
- package/src/surfaces/inspector-dock.json +187 -0
- package/src/surfaces/inspector-dock.ts +161 -0
- package/src/surfaces/jump-bar.json +243 -0
- package/src/surfaces/jump-bar.ts +129 -0
- package/src/surfaces/jxsuite-update.json +146 -0
- package/src/surfaces/jxsuite-update.ts +149 -0
- package/src/surfaces/library-pane.json +1509 -0
- package/src/surfaces/library-pane.ts +363 -0
- package/src/surfaces/logic-panel.json +1949 -0
- package/src/surfaces/logic-panel.ts +369 -0
- package/src/surfaces/logic-workspace.json +685 -0
- package/src/surfaces/logic-workspace.ts +238 -0
- package/src/surfaces/media-browser.json +303 -0
- package/src/surfaces/media-browser.ts +145 -0
- package/src/surfaces/media-field.json +163 -0
- package/src/surfaces/media-field.ts +116 -0
- package/src/surfaces/media-pane.json +513 -0
- package/src/surfaces/media-pane.ts +197 -0
- package/src/surfaces/menu.json +232 -0
- package/src/surfaces/menu.ts +375 -0
- package/src/surfaces/navigator-dock.json +113 -0
- package/src/surfaces/navigator-dock.ts +130 -0
- package/src/surfaces/new-project.json +1654 -0
- package/src/surfaces/new-project.ts +411 -0
- package/src/surfaces/palette.json +535 -0
- package/src/surfaces/pane-context.json +1497 -0
- package/src/surfaces/pane-context.ts +513 -0
- package/src/surfaces/pane-grid.json +188 -0
- package/src/surfaces/pane-grid.ts +250 -0
- package/src/surfaces/panel-activity.json +563 -0
- package/src/surfaces/panel-activity.ts +187 -0
- package/src/surfaces/panel-data.json +141 -0
- package/src/surfaces/panel-data.ts +156 -0
- package/src/surfaces/panel-deploy-checklist.json +283 -0
- package/src/surfaces/panel-deploy-checklist.ts +175 -0
- package/src/surfaces/panel-elements.json +367 -0
- package/src/surfaces/panel-elements.ts +175 -0
- package/src/surfaces/panel-i18n.json +569 -0
- package/src/surfaces/panel-i18n.ts +212 -0
- package/src/surfaces/panel-imports.json +780 -0
- package/src/surfaces/panel-imports.ts +209 -0
- package/src/surfaces/panel-outline.json +734 -0
- package/src/surfaces/panel-outline.ts +326 -0
- package/src/surfaces/panel-page.json +1017 -0
- package/src/surfaces/panel-page.ts +240 -0
- package/src/surfaces/panel-problems.json +497 -0
- package/src/surfaces/panel-problems.ts +157 -0
- package/src/surfaces/panel-signals.json +1973 -0
- package/src/surfaces/panel-signals.ts +436 -0
- package/src/surfaces/panel-stylebook-layers.json +317 -0
- package/src/surfaces/panel-stylebook-layers.ts +155 -0
- package/src/surfaces/preferences.json +850 -0
- package/src/surfaces/preferences.ts +332 -0
- package/src/surfaces/progress-modal.json +110 -0
- package/src/surfaces/progress-modal.ts +150 -0
- package/src/surfaces/properties-panel.json +1578 -0
- package/src/surfaces/properties-panel.ts +337 -0
- package/src/surfaces/publish.json +945 -0
- package/src/surfaces/publish.ts +392 -0
- package/src/surfaces/push-plan.json +110 -0
- package/src/surfaces/push-plan.ts +87 -0
- package/src/surfaces/rail.json +255 -0
- package/src/surfaces/rail.ts +206 -0
- package/src/surfaces/reference-field.json +191 -0
- package/src/surfaces/reference-field.ts +168 -0
- package/src/surfaces/schema-builder.json +1011 -0
- package/src/surfaces/schema-builder.ts +181 -0
- package/src/surfaces/schema-form.json +1013 -0
- package/src/surfaces/schema-form.ts +287 -0
- package/src/surfaces/secret-field.json +60 -0
- package/src/surfaces/secret-field.ts +146 -0
- package/src/surfaces/seo.json +932 -0
- package/src/surfaces/seo.ts +334 -0
- package/src/surfaces/settings-contexts.json +538 -0
- package/src/surfaces/settings-contexts.ts +196 -0
- package/src/surfaces/settings-contributed.json +489 -0
- package/src/surfaces/settings-contributed.ts +199 -0
- package/src/surfaces/settings-css-vars.json +820 -0
- package/src/surfaces/settings-css-vars.ts +199 -0
- package/src/surfaces/settings-defs.json +1363 -0
- package/src/surfaces/settings-defs.ts +232 -0
- package/src/surfaces/settings-deploy.json +108 -0
- package/src/surfaces/settings-deploy.ts +135 -0
- package/src/surfaces/settings-extensions.json +486 -0
- package/src/surfaces/settings-extensions.ts +201 -0
- package/src/surfaces/settings-head.json +779 -0
- package/src/surfaces/settings-head.ts +397 -0
- package/src/surfaces/settings-locales.json +420 -0
- package/src/surfaces/settings-locales.ts +137 -0
- package/src/surfaces/settings-overview.json +423 -0
- package/src/surfaces/settings-overview.ts +218 -0
- package/src/surfaces/settings-packages.json +549 -0
- package/src/surfaces/settings-packages.ts +158 -0
- package/src/surfaces/settings-pane.json +147 -0
- package/src/surfaces/settings-pane.ts +150 -0
- package/src/surfaces/settings-rawjson.json +104 -0
- package/src/surfaces/settings-rawjson.ts +107 -0
- package/src/surfaces/shell.json +99 -0
- package/src/surfaces/shell.ts +30 -0
- package/src/surfaces/slash-menu.json +180 -0
- package/src/surfaces/slash-menu.ts +272 -0
- package/src/surfaces/statements.json +607 -0
- package/src/surfaces/statements.ts +238 -0
- package/src/surfaces/statusbar.json +94 -0
- package/src/{panels → surfaces}/statusbar.ts +163 -90
- package/src/surfaces/style-panel.json +1643 -0
- package/src/surfaces/style-panel.ts +338 -0
- package/src/surfaces/stylebook-chrome.json +122 -0
- package/src/surfaces/stylebook-chrome.ts +122 -0
- package/src/surfaces/tab-strip.json +448 -0
- package/src/surfaces/tab-strip.ts +200 -0
- package/src/surfaces/target-line.json +419 -0
- package/src/surfaces/target-line.ts +349 -0
- package/src/surfaces/toasts.json +189 -0
- package/src/surfaces/welcome.json +571 -0
- package/src/surfaces/welcome.ts +338 -0
- package/src/tabs/project-config.ts +71 -15
- package/src/tabs/tab.ts +52 -1
- package/src/types.ts +22 -0
- package/src/ui/ai-credentials-form.ts +104 -100
- package/src/ui/ai-managed-connect.ts +54 -36
- package/src/ui/ai-model-picker.ts +75 -47
- package/src/ui/cf-account-picker.ts +76 -141
- package/src/ui/color-selector.ts +80 -301
- package/src/ui/dynamic-slot.ts +43 -283
- package/src/ui/expression-editor.ts +797 -576
- package/src/ui/field-input.ts +8 -181
- package/src/ui/form-controls.ts +838 -366
- package/src/ui/formula-chips.ts +80 -85
- package/src/ui/kit.ts +33 -0
- package/src/ui/layers.ts +293 -593
- package/src/ui/media-picker.ts +203 -284
- package/src/ui/panel-resize.ts +251 -64
- package/src/ui/popover-surface.ts +153 -0
- package/src/ui/progress-modal.ts +22 -60
- package/src/ui/regions.ts +13 -7
- package/src/ui/schema-form.ts +906 -440
- package/src/ui/surface.ts +122 -0
- package/src/ui/unit-selector.ts +15 -131
- package/src/ui/value-source.ts +14 -0
- package/src/ui/virtual-window.ts +88 -9
- package/src/utils/canvas-media.ts +72 -0
- package/src/utils/edit-display.ts +7 -2
- package/src/utils/element-selectors.ts +69 -0
- package/src/utils/link-target.ts +2 -2
- package/src/utils/studio-utils.ts +0 -10
- package/src/view.ts +15 -0
- package/src/workspace/pane-derive.ts +36 -14
- package/src/workspace/workspace.ts +15 -6
- package/styles/canvas.css +72 -467
- package/styles/forced-colors.css +48 -82
- package/styles/forced-colors.json +41 -0
- package/styles/inspector.css +2 -1301
- package/styles/overlays.css +96 -2823
- package/styles/panels.css +6 -1423
- package/styles/shell-frame.css +272 -0
- package/styles/shell-frame.json +227 -0
- package/styles/shell.css +0 -1568
- package/styles/tokens.css +65 -136
- package/styles/tokens.json +84 -0
- package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
- package/dist/chunks/adapter-jkcyhgtr.js +0 -14
- package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
- package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
- package/dist/chunks/components-zmx10t62.js +0 -21
- package/dist/chunks/content-source-48xg86wj.js +0 -49
- package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
- package/dist/chunks/editor-cptq5jp1.js +0 -37
- package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
- package/dist/chunks/extension-sections-txv08xxg.js +0 -311
- package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
- package/dist/chunks/files-3kfkwhk4.js +0 -93
- package/dist/chunks/flow-qbfgrpeh.js +0 -634
- package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
- package/dist/chunks/focus-visible-6eg70z18.js +0 -167
- package/dist/chunks/format-host-w16q81tf.js +0 -43
- package/dist/chunks/fs-events-hrrg328g.js +0 -21
- package/dist/chunks/git-panel-532whr1y.js +0 -59
- package/dist/chunks/index-zqxmqxmh.js +0 -109
- package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
- package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
- package/dist/chunks/layers-q75tj2mf.js +0 -45
- package/dist/chunks/lit-html-j055mkh8.js +0 -22
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
- package/dist/chunks/notify-yjm79n4s.js +0 -30
- package/dist/chunks/parse-24d0gay3.js +0 -19
- package/dist/chunks/platform-ba1j1gbh.js +0 -16
- package/dist/chunks/publish-panel-v9st96ja.js +0 -29
- package/dist/chunks/quick-search-zccjp6gv.js +0 -66
- package/dist/chunks/references-3qx8fkyp.js +0 -29
- package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
- package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
- package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
- package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
- package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
- package/dist/chunks/studio-0a5qrh01.js +0 -3
- package/dist/chunks/studio-0bq6eh7v.js +0 -12
- package/dist/chunks/studio-18j3bmcz.js +0 -14
- package/dist/chunks/studio-1ngbejft.js +0 -181
- package/dist/chunks/studio-4b5z1x43.js +0 -424
- package/dist/chunks/studio-4n2b5gzb.js +0 -357
- package/dist/chunks/studio-4rzyrtjz.js +0 -426
- package/dist/chunks/studio-5fx3g9jg.js +0 -4325
- package/dist/chunks/studio-5myknm0a.js +0 -1903
- package/dist/chunks/studio-7afd21nw.js +0 -67
- package/dist/chunks/studio-7nte2hj4.js +0 -613
- package/dist/chunks/studio-8jpzqk5s.js +0 -128
- package/dist/chunks/studio-8q517dfr.js +0 -24
- package/dist/chunks/studio-950k0dx6.js +0 -6939
- package/dist/chunks/studio-9n4mrmnb.js +0 -184
- package/dist/chunks/studio-a0tswy0w.js +0 -58
- package/dist/chunks/studio-ahnzd9rg.js +0 -23
- package/dist/chunks/studio-cmb03bg5.js +0 -13890
- package/dist/chunks/studio-d1ks410h.js +0 -175
- package/dist/chunks/studio-ef49pnzx.js +0 -387
- package/dist/chunks/studio-eqq1w638.js +0 -113
- package/dist/chunks/studio-ew0djmhg.js +0 -341
- package/dist/chunks/studio-gfgb8pe6.js +0 -1652
- package/dist/chunks/studio-h4rxscfc.js +0 -10
- package/dist/chunks/studio-jc994dnr.js +0 -1066
- package/dist/chunks/studio-kyh9avvv.js +0 -28
- package/dist/chunks/studio-m3hwn7y1.js +0 -120
- package/dist/chunks/studio-mtfnaqs9.js +0 -659
- package/dist/chunks/studio-n6j9t10q.js +0 -318
- package/dist/chunks/studio-n8b9wqwy.js +0 -291
- package/dist/chunks/studio-qcy05dp8.js +0 -1743
- package/dist/chunks/studio-qq6jym04.js +0 -53
- package/dist/chunks/studio-rdx8jcxy.js +0 -83
- package/dist/chunks/studio-rn9ta2mz.js +0 -111
- package/dist/chunks/studio-t7qb9db8.js +0 -665
- package/dist/chunks/studio-tmc5emrw.js +0 -8
- package/dist/chunks/studio-z6nw2ek2.js +0 -18
- package/dist/chunks/studio-zczps70q.js +0 -378
- package/dist/chunks/studio-zqazsct2.js +0 -1823
- package/dist/chunks/workspace-fjx52ryv.js +0 -89
- package/src/panels/activity-bar.ts +0 -251
- package/src/panels/panel-scheduler.ts +0 -219
- package/src/panels/style-inputs.ts +0 -237
- package/src/panels/target-line.ts +0 -274
- package/src/panels/welcome-screen.ts +0 -346
- package/src/ui/button-group.ts +0 -102
- package/src/ui/choice-field.ts +0 -72
- package/src/ui/field-row.ts +0 -249
- package/src/ui/formula-palette.ts +0 -213
- package/src/ui/icons.ts +0 -52
- package/src/ui/jx-theme.ts +0 -130
- package/src/ui/spectrum.ts +0 -340
- package/src/ui/value-selector.ts +0 -174
- package/src/ui/widgets.ts +0 -172
- /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
- /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
- /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
- /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
|
@@ -3,19 +3,26 @@
|
|
|
3
3
|
* Layers panel — the Outline: the document tree, with collapse, selection, drag-and-drop
|
|
4
4
|
* reordering, and per-row actions that are RENDERINGS of the command registry.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* **One panel, two bodies, and both of them are documents now.** The tree is
|
|
7
|
+
* `surfaces/panel-outline.json`; the Project Styles catalogue is
|
|
8
|
+
* `surfaces/panel-stylebook-layers.json` (`panels/stylebook-layers-panel.ts`). This module renders
|
|
9
|
+
* neither. It owns the record — only the record can know which body the pane is asking for — and
|
|
10
|
+
* everything that is a DECISION: which rows exist and in what order, what a row is called, what the
|
|
11
|
+
* window onto them is, which verbs the registry places on which row, and the whole of the keyboard
|
|
12
|
+
* model. The surface reads values. The two bodies are appended into the same `.panel-content`, so
|
|
13
|
+
* each mode takes the other's document down on the way in; that symmetry is the whole of the seam.
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
* action buttons, always visible, on every row — five custom elements with shadow roots per visible
|
|
10
|
-
* row. They now collapse to the selected row plus the hovered one, which is Gutenberg's rule and
|
|
11
|
-
* the one plan §3.2 ⑩ codifies: the floating bar owns selection-scoped verbs, the inspector owns
|
|
12
|
-
* values. The verbs, their names, their chords and their disabled reasons all come from the records
|
|
13
|
-
* in `block-action-bar.ts` — the surface renders, it does not decide.
|
|
15
|
+
* Four things this file is deliberate about.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* **Rows are `registry.forPlacement("outline/row")`.** Every row used to carry five hand-built
|
|
18
|
+
* action buttons, always visible, on every row. They collapse to the selected row plus the hovered
|
|
19
|
+
* one, which is Gutenberg's rule and the one plan §3.2 ⑩ codifies: the floating bar owns
|
|
20
|
+
* selection-scoped verbs, the inspector owns values. The verbs, their names, their chords and their
|
|
21
|
+
* disabled reasons all come from the records in `block-action-bar.ts` — the surface renders, it
|
|
22
|
+
* does not decide. A row that is neither selected nor hovered is projected with an EMPTY command
|
|
23
|
+
* list, so the cluster costs nothing rather than being hidden by a rule: a `display: none` kit
|
|
24
|
+
* element is still an upgraded custom element, which is the whole cost the collapse exists to
|
|
25
|
+
* remove.
|
|
19
26
|
*
|
|
20
27
|
* **A row says something.** On a real page the tree was a wall of rows all reading "div": only
|
|
21
28
|
* text-bearing nodes got a preview and containers got the tag they already wear as a coloured
|
|
@@ -24,25 +31,23 @@
|
|
|
24
31
|
*
|
|
25
32
|
* **It is a tree, and it is reachable.** `role="tree"` / `role="treeitem"` with a roving tabindex
|
|
26
33
|
* and the arrow-key model ARIA specifies: ↑↓ walk the visible rows, → expands then descends, ←
|
|
27
|
-
* collapses then ascends, Enter/F2 renames.
|
|
34
|
+
* collapses then ascends, Enter/F2 renames. The keys are cases in the document (`$switch` on
|
|
35
|
+
* `event#/key`); what each one MEANS is {@link onOutlineKey}.
|
|
28
36
|
*
|
|
29
37
|
* **The rows are a MODEL, and the DOM holds a window onto it** ({@link OutlineRow}, `ui/
|
|
30
|
-
* virtual-window.ts`). A 5 000-node page drew 5 000 rows
|
|
31
|
-
*
|
|
32
|
-
* the DOM
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
+
* virtual-window.ts`). A 5 000-node page drew 5 000 rows on every repaint. It now draws the
|
|
39
|
+
* viewport plus three rows of overscan — and because the DOM is no longer the whole list, nothing
|
|
40
|
+
* may ASK the DOM what the whole list is. Every question about "which rows exist, and in what
|
|
41
|
+
* order" — the shift-range, the arrow walk, Home/End, ←'s climb to the parent, the reveal that
|
|
42
|
+
* follows the selection — is answered from the array {@link buildOutlineRows} produces. A
|
|
43
|
+
* shift-range read off the DOM would silently select the wrong set the moment either end of it
|
|
44
|
+
* scrolled out of the window: a correctness bug wearing a performance change's clothes.
|
|
45
|
+
*
|
|
46
|
+
* @docs studio/design/layers
|
|
38
47
|
*/
|
|
39
48
|
|
|
40
|
-
import {
|
|
49
|
+
import { nothing } from "lit-html";
|
|
41
50
|
import { displayTagName, isTagExpression, tagNameCandidates } from "@jxsuite/schema/guards";
|
|
42
|
-
import { classMap } from "lit-html/directives/class-map.js";
|
|
43
|
-
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
44
|
-
import { ref } from "lit-html/directives/ref.js";
|
|
45
|
-
import { repeat } from "lit-html/directives/repeat.js";
|
|
46
51
|
import {
|
|
47
52
|
VOID_ELEMENTS,
|
|
48
53
|
childIndex,
|
|
@@ -66,15 +71,13 @@ import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
|
66
71
|
import { mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
67
72
|
import { view } from "../view";
|
|
68
73
|
import { setActivityTab } from "../shell";
|
|
69
|
-
import { renderEmptyState } from "./empty-state";
|
|
70
74
|
import { registerPanel } from "./panel-registry";
|
|
71
|
-
import {
|
|
75
|
+
import { detachStylebookLayers, mountStylebookLayersPanel } from "./stylebook-layers-panel";
|
|
72
76
|
import { selectStylebookTag, stylebookMeta } from "./stylebook-panel";
|
|
73
77
|
import { isInlineElement } from "../editor/inline-edit";
|
|
74
78
|
import { showContextMenu } from "../editor/context-menu";
|
|
75
|
-
import {
|
|
79
|
+
import { revealPathInCanvas } from "../canvas/popover-state";
|
|
76
80
|
import {
|
|
77
|
-
commandIcon,
|
|
78
81
|
commandTooltip,
|
|
79
82
|
runCommand,
|
|
80
83
|
selectionCommandRegistry,
|
|
@@ -87,15 +90,25 @@ import {
|
|
|
87
90
|
revealListRow,
|
|
88
91
|
watchListWindow,
|
|
89
92
|
} from "../ui/virtual-window";
|
|
90
|
-
import
|
|
93
|
+
import { mountOutlineSurface } from "../surfaces/panel-outline";
|
|
94
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
91
95
|
import type { ListWindowWatch } from "../ui/virtual-window";
|
|
92
|
-
import type {
|
|
96
|
+
import type { PanelBody } from "./panel-registry";
|
|
97
|
+
import type {
|
|
98
|
+
OutlineCommandView,
|
|
99
|
+
OutlineRowView,
|
|
100
|
+
OutlineSurfaceHandle,
|
|
101
|
+
OutlineValues,
|
|
102
|
+
} from "../surfaces/panel-outline";
|
|
93
103
|
|
|
94
104
|
// ─── What a row says ─────────────────────────────────────────────────────────
|
|
95
105
|
|
|
96
106
|
/** How much of a text preview a 240px column can carry before it is just noise. */
|
|
97
107
|
const LABEL_MAX = 32;
|
|
98
108
|
|
|
109
|
+
/** How much of a text NODE's own content the row previews. */
|
|
110
|
+
const TEXT_PREVIEW_MAX = 40;
|
|
111
|
+
|
|
99
112
|
/**
|
|
100
113
|
* Tags whose human name is worth more than the tag itself.
|
|
101
114
|
*
|
|
@@ -208,144 +221,47 @@ export function outlineLabel(node: JxMutableNode): string {
|
|
|
208
221
|
export const OUTLINE_ROW_MAX_ITEMS = 4;
|
|
209
222
|
|
|
210
223
|
/**
|
|
211
|
-
* The row's action cluster.
|
|
224
|
+
* The row's action cluster, as values.
|
|
225
|
+
*
|
|
226
|
+
* The glyph is `command.icon`, verbatim — the same one line `block-action-bar.ts`'s own `toolOf`
|
|
227
|
+
* projection is, because the records carry kit names and "what glyph does this record draw" must
|
|
228
|
+
* have exactly one answer. A record with no icon projects "" and the button draws its title.
|
|
212
229
|
*
|
|
213
230
|
* Wrapped in {@link withCommandTarget} so every record is evaluated against THIS ROW'S node rather
|
|
214
231
|
* than the selection — `PLACEMENT_MATRIX["outline/row"]` says row actions act on the row's node,
|
|
215
232
|
* and the hovered row is not the selected one.
|
|
216
233
|
*/
|
|
217
|
-
export function
|
|
234
|
+
export function rowCommandViews(
|
|
235
|
+
registry: CommandRegistry,
|
|
236
|
+
path: JxPath,
|
|
237
|
+
key: string,
|
|
238
|
+
): { commands: OutlineCommandView[]; overflow: AnyCommand[] } {
|
|
218
239
|
return withCommandTarget(path, () => {
|
|
219
240
|
const placed = registry.forPlacement("outline/row");
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
e.stopPropagation();
|
|
233
|
-
(e.currentTarget as HTMLElement).blur();
|
|
234
|
-
runCommand(registry, command.id, path);
|
|
235
|
-
}}
|
|
236
|
-
>${commandIcon(command)}</sp-action-button
|
|
237
|
-
>`,
|
|
238
|
-
)}
|
|
239
|
-
${
|
|
240
|
-
overflow.length === 0
|
|
241
|
-
? nothing
|
|
242
|
-
: html`<sp-action-button
|
|
243
|
-
quiet
|
|
244
|
-
size="xs"
|
|
245
|
-
class="layer-action layer-overflow"
|
|
246
|
-
aria-label="More actions"
|
|
247
|
-
aria-haspopup="menu"
|
|
248
|
-
title="More actions"
|
|
249
|
-
@click=${(e: MouseEvent) => {
|
|
250
|
-
e.stopPropagation();
|
|
251
|
-
showCommandOverflow(e.currentTarget as HTMLElement, registry, overflow, path);
|
|
252
|
-
}}
|
|
253
|
-
>
|
|
254
|
-
<sp-icon-more slot="icon"></sp-icon-more>
|
|
255
|
-
</sp-action-button>`
|
|
256
|
-
}`;
|
|
241
|
+
return {
|
|
242
|
+
commands: placed.slice(0, OUTLINE_ROW_MAX_ITEMS).map((command) => ({
|
|
243
|
+
destructive: command.destructive === true ? "true" : "false",
|
|
244
|
+
disabled: registry.disabledReason(command.id) !== undefined,
|
|
245
|
+
icon: command.icon ?? "",
|
|
246
|
+
id: command.id,
|
|
247
|
+
row: key,
|
|
248
|
+
title: command.title,
|
|
249
|
+
tooltip: commandTooltip(registry, command),
|
|
250
|
+
})),
|
|
251
|
+
overflow: placed.slice(OUTLINE_ROW_MAX_ITEMS),
|
|
252
|
+
};
|
|
257
253
|
});
|
|
258
254
|
}
|
|
259
255
|
|
|
260
|
-
// ─── The
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* The row whose cluster this module mounted on hover, so it can be taken down again.
|
|
264
|
-
*
|
|
265
|
-
* One at a time: the pointer is in one place. The SELECTED row's cluster is lit's (it is in the row
|
|
266
|
-
* template), and is never touched from here.
|
|
267
|
-
*/
|
|
268
|
-
let _hoverActionsRow: HTMLElement | null = null;
|
|
269
|
-
|
|
270
|
-
/** The empty span a non-selected row keeps for {@link mountHoverActions} to render into. */
|
|
271
|
-
function actionSlot(row: HTMLElement): HTMLElement | null {
|
|
272
|
-
return row.classList.contains("selected")
|
|
273
|
-
? null
|
|
274
|
-
: row.querySelector<HTMLElement>(".layer-actions");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Take down the hover cluster, if one is up. */
|
|
278
|
-
export function clearHoverActions(): void {
|
|
279
|
-
const slot = _hoverActionsRow ? actionSlot(_hoverActionsRow) : null;
|
|
280
|
-
if (slot) {
|
|
281
|
-
litRender(nothing, slot);
|
|
282
|
-
}
|
|
283
|
-
_hoverActionsRow = null;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Build `row`'s cluster into its slot, replacing whatever was there. */
|
|
287
|
-
function mountHoverActions(row: HTMLElement): void {
|
|
288
|
-
const slot = actionSlot(row);
|
|
289
|
-
const key = row.dataset.path;
|
|
290
|
-
if (!slot || key === undefined) {
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
_hoverActionsRow = row;
|
|
294
|
-
litRender(renderRowCommands(selectionCommandRegistry(), pathFromKey(key)), slot);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Delegated `mouseover` (which bubbles; `mouseenter` does not): reveal the cluster on the row under
|
|
299
|
-
* the pointer, and only that row.
|
|
300
|
-
*/
|
|
301
|
-
export function onTreeHover(e: Event): void {
|
|
302
|
-
const row = (e.target as HTMLElement | null)?.closest<HTMLElement>('.layer-row[role="treeitem"]');
|
|
303
|
-
if (!row || row === _hoverActionsRow) {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
clearHoverActions();
|
|
307
|
-
mountHoverActions(row);
|
|
308
|
-
}
|
|
256
|
+
// ─── The tree, as a keyboard surface ─────────────────────────────────────────
|
|
309
257
|
|
|
310
258
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* Deferred by a microtask on purpose. A `ref` on the tree element commits BEFORE the child part
|
|
314
|
-
* holding the rows, so on a first render the callback sees an empty tree; and the template this
|
|
315
|
-
* module returns is rendered by its caller, so a microtask is the first tick at which the new DOM
|
|
316
|
-
* exists either way.
|
|
317
|
-
*
|
|
318
|
-
* - The roving tabindex needs to know which rows survived their collapsed ancestors.
|
|
319
|
-
* - The hover cluster needs re-mounting: rows are keyed by path, so a document edit can leave the
|
|
320
|
-
* pointer on a DOM row that now stands for a different node, with different disabled reasons.
|
|
259
|
+
* Pixels of indent per level, the depth past which the column stops paying, and the row's own
|
|
260
|
+
* gutter.
|
|
321
261
|
*/
|
|
322
|
-
export function afterTreeRender(tree: HTMLElement): void {
|
|
323
|
-
const row = _hoverActionsRow;
|
|
324
|
-
queueMicrotask(() => {
|
|
325
|
-
adoptOutlineTree(tree);
|
|
326
|
-
applyTreeRovingTabindex(tree);
|
|
327
|
-
// A keyboard jump that had to scroll first left its target here, for the render it provoked.
|
|
328
|
-
const wanted = _pendingFocusKey;
|
|
329
|
-
if (wanted !== null) {
|
|
330
|
-
_pendingFocusKey = null;
|
|
331
|
-
focusRow(tree, rowElementFor(tree, wanted) ?? undefined);
|
|
332
|
-
}
|
|
333
|
-
if (!row || _hoverActionsRow !== row) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (row.isConnected && !row.classList.contains("selected")) {
|
|
337
|
-
mountHoverActions(row);
|
|
338
|
-
} else {
|
|
339
|
-
_hoverActionsRow = null;
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ─── The tree, as a keyboard surface ─────────────────────────────────────────
|
|
345
|
-
|
|
346
|
-
/** Pixels of indent per level, and the depth past which the column stops paying for more. */
|
|
347
262
|
const INDENT_STEP = 16;
|
|
348
263
|
const INDENT_MAX_DEPTH = 6;
|
|
264
|
+
const INDENT_BASE = 8;
|
|
349
265
|
|
|
350
266
|
/**
|
|
351
267
|
* The indent for `depth`, capped.
|
|
@@ -368,12 +284,12 @@ function pathFromKey(key: string): JxPath {
|
|
|
368
284
|
* One row the Outline WOULD draw, whether or not it is currently in the window.
|
|
369
285
|
*
|
|
370
286
|
* Built in a first pass that decides visibility (collapsed ancestors, inline elements, the content
|
|
371
|
-
* root) and costs nothing but the decision; the
|
|
372
|
-
* window only. Splitting the two is the whole saving — the expensive half is the
|
|
373
|
-
*
|
|
287
|
+
* root) and costs nothing but the decision; the projections are built in a second pass, for the
|
|
288
|
+
* window only. Splitting the two is the whole saving — the expensive half is the kit elements a
|
|
289
|
+
* row's cluster mounts, not the walk that finds it.
|
|
374
290
|
*/
|
|
375
291
|
interface OutlineRow {
|
|
376
|
-
/** `pathKey(path)` — the drag-and-drop and roving-
|
|
292
|
+
/** `pathKey(path)` — the drag-and-drop and roving-focus key, and the `$map`'s reconcile key. */
|
|
377
293
|
key: string;
|
|
378
294
|
path: JxPath;
|
|
379
295
|
depth: number;
|
|
@@ -383,8 +299,8 @@ interface OutlineRow {
|
|
|
383
299
|
* Whether this row is a `role="treeitem"`.
|
|
384
300
|
*
|
|
385
301
|
* Text-node rows are drawn but are not tree items, and were never part of the keyboard walk or a
|
|
386
|
-
* shift-range
|
|
387
|
-
*
|
|
302
|
+
* shift-range — so moving those questions to the model has to carry the same distinction rather
|
|
303
|
+
* than quietly start selecting text nodes.
|
|
388
304
|
*/
|
|
389
305
|
item: boolean;
|
|
390
306
|
node: JxMutableNode | string | number | boolean;
|
|
@@ -396,26 +312,33 @@ interface OutlineRow {
|
|
|
396
312
|
}
|
|
397
313
|
|
|
398
314
|
/**
|
|
399
|
-
* The declared height of one row — `
|
|
315
|
+
* The declared height of one row — `surfaces/panel-outline.json`'s `[part="row"] { block-size }`.
|
|
400
316
|
*
|
|
401
317
|
* A window needs a row height BEFORE the first row exists, so this constant is what the first paint
|
|
402
318
|
* windows by; {@link outlineRowHeight} measures a real row afterwards and believes the measurement.
|
|
403
|
-
* The
|
|
319
|
+
* The document declares the height explicitly so the two cannot drift apart in silence.
|
|
404
320
|
*/
|
|
405
321
|
export const OUTLINE_ROW_HEIGHT = 24;
|
|
406
322
|
|
|
323
|
+
/** The selector the window, the reveal and the focus move address a drawn row by. */
|
|
324
|
+
const ROW_SELECTOR = '[part="row"]';
|
|
325
|
+
|
|
407
326
|
/** The rows the Outline last built, in display order. */
|
|
408
327
|
let _outlineRows: OutlineRow[] = [];
|
|
409
|
-
/** The
|
|
328
|
+
/** The `[part="tree"]` element, kept between renders so a window can be computed for the next one. */
|
|
410
329
|
let _outlineList: HTMLElement | null = null;
|
|
411
330
|
/** The scroll watch that repaints the Outline as its scroller moves. */
|
|
412
331
|
let _outlineWatch: ListWindowWatch | null = null;
|
|
413
|
-
/** The Navigator repaint, captured per
|
|
332
|
+
/** The Navigator repaint, captured per mount so the scroll watch never holds a stale one. */
|
|
414
333
|
let _outlineRerender: (() => void) | null = null;
|
|
334
|
+
/** The row under the pointer, which is the second row that carries a cluster. */
|
|
335
|
+
let _hoveredKey: string | null = null;
|
|
336
|
+
/** The row being renamed, the text typed into it so far, and whose repaint to spend on commit. */
|
|
337
|
+
let _editing: { key: string; text: string; rerender: () => void } | null = null;
|
|
415
338
|
|
|
416
339
|
/** The height one row actually has; the declared constant until a row has been laid out. */
|
|
417
340
|
function outlineRowHeight(): number {
|
|
418
|
-
return measuredRowHeight(_outlineList,
|
|
341
|
+
return measuredRowHeight(_outlineList, ROW_SELECTOR, OUTLINE_ROW_HEIGHT);
|
|
419
342
|
}
|
|
420
343
|
|
|
421
344
|
/** The model index of the row keyed `key`, or -1. */
|
|
@@ -475,38 +398,45 @@ function revealOutlineRow(index: number): boolean {
|
|
|
475
398
|
/**
|
|
476
399
|
* Repaint the Outline because its window changed.
|
|
477
400
|
*
|
|
478
|
-
* Deferred to a microtask so a scroll that arrives while
|
|
479
|
-
* render that is producing the rows.
|
|
401
|
+
* Deferred to a microtask so a scroll that arrives while the runtime is committing cannot re-enter
|
|
402
|
+
* the render that is producing the rows.
|
|
480
403
|
*
|
|
481
404
|
* **Never during a drag.** `panels/dnd.ts` holds the drop targets it registered on the rendered
|
|
482
405
|
* rows and shifts the rows either side of the pointer by a transform; re-rendering underneath it
|
|
483
406
|
* would drop both. A wheel-scroll mid-drag therefore keeps the window the drag started with, and
|
|
484
|
-
* the drop's own repaint restores it.
|
|
407
|
+
* the drop's own repaint restores it. The drag marks its row with `data-dragging`, which is an
|
|
408
|
+
* attribute rather than a class for the reason the document's style block gives.
|
|
485
409
|
*
|
|
486
410
|
* **Never for a tree that is gone.** The stylebook draws a different panel into the same dock,
|
|
487
411
|
* whose scroller is the one this watch is still listening to.
|
|
488
412
|
*/
|
|
489
413
|
function outlineWindowChanged(): void {
|
|
490
|
-
if (_outlineList?.isConnected !== true || _outlineList.querySelector("
|
|
414
|
+
if (_outlineList?.isConnected !== true || _outlineList.querySelector("[data-dragging]")) {
|
|
491
415
|
return;
|
|
492
416
|
}
|
|
493
417
|
queueMicrotask(() => _outlineRerender?.());
|
|
494
418
|
}
|
|
495
419
|
|
|
496
420
|
/**
|
|
497
|
-
*
|
|
421
|
+
* Keep the tree watching whatever scrolls it.
|
|
498
422
|
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
423
|
+
* Split from {@link OutlineActions.treeReady} on purpose, and the split is the whole of why the
|
|
424
|
+
* window works. `onNodeCreated` hands over the element as it is BUILT — one tick before it is in
|
|
425
|
+
* the document — so `nearestScroller` walking up from it at that moment finds nothing, binds no
|
|
426
|
+
* watch, and the tree silently draws all five thousand rows for the rest of the session. The
|
|
427
|
+
* element is remembered there, because the next projection's `listWindow` needs it; the watch is
|
|
428
|
+
* bound HERE, from the post-mount tick, where the element is connected and the scroller resolves.
|
|
503
429
|
*
|
|
504
430
|
* Idempotent by construction: `watchListWindow` hands back the same watch for the same element and
|
|
505
|
-
* scroller, so calling
|
|
431
|
+
* scroller, so calling it after every draw costs a comparison. The first paint of a session draws
|
|
432
|
+
* every row, because nothing can be measured before them; the watch's opening measurement is what
|
|
433
|
+
* asks for the second, windowed one.
|
|
506
434
|
*/
|
|
507
|
-
function
|
|
508
|
-
_outlineList
|
|
509
|
-
|
|
435
|
+
function watchOutlineTree(): void {
|
|
436
|
+
if (!_outlineList) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
_outlineWatch = watchListWindow(_outlineWatch, _outlineList, {
|
|
510
440
|
count: () => _outlineRows.length,
|
|
511
441
|
onChange: outlineWindowChanged,
|
|
512
442
|
rowHeight: outlineRowHeight,
|
|
@@ -517,13 +447,13 @@ function adoptOutlineTree(tree: HTMLElement): void {
|
|
|
517
447
|
* The node an Outline row stands for, read back off the row.
|
|
518
448
|
*
|
|
519
449
|
* Rows carry their `JxPath` verbatim, as JSON, in `data-jx-path` — node IDENTITY in the DOM. That
|
|
520
|
-
* is a different thing from the neighbouring `data-
|
|
521
|
-
* string and
|
|
522
|
-
* `["children", 0]` share a key and are different
|
|
523
|
-
* key at all.
|
|
450
|
+
* is a different thing from the neighbouring `data-value`, which is `pathKey`'s lossy `join("/")`
|
|
451
|
+
* string and is the row's `value`: the drag-and-drop key, the caret key, and the detail of every
|
|
452
|
+
* event the row provokes. `["children", "0"]` and `["children", 0]` share a key and are different
|
|
453
|
+
* nodes, and a segment containing a slash has no key at all.
|
|
524
454
|
*
|
|
525
|
-
* Everything that has to point at a node from outside the
|
|
526
|
-
*
|
|
455
|
+
* Everything that has to point at a node from outside the projection — the context menu, drag
|
|
456
|
+
* reorder, canvas to Outline sync, a collaborator's cursor, a jump from Problems — needs the
|
|
527
457
|
* unambiguous one.
|
|
528
458
|
*/
|
|
529
459
|
export function outlineRowPath(el: Element | null): JxPath | null {
|
|
@@ -539,77 +469,29 @@ export function outlineRowPath(el: Element | null): JxPath | null {
|
|
|
539
469
|
}
|
|
540
470
|
}
|
|
541
471
|
|
|
542
|
-
/** Every row currently on screen, in visual order. */
|
|
543
|
-
function treeRows(tree: HTMLElement): HTMLElement[] {
|
|
544
|
-
return [...tree.querySelectorAll<HTMLElement>('.layer-row[role="treeitem"]')];
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Roving tabindex over the rows: the selected row is the tab stop, else the first.
|
|
549
|
-
*
|
|
550
|
-
* Applied after every render ({@link afterTreeRender}) rather than baked into each row's template,
|
|
551
|
-
* because "is the selected row actually on screen" is only known once the collapsed ancestors have
|
|
552
|
-
* been skipped — a selection inside a collapsed branch would otherwise leave the tree with no tab
|
|
553
|
-
* stop at all.
|
|
554
|
-
*/
|
|
555
|
-
export function applyTreeRovingTabindex(tree: HTMLElement): void {
|
|
556
|
-
const rows = treeRows(tree);
|
|
557
|
-
if (rows.length === 0) {
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
// The PRIMARY row is the tab stop, not merely the first selected one: a multi-selection has one
|
|
561
|
-
// Keyboard position, and it is the row the author last pointed at. `data-primary` is stamped by
|
|
562
|
-
// The row template, so the two never have to re-derive the same answer.
|
|
563
|
-
const primary = rows.find((row) => row.dataset.primary !== undefined);
|
|
564
|
-
const selected = primary ?? rows.find((row) => row.getAttribute("aria-selected") === "true");
|
|
565
|
-
const stop = selected ?? rows[0]!;
|
|
566
|
-
for (const row of rows) {
|
|
567
|
-
row.tabIndex = row === stop ? 0 : -1;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/** Focus `row`, and make it the tree's single tab stop. */
|
|
572
|
-
function focusRow(tree: HTMLElement, row: HTMLElement | undefined): void {
|
|
573
|
-
if (!row) {
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
for (const other of treeRows(tree)) {
|
|
577
|
-
other.tabIndex = other === row ? 0 : -1;
|
|
578
|
-
}
|
|
579
|
-
row.focus();
|
|
580
|
-
}
|
|
581
|
-
|
|
582
472
|
/** The rendered row for a model key, or null when the window does not currently hold it. */
|
|
583
|
-
function rowElementFor(
|
|
584
|
-
return
|
|
473
|
+
function rowElementFor(key: string): HTMLElement | null {
|
|
474
|
+
return (
|
|
475
|
+
_outlineList?.querySelector<HTMLElement>(`${ROW_SELECTOR}[data-value="${CSS.escape(key)}"]`) ??
|
|
476
|
+
null
|
|
477
|
+
);
|
|
585
478
|
}
|
|
586
479
|
|
|
587
480
|
/**
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
481
|
+
* Bring the model row at `index` into the window, when the window does not already hold it.
|
|
482
|
+
*
|
|
483
|
+
* **The keyboard is deliberately not touched here.** `jx-tree` keeps it across a handed-off caret
|
|
484
|
+
* move: it focuses the caret row once its own sync has written the roving `tabindex` onto the
|
|
485
|
+
* revealed element, which is the only moment focusing it does anything at all. What this used to do
|
|
486
|
+
* instead was queue a microtask and focus whatever answered to the key then — and the microtask was
|
|
487
|
+
* queued BEFORE {@link redrawOutline}, so it ran while the previous projection was still drawn and
|
|
488
|
+
* found either the row that used to stand at that key or, for a row the window did not hold,
|
|
489
|
+
* nothing. One shot, spent early, every time.
|
|
596
490
|
*/
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
/** Move the keyboard to the model row at `index`, bringing it into the window if it is outside one. */
|
|
600
|
-
function focusModelRow(tree: HTMLElement, index: number): void {
|
|
491
|
+
function revealModelRow(index: number): void {
|
|
601
492
|
const row = _outlineRows[index];
|
|
602
|
-
if (!row) {
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
const el = rowElementFor(tree, row.key);
|
|
606
|
-
if (el) {
|
|
607
|
-
focusRow(tree, el);
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
if (revealOutlineRow(index)) {
|
|
611
|
-
_pendingFocusKey = row.key;
|
|
612
|
-
_outlineRerender?.();
|
|
493
|
+
if (row && !rowElementFor(row.key)) {
|
|
494
|
+
revealOutlineRow(index);
|
|
613
495
|
}
|
|
614
496
|
}
|
|
615
497
|
|
|
@@ -629,10 +511,6 @@ function selectModelRow(index: number, gesture: { range?: boolean } = {}): void
|
|
|
629
511
|
* - Ctrl/Cmd: toggle this path in or out, leaving the rest alone.
|
|
630
512
|
* - Shift: the contiguous run of VISIBLE rows from the anchor to here.
|
|
631
513
|
*
|
|
632
|
-
* It took the tree element as its first parameter while a range was resolved by reading the rows
|
|
633
|
-
* out of the DOM. It no longer is ({@link visibleRowPaths}), and the parameter went with the read —
|
|
634
|
-
* a signature that still asked for the tree would suggest the answer depends on what is painted.
|
|
635
|
-
*
|
|
636
514
|
* @param {JxPath} path
|
|
637
515
|
* @param {{ additive?: boolean; range?: boolean }} gesture
|
|
638
516
|
*/
|
|
@@ -660,76 +538,170 @@ export function applyRowSelection(
|
|
|
660
538
|
}
|
|
661
539
|
|
|
662
540
|
/**
|
|
663
|
-
* The
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
* Every one of those moves is an index into {@link outlineRows}, not into the rendered rows. The DOM
|
|
672
|
-
* answer and the model answer agreed exactly while the tree drew everything; now the DOM holds a
|
|
673
|
-
* window, and asking it for "the row after this one" would stop the walk at the window's edge and
|
|
674
|
-
* make ← climb to whichever ancestor happened to be painted.
|
|
541
|
+
* The reader meant that row, and `mode` says what by.
|
|
542
|
+
*
|
|
543
|
+
* The three modes are the ARIA tree's own — a plain move or click replaces, `Ctrl`/`Cmd` toggles,
|
|
544
|
+
* `Shift` extends — and each maps onto the gesture {@link applyRowSelection} already had. What
|
|
545
|
+
* `jx-tree` cannot do is RESOLVE the range: naming every row between two of them means naming rows
|
|
546
|
+
* the drawn slice does not have, so the element dispatches the intent and the range is resolved
|
|
547
|
+
* here against {@link visibleRowPaths}, which reads the MODEL.
|
|
675
548
|
*/
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
collapsed: Set<string>,
|
|
679
|
-
rerender: () => void,
|
|
680
|
-
): void {
|
|
681
|
-
const tree = e.currentTarget as HTMLElement;
|
|
682
|
-
const row = (e.target as HTMLElement).closest<HTMLElement>('.layer-row[role="treeitem"]');
|
|
549
|
+
function selectOutlineRow(key: string, mode: string): void {
|
|
550
|
+
const row = _outlineRows[outlineIndexOfKey(key)];
|
|
683
551
|
if (!row) {
|
|
684
552
|
return;
|
|
685
553
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
554
|
+
applyRowSelection(row.path, { additive: mode === "toggle", range: mode === "range" });
|
|
555
|
+
redrawOutline();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* A row should be opened or closed.
|
|
560
|
+
*
|
|
561
|
+
* The detail says the state the row should be PUT INTO, so there is nothing left to work out about
|
|
562
|
+
* which way a toggle was going. The guard is still load bearing, in BOTH directions: a leaf's
|
|
563
|
+
* twisty box is drawn empty and still answers a click, and the element reads a leaf's expansion as
|
|
564
|
+
* "open me" — so the dead 14px in front of a row with nothing under it is a click that must change
|
|
565
|
+
* nothing at all, rather than one that quietly reaches into the collapsed set on its way past. The
|
|
566
|
+
* row must also still be in the model: a key from a stale projection would go into that set with
|
|
567
|
+
* nothing left to take it out again.
|
|
568
|
+
*/
|
|
569
|
+
function expandOutlineRow(key: string, expanded: boolean): void {
|
|
570
|
+
const row = _outlineRows[outlineIndexOfKey(key)];
|
|
571
|
+
if (!row || !isExpandable(row)) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const collapsed = outlineCollapsed();
|
|
575
|
+
if (expanded) {
|
|
576
|
+
collapsed.delete(key);
|
|
577
|
+
} else {
|
|
578
|
+
collapsed.add(key);
|
|
579
|
+
}
|
|
580
|
+
redrawOutline();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The caret has to reach a row the window did not draw.
|
|
585
|
+
*
|
|
586
|
+
* `jx-tree` walks the DRAWN rows, which is all it can see; when a pad says the model continues that
|
|
587
|
+
* way it dispatches `move` and performs nothing. Each key is answered here exactly as the element
|
|
588
|
+
* would have answered it over a slice that held everything — and ← is the one that could never have
|
|
589
|
+
* been the element's, because the model records each row's PARENT as it builds it and a scan for
|
|
590
|
+
* "the nearest row above at a shallower level" can only ever see painted rows.
|
|
591
|
+
*
|
|
592
|
+
* The selection comes with it, for two reasons that point the same way. In an outline "focus
|
|
593
|
+
* follows selection" is what an author means by pressing Down; and `jx-tree` raises `select` beside
|
|
594
|
+
* `change` on every arrow it can perform itself, so a step the window could not satisfy must not be
|
|
595
|
+
* the one step that silently does not — which is also what puts the tab stop on the revealed row,
|
|
596
|
+
* rather than leaving the reader FOCUSED on one row while Tab comes back to another.
|
|
597
|
+
*
|
|
598
|
+
* @param {string} from The row the caret is on
|
|
599
|
+
* @param {string} keyName The key the element could not perform
|
|
600
|
+
*/
|
|
601
|
+
function moveOutlineCaret(from: string, keyName: string): void {
|
|
602
|
+
const index = outlineIndexOfKey(from);
|
|
603
|
+
let target: number;
|
|
604
|
+
switch (keyName) {
|
|
605
|
+
case "Home": {
|
|
606
|
+
target = outlineStep(-1, 1);
|
|
607
|
+
break;
|
|
698
608
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
collapsed.delete(key);
|
|
703
|
-
rerender();
|
|
704
|
-
} else if (expanded === "true") {
|
|
705
|
-
focusModelRow(tree, outlineStep(index, 1));
|
|
609
|
+
case "End": {
|
|
610
|
+
target = outlineStep(_outlineRows.length, -1);
|
|
611
|
+
break;
|
|
706
612
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// Backwards scan for a smaller `aria-level` — which could only ever see painted rows.
|
|
715
|
-
const parent = _outlineRows[index]?.parent ?? -1;
|
|
716
|
-
if (parent >= 0) {
|
|
717
|
-
selectModelRow(parent);
|
|
718
|
-
focusModelRow(tree, parent);
|
|
719
|
-
}
|
|
613
|
+
case "ArrowUp": {
|
|
614
|
+
target = outlineStep(index, -1);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
case "ArrowLeft": {
|
|
618
|
+
target = _outlineRows[index]?.parent ?? -1;
|
|
619
|
+
break;
|
|
720
620
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
focusModelRow(tree, target);
|
|
621
|
+
// ↓ and → both ask for the next drawn row; over the model they are the same step.
|
|
622
|
+
case "ArrowDown":
|
|
623
|
+
case "ArrowRight": {
|
|
624
|
+
target = outlineStep(index, 1);
|
|
625
|
+
break;
|
|
727
626
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
627
|
+
default: {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
landOutlineCaret(target);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Put the caret on the model row at `target`, or do nothing when there is no such row.
|
|
636
|
+
*
|
|
637
|
+
* Shared by the two events the element cannot answer for itself, because they end the same way and
|
|
638
|
+
* that is not a coincidence: both are a row named over the MODEL, and reaching it means selecting
|
|
639
|
+
* it, revealing it and repainting the window around it in that order. The selection is also what
|
|
640
|
+
* moves the CARET — the projection seeds `current` from the primary selection — and that write is
|
|
641
|
+
* the whole of this panel's side of the handoff: `jx-tree` takes the keyboard to the revealed row
|
|
642
|
+
* itself, once its own sync has made that row focusable.
|
|
643
|
+
*
|
|
644
|
+
* @param {number} target
|
|
645
|
+
*/
|
|
646
|
+
function landOutlineCaret(target: number): void {
|
|
647
|
+
if (!_outlineRows[target]) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
selectModelRow(target);
|
|
651
|
+
revealModelRow(target);
|
|
652
|
+
redrawOutline();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* What a row is CALLED — the one string the reader sees, hears and types the first letter of.
|
|
657
|
+
*
|
|
658
|
+
* One function rather than the three expressions it replaced, because typeahead now matches over
|
|
659
|
+
* the MODEL and the drawn row's `label` is what it must agree with. `jx-tree` matches a drawn row
|
|
660
|
+
* on its `label` prop precisely because that is the row's accessible name; a model-side search that
|
|
661
|
+
* computed the name a second way would move the caret to a row the reader was never told about, and
|
|
662
|
+
* nothing on screen would explain it.
|
|
663
|
+
*
|
|
664
|
+
* It answers for a TEXT row too, and that is what makes the `item` filter in
|
|
665
|
+
* {@link typeaheadOutlineCaret} do real work rather than merely read well: a text row draws its own
|
|
666
|
+
* preview, so it has a name a letter could reach, and the only thing keeping the caret off it is
|
|
667
|
+
* that it is not a tree item — the same rule {@link outlineStep} applies to the arrows.
|
|
668
|
+
*
|
|
669
|
+
* @param {OutlineRow} row
|
|
670
|
+
*/
|
|
671
|
+
function outlineRowLabel(row: OutlineRow): string {
|
|
672
|
+
if (!row.item) {
|
|
673
|
+
const text = String(row.node);
|
|
674
|
+
return text.length > TEXT_PREVIEW_MAX ? `${text.slice(0, TEXT_PREVIEW_MAX)}…` : text;
|
|
675
|
+
}
|
|
676
|
+
const node = row.node as JxMutableNode;
|
|
677
|
+
return row.nodeType === "case-ref" ? node.$ref || "external" : outlineLabel(node);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* A printable character on a windowed tree: find the model row it names, and go there.
|
|
682
|
+
*
|
|
683
|
+
* `jx-tree` resolves typeahead itself only when the drawn rows ARE the model; past that it
|
|
684
|
+
* dispatches, because a search over a slice always answers and the answer is the wrong row. The
|
|
685
|
+
* search here is the element's own, moved onto the full list: forward from the caret, wrapping
|
|
686
|
+
* once, matching the row's name case-insensitively, and skipping the rows that are drawn but are
|
|
687
|
+
* not tree items — text nodes, which the arrows already step over.
|
|
688
|
+
*
|
|
689
|
+
* Every model row is named on each keystroke, and that is affordable where it would not be in a
|
|
690
|
+
* paint: this runs once per key rather than once per frame, and {@link outlineRowView} is still
|
|
691
|
+
* called for the window alone.
|
|
692
|
+
*
|
|
693
|
+
* @param {string} from The row the caret is on
|
|
694
|
+
* @param {string} char The character typed, lowercased
|
|
695
|
+
*/
|
|
696
|
+
function typeaheadOutlineCaret(from: string, char: string): void {
|
|
697
|
+
const start = outlineIndexOfKey(from);
|
|
698
|
+
const total = _outlineRows.length;
|
|
699
|
+
for (let step = 1; step <= total; step++) {
|
|
700
|
+
const at = (start + step + total) % total;
|
|
701
|
+
const row = _outlineRows[at];
|
|
702
|
+
if (row?.item && outlineRowLabel(row).trim().toLowerCase().startsWith(char)) {
|
|
703
|
+
landOutlineCaret(at);
|
|
704
|
+
return;
|
|
733
705
|
}
|
|
734
706
|
}
|
|
735
707
|
}
|
|
@@ -737,88 +709,86 @@ export function onTreeKeydown(
|
|
|
737
709
|
/**
|
|
738
710
|
* Start inline title editing on a layer row.
|
|
739
711
|
*
|
|
712
|
+
* The input is a CASE of the row now, not a node this module creates and inserts: the document
|
|
713
|
+
* draws it when the projection says this row is the one being renamed, and takes it away again when
|
|
714
|
+
* the projection stops saying so. That is what retired the four imperative writes the predecessor
|
|
715
|
+
* made into a tree it did not own — and with them the stale `display: none` a keyed re-render could
|
|
716
|
+
* leave on the label of whichever node inherited the row.
|
|
717
|
+
*
|
|
718
|
+
* A row the window does not hold is scrolled to and left to the scroll watch, which is the same
|
|
719
|
+
* "the row exists on the next pass" the reveal has always relied on. Renaming through a command
|
|
720
|
+
* (⌘↵, the palette, the block bar) is the one gesture in the app where re-pressing it is obvious.
|
|
721
|
+
*
|
|
740
722
|
* @param {JxPath} path
|
|
741
723
|
* @param {() => void} rerender
|
|
742
724
|
*/
|
|
743
725
|
export function startLayerTitleEdit(path: JxPath, rerender: () => void) {
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
this file uses. The predecessor was a bare `document.querySelector` with the key interpolated
|
|
747
|
-
raw, which is wrong twice over: a second pane — or the stylebook, which draws a layer tree of
|
|
748
|
-
its own — puts more than one `.layer-row[data-path=…]` in the document, and the first match
|
|
749
|
-
wins rather than the right one; and an unescaped key containing a quote or a bracket is not a
|
|
750
|
-
selector the parser accepts, so a legitimately-named node throws instead of renaming.
|
|
751
|
-
`_outlineList` is null before the first render, which is the same "row the window does not
|
|
752
|
-
hold" case the reveal-and-repaint path below already handles. */
|
|
753
|
-
const row = _outlineList ? rowElementFor(_outlineList, key) : null;
|
|
754
|
-
if (!row) {
|
|
755
|
-
// Renaming through a command (⌘↵, the palette, the block bar) can name a row the window does
|
|
756
|
-
// Not hold. Scroll to it and repaint; the row exists on the next pass, and the caller's own
|
|
757
|
-
// Chord is the one gesture in the app where re-pressing it is obvious.
|
|
758
|
-
const index = outlineIndexOfPath(path);
|
|
759
|
-
if (index >= 0 && revealOutlineRow(index)) {
|
|
760
|
-
rerender();
|
|
761
|
-
}
|
|
726
|
+
const tab = activeTab.value;
|
|
727
|
+
if (!tab) {
|
|
762
728
|
return;
|
|
763
729
|
}
|
|
764
|
-
const
|
|
765
|
-
|
|
730
|
+
const index = outlineIndexOfPath(path);
|
|
731
|
+
const row = _outlineRows[index];
|
|
732
|
+
/* The row's own node, not a second `getNodeAtPath` walk: the model already holds it, and reading
|
|
733
|
+
it back off the model is what makes "a text line has no `$title` to write" a real branch rather
|
|
734
|
+
than a defensive null check nothing can reach. */
|
|
735
|
+
if (!row?.item || typeof row.node !== "object") {
|
|
766
736
|
return;
|
|
767
737
|
}
|
|
738
|
+
_editing = { key: row.key, rerender, text: row.node.$title || "" };
|
|
739
|
+
if (!rowElementFor(row.key)) {
|
|
740
|
+
revealOutlineRow(index);
|
|
741
|
+
}
|
|
742
|
+
redrawOutline();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Whether a rename is live on `key`. */
|
|
746
|
+
function isEditing(key: string): boolean {
|
|
747
|
+
return _editing?.key === key;
|
|
748
|
+
}
|
|
768
749
|
|
|
750
|
+
/** Record what the rename input holds, so a blur can commit it without reading the DOM back. */
|
|
751
|
+
function editInput(text: string): void {
|
|
752
|
+
if (_editing) {
|
|
753
|
+
_editing.text = text;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Write the typed title (or clear it) and end the rename. Idempotent — a blur follows an Enter. */
|
|
758
|
+
function editCommit(): void {
|
|
759
|
+
const session = _editing;
|
|
769
760
|
const tab = activeTab.value;
|
|
770
|
-
if (!
|
|
761
|
+
if (!session) {
|
|
771
762
|
return;
|
|
772
763
|
}
|
|
773
|
-
|
|
774
|
-
|
|
764
|
+
_editing = null;
|
|
765
|
+
const value = session.text.trim();
|
|
766
|
+
if (tab) {
|
|
767
|
+
transactDoc(tab, (t) =>
|
|
768
|
+
mutateUpdateProperty(t, pathFromKey(session.key), "$title", value || undefined),
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
session.rerender();
|
|
772
|
+
redrawOutline();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** End the rename without writing anything. */
|
|
776
|
+
function editCancel(): void {
|
|
777
|
+
const session = _editing;
|
|
778
|
+
if (!session) {
|
|
775
779
|
return;
|
|
776
780
|
}
|
|
781
|
+
_editing = null;
|
|
782
|
+
session.rerender();
|
|
783
|
+
redrawOutline();
|
|
784
|
+
}
|
|
777
785
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
label.after(input);
|
|
785
|
-
input.focus();
|
|
786
|
-
input.select();
|
|
787
|
-
|
|
788
|
-
let committed = false;
|
|
789
|
-
const cleanup = () => {
|
|
790
|
-
input.remove();
|
|
791
|
-
label.style.display = "";
|
|
792
|
-
};
|
|
793
|
-
const commit = () => {
|
|
794
|
-
if (committed) {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
committed = true;
|
|
798
|
-
cleanup();
|
|
799
|
-
const val = input.value.trim();
|
|
800
|
-
transactDoc(tab, (t) => mutateUpdateProperty(t, path, "$title", val || undefined));
|
|
801
|
-
rerender();
|
|
802
|
-
};
|
|
803
|
-
const cancel = () => {
|
|
804
|
-
if (committed) {
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
committed = true;
|
|
808
|
-
cleanup();
|
|
809
|
-
rerender();
|
|
810
|
-
};
|
|
811
|
-
input.addEventListener("blur", commit);
|
|
812
|
-
input.addEventListener("keydown", (e: KeyboardEvent) => {
|
|
813
|
-
if (e.key === "Enter") {
|
|
814
|
-
e.preventDefault();
|
|
815
|
-
input.blur();
|
|
816
|
-
}
|
|
817
|
-
if (e.key === "Escape") {
|
|
818
|
-
e.preventDefault();
|
|
819
|
-
cancel();
|
|
820
|
-
}
|
|
821
|
-
});
|
|
786
|
+
// ─── The rows, and the window ────────────────────────────────────────────────
|
|
787
|
+
|
|
788
|
+
/** The collapsed set, which is module state on `view` and survives every repaint. */
|
|
789
|
+
function outlineCollapsed(): Set<string> {
|
|
790
|
+
view._layersCollapsed ||= new Set();
|
|
791
|
+
return view._layersCollapsed;
|
|
822
792
|
}
|
|
823
793
|
|
|
824
794
|
/**
|
|
@@ -826,9 +796,9 @@ export function startLayerTitleEdit(path: JxPath, rerender: () => void) {
|
|
|
826
796
|
*
|
|
827
797
|
* Every line here is a decision about VISIBILITY — a collapsed ancestor, an inline element the
|
|
828
798
|
* canvas edits as text rather than as a block, the root the content mode does not own — and no line
|
|
829
|
-
* here builds a
|
|
830
|
-
* comparisons per node, while the half it defers mounts
|
|
831
|
-
*
|
|
799
|
+
* here builds a projection. That split is the whole saving: the walk is O(nodes) and costs a few
|
|
800
|
+
* comparisons per node, while the half it defers mounts a kit element per verb on the rows that
|
|
801
|
+
* offer them. Only the rows in the window ever pay it.
|
|
832
802
|
*
|
|
833
803
|
* @param {JxMutableNode} doc @param {string} mode @param {Set<string>} collapsed
|
|
834
804
|
*/
|
|
@@ -917,326 +887,465 @@ function numberOutlineSets(rows: OutlineRow[]): OutlineRow[] {
|
|
|
917
887
|
return rows;
|
|
918
888
|
}
|
|
919
889
|
|
|
890
|
+
/** Whether the row has anything under it — children, a map template, or `$switch` cases. */
|
|
891
|
+
function isExpandable(row: OutlineRow): boolean {
|
|
892
|
+
if (!row.item || typeof row.node !== "object") {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
const node = row.node as JxMutableNode;
|
|
896
|
+
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
|
|
897
|
+
const hasMapChildren =
|
|
898
|
+
node.children !== undefined &&
|
|
899
|
+
typeof node.children === "object" &&
|
|
900
|
+
(node.children as unknown as Record<string, unknown>).$prototype === "Array";
|
|
901
|
+
const hasCases =
|
|
902
|
+
Boolean(node.$switch) &&
|
|
903
|
+
typeof node.cases === "object" &&
|
|
904
|
+
node.cases !== null &&
|
|
905
|
+
Object.keys(node.cases).length > 0;
|
|
906
|
+
return hasChildren || hasMapChildren || hasCases || (row.nodeType === "map" && Boolean(node.map));
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** The badge: what it says, which of the six drawings it gets, and what it says on hover. */
|
|
910
|
+
function rowBadge(row: OutlineRow): {
|
|
911
|
+
badge: string;
|
|
912
|
+
badgeKind: string;
|
|
913
|
+
badgeTitle: string | null;
|
|
914
|
+
} {
|
|
915
|
+
const node = row.node as JxMutableNode;
|
|
916
|
+
if (row.nodeType === "map") {
|
|
917
|
+
return { badge: "↻", badgeKind: "map", badgeTitle: "Repeating list — one copy per item" };
|
|
918
|
+
}
|
|
919
|
+
if (row.nodeType === "case" || row.nodeType === "case-ref") {
|
|
920
|
+
const name = String(row.path.at(-1) ?? "");
|
|
921
|
+
return { badge: name, badgeKind: "case", badgeTitle: `Condition case: ${name}` };
|
|
922
|
+
}
|
|
923
|
+
if (node.$switch) {
|
|
924
|
+
return { badge: "⇄", badgeKind: "switch", badgeTitle: "Condition" };
|
|
925
|
+
}
|
|
926
|
+
if (node.tagName === "slot") {
|
|
927
|
+
const slotName = node.attributes?.name;
|
|
928
|
+
return {
|
|
929
|
+
badge: "▣",
|
|
930
|
+
badgeKind: "slot",
|
|
931
|
+
badgeTitle:
|
|
932
|
+
typeof slotName === "string" && slotName.trim()
|
|
933
|
+
? `Slot "${slotName.trim()}"`
|
|
934
|
+
: "Default slot",
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/* The ROW BADGE, which is the one place the tag is shown as itself rather than folded into a
|
|
938
|
+
label — so it is the one that rendered `[object Object]` for a chosen tag after the other reads
|
|
939
|
+
were fixed. `outlineLabel` never returns a tag (it prefers a title, an id, text, a class, a
|
|
940
|
+
landmark), which is why fixing `nodeLabel` did not reach here. */
|
|
941
|
+
return {
|
|
942
|
+
badge: displayTagName(node.tagName) || "div",
|
|
943
|
+
badgeKind: "tag",
|
|
944
|
+
badgeTitle: isTagExpression(node.tagName)
|
|
945
|
+
? `Tag chosen when the element is created: ${tagNameCandidates(node.tagName).join(" or ")}`
|
|
946
|
+
: null,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
920
950
|
/** A text node's row: drawn, and deliberately not a tree item — there is nothing to do to it. */
|
|
921
|
-
function
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
951
|
+
function textRowView(row: OutlineRow): OutlineRowView {
|
|
952
|
+
return {
|
|
953
|
+
actions: "hidden",
|
|
954
|
+
badge: "text",
|
|
955
|
+
badgeKind: "text",
|
|
956
|
+
badgeTitle: null,
|
|
957
|
+
commands: [],
|
|
958
|
+
dndDepth: null,
|
|
959
|
+
dndExpanded: null,
|
|
960
|
+
dndRow: null,
|
|
961
|
+
dndVoid: null,
|
|
962
|
+
draggable: "false",
|
|
963
|
+
editValue: "",
|
|
964
|
+
editing: "false",
|
|
965
|
+
expanded: "",
|
|
966
|
+
indent: `${indentWidth(row.depth) + INDENT_BASE}px`,
|
|
967
|
+
jxPath: JSON.stringify(row.path),
|
|
968
|
+
key: row.key,
|
|
969
|
+
kind: "text",
|
|
970
|
+
label: outlineRowLabel(row),
|
|
971
|
+
labelItalic: "false",
|
|
972
|
+
level: "",
|
|
973
|
+
overflow: "false",
|
|
974
|
+
placeholder: "",
|
|
975
|
+
posInSet: "",
|
|
976
|
+
selected: "false",
|
|
977
|
+
setSize: "",
|
|
978
|
+
};
|
|
938
979
|
}
|
|
939
980
|
|
|
940
981
|
/**
|
|
941
|
-
* Pass two: one row,
|
|
982
|
+
* Pass two: one row, projected.
|
|
942
983
|
*
|
|
943
984
|
* Called for the rows in the window and for no others, which is why it takes the document facts it
|
|
944
985
|
* needs (`selection`, `mode`) as arguments instead of reading `activeTab` for each row.
|
|
945
986
|
*/
|
|
946
|
-
function
|
|
987
|
+
function outlineRowView(
|
|
947
988
|
row: OutlineRow,
|
|
948
989
|
doc: { selection: JxPath[]; mode: string },
|
|
949
990
|
collapsed: Set<string>,
|
|
950
991
|
registry: CommandRegistry,
|
|
951
|
-
|
|
952
|
-
): TemplateResult {
|
|
992
|
+
): OutlineRowView {
|
|
953
993
|
if (!row.item) {
|
|
954
|
-
return
|
|
994
|
+
return textRowView(row);
|
|
955
995
|
}
|
|
956
996
|
const { depth, key, nodeType, path } = row;
|
|
957
|
-
const
|
|
997
|
+
const node = row.node as JxMutableNode;
|
|
958
998
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const isSelected = isPathSelected(doc.selection, path);
|
|
962
|
-
const isPrimary = pathsEqual(path, primarySelection(doc.selection));
|
|
963
|
-
const hasChildren = Array.isArray(jxNode.children) && jxNode.children.length > 0;
|
|
964
|
-
const hasMapChildren =
|
|
965
|
-
jxNode.children &&
|
|
966
|
-
typeof jxNode.children === "object" &&
|
|
967
|
-
(jxNode.children as unknown as Record<string, unknown>).$prototype === "Array";
|
|
968
|
-
const hasCases =
|
|
969
|
-
jxNode.$switch &&
|
|
970
|
-
jxNode.cases &&
|
|
971
|
-
typeof jxNode.cases === "object" &&
|
|
972
|
-
Object.keys(jxNode.cases).length > 0;
|
|
973
|
-
const isExpandable =
|
|
974
|
-
hasChildren || hasMapChildren || hasCases || (nodeType === "map" && jxNode.map);
|
|
999
|
+
const selected = isPathSelected(doc.selection, path);
|
|
1000
|
+
const expandable = isExpandable(row);
|
|
975
1001
|
// Array nodes can't accept dropped children (their content is the single map template), so they
|
|
976
1002
|
// Block the make-child drop instruction like void elements do.
|
|
977
1003
|
const isVoidEl =
|
|
978
|
-
VOID_ELEMENTS.has((displayTagName(
|
|
979
|
-
nodeType === "map";
|
|
980
|
-
|
|
981
|
-
/** @type {string} */
|
|
982
|
-
let badgeClass;
|
|
983
|
-
/** @type {string | number} */
|
|
984
|
-
let badgeText;
|
|
985
|
-
/** @type {string | undefined} */
|
|
986
|
-
let badgeTitle;
|
|
987
|
-
if (nodeType === "map") {
|
|
988
|
-
badgeClass = "layer-tag map-tag";
|
|
989
|
-
badgeText = "↻";
|
|
990
|
-
badgeTitle = "Repeating list — one copy per item";
|
|
991
|
-
} else if (nodeType === "case" || nodeType === "case-ref") {
|
|
992
|
-
badgeClass = "layer-tag case-tag";
|
|
993
|
-
badgeText = path.at(-1);
|
|
994
|
-
badgeTitle = `Condition case: ${path.at(-1)}`;
|
|
995
|
-
} else if (jxNode.$switch) {
|
|
996
|
-
badgeClass = "layer-tag switch-tag";
|
|
997
|
-
badgeText = "⇄";
|
|
998
|
-
badgeTitle = "Condition";
|
|
999
|
-
} else if (jxNode.tagName === "slot") {
|
|
1000
|
-
const slotName = jxNode.attributes?.name;
|
|
1001
|
-
badgeClass = "layer-tag slot-tag";
|
|
1002
|
-
badgeText = "▣";
|
|
1003
|
-
badgeTitle =
|
|
1004
|
-
typeof slotName === "string" && slotName.trim()
|
|
1005
|
-
? `Slot "${slotName.trim()}"`
|
|
1006
|
-
: "Default slot";
|
|
1007
|
-
} else {
|
|
1008
|
-
badgeClass = "layer-tag";
|
|
1009
|
-
/* The ROW BADGE, which is the one place the tag is shown as itself rather than folded into a
|
|
1010
|
-
label — so it is the one that rendered `[object Object]` for a chosen tag after the other
|
|
1011
|
-
reads were fixed. `outlineLabel` never returns a tag (it prefers a title, an id, text, a
|
|
1012
|
-
class, a landmark), which is why fixing `nodeLabel` did not reach here. */
|
|
1013
|
-
badgeText = displayTagName(jxNode.tagName) || "div";
|
|
1014
|
-
badgeTitle = isTagExpression(jxNode.tagName)
|
|
1015
|
-
? `Tag chosen when the element is created: ${tagNameCandidates(jxNode.tagName).join(" or ")}`
|
|
1016
|
-
: undefined;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
/** @type {string} */
|
|
1020
|
-
let labelText;
|
|
1021
|
-
/** @type {boolean} */
|
|
1022
|
-
let labelItalic;
|
|
1023
|
-
if (nodeType === "case-ref") {
|
|
1024
|
-
labelText = jxNode.$ref || "external";
|
|
1025
|
-
labelItalic = true;
|
|
1026
|
-
} else {
|
|
1027
|
-
labelText = outlineLabel(jxNode);
|
|
1028
|
-
labelItalic = false;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1004
|
+
VOID_ELEMENTS.has((displayTagName(node.tagName) || "div").toLowerCase()) || nodeType === "map";
|
|
1031
1005
|
// Array (repeater) nodes are first-class structural nodes — movable/draggable/deletable like
|
|
1032
1006
|
// Elements. Both sit at a numeric child index; templates (path tail "map") and case nodes do
|
|
1033
1007
|
// Not, so they stay selectable/editable but not structurally manipulable.
|
|
1034
|
-
const
|
|
1008
|
+
const structural =
|
|
1035
1009
|
(nodeType === "element" || nodeType === "map") && typeof childIndex(path) === "number";
|
|
1036
1010
|
const isRoot = doc.mode === "content" ? path.length === 0 : path.length < 2;
|
|
1011
|
+
const grabbable = structural && !isRoot;
|
|
1012
|
+
const open = expandable && !collapsed.has(key);
|
|
1013
|
+
const editing = isEditing(key);
|
|
1014
|
+
|
|
1015
|
+
/* The cluster exists for the selected row and the hovered one, and for no other. While the row is
|
|
1016
|
+
being renamed it exists for neither: the input owns the row's whole width, and the verbs would
|
|
1017
|
+
sit on top of its right edge. */
|
|
1018
|
+
const showActions = grabbable && !editing && (selected || key === _hoveredKey);
|
|
1019
|
+
const { commands, overflow } = showActions
|
|
1020
|
+
? rowCommandViews(registry, path, key)
|
|
1021
|
+
: { commands: [], overflow: [] };
|
|
1022
|
+
|
|
1023
|
+
const { badge, badgeKind, badgeTitle } = rowBadge(row);
|
|
1024
|
+
const { $title: _ignored, ...withoutTitle } = node;
|
|
1025
|
+
return {
|
|
1026
|
+
actions: showActions ? "shown" : "hidden",
|
|
1027
|
+
badge,
|
|
1028
|
+
badgeKind,
|
|
1029
|
+
badgeTitle,
|
|
1030
|
+
commands,
|
|
1031
|
+
dndDepth: structural ? String(depth) : null,
|
|
1032
|
+
dndExpanded: structural && open ? "" : null,
|
|
1033
|
+
dndRow: structural ? key : null,
|
|
1034
|
+
dndVoid: structural && isVoidEl ? "" : null,
|
|
1035
|
+
draggable: grabbable ? "true" : "false",
|
|
1036
|
+
editValue: editing ? (node.$title ?? "") : "",
|
|
1037
|
+
editing: editing ? "true" : "false",
|
|
1038
|
+
/* `""` is a LEAF, and it is not "no answer": it is what tells `jx-tree-item` to draw no chevron
|
|
1039
|
+
and write no `aria-expanded`, and what makes `ArrowRight` on a row with nothing under it do
|
|
1040
|
+
nothing instead of stepping onto the row below. The chevron used to be a second field saying
|
|
1041
|
+
the same three things. */
|
|
1042
|
+
expanded: expandable ? (open ? "true" : "false") : "",
|
|
1043
|
+
indent: `${indentWidth(depth) + INDENT_BASE}px`,
|
|
1044
|
+
jxPath: JSON.stringify(path),
|
|
1045
|
+
key,
|
|
1046
|
+
kind: "element",
|
|
1047
|
+
label: outlineRowLabel(row),
|
|
1048
|
+
labelItalic: nodeType === "case-ref" ? "true" : "false",
|
|
1049
|
+
level: String(depth + 1),
|
|
1050
|
+
overflow: overflow.length > 0 ? "true" : "false",
|
|
1051
|
+
placeholder: outlineLabel(withoutTitle) || displayTagName(node.tagName) || "div",
|
|
1052
|
+
posInSet: String(row.posInSet),
|
|
1053
|
+
selected: selected ? "true" : "false",
|
|
1054
|
+
setSize: String(row.setSize),
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1037
1057
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
panToElement(path);
|
|
1058
|
-
}}
|
|
1059
|
-
@dblclick=${
|
|
1060
|
-
isStructural
|
|
1061
|
-
? (e: MouseEvent) => {
|
|
1062
|
-
e.stopPropagation();
|
|
1063
|
-
startLayerTitleEdit(path, ctx.rerender);
|
|
1064
|
-
}
|
|
1065
|
-
: nothing
|
|
1066
|
-
}
|
|
1067
|
-
@contextmenu=${
|
|
1068
|
-
isStructural
|
|
1069
|
-
? (e: MouseEvent) =>
|
|
1070
|
-
showContextMenu(e, path, {
|
|
1071
|
-
rerender: ctx.rerender,
|
|
1072
|
-
})
|
|
1073
|
-
: nothing
|
|
1074
|
-
}
|
|
1075
|
-
>
|
|
1076
|
-
<span class="layer-indent" style="width:${indentWidth(depth)}px"></span>
|
|
1077
|
-
<span class="layer-toggle"
|
|
1078
|
-
>${
|
|
1079
|
-
isExpandable
|
|
1080
|
-
? html`
|
|
1081
|
-
${
|
|
1082
|
-
collapsed.has(key)
|
|
1083
|
-
? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
|
|
1084
|
-
: html`<sp-icon-chevron-down></sp-icon-chevron-down>`
|
|
1085
|
-
}
|
|
1086
|
-
`
|
|
1087
|
-
: nothing
|
|
1088
|
-
}</span
|
|
1089
|
-
>
|
|
1090
|
-
<span class=${badgeClass} title=${ifDefined(badgeTitle ?? undefined)}>${badgeText}</span>
|
|
1091
|
-
<span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
|
|
1092
|
-
>${labelText}</span
|
|
1093
|
-
>
|
|
1094
|
-
${
|
|
1095
|
-
isStructural && !isRoot
|
|
1096
|
-
? html`<span class="layer-drag-handle" title="Drag to reorder">⠿</span>`
|
|
1097
|
-
: nothing
|
|
1098
|
-
}
|
|
1099
|
-
${
|
|
1100
|
-
// The selected row's cluster is declared here so it survives every re-render; every
|
|
1101
|
-
// Other structural row keeps an EMPTY slot, which the pointer fills (mountHoverActions)
|
|
1102
|
-
// And empties again. The two branches are different templates, so lit swaps the DOM
|
|
1103
|
-
// Between them and a hover cluster can never outlive the row becoming selected.
|
|
1104
|
-
isStructural && !isRoot
|
|
1105
|
-
? isSelected
|
|
1106
|
-
? html`<span class="layer-actions">${renderRowCommands(registry, path)}</span>`
|
|
1107
|
-
: html`<span class="layer-actions"></span>`
|
|
1108
|
-
: nothing
|
|
1109
|
-
}
|
|
1110
|
-
</div>
|
|
1111
|
-
`;
|
|
1058
|
+
/**
|
|
1059
|
+
* The row the caret starts on, as the projection reports it.
|
|
1060
|
+
*
|
|
1061
|
+
* A SEED, not a clamp. The tab stop this replaced was decided over the WINDOW and re-decided on
|
|
1062
|
+
* every paint, so a wheel moved it; `jx-tree` owns the caret, holds it across every repaint that
|
|
1063
|
+
* does not change `current`, and clamps only the tab STOP into the drawn rows — so this module
|
|
1064
|
+
* keeps no caret of its own. What it owes the element is a row to start on and a row to come back
|
|
1065
|
+
* to when the one it was on has gone, and the primary selection is that row.
|
|
1066
|
+
*
|
|
1067
|
+
* It must NAME one, and the fallback is what guarantees it rather than tidiness: `move` reports the
|
|
1068
|
+
* caret's own row as `from`, and a tree nobody has touched yet — or one scrolled far enough that
|
|
1069
|
+
* the selected row is not drawn — would report `""` and send every ↓ off the bottom of the window
|
|
1070
|
+
* back to the top of the document.
|
|
1071
|
+
*/
|
|
1072
|
+
function outlineCaretKey(window: OutlineRow[], selection: JxPath[]): string {
|
|
1073
|
+
const primary = primarySelection(selection);
|
|
1074
|
+
const held = primary === null ? -1 : outlineIndexOfPath(primary);
|
|
1075
|
+
const row = _outlineRows[held];
|
|
1076
|
+
return row?.item === true ? row.key : (window.find((line) => line.item)?.key ?? "");
|
|
1112
1077
|
}
|
|
1113
1078
|
|
|
1114
1079
|
/**
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1080
|
+
* What the surface should be showing right now.
|
|
1081
|
+
*
|
|
1082
|
+
* Exported because it is the half of this module that is worth testing on its own: given the open
|
|
1083
|
+
* document, the collapsed set and the window, it is the whole of what the Outline draws.
|
|
1084
|
+
*
|
|
1085
|
+
* @returns {OutlineValues}
|
|
1117
1086
|
*/
|
|
1118
|
-
export function
|
|
1119
|
-
navigateToComponent: (path: string) => void;
|
|
1120
|
-
rerender: () => void;
|
|
1121
|
-
}) {
|
|
1087
|
+
export function outlineValues(): OutlineValues {
|
|
1122
1088
|
const tab = activeTab.value;
|
|
1123
|
-
|
|
1124
|
-
// One — the Navigator's scheduler is the only thing that knows how to draw the Outline.
|
|
1125
|
-
_outlineRerender = ctx.rerender;
|
|
1126
|
-
|
|
1127
|
-
for (const fn of view.dndCleanups) {
|
|
1128
|
-
fn();
|
|
1129
|
-
}
|
|
1130
|
-
view.dndCleanups = [];
|
|
1131
|
-
|
|
1132
|
-
view._layersCollapsed ||= new Set();
|
|
1133
|
-
const collapsed = view._layersCollapsed;
|
|
1089
|
+
const collapsed = outlineCollapsed();
|
|
1134
1090
|
const registry = selectionCommandRegistry();
|
|
1091
|
+
const mode = tab?.doc.mode ?? "";
|
|
1135
1092
|
|
|
1136
|
-
_outlineRows = buildOutlineRows(tab
|
|
1093
|
+
_outlineRows = tab ? buildOutlineRows(tab.doc.document, mode, collapsed) : [];
|
|
1137
1094
|
// The window is computed from the PREVIOUS render's tree element, because that is the only one
|
|
1138
|
-
// That exists while this
|
|
1095
|
+
// That exists while this projection is being built. On the first paint of a session there is none
|
|
1139
1096
|
// And `listWindow` answers "all of them" — which is exactly what the Outline did before it
|
|
1140
|
-
// Windowed, and what the
|
|
1097
|
+
// Windowed, and what the tree's `onNodeCreated` then measures in order to ask for a second pass.
|
|
1141
1098
|
const range = listWindow(_outlineList, {
|
|
1142
1099
|
count: _outlineRows.length,
|
|
1143
1100
|
rowHeight: outlineRowHeight(),
|
|
1144
1101
|
});
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const key = (row as HTMLElement).dataset.path;
|
|
1175
|
-
if (!key) {
|
|
1176
|
-
return;
|
|
1177
|
-
}
|
|
1178
|
-
if (collapsed.has(key)) {
|
|
1179
|
-
collapsed.delete(key);
|
|
1180
|
-
} else {
|
|
1181
|
-
collapsed.add(key);
|
|
1182
|
-
}
|
|
1183
|
-
ctx.rerender();
|
|
1184
|
-
}}
|
|
1185
|
-
>
|
|
1186
|
-
${
|
|
1187
|
-
_outlineRows.length === 0
|
|
1188
|
-
? renderEmptyState({
|
|
1189
|
-
actions: [
|
|
1190
|
-
{
|
|
1191
|
-
label: "Add an element",
|
|
1192
|
-
run: () => {
|
|
1193
|
-
// `"insert"`, not `"blocks"`. The panel was renamed in P3.1 and this call
|
|
1194
|
-
// Kept the old id for three phases, so the one action an empty page offers
|
|
1195
|
-
// Landed the Navigator on "No Navigator panel is registered as blocks".
|
|
1196
|
-
// `setActivityTab` takes a `NavigatorPanelId` now, so this cannot recur.
|
|
1197
|
-
setActivityTab("insert");
|
|
1198
|
-
},
|
|
1199
|
-
},
|
|
1200
|
-
],
|
|
1201
|
-
message: "This page is empty. Everything you add to it is listed here, in order.",
|
|
1202
|
-
})
|
|
1203
|
-
: html`
|
|
1204
|
-
<!-- The rows the window left above and below, as the height they would have
|
|
1205
|
-
occupied: the scrollbar stays the length of the whole document, and
|
|
1206
|
-
aria-hidden keeps two empty spacers out of a tree that owns treeitems. -->
|
|
1207
|
-
<div style="height:${range.padTop}px" aria-hidden="true"></div>
|
|
1208
|
-
${repeat(
|
|
1209
|
-
layerRows,
|
|
1210
|
-
(r) => r.key,
|
|
1211
|
-
(r) => r.tpl,
|
|
1212
|
-
)}
|
|
1213
|
-
<div style="height:${range.padBottom}px" aria-hidden="true"></div>
|
|
1214
|
-
`
|
|
1215
|
-
}
|
|
1216
|
-
</div>
|
|
1217
|
-
</div>
|
|
1218
|
-
`;
|
|
1102
|
+
const selection = tab?.session.selection ?? [];
|
|
1103
|
+
const drawn = _outlineRows.slice(range.start, range.end);
|
|
1104
|
+
return {
|
|
1105
|
+
current: outlineCaretKey(drawn, selection),
|
|
1106
|
+
emptyLabel: "Add an element",
|
|
1107
|
+
emptyMessage: "This page is empty. Everything you add to it is listed here, in order.",
|
|
1108
|
+
padBottom: range.padBottom,
|
|
1109
|
+
padTop: range.padTop,
|
|
1110
|
+
rows: drawn.map((row) => outlineRowView(row, { mode, selection }, collapsed, registry)),
|
|
1111
|
+
view: _outlineRows.length === 0 ? "empty" : "rows",
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// ─── The standing surface ────────────────────────────────────────────────────
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* The surface standing in the Navigator, and the node it was mounted into.
|
|
1119
|
+
*
|
|
1120
|
+
* One slot rather than a per-host map, for the reason `panels/stylebook-layers-panel.ts` gives:
|
|
1121
|
+
* there is one Navigator, so a mount into a DIFFERENT node is the old one being replaced, and
|
|
1122
|
+
* holding both would leave the first one's effects running against a scope nobody writes any more.
|
|
1123
|
+
*/
|
|
1124
|
+
let standing: { host: HTMLElement; handle: OutlineSurfaceHandle } | null = null;
|
|
1125
|
+
/** Re-register drag-and-drop once the rows the projection asked for are actually in the DOM. */
|
|
1126
|
+
let _registerDnD: (() => void) | null = null;
|
|
1127
|
+
|
|
1128
|
+
/** Update the standing surface with a fresh projection. A no-op before the first mount. */
|
|
1129
|
+
function redrawOutline(): void {
|
|
1130
|
+
standing?.handle.update(outlineValues());
|
|
1219
1131
|
}
|
|
1220
1132
|
|
|
1221
1133
|
/**
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
* with no element to call `scrollIntoView` on, and the reveal has to be arithmetic: scroll to where
|
|
1228
|
-
* the model says the row is, and let the scroll watch draw it. Silently doing nothing would be the
|
|
1229
|
-
* windowing bug that looks like a selection bug.
|
|
1134
|
+
* The selection the last reveal followed, or null before there has been one.
|
|
1135
|
+
*
|
|
1136
|
+
* `<tab id>|<path>`, because a tab switch moves the selection as surely as a click does and two
|
|
1137
|
+
* documents can hold the same path. Reset by {@link detachOutline}, so coming back to the tree from
|
|
1138
|
+
* Project Styles shows the reader where they are rather than wherever the scroller was left.
|
|
1230
1139
|
*/
|
|
1231
|
-
|
|
1232
|
-
|
|
1140
|
+
let _revealedSelection: string | null = null;
|
|
1141
|
+
|
|
1142
|
+
/** What the Outline is being asked to keep on screen — the identity a reveal follows. */
|
|
1143
|
+
function selectionRevealKey(): string {
|
|
1144
|
+
const tab = activeTab.value;
|
|
1145
|
+
const path = primarySelection(tab?.session.selection);
|
|
1146
|
+
return `${tab?.id ?? ""}|${path === null ? "" : JSON.stringify(path)}`;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Reveal the selected row when the SELECTION is what moved, and never merely because a repaint did.
|
|
1151
|
+
*
|
|
1152
|
+
* The Outline was unbrowsable past its own selection, and the loop is short enough to state: a
|
|
1153
|
+
* scroll changes the window, the window asks for a repaint, the repaint re-runs the panel's
|
|
1154
|
+
* `afterRender`, and the mount ended by scrolling the selected row back into view. Measured on
|
|
1155
|
+
* `sites/jxsuite.com/pages/compare.json` with the selected row 840px down a 890px viewport: asked
|
|
1156
|
+
* 400 → got 400, asked 800 → got 800, asked 1500 → got 555, asked 2400 → got 555. Everything past
|
|
1157
|
+
* the selection was unreachable, which for a document outline is most of the document.
|
|
1158
|
+
*
|
|
1159
|
+
* So the reveal is bound to the thing it is FOR. The selection moving is the event — the canvas
|
|
1160
|
+
* selecting a node three thousand rows down, a jump from Problems, a keyboard walk, a collaborator
|
|
1161
|
+
* — and a repaint that leaves the selection where it was is not that event. The in-tree gestures
|
|
1162
|
+
* pass this gate as well as the external ones, which is right: they change the selection too, and
|
|
1163
|
+
* `scrollIntoView({ block: "nearest" })` on a row already in view does nothing.
|
|
1164
|
+
*/
|
|
1165
|
+
function revealChangedSelection(): void {
|
|
1166
|
+
const key = selectionRevealKey();
|
|
1167
|
+
if (key === _revealedSelection) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
_revealedSelection = key;
|
|
1171
|
+
revealSelectedRow();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Keep the selected row on screen.
|
|
1176
|
+
*
|
|
1177
|
+
* Two cases, where there used to be one. If the row is drawn, it scrolls itself into view. If it is
|
|
1178
|
+
* NOT — the canvas selected a node three thousand rows down, a jump from Problems, a collaborator's
|
|
1179
|
+
* edit — then the row the author is meant to see is precisely the one with no element to call
|
|
1180
|
+
* `scrollIntoView` on, and the reveal has to be arithmetic: scroll to where the model says the row
|
|
1181
|
+
* is, and let the scroll watch draw it. Silently doing nothing would be the windowing bug that
|
|
1182
|
+
* looks like a selection bug.
|
|
1183
|
+
*
|
|
1184
|
+
* WHEN this runs is {@link revealChangedSelection}'s decision, not this function's.
|
|
1185
|
+
*/
|
|
1186
|
+
function revealSelectedRow(): void {
|
|
1187
|
+
const drawn = _outlineList?.querySelector(`${ROW_SELECTOR}[aria-selected="true"]`);
|
|
1233
1188
|
if (drawn) {
|
|
1234
|
-
drawn.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1189
|
+
drawn.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
|
|
1235
1190
|
return;
|
|
1236
1191
|
}
|
|
1237
1192
|
revealOutlineRow(outlineIndexOfPath(primarySelection(activeTab.value?.session.selection)));
|
|
1238
1193
|
}
|
|
1239
1194
|
|
|
1195
|
+
/** The pointer moved onto a row: it, and the selected row, are the two that carry a cluster. */
|
|
1196
|
+
function hoverRow(key: string): void {
|
|
1197
|
+
if (_hoveredKey === key) {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
_hoveredKey = key;
|
|
1201
|
+
redrawOutline();
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/** The pointer left the tree, so no row is under it. Also the suites' reset. */
|
|
1205
|
+
export function clearOutlineHover(): void {
|
|
1206
|
+
if (_hoveredKey === null) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
_hoveredKey = null;
|
|
1210
|
+
redrawOutline();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Draw the Outline — mounting the document the first time, updating it every time after.
|
|
1215
|
+
*
|
|
1216
|
+
* `panels/left-panel.ts` hands a panel's `afterRender` the CONTENT box itself, which is the node
|
|
1217
|
+
* lit renders this panel's (empty) body into and the one whose comment markers must survive. The
|
|
1218
|
+
* `.panel-content` lookup below is what that call used to require and is kept for a caller handing
|
|
1219
|
+
* the body around it; either way the document must not be appended to the wrong one of the two, or
|
|
1220
|
+
* the tree is drawn under whatever the Navigator paints next.
|
|
1221
|
+
*
|
|
1222
|
+
* @param {{ rerender: () => void; registerDnD: () => void }} ctx
|
|
1223
|
+
* @param {HTMLElement} host - The panel's content box
|
|
1224
|
+
*/
|
|
1225
|
+
export function mountOutlinePanel(
|
|
1226
|
+
ctx: { rerender: () => void; registerDnD: () => void },
|
|
1227
|
+
host: HTMLElement,
|
|
1228
|
+
): void {
|
|
1229
|
+
const target = host.querySelector<HTMLElement>(".panel-content") ?? host;
|
|
1230
|
+
if (standing && (standing.host !== target || !standing.handle.connected())) {
|
|
1231
|
+
standing.handle.dispose();
|
|
1232
|
+
standing = null;
|
|
1233
|
+
}
|
|
1234
|
+
// The scroll watch outlives this call and must never repaint through a closure from an earlier
|
|
1235
|
+
// One — the Navigator's scheduler is the only thing that knows how to draw the Outline.
|
|
1236
|
+
_outlineRerender = ctx.rerender;
|
|
1237
|
+
_registerDnD = ctx.registerDnD;
|
|
1238
|
+
|
|
1239
|
+
const values = outlineValues();
|
|
1240
|
+
if (standing) {
|
|
1241
|
+
standing.handle.update(values);
|
|
1242
|
+
} else {
|
|
1243
|
+
standing = {
|
|
1244
|
+
handle: mountOutlineSurface(target, values, {
|
|
1245
|
+
contextMenu: (_scope, event) => {
|
|
1246
|
+
const path = outlineRowPath(event.target as Element | null);
|
|
1247
|
+
if (path) {
|
|
1248
|
+
showContextMenu(event as MouseEvent, path, { rerender: () => _outlineRerender?.() });
|
|
1249
|
+
redrawOutline();
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
editCancel,
|
|
1253
|
+
editCommit,
|
|
1254
|
+
editInput,
|
|
1255
|
+
editReady: (element) => {
|
|
1256
|
+
/* On a microtask, because `onNodeCreated` fires as the node is BUILT and one tick before
|
|
1257
|
+
it is in the document — which is exactly why it is the seam (guidelines §9.4): it
|
|
1258
|
+
hands over the element earlier than awaiting the mount would, and the host decides
|
|
1259
|
+
when it is worth anything. An unconnected input cannot take focus. */
|
|
1260
|
+
queueMicrotask(() => {
|
|
1261
|
+
const input = element as HTMLInputElement;
|
|
1262
|
+
if (input.isConnected) {
|
|
1263
|
+
input.focus();
|
|
1264
|
+
input.select?.();
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
},
|
|
1268
|
+
expand: expandOutlineRow,
|
|
1269
|
+
emptyAction: () => {
|
|
1270
|
+
/* `"insert"`, not `"blocks"`. The panel was renamed in P3.1 and this call kept the old id
|
|
1271
|
+
for three phases, so the one action an empty page offers landed the Navigator on "No
|
|
1272
|
+
Navigator panel is registered as blocks". `setActivityTab` takes a `NavigatorPanelId`
|
|
1273
|
+
now, so this cannot recur. */
|
|
1274
|
+
setActivityTab("insert");
|
|
1275
|
+
},
|
|
1276
|
+
hover: hoverRow,
|
|
1277
|
+
hoverOut: clearOutlineHover,
|
|
1278
|
+
move: moveOutlineCaret,
|
|
1279
|
+
overflowRow: (key, opener) => {
|
|
1280
|
+
const path = pathFromKey(key);
|
|
1281
|
+
const registry = selectionCommandRegistry();
|
|
1282
|
+
const { overflow } = rowCommandViews(registry, path, key);
|
|
1283
|
+
if (opener instanceof HTMLElement && overflow.length > 0) {
|
|
1284
|
+
showCommandOverflow(opener, registry, overflow, path);
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
rename: (key) => {
|
|
1288
|
+
/* The selection follows the rename, which is what `Enter` always did: the row being
|
|
1289
|
+
renamed is the row the inspector and the canvas are about. */
|
|
1290
|
+
const index = outlineIndexOfKey(key);
|
|
1291
|
+
selectModelRow(index);
|
|
1292
|
+
startLayerTitleEdit(pathFromKey(key), () => _outlineRerender?.());
|
|
1293
|
+
},
|
|
1294
|
+
reveal: (key) => {
|
|
1295
|
+
revealPathInCanvas(pathFromKey(key));
|
|
1296
|
+
},
|
|
1297
|
+
runRow: (id, key, control) => {
|
|
1298
|
+
if (control instanceof HTMLElement) {
|
|
1299
|
+
control.blur();
|
|
1300
|
+
}
|
|
1301
|
+
runCommand(selectionCommandRegistry(), id, pathFromKey(key));
|
|
1302
|
+
redrawOutline();
|
|
1303
|
+
},
|
|
1304
|
+
select: selectOutlineRow,
|
|
1305
|
+
treeReady: (element) => {
|
|
1306
|
+
_outlineList = element;
|
|
1307
|
+
},
|
|
1308
|
+
typeahead: typeaheadOutlineCaret,
|
|
1309
|
+
}),
|
|
1310
|
+
host: target,
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
/* A resolved promise's `.then` is a microtask, so this is one tick after the projection either
|
|
1314
|
+
way — but on the FIRST pass the mount is genuinely asynchronous (the kit has to be defined
|
|
1315
|
+
before a document can render), and a `requestAnimationFrame` inside `registerLayersDnD` would
|
|
1316
|
+
otherwise find an empty container and register nothing at all. */
|
|
1317
|
+
void standing.handle.ready.then(() => {
|
|
1318
|
+
watchOutlineTree();
|
|
1319
|
+
/* `panels/dnd.ts` releases what it previously took, so this is a re-registration and not a
|
|
1320
|
+
second one. Taking the list down HERE, before the call, is what put 369 duplicate-registration
|
|
1321
|
+
warnings in one session's console: the registration is deferred a frame, so a second repaint
|
|
1322
|
+
arriving inside that frame released an empty list and queued a second pass over the same rows. */
|
|
1323
|
+
_registerDnD?.();
|
|
1324
|
+
revealChangedSelection();
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Take the tree down.
|
|
1330
|
+
*
|
|
1331
|
+
* Called by the record when the pane starts showing Project Styles. Without it the document
|
|
1332
|
+
* survives its own irrelevance: both bodies are appended into the same `.panel-content`, so the one
|
|
1333
|
+
* nobody took down sits under the one that replaced it for the rest of the session.
|
|
1334
|
+
*/
|
|
1335
|
+
export function detachOutline(): void {
|
|
1336
|
+
standing?.handle.dispose();
|
|
1337
|
+
standing = null;
|
|
1338
|
+
_outlineList = null;
|
|
1339
|
+
_outlineWatch?.window.destroy();
|
|
1340
|
+
_outlineWatch = null;
|
|
1341
|
+
_hoveredKey = null;
|
|
1342
|
+
_editing = null;
|
|
1343
|
+
/* The tree the next mount draws is a fresh scroller at the top, so whatever the last reveal
|
|
1344
|
+
followed says nothing about it: forgetting is what makes the first paint after a return show
|
|
1345
|
+
the reader where the selection is. */
|
|
1346
|
+
_revealedSelection = null;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1240
1349
|
/**
|
|
1241
1350
|
* Contribute the Outline panel.
|
|
1242
1351
|
*
|
|
@@ -1250,23 +1359,28 @@ export function registerLayersPanel(): void {
|
|
|
1250
1359
|
title: "Outline",
|
|
1251
1360
|
level: "document",
|
|
1252
1361
|
dock: "navigator",
|
|
1253
|
-
icon: "
|
|
1362
|
+
icon: "stack",
|
|
1254
1363
|
requiresDocument: "Open a page to see the elements it is built from.",
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1364
|
+
/*
|
|
1365
|
+
* One panel, two bodies, and neither of them is lit's. The body is a document either way, so
|
|
1366
|
+
* there is nothing for lit to draw here — a document CLEARS nothing and is APPENDED, and lit's
|
|
1367
|
+
* own comment markers inside `.panel-content` are what it finds its (empty) content by.
|
|
1368
|
+
*/
|
|
1369
|
+
render: (): PanelBody => nothing,
|
|
1370
|
+
/*
|
|
1371
|
+
* `afterRender` runs on every repaint, and both mounts are idempotent — the standing surface is
|
|
1372
|
+
* updated where it is still there and re-mounted only where something has taken it out. The
|
|
1373
|
+
* detach on each side is not symmetry for its own sake: the two documents are appended into the
|
|
1374
|
+
* same node, so a body nobody took down would sit under the one that replaced it.
|
|
1375
|
+
*/
|
|
1265
1376
|
afterRender: (ctx, host) => {
|
|
1266
|
-
if (ctx.deps.getCanvasMode()
|
|
1267
|
-
|
|
1377
|
+
if (ctx.deps.getCanvasMode() === "stylebook") {
|
|
1378
|
+
detachOutline();
|
|
1379
|
+
mountStylebookLayersPanel({ selectStylebookTag, stylebookMeta }, host);
|
|
1380
|
+
return;
|
|
1268
1381
|
}
|
|
1269
|
-
|
|
1382
|
+
detachStylebookLayers();
|
|
1383
|
+
mountOutlinePanel({ registerDnD: ctx.deps.registerLayersDnD, rerender: ctx.rerender }, host);
|
|
1270
1384
|
},
|
|
1271
1385
|
});
|
|
1272
1386
|
}
|