@jxsuite/studio 2.0.0 → 2.1.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/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-0hzentwq.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-0hzentwq.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-2c188n41.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-97qcgvdn.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-hhj92gcr.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-ygzav7z5.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-n9zmpxjg.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +23711 -35185
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +34 -9
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +270 -262
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -120
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +75 -2
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +113 -0
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qct0gt8f.js +0 -531
- package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
- package/dist/chunks/files-02zn5y24.js +0 -50
- package/dist/chunks/files-02zn5y24.js.map +0 -9
- package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
- package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
- package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
- package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
- package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
- package/dist/chunks/settings-modal-gj0hrah4.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-568nx4t5.js +0 -469
- package/dist/chunks/studio-568nx4t5.js.map +0 -14
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-asvf0j9e.js +0 -429
- package/dist/chunks/studio-asvf0j9e.js.map +0 -13
- package/dist/chunks/studio-d77qm3pk.js +0 -3427
- package/dist/chunks/studio-d77qm3pk.js.map +0 -26
- package/dist/chunks/studio-j6gqyjf4.js +0 -1854
- package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
- package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-rq0vxb19.js +0 -3744
- package/dist/chunks/studio-rq0vxb19.js.map +0 -22
- package/dist/chunks/studio-wd6ewdvd.js +0 -853
- package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-ygzav7z5.js.map +0 -46
- package/dist/chunks/studio-zmy7241e.js +0 -399
- package/dist/chunks/studio-zmy7241e.js.map +0 -12
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-2c188n41.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-hhj92gcr.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Layers panel —
|
|
4
|
-
*
|
|
3
|
+
* Layers panel — the Outline: the document tree, with collapse, selection, drag-and-drop
|
|
4
|
+
* reordering, and per-row actions that are RENDERINGS of the command registry.
|
|
5
|
+
*
|
|
6
|
+
* Three things this file is deliberate about.
|
|
7
|
+
*
|
|
8
|
+
* **Rows are `registry.forPlacement("outline/row")`.** Every row used to carry five hand-built
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* The hovered row's cluster is mounted imperatively ({@link mountHoverActions}) rather than by a
|
|
16
|
+
* `display: none` CSS rule, because a hidden `sp-action-button` is still an upgraded custom element
|
|
17
|
+
* with a shadow root: CSS reveal would have kept the whole cost the collapse exists to remove. At
|
|
18
|
+
* most two clusters exist at any moment — the selected row's, and the one under the pointer.
|
|
19
|
+
*
|
|
20
|
+
* **A row says something.** On a real page the tree was a wall of rows all reading "div": only
|
|
21
|
+
* text-bearing nodes got a preview and containers got the tag they already wear as a coloured
|
|
22
|
+
* badge. {@link outlineLabel} derives an identity instead — a title, an `$id`, a class, a landmark
|
|
23
|
+
* name, the first text inside — and returns "" rather than repeat the badge.
|
|
24
|
+
*
|
|
25
|
+
* **It is a tree, and it is reachable.** `role="tree"` / `role="treeitem"` with a roving tabindex
|
|
26
|
+
* and the arrow-key model ARIA specifies: ↑↓ walk the visible rows, → expands then descends, ←
|
|
27
|
+
* collapses then ascends, Enter/F2 renames.
|
|
28
|
+
*
|
|
29
|
+
* **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, each carrying an `sp-icon` custom
|
|
31
|
+
* element, on every repaint. It now draws the viewport plus three rows of overscan — and because
|
|
32
|
+
* the DOM is no longer the whole list, nothing may ASK the DOM what the whole list is. Every
|
|
33
|
+
* question about "which rows exist, and in what order" — the shift-range, the arrow walk, Home/End,
|
|
34
|
+
* ←'s climb to the parent, the reveal that follows the selection — is answered from the array
|
|
35
|
+
* {@link buildOutlineRows} produces. A shift-range read off the DOM would silently select the wrong
|
|
36
|
+
* set the moment either end of it scrolled out of the window: a correctness bug wearing a
|
|
37
|
+
* performance change's clothes.
|
|
5
38
|
*/
|
|
6
39
|
|
|
7
|
-
import { html, nothing } from "lit-html";
|
|
40
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
41
|
+
import { displayTagName, isTagExpression, tagNameCandidates } from "@jxsuite/schema/guards";
|
|
8
42
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
9
43
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
44
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
10
45
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
11
46
|
import {
|
|
12
47
|
VOID_ELEMENTS,
|
|
13
48
|
childIndex,
|
|
14
|
-
childList,
|
|
15
49
|
flattenTree,
|
|
16
50
|
getNodeAtPath,
|
|
17
51
|
nodeLabel,
|
|
@@ -20,20 +54,686 @@ import {
|
|
|
20
54
|
pathsEqual,
|
|
21
55
|
} from "../store";
|
|
22
56
|
import { activeTab } from "../workspace/workspace";
|
|
57
|
+
import {
|
|
58
|
+
isSelected as isPathSelected,
|
|
59
|
+
primarySelection,
|
|
60
|
+
rangeSelection,
|
|
61
|
+
selectionAnchor,
|
|
62
|
+
toggleSelected,
|
|
63
|
+
} from "../tabs/selection";
|
|
23
64
|
import type { JxPath } from "../state";
|
|
24
65
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
25
|
-
import {
|
|
26
|
-
mutateMoveNode,
|
|
27
|
-
mutateRemoveNode,
|
|
28
|
-
mutateUpdateProperty,
|
|
29
|
-
transactDoc,
|
|
30
|
-
} from "../tabs/transact";
|
|
66
|
+
import { mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
31
67
|
import { view } from "../view";
|
|
68
|
+
import { setActivityTab } from "../shell";
|
|
69
|
+
import { renderEmptyState } from "./empty-state";
|
|
70
|
+
import { registerPanel } from "./panel-registry";
|
|
71
|
+
import { renderStylebookLayersTemplate } from "./stylebook-layers-panel";
|
|
72
|
+
import { selectStylebookTag, stylebookMeta } from "./stylebook-panel";
|
|
32
73
|
import { isInlineElement } from "../editor/inline-edit";
|
|
33
74
|
import { showContextMenu } from "../editor/context-menu";
|
|
34
75
|
import { panToElement } from "../canvas/canvas-utils";
|
|
76
|
+
import {
|
|
77
|
+
commandIcon,
|
|
78
|
+
commandTooltip,
|
|
79
|
+
runCommand,
|
|
80
|
+
selectionCommandRegistry,
|
|
81
|
+
showCommandOverflow,
|
|
82
|
+
withCommandTarget,
|
|
83
|
+
} from "./block-action-bar";
|
|
84
|
+
import {
|
|
85
|
+
listWindow,
|
|
86
|
+
measuredRowHeight,
|
|
87
|
+
revealListRow,
|
|
88
|
+
watchListWindow,
|
|
89
|
+
} from "../ui/virtual-window";
|
|
90
|
+
import type { CommandRegistry } from "../commands/registry";
|
|
91
|
+
import type { ListWindowWatch } from "../ui/virtual-window";
|
|
35
92
|
import type { TemplateResult } from "lit-html";
|
|
36
93
|
|
|
94
|
+
// ─── What a row says ─────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/** How much of a text preview a 240px column can carry before it is just noise. */
|
|
97
|
+
const LABEL_MAX = 32;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Tags whose human name is worth more than the tag itself.
|
|
101
|
+
*
|
|
102
|
+
* Deliberately short. `section`, `ul` and `table` are omitted: "Section" next to a `section` badge
|
|
103
|
+
* is the repetition this function exists to remove.
|
|
104
|
+
*/
|
|
105
|
+
const LANDMARK_NAMES: Readonly<Record<string, string>> = {
|
|
106
|
+
article: "Article",
|
|
107
|
+
aside: "Sidebar",
|
|
108
|
+
dialog: "Dialog",
|
|
109
|
+
figure: "Figure",
|
|
110
|
+
footer: "Footer",
|
|
111
|
+
form: "Form",
|
|
112
|
+
header: "Header",
|
|
113
|
+
main: "Main",
|
|
114
|
+
nav: "Navigation",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Trim and ellipsize to {@link LABEL_MAX}. */
|
|
118
|
+
function truncate(text: string): string {
|
|
119
|
+
const clean = text.replaceAll(/\s+/gu, " ").trim();
|
|
120
|
+
return clean.length > LABEL_MAX ? `${clean.slice(0, LABEL_MAX)}…` : clean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The first text anywhere under `node`, or "".
|
|
125
|
+
*
|
|
126
|
+
* Bounded at three levels and short-circuited on the first hit: a container's identity is usually
|
|
127
|
+
* its heading or its first line, and walking a whole subtree per row would cost the render what the
|
|
128
|
+
* five-buttons-per-row build already cost it.
|
|
129
|
+
*/
|
|
130
|
+
function firstText(node: JxMutableNode, depth = 0): string {
|
|
131
|
+
if (typeof node.textContent === "string" && node.textContent.trim()) {
|
|
132
|
+
return node.textContent.trim();
|
|
133
|
+
}
|
|
134
|
+
if (depth >= 3 || !Array.isArray(node.children)) {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
for (const child of node.children) {
|
|
138
|
+
if (typeof child === "string" && child.trim()) {
|
|
139
|
+
return child.trim();
|
|
140
|
+
}
|
|
141
|
+
if (child && typeof child === "object") {
|
|
142
|
+
const found = firstText(child, depth + 1);
|
|
143
|
+
if (found) {
|
|
144
|
+
return found;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The node's first class name, when it has a plain (unbound) `class` attribute. */
|
|
152
|
+
function firstClass(node: JxMutableNode): string {
|
|
153
|
+
const value = node.attributes?.class;
|
|
154
|
+
return typeof value === "string" ? (value.trim().split(/\s+/u)[0] ?? "") : "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* What an Outline row says about `node`, beyond the tag its badge already shows.
|
|
159
|
+
*
|
|
160
|
+
* Returns "" when the node has no identity of its own — the badge is then the whole answer, which
|
|
161
|
+
* is honest, and quieter than a column of "div".
|
|
162
|
+
*
|
|
163
|
+
* This is NOT `nodeLabel()`. `nodeLabel` answers "name this node anywhere" and prefixes the tag (`p
|
|
164
|
+
* — Hello`), which is right for the canvas overlay and the status bar and wrong here, where the tag
|
|
165
|
+
* is already a coloured badge two pixels to the left. Those surfaces are unchanged.
|
|
166
|
+
*/
|
|
167
|
+
export function outlineLabel(node: JxMutableNode): string {
|
|
168
|
+
if (node.$title) {
|
|
169
|
+
return truncate(node.$title);
|
|
170
|
+
}
|
|
171
|
+
// Repeaters and slots have a real name of their own; nodeLabel already composes it.
|
|
172
|
+
if (node.$prototype === "Array" || node.tagName === "slot") {
|
|
173
|
+
return nodeLabel(node);
|
|
174
|
+
}
|
|
175
|
+
if (node.$id) {
|
|
176
|
+
return `#${truncate(node.$id)}`;
|
|
177
|
+
}
|
|
178
|
+
if (typeof node.textContent === "string" && node.textContent.trim()) {
|
|
179
|
+
return truncate(node.textContent);
|
|
180
|
+
}
|
|
181
|
+
const cls = firstClass(node);
|
|
182
|
+
if (cls) {
|
|
183
|
+
return `.${truncate(cls)}`;
|
|
184
|
+
}
|
|
185
|
+
const landmark = LANDMARK_NAMES[displayTagName(node.tagName).toLowerCase()];
|
|
186
|
+
if (landmark) {
|
|
187
|
+
return landmark;
|
|
188
|
+
}
|
|
189
|
+
const inner = firstText(node);
|
|
190
|
+
if (inner) {
|
|
191
|
+
return `“${truncate(inner)}”`;
|
|
192
|
+
}
|
|
193
|
+
const count = Array.isArray(node.children) ? node.children.length : 0;
|
|
194
|
+
return count > 0 ? `${count} item${count === 1 ? "" : "s"}` : "";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Row actions, from the registry ──────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Verbs shown inline on a row before the rest fold into `⋮`. A 240px column is the budget.
|
|
201
|
+
*
|
|
202
|
+
* Four is what the Outline's own verbs cost: the moves (up, down, into previous, out of parent),
|
|
203
|
+
* which are the reason a document tree has rows you can grab at all. Duplicate and Delete sort
|
|
204
|
+
* after them (`3_structure`, `9_danger`) and so ride in the `⋮` menu with their names and chords
|
|
205
|
+
* intact — they are also on the block action bar, in the row's context menu, and on ⌘D / Delete.
|
|
206
|
+
* The four moves are on none of those.
|
|
207
|
+
*/
|
|
208
|
+
export const OUTLINE_ROW_MAX_ITEMS = 4;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The row's action cluster.
|
|
212
|
+
*
|
|
213
|
+
* Wrapped in {@link withCommandTarget} so every record is evaluated against THIS ROW'S node rather
|
|
214
|
+
* than the selection — `PLACEMENT_MATRIX["outline/row"]` says row actions act on the row's node,
|
|
215
|
+
* and the hovered row is not the selected one.
|
|
216
|
+
*/
|
|
217
|
+
export function renderRowCommands(registry: CommandRegistry, path: JxPath) {
|
|
218
|
+
return withCommandTarget(path, () => {
|
|
219
|
+
const placed = registry.forPlacement("outline/row");
|
|
220
|
+
const shown = placed.slice(0, OUTLINE_ROW_MAX_ITEMS);
|
|
221
|
+
const overflow = placed.slice(OUTLINE_ROW_MAX_ITEMS);
|
|
222
|
+
return html`${shown.map(
|
|
223
|
+
(command) => html`<sp-action-button
|
|
224
|
+
quiet
|
|
225
|
+
size="xs"
|
|
226
|
+
class=${command.destructive ? "layer-action layer-delete" : "layer-action"}
|
|
227
|
+
data-command=${command.id}
|
|
228
|
+
aria-label=${command.title}
|
|
229
|
+
title=${commandTooltip(registry, command)}
|
|
230
|
+
?disabled=${registry.disabledReason(command.id) !== undefined}
|
|
231
|
+
@click=${(e: MouseEvent) => {
|
|
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
|
+
}`;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── The hovered row's cluster ───────────────────────────────────────────────
|
|
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
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The two things that can only be decided once the rows are in the document.
|
|
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.
|
|
321
|
+
*/
|
|
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
|
+
const INDENT_STEP = 16;
|
|
348
|
+
const INDENT_MAX_DEPTH = 6;
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The indent for `depth`, capped.
|
|
352
|
+
*
|
|
353
|
+
* Uncapped, a depth-12 row pushed 192px of empty space in front of a badge and a label inside a
|
|
354
|
+
* 240px column, which is what made the panel scroll sideways instead of reading as a tree.
|
|
355
|
+
*/
|
|
356
|
+
export function indentWidth(depth: number): number {
|
|
357
|
+
return Math.min(depth, INDENT_MAX_DEPTH) * INDENT_STEP;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** The inverse of {@link pathKey}: numeric segments come back as numbers, as the doc stores them. */
|
|
361
|
+
function pathFromKey(key: string): JxPath {
|
|
362
|
+
return key ? (key.split("/").map((s) => (/^\d+$/u.test(s) ? Number(s) : s)) as JxPath) : [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── The row model, and the window onto it ───────────────────────────────────
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* One row the Outline WOULD draw, whether or not it is currently in the window.
|
|
369
|
+
*
|
|
370
|
+
* Built in a first pass that decides visibility (collapsed ancestors, inline elements, the content
|
|
371
|
+
* root) and costs nothing but the decision; the templates are built in a second pass, for the
|
|
372
|
+
* window only. Splitting the two is the whole saving — the expensive half is the `sp-icon` and
|
|
373
|
+
* `sp-action-button` custom elements a row template mounts, not the walk that finds it.
|
|
374
|
+
*/
|
|
375
|
+
interface OutlineRow {
|
|
376
|
+
/** `pathKey(path)` — the drag-and-drop and roving-tabindex key, and lit's `repeat` key. */
|
|
377
|
+
key: string;
|
|
378
|
+
path: JxPath;
|
|
379
|
+
depth: number;
|
|
380
|
+
/** Index of this row's parent in the model, or -1 at the top level. Feeds ← and `aria-setsize`. */
|
|
381
|
+
parent: number;
|
|
382
|
+
/**
|
|
383
|
+
* Whether this row is a `role="treeitem"`.
|
|
384
|
+
*
|
|
385
|
+
* Text-node rows are drawn but are not tree items, and were never part of the keyboard walk or a
|
|
386
|
+
* shift-range: `treeRows()` selected on the role, so moving those questions to the model has to
|
|
387
|
+
* carry the same distinction rather than quietly start selecting text nodes.
|
|
388
|
+
*/
|
|
389
|
+
item: boolean;
|
|
390
|
+
node: JxMutableNode | string | number | boolean;
|
|
391
|
+
nodeType: string;
|
|
392
|
+
/** 1-based position among the row's `role="treeitem"` siblings — see {@link numberOutlineSets}. */
|
|
393
|
+
posInSet: number;
|
|
394
|
+
/** How many `role="treeitem"` siblings the row has, itself included. */
|
|
395
|
+
setSize: number;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The declared height of one row — `styles/panels.css` `.layer-row { block-size: 24px }`.
|
|
400
|
+
*
|
|
401
|
+
* A window needs a row height BEFORE the first row exists, so this constant is what the first paint
|
|
402
|
+
* windows by; {@link outlineRowHeight} measures a real row afterwards and believes the measurement.
|
|
403
|
+
* The stylesheet declares the height explicitly so the two cannot drift apart in silence.
|
|
404
|
+
*/
|
|
405
|
+
export const OUTLINE_ROW_HEIGHT = 24;
|
|
406
|
+
|
|
407
|
+
/** The rows the Outline last built, in display order. */
|
|
408
|
+
let _outlineRows: OutlineRow[] = [];
|
|
409
|
+
/** The `.layers-tree` element, kept between renders so a window can be computed for the next one. */
|
|
410
|
+
let _outlineList: HTMLElement | null = null;
|
|
411
|
+
/** The scroll watch that repaints the Outline as its scroller moves. */
|
|
412
|
+
let _outlineWatch: ListWindowWatch | null = null;
|
|
413
|
+
/** The Navigator repaint, captured per render so the scroll watch never holds a stale one. */
|
|
414
|
+
let _outlineRerender: (() => void) | null = null;
|
|
415
|
+
|
|
416
|
+
/** The height one row actually has; the declared constant until a row has been laid out. */
|
|
417
|
+
function outlineRowHeight(): number {
|
|
418
|
+
return measuredRowHeight(_outlineList, ".layer-row", OUTLINE_ROW_HEIGHT);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** The model index of the row keyed `key`, or -1. */
|
|
422
|
+
function outlineIndexOfKey(key: string | undefined): number {
|
|
423
|
+
return key === undefined ? -1 : _outlineRows.findIndex((row) => row.key === key);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** The model index of `path`, or -1. Used by the reveal, which is given a path and not a row. */
|
|
427
|
+
function outlineIndexOfPath(path: JxPath | null): number {
|
|
428
|
+
return path === null ? -1 : _outlineRows.findIndex((row) => pathsEqual(row.path, path));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The next `role="treeitem"` row from `index`, walking by `step`; -1 at the ends.
|
|
433
|
+
*
|
|
434
|
+
* The step is over the MODEL, so ↓ at the bottom of the window moves to the row below the window
|
|
435
|
+
* rather than stopping dead — which is what a DOM-indexed walk did the moment the tree windowed.
|
|
436
|
+
*/
|
|
437
|
+
function outlineStep(index: number, step: 1 | -1): number {
|
|
438
|
+
for (let i = index + step; i >= 0 && i < _outlineRows.length; i += step) {
|
|
439
|
+
if (_outlineRows[i]!.item) {
|
|
440
|
+
return i;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return -1;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The visible rows' paths, in display order — the list a shift-range is a range OF.
|
|
448
|
+
*
|
|
449
|
+
* Read from the MODEL, not from the DOM. It used to walk `data-jx-path` off the rendered rows,
|
|
450
|
+
* which was exact while every row was rendered and became a silent lie the moment the tree
|
|
451
|
+
* windowed: `rangeSelection` degenerates to `[target]` when the anchor is absent from the list it
|
|
452
|
+
* is given (`tabs/selection.ts`), so shift-clicking with a scrolled-past anchor would have selected
|
|
453
|
+
* one row and said nothing. "Visible" still means what it always meant — a collapsed ancestor
|
|
454
|
+
* removes its descendants from the model too — it just no longer means "painted".
|
|
455
|
+
*/
|
|
456
|
+
function visibleRowPaths(): JxPath[] {
|
|
457
|
+
const paths: JxPath[] = [];
|
|
458
|
+
for (const row of _outlineRows) {
|
|
459
|
+
if (row.item) {
|
|
460
|
+
paths.push(row.path);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return paths;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Scroll the model row at `index` into the window, and say whether the scroller moved.
|
|
468
|
+
*
|
|
469
|
+
* The repaint is the scroll watch's, not this function's — see `revealListRow`.
|
|
470
|
+
*/
|
|
471
|
+
function revealOutlineRow(index: number): boolean {
|
|
472
|
+
return revealListRow(_outlineList, index, outlineRowHeight());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Repaint the Outline because its window changed.
|
|
477
|
+
*
|
|
478
|
+
* Deferred to a microtask so a scroll that arrives while lit is committing cannot re-enter the
|
|
479
|
+
* render that is producing the rows.
|
|
480
|
+
*
|
|
481
|
+
* **Never during a drag.** `panels/dnd.ts` holds the drop targets it registered on the rendered
|
|
482
|
+
* rows and shifts the rows either side of the pointer by a transform; re-rendering underneath it
|
|
483
|
+
* 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.
|
|
485
|
+
*
|
|
486
|
+
* **Never for a tree that is gone.** The stylebook draws a different panel into the same dock,
|
|
487
|
+
* whose scroller is the one this watch is still listening to.
|
|
488
|
+
*/
|
|
489
|
+
function outlineWindowChanged(): void {
|
|
490
|
+
if (_outlineList?.isConnected !== true || _outlineList.querySelector(".layer-row.dragging")) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
queueMicrotask(() => _outlineRerender?.());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Adopt the rendered tree: remember it, and keep it watching its scroller.
|
|
498
|
+
*
|
|
499
|
+
* Called from {@link afterTreeRender}, one microtask after the rows are committed — the first moment
|
|
500
|
+
* they exist AND the element can be resolved to whatever scrolls it (`#left-panel`, or the bottom
|
|
501
|
+
* dock). The first paint of a session therefore draws every row, because nothing can be measured
|
|
502
|
+
* before it; the watch's opening measurement is what asks for the second, windowed one.
|
|
503
|
+
*
|
|
504
|
+
* Idempotent by construction: `watchListWindow` hands back the same watch for the same element and
|
|
505
|
+
* scroller, so calling this after every render costs a comparison.
|
|
506
|
+
*/
|
|
507
|
+
function adoptOutlineTree(tree: HTMLElement): void {
|
|
508
|
+
_outlineList = tree;
|
|
509
|
+
_outlineWatch = watchListWindow(_outlineWatch, tree, {
|
|
510
|
+
count: () => _outlineRows.length,
|
|
511
|
+
onChange: outlineWindowChanged,
|
|
512
|
+
rowHeight: outlineRowHeight,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* The node an Outline row stands for, read back off the row.
|
|
518
|
+
*
|
|
519
|
+
* 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-path`, which is `pathKey`'s lossy `join("/")`
|
|
521
|
+
* string and exists as the drag-and-drop and roving-tabindex Map key; `["children", "0"]` and
|
|
522
|
+
* `["children", 0]` share a key and are different nodes, and a segment containing a slash has no
|
|
523
|
+
* key at all.
|
|
524
|
+
*
|
|
525
|
+
* Everything that has to point at a node from outside the render — shift-range multi-select,
|
|
526
|
+
* drag-reorder, canvas to Outline sync, a collaborator's cursor, a jump from Problems — needs the
|
|
527
|
+
* unambiguous one.
|
|
528
|
+
*/
|
|
529
|
+
export function outlineRowPath(el: Element | null): JxPath | null {
|
|
530
|
+
const row = el?.closest<HTMLElement>("[data-jx-path]");
|
|
531
|
+
if (!row?.dataset.jxPath) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const parsed: unknown = JSON.parse(row.dataset.jxPath);
|
|
536
|
+
return Array.isArray(parsed) ? (parsed as JxPath) : null;
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
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
|
+
/** The rendered row for a model key, or null when the window does not currently hold it. */
|
|
583
|
+
function rowElementFor(tree: HTMLElement, key: string): HTMLElement | null {
|
|
584
|
+
return tree.querySelector<HTMLElement>(`.layer-row[data-path="${CSS.escape(key)}"]`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The row a repaint should hand the keyboard to, once it exists.
|
|
589
|
+
*
|
|
590
|
+
* A keyboard jump can name a row the window does not hold — End on a 5 000-row page, or ← climbing
|
|
591
|
+
* to a parent far above the viewport. Scrolling to it is immediate; DRAWING it is the Navigator
|
|
592
|
+
* scheduler's business, and `ctx.rerender` is explicitly "not a synchronous re-render". So the
|
|
593
|
+
* request outlives the keystroke by exactly one render: {@link afterTreeRender} spends it, and
|
|
594
|
+
* spends it once — a focus request that survived its own repaint is stale, and stealing the
|
|
595
|
+
* keyboard later is worse than not having moved it.
|
|
596
|
+
*/
|
|
597
|
+
let _pendingFocusKey: string | null = null;
|
|
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 {
|
|
601
|
+
const row = _outlineRows[index];
|
|
602
|
+
if (!row) {
|
|
603
|
+
return;
|
|
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?.();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Select the model row at `index`, so the canvas and the inspector follow the keyboard. */
|
|
617
|
+
function selectModelRow(index: number, gesture: { range?: boolean } = {}): void {
|
|
618
|
+
const row = _outlineRows[index];
|
|
619
|
+
if (row) {
|
|
620
|
+
applyRowSelection(row.path, gesture);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Apply one row activation to the selection, honouring the two accumulate gestures (§6.5).
|
|
626
|
+
*
|
|
627
|
+
* - Plain: replace the selection with this path. **This is the only branch a keyboard walk or an
|
|
628
|
+
* unmodified click can reach, and it is byte-identical to what the Outline always did.**
|
|
629
|
+
* - Ctrl/Cmd: toggle this path in or out, leaving the rest alone.
|
|
630
|
+
* - Shift: the contiguous run of VISIBLE rows from the anchor to here.
|
|
631
|
+
*
|
|
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
|
+
* @param {JxPath} path
|
|
637
|
+
* @param {{ additive?: boolean; range?: boolean }} gesture
|
|
638
|
+
*/
|
|
639
|
+
export function applyRowSelection(
|
|
640
|
+
path: JxPath,
|
|
641
|
+
gesture: { additive?: boolean; range?: boolean } = {},
|
|
642
|
+
): void {
|
|
643
|
+
const tab = activeTab.value;
|
|
644
|
+
if (!tab) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (gesture.range) {
|
|
648
|
+
tab.session.selection = rangeSelection(
|
|
649
|
+
visibleRowPaths(),
|
|
650
|
+
selectionAnchor(tab.session.selection),
|
|
651
|
+
path,
|
|
652
|
+
);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (gesture.additive) {
|
|
656
|
+
tab.session.selection = toggleSelected(tab.session.selection, path);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
tab.session.selection = [path];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* The ARIA tree keyboard model.
|
|
664
|
+
*
|
|
665
|
+
* ↑ / ↓ walk the visible rows and take the selection with them — in an outline, "focus follows
|
|
666
|
+
* selection" is what an author means by pressing Down. → expands a collapsed row and otherwise
|
|
667
|
+
* descends into it; ← collapses an expanded one and otherwise climbs to its parent. Enter and F2
|
|
668
|
+
* rename. Delete is deliberately absent: it is `selection.delete`'s chord, and the registry owns
|
|
669
|
+
* it.
|
|
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.
|
|
675
|
+
*/
|
|
676
|
+
export function onTreeKeydown(
|
|
677
|
+
e: KeyboardEvent,
|
|
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"]');
|
|
683
|
+
if (!row) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const key = row.dataset.path ?? "";
|
|
687
|
+
const index = outlineIndexOfKey(row.dataset.path);
|
|
688
|
+
const expanded = row.getAttribute("aria-expanded");
|
|
689
|
+
|
|
690
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
691
|
+
e.preventDefault();
|
|
692
|
+
const next = outlineStep(index, e.key === "ArrowDown" ? 1 : -1);
|
|
693
|
+
if (next >= 0) {
|
|
694
|
+
// Shift+Arrow extends the range, the same gesture shift-click makes and through the same
|
|
695
|
+
// Function. Without Shift the walk replaces the selection, exactly as it always has.
|
|
696
|
+
selectModelRow(next, { range: e.shiftKey });
|
|
697
|
+
focusModelRow(tree, next);
|
|
698
|
+
}
|
|
699
|
+
} else if (e.key === "ArrowRight") {
|
|
700
|
+
e.preventDefault();
|
|
701
|
+
if (expanded === "false") {
|
|
702
|
+
collapsed.delete(key);
|
|
703
|
+
rerender();
|
|
704
|
+
} else if (expanded === "true") {
|
|
705
|
+
focusModelRow(tree, outlineStep(index, 1));
|
|
706
|
+
}
|
|
707
|
+
} else if (e.key === "ArrowLeft") {
|
|
708
|
+
e.preventDefault();
|
|
709
|
+
if (expanded === "true") {
|
|
710
|
+
collapsed.add(key);
|
|
711
|
+
rerender();
|
|
712
|
+
} else {
|
|
713
|
+
// The model records each row's parent as it builds them, so the climb is exact rather than a
|
|
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
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} else if (e.key === "Home" || e.key === "End") {
|
|
722
|
+
e.preventDefault();
|
|
723
|
+
const target = e.key === "Home" ? outlineStep(-1, 1) : outlineStep(_outlineRows.length, -1);
|
|
724
|
+
if (target >= 0) {
|
|
725
|
+
selectModelRow(target);
|
|
726
|
+
focusModelRow(tree, target);
|
|
727
|
+
}
|
|
728
|
+
} else if (e.key === "Enter" || e.key === "F2") {
|
|
729
|
+
e.preventDefault();
|
|
730
|
+
selectModelRow(index);
|
|
731
|
+
if (row.dataset.path !== undefined) {
|
|
732
|
+
startLayerTitleEdit(pathFromKey(row.dataset.path), rerender);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
37
737
|
/**
|
|
38
738
|
* Start inline title editing on a layer row.
|
|
39
739
|
*
|
|
@@ -44,6 +744,13 @@ export function startLayerTitleEdit(path: JxPath, rerender: () => void) {
|
|
|
44
744
|
const key = pathKey(path);
|
|
45
745
|
const row = document.querySelector(`.layer-row[data-path="${key}"]`);
|
|
46
746
|
if (!row) {
|
|
747
|
+
// Renaming through a command (⌘↵, the palette, the block bar) can name a row the window does
|
|
748
|
+
// Not hold. Scroll to it and repaint; the row exists on the next pass, and the caller's own
|
|
749
|
+
// Chord is the one gesture in the app where re-pressing it is obvious.
|
|
750
|
+
const index = outlineIndexOfPath(path);
|
|
751
|
+
if (index >= 0 && revealOutlineRow(index)) {
|
|
752
|
+
rerender();
|
|
753
|
+
}
|
|
47
754
|
return;
|
|
48
755
|
}
|
|
49
756
|
const label = row.querySelector(".layer-label") as HTMLElement | null;
|
|
@@ -65,7 +772,7 @@ export function startLayerTitleEdit(path: JxPath, rerender: () => void) {
|
|
|
65
772
|
input.className = "layer-title-input";
|
|
66
773
|
input.value = node.$title || "";
|
|
67
774
|
const { $title: _, ...nodeWithoutTitle } = node;
|
|
68
|
-
input.placeholder =
|
|
775
|
+
input.placeholder = outlineLabel(nodeWithoutTitle) || displayTagName(node.tagName) || "div";
|
|
69
776
|
label.after(input);
|
|
70
777
|
input.focus();
|
|
71
778
|
input.select();
|
|
@@ -107,352 +814,345 @@ export function startLayerTitleEdit(path: JxPath, rerender: () => void) {
|
|
|
107
814
|
}
|
|
108
815
|
|
|
109
816
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
817
|
+
* Pass one: the rows the Outline WOULD draw, in display order.
|
|
818
|
+
*
|
|
819
|
+
* Every line here is a decision about VISIBILITY — a collapsed ancestor, an inline element the
|
|
820
|
+
* canvas edits as text rather than as a block, the root the content mode does not own — and no line
|
|
821
|
+
* here builds a template. That split is the whole saving: the walk is O(nodes) and costs a few
|
|
822
|
+
* comparisons per node, while the half it defers mounts an `sp-icon` per row and an action cluster
|
|
823
|
+
* on the selected one. Only the rows in the window ever pay it.
|
|
824
|
+
*
|
|
825
|
+
* @param {JxMutableNode} doc @param {string} mode @param {Set<string>} collapsed
|
|
112
826
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const tab = activeTab.value;
|
|
118
|
-
|
|
119
|
-
for (const fn of view.dndCleanups) {
|
|
120
|
-
fn();
|
|
121
|
-
}
|
|
122
|
-
view.dndCleanups = [];
|
|
123
|
-
|
|
124
|
-
const rows = flattenTree(tab!.doc.document);
|
|
125
|
-
const collapsed = (view._layersCollapsed ||= new Set());
|
|
126
|
-
|
|
127
|
-
const layerRows: { key: string; tpl: TemplateResult }[] = [];
|
|
827
|
+
function buildOutlineRows(doc: JxMutableNode, mode: string, collapsed: Set<string>): OutlineRow[] {
|
|
828
|
+
const rows: OutlineRow[] = [];
|
|
829
|
+
/** Indices of the rows still open above the row being decided; the last of them is its parent. */
|
|
830
|
+
const ancestors: number[] = [];
|
|
128
831
|
// Rows arrive in pre-order, so "is any ancestor collapsed?" is a running depth comparison rather
|
|
129
832
|
// Than a per-row walk back up the path. The old form did `path.slice(0, d)` + `pathKey(sub)` for
|
|
130
833
|
// Every ancestor of every row — O(depth) array copies and string joins per row, on every render.
|
|
131
834
|
let collapsedAtDepth: number | null = null;
|
|
132
|
-
for (const { node, path, depth, nodeType } of
|
|
835
|
+
for (const { node, path, depth, nodeType } of flattenTree(doc)) {
|
|
133
836
|
if (collapsedAtDepth !== null && depth > collapsedAtDepth) {
|
|
134
837
|
continue;
|
|
135
838
|
}
|
|
136
839
|
// Back at or above the collapsed ancestor's depth: it no longer covers this row.
|
|
137
840
|
collapsedAtDepth = null;
|
|
138
|
-
const
|
|
139
|
-
if (collapsed.has(
|
|
841
|
+
const key = pathKey(path);
|
|
842
|
+
if (collapsed.has(key)) {
|
|
140
843
|
collapsedAtDepth = depth;
|
|
141
844
|
}
|
|
142
845
|
|
|
143
|
-
if (
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (nodeType === "text") {
|
|
148
|
-
const textPreview = String(node).length > 40 ? `${String(node).slice(0, 40)}…` : String(node);
|
|
149
|
-
layerRows.push({
|
|
150
|
-
key: rowKey,
|
|
151
|
-
tpl: html`
|
|
152
|
-
<div
|
|
153
|
-
class="layer-row"
|
|
154
|
-
style="padding-left:${depth * 16 + 8}px; opacity: 0.6; font-style: italic;"
|
|
155
|
-
>
|
|
156
|
-
<span
|
|
157
|
-
class="layer-tag"
|
|
158
|
-
style="background: var(--spectrum-gray-500, #64748b); font-size: 0.65rem;"
|
|
159
|
-
>text</span
|
|
160
|
-
>
|
|
161
|
-
<span class="layer-label">${textPreview}</span>
|
|
162
|
-
</div>
|
|
163
|
-
`,
|
|
164
|
-
});
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// After text-node skip, node is guaranteed to be JxMutableNode (not a primitive)
|
|
169
|
-
if (typeof node !== "object" || node === null) {
|
|
846
|
+
if (mode === "content" && path.length === 0) {
|
|
170
847
|
continue;
|
|
171
848
|
}
|
|
172
|
-
const jxNode: JxMutableNode = node as JxMutableNode;
|
|
173
849
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
850
|
+
const isText = nodeType === "text";
|
|
851
|
+
if (!isText) {
|
|
852
|
+
// After the text-node branch, a row's node is a JxMutableNode or it is not a row at all.
|
|
853
|
+
if (typeof node !== "object" || node === null) {
|
|
178
854
|
continue;
|
|
179
855
|
}
|
|
856
|
+
if (path.length >= 2 && nodeType === "element") {
|
|
857
|
+
const parentPath = parentElementPath(path);
|
|
858
|
+
const parentNode = parentPath ? getNodeAtPath(doc, parentPath) : null;
|
|
859
|
+
if (parentNode && isInlineElement(node, parentNode)) {
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
180
863
|
}
|
|
181
864
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const hasChildren = Array.isArray(jxNode.children) && jxNode.children.length > 0;
|
|
185
|
-
const hasMapChildren =
|
|
186
|
-
jxNode.children &&
|
|
187
|
-
typeof jxNode.children === "object" &&
|
|
188
|
-
(jxNode.children as unknown as Record<string, unknown>).$prototype === "Array";
|
|
189
|
-
const hasCases =
|
|
190
|
-
jxNode.$switch &&
|
|
191
|
-
jxNode.cases &&
|
|
192
|
-
typeof jxNode.cases === "object" &&
|
|
193
|
-
Object.keys(jxNode.cases).length > 0;
|
|
194
|
-
const isExpandable =
|
|
195
|
-
hasChildren || hasMapChildren || hasCases || (nodeType === "map" && jxNode.map);
|
|
196
|
-
// Array nodes can't accept dropped children (their content is the single map template), so they
|
|
197
|
-
// Block the make-child drop instruction like void elements do.
|
|
198
|
-
const isVoidEl =
|
|
199
|
-
VOID_ELEMENTS.has((jxNode.tagName || "div").toLowerCase()) || nodeType === "map";
|
|
200
|
-
|
|
201
|
-
/** @type {string} */
|
|
202
|
-
let badgeClass;
|
|
203
|
-
/** @type {string | number} */
|
|
204
|
-
let badgeText;
|
|
205
|
-
/** @type {string | undefined} */
|
|
206
|
-
let badgeTitle;
|
|
207
|
-
if (nodeType === "map") {
|
|
208
|
-
badgeClass = "layer-tag map-tag";
|
|
209
|
-
badgeText = "↻";
|
|
210
|
-
badgeTitle = "Repeater (mapped array)";
|
|
211
|
-
} else if (nodeType === "case" || nodeType === "case-ref") {
|
|
212
|
-
badgeClass = "layer-tag case-tag";
|
|
213
|
-
badgeText = path.at(-1);
|
|
214
|
-
badgeTitle = `$switch case: ${path.at(-1)}`;
|
|
215
|
-
} else if (jxNode.$switch) {
|
|
216
|
-
badgeClass = "layer-tag switch-tag";
|
|
217
|
-
badgeText = "⇄";
|
|
218
|
-
badgeTitle = "$switch";
|
|
219
|
-
} else if (jxNode.tagName === "slot") {
|
|
220
|
-
const slotName = jxNode.attributes?.name;
|
|
221
|
-
badgeClass = "layer-tag slot-tag";
|
|
222
|
-
badgeText = "▣";
|
|
223
|
-
badgeTitle =
|
|
224
|
-
typeof slotName === "string" && slotName.trim()
|
|
225
|
-
? `Slot "${slotName.trim()}"`
|
|
226
|
-
: "Default slot";
|
|
227
|
-
} else {
|
|
228
|
-
badgeClass = "layer-tag";
|
|
229
|
-
badgeText = jxNode.tagName || "div";
|
|
230
|
-
badgeTitle = undefined;
|
|
865
|
+
while (ancestors.length > 0 && rows[ancestors.at(-1)!]!.depth >= depth) {
|
|
866
|
+
ancestors.pop();
|
|
231
867
|
}
|
|
868
|
+
rows.push({
|
|
869
|
+
depth,
|
|
870
|
+
item: !isText,
|
|
871
|
+
key,
|
|
872
|
+
node,
|
|
873
|
+
nodeType,
|
|
874
|
+
parent: ancestors.at(-1) ?? -1,
|
|
875
|
+
path,
|
|
876
|
+
posInSet: 0,
|
|
877
|
+
setSize: 0,
|
|
878
|
+
});
|
|
879
|
+
ancestors.push(rows.length - 1);
|
|
880
|
+
}
|
|
881
|
+
return numberOutlineSets(rows);
|
|
882
|
+
}
|
|
232
883
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
884
|
+
/**
|
|
885
|
+
* Stamp each row's position among its siblings — the two attributes a WINDOWED tree cannot omit.
|
|
886
|
+
*
|
|
887
|
+
* A tree that draws every row lets the assistive technology count them itself. A windowed one hands
|
|
888
|
+
* it eleven rows out of five thousand, and without `aria-posinset`/`aria-setsize` it will read
|
|
889
|
+
* "item 3 of 11" for a document with hundreds of sections: not a missing nicety, a false statement.
|
|
890
|
+
* Only `role="treeitem"` rows are counted — a text-node row is drawn, but it is not in the set.
|
|
891
|
+
*/
|
|
892
|
+
function numberOutlineSets(rows: OutlineRow[]): OutlineRow[] {
|
|
893
|
+
const totals = new Map<number, number>();
|
|
894
|
+
for (const row of rows) {
|
|
895
|
+
if (row.item) {
|
|
896
|
+
totals.set(row.parent, (totals.get(row.parent) ?? 0) + 1);
|
|
243
897
|
}
|
|
898
|
+
}
|
|
899
|
+
const seen = new Map<number, number>();
|
|
900
|
+
for (const row of rows) {
|
|
901
|
+
if (!row.item) {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const position = (seen.get(row.parent) ?? 0) + 1;
|
|
905
|
+
seen.set(row.parent, position);
|
|
906
|
+
row.posInSet = position;
|
|
907
|
+
row.setSize = totals.get(row.parent) ?? position;
|
|
908
|
+
}
|
|
909
|
+
return rows;
|
|
910
|
+
}
|
|
244
911
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
912
|
+
/** A text node's row: drawn, and deliberately not a tree item — there is nothing to do to it. */
|
|
913
|
+
function textRowTemplate(row: OutlineRow): TemplateResult {
|
|
914
|
+
const text = String(row.node);
|
|
915
|
+
const preview = text.length > 40 ? `${text.slice(0, 40)}…` : text;
|
|
916
|
+
return html`
|
|
917
|
+
<div
|
|
918
|
+
class="layer-row"
|
|
919
|
+
data-jx-path=${JSON.stringify(row.path)}
|
|
920
|
+
style="padding-left:${indentWidth(row.depth) + 8}px; opacity: 0.6; font-style: italic;"
|
|
921
|
+
>
|
|
922
|
+
<span
|
|
923
|
+
class="layer-tag"
|
|
924
|
+
style="background: var(--spectrum-gray-500, #64748b); font-size: 0.65rem;"
|
|
925
|
+
>text</span
|
|
926
|
+
>
|
|
927
|
+
<span class="layer-label">${preview}</span>
|
|
928
|
+
</div>
|
|
929
|
+
`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Pass two: one row, drawn.
|
|
934
|
+
*
|
|
935
|
+
* Called for the rows in the window and for no others, which is why it takes the document facts it
|
|
936
|
+
* needs (`selection`, `mode`) as arguments instead of reading `activeTab` for each row.
|
|
937
|
+
*/
|
|
938
|
+
function outlineRowTemplate(
|
|
939
|
+
row: OutlineRow,
|
|
940
|
+
doc: { selection: JxPath[]; mode: string },
|
|
941
|
+
collapsed: Set<string>,
|
|
942
|
+
registry: CommandRegistry,
|
|
943
|
+
ctx: { rerender: () => void },
|
|
944
|
+
): TemplateResult {
|
|
945
|
+
if (!row.item) {
|
|
946
|
+
return textRowTemplate(row);
|
|
947
|
+
}
|
|
948
|
+
const { depth, key, nodeType, path } = row;
|
|
949
|
+
const jxNode = row.node as JxMutableNode;
|
|
950
|
+
|
|
951
|
+
// Every member of the set draws selected; the PRIMARY additionally carries the roving tab stop,
|
|
952
|
+
// So a batch has one keyboard position rather than six.
|
|
953
|
+
const isSelected = isPathSelected(doc.selection, path);
|
|
954
|
+
const isPrimary = pathsEqual(path, primarySelection(doc.selection));
|
|
955
|
+
const hasChildren = Array.isArray(jxNode.children) && jxNode.children.length > 0;
|
|
956
|
+
const hasMapChildren =
|
|
957
|
+
jxNode.children &&
|
|
958
|
+
typeof jxNode.children === "object" &&
|
|
959
|
+
(jxNode.children as unknown as Record<string, unknown>).$prototype === "Array";
|
|
960
|
+
const hasCases =
|
|
961
|
+
jxNode.$switch &&
|
|
962
|
+
jxNode.cases &&
|
|
963
|
+
typeof jxNode.cases === "object" &&
|
|
964
|
+
Object.keys(jxNode.cases).length > 0;
|
|
965
|
+
const isExpandable =
|
|
966
|
+
hasChildren || hasMapChildren || hasCases || (nodeType === "map" && jxNode.map);
|
|
967
|
+
// Array nodes can't accept dropped children (their content is the single map template), so they
|
|
968
|
+
// Block the make-child drop instruction like void elements do.
|
|
969
|
+
const isVoidEl =
|
|
970
|
+
VOID_ELEMENTS.has((displayTagName(jxNode.tagName) || "div").toLowerCase()) ||
|
|
971
|
+
nodeType === "map";
|
|
972
|
+
|
|
973
|
+
/** @type {string} */
|
|
974
|
+
let badgeClass;
|
|
975
|
+
/** @type {string | number} */
|
|
976
|
+
let badgeText;
|
|
977
|
+
/** @type {string | undefined} */
|
|
978
|
+
let badgeTitle;
|
|
979
|
+
if (nodeType === "map") {
|
|
980
|
+
badgeClass = "layer-tag map-tag";
|
|
981
|
+
badgeText = "↻";
|
|
982
|
+
badgeTitle = "Repeating list — one copy per item";
|
|
983
|
+
} else if (nodeType === "case" || nodeType === "case-ref") {
|
|
984
|
+
badgeClass = "layer-tag case-tag";
|
|
985
|
+
badgeText = path.at(-1);
|
|
986
|
+
badgeTitle = `Condition case: ${path.at(-1)}`;
|
|
987
|
+
} else if (jxNode.$switch) {
|
|
988
|
+
badgeClass = "layer-tag switch-tag";
|
|
989
|
+
badgeText = "⇄";
|
|
990
|
+
badgeTitle = "Condition";
|
|
991
|
+
} else if (jxNode.tagName === "slot") {
|
|
992
|
+
const slotName = jxNode.attributes?.name;
|
|
993
|
+
badgeClass = "layer-tag slot-tag";
|
|
994
|
+
badgeText = "▣";
|
|
995
|
+
badgeTitle =
|
|
996
|
+
typeof slotName === "string" && slotName.trim()
|
|
997
|
+
? `Slot "${slotName.trim()}"`
|
|
998
|
+
: "Default slot";
|
|
999
|
+
} else {
|
|
1000
|
+
badgeClass = "layer-tag";
|
|
1001
|
+
/* The ROW BADGE, which is the one place the tag is shown as itself rather than folded into a
|
|
1002
|
+
label — so it is the one that rendered `[object Object]` for a chosen tag after the other
|
|
1003
|
+
reads were fixed. `outlineLabel` never returns a tag (it prefers a title, an id, text, a
|
|
1004
|
+
class, a landmark), which is why fixing `nodeLabel` did not reach here. */
|
|
1005
|
+
badgeText = displayTagName(jxNode.tagName) || "div";
|
|
1006
|
+
badgeTitle = isTagExpression(jxNode.tagName)
|
|
1007
|
+
? `Tag chosen when the element is created: ${tagNameCandidates(jxNode.tagName).join(" or ")}`
|
|
1008
|
+
: undefined;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** @type {string} */
|
|
1012
|
+
let labelText;
|
|
1013
|
+
/** @type {boolean} */
|
|
1014
|
+
let labelItalic;
|
|
1015
|
+
if (nodeType === "case-ref") {
|
|
1016
|
+
labelText = jxNode.$ref || "external";
|
|
1017
|
+
labelItalic = true;
|
|
1018
|
+
} else {
|
|
1019
|
+
labelText = outlineLabel(jxNode);
|
|
1020
|
+
labelItalic = false;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Array (repeater) nodes are first-class structural nodes — movable/draggable/deletable like
|
|
1024
|
+
// Elements. Both sit at a numeric child index; templates (path tail "map") and case nodes do
|
|
1025
|
+
// Not, so they stay selectable/editable but not structurally manipulable.
|
|
1026
|
+
const isStructural =
|
|
1027
|
+
(nodeType === "element" || nodeType === "map") && typeof childIndex(path) === "number";
|
|
1028
|
+
const isRoot = doc.mode === "content" ? path.length === 0 : path.length < 2;
|
|
1029
|
+
|
|
1030
|
+
return html`
|
|
1031
|
+
<div
|
|
1032
|
+
class=${classMap({ "layer-row": true, selected: isSelected })}
|
|
1033
|
+
role="treeitem"
|
|
1034
|
+
aria-level=${depth + 1}
|
|
1035
|
+
aria-posinset=${row.posInSet}
|
|
1036
|
+
aria-setsize=${row.setSize}
|
|
1037
|
+
aria-selected=${isSelected ? "true" : "false"}
|
|
1038
|
+
aria-expanded=${isExpandable ? (collapsed.has(key) ? "false" : "true") : nothing}
|
|
1039
|
+
tabindex=${isPrimary ? "0" : "-1"}
|
|
1040
|
+
data-primary=${isPrimary ? "" : nothing}
|
|
1041
|
+
data-jx-path=${JSON.stringify(path)}
|
|
1042
|
+
data-path=${key}
|
|
1043
|
+
data-dnd-row=${isStructural ? key : nothing}
|
|
1044
|
+
data-dnd-depth=${isStructural ? depth : nothing}
|
|
1045
|
+
data-dnd-void=${isStructural && isVoidEl ? "" : nothing}
|
|
1046
|
+
data-dnd-expanded=${isStructural && isExpandable && !collapsed.has(key) ? "" : nothing}
|
|
1047
|
+
@click=${(e: MouseEvent) => {
|
|
1048
|
+
applyRowSelection(path, { additive: e.ctrlKey || e.metaKey, range: e.shiftKey });
|
|
1049
|
+
panToElement(path);
|
|
1050
|
+
}}
|
|
1051
|
+
@dblclick=${
|
|
1052
|
+
isStructural
|
|
1053
|
+
? (e: MouseEvent) => {
|
|
1054
|
+
e.stopPropagation();
|
|
1055
|
+
startLayerTitleEdit(path, ctx.rerender);
|
|
1056
|
+
}
|
|
1057
|
+
: nothing
|
|
268
1058
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
1059
|
+
@contextmenu=${
|
|
1060
|
+
isStructural
|
|
1061
|
+
? (e: MouseEvent) =>
|
|
1062
|
+
showContextMenu(e, path, {
|
|
1063
|
+
rerender: ctx.rerender,
|
|
1064
|
+
})
|
|
1065
|
+
: nothing
|
|
274
1066
|
}
|
|
275
|
-
|
|
276
|
-
|
|
1067
|
+
>
|
|
1068
|
+
<span class="layer-indent" style="width:${indentWidth(depth)}px"></span>
|
|
1069
|
+
<span class="layer-toggle"
|
|
1070
|
+
>${
|
|
1071
|
+
isExpandable
|
|
1072
|
+
? html`
|
|
1073
|
+
${
|
|
1074
|
+
collapsed.has(key)
|
|
1075
|
+
? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
|
|
1076
|
+
: html`<sp-icon-chevron-down></sp-icon-chevron-down>`
|
|
1077
|
+
}
|
|
1078
|
+
`
|
|
1079
|
+
: nothing
|
|
1080
|
+
}</span
|
|
1081
|
+
>
|
|
1082
|
+
<span class=${badgeClass} title=${ifDefined(badgeTitle ?? undefined)}>${badgeText}</span>
|
|
1083
|
+
<span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
|
|
1084
|
+
>${labelText}</span
|
|
1085
|
+
>
|
|
1086
|
+
${
|
|
1087
|
+
isStructural && !isRoot
|
|
1088
|
+
? html`<span class="layer-drag-handle" title="Drag to reorder">⠿</span>`
|
|
1089
|
+
: nothing
|
|
277
1090
|
}
|
|
278
|
-
|
|
279
|
-
|
|
1091
|
+
${
|
|
1092
|
+
// The selected row's cluster is declared here so it survives every re-render; every
|
|
1093
|
+
// Other structural row keeps an EMPTY slot, which the pointer fills (mountHoverActions)
|
|
1094
|
+
// And empties again. The two branches are different templates, so lit swaps the DOM
|
|
1095
|
+
// Between them and a hover cluster can never outlive the row becoming selected.
|
|
1096
|
+
isStructural && !isRoot
|
|
1097
|
+
? isSelected
|
|
1098
|
+
? html`<span class="layer-actions">${renderRowCommands(registry, path)}</span>`
|
|
1099
|
+
: html`<span class="layer-actions"></span>`
|
|
1100
|
+
: nothing
|
|
280
1101
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
data-dnd-void=${isStructural && isVoidEl ? "" : nothing}
|
|
301
|
-
data-dnd-expanded=${isStructural && isExpandable && !collapsed.has(key) ? "" : nothing}
|
|
302
|
-
@click=${() => {
|
|
303
|
-
activeTab.value!.session.selection = path;
|
|
304
|
-
panToElement(path);
|
|
305
|
-
}}
|
|
306
|
-
@dblclick=${
|
|
307
|
-
isStructural
|
|
308
|
-
? (e: MouseEvent) => {
|
|
309
|
-
e.stopPropagation();
|
|
310
|
-
startLayerTitleEdit(path, ctx.rerender);
|
|
311
|
-
}
|
|
312
|
-
: nothing
|
|
313
|
-
}
|
|
314
|
-
@contextmenu=${
|
|
315
|
-
isStructural
|
|
316
|
-
? (e: MouseEvent) =>
|
|
317
|
-
showContextMenu(e, path, {
|
|
318
|
-
onEditComponent: ctx.navigateToComponent,
|
|
319
|
-
rerender: ctx.rerender,
|
|
320
|
-
})
|
|
321
|
-
: nothing
|
|
322
|
-
}
|
|
323
|
-
>
|
|
324
|
-
<span class="layer-indent" style="width:${depth * 16}px"></span>
|
|
325
|
-
<span class="layer-toggle"
|
|
326
|
-
>${
|
|
327
|
-
isExpandable
|
|
328
|
-
? html`
|
|
329
|
-
${
|
|
330
|
-
collapsed.has(key)
|
|
331
|
-
? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
|
|
332
|
-
: html`<sp-icon-chevron-down></sp-icon-chevron-down>`
|
|
333
|
-
}
|
|
334
|
-
`
|
|
335
|
-
: nothing
|
|
336
|
-
}</span
|
|
337
|
-
>
|
|
338
|
-
<span class=${badgeClass} title=${ifDefined(badgeTitle ?? undefined)}>${badgeText}</span>
|
|
339
|
-
<span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
|
|
340
|
-
>${labelText}</span
|
|
341
|
-
>
|
|
342
|
-
${
|
|
343
|
-
isStructural && !isRoot
|
|
344
|
-
? html`<span class="layer-drag-handle" title="Drag to reorder">⠿</span>`
|
|
345
|
-
: nothing
|
|
346
|
-
}
|
|
347
|
-
${
|
|
348
|
-
isStructural && !isRoot && isSelected
|
|
349
|
-
? html`
|
|
350
|
-
<span class="layer-actions">
|
|
351
|
-
${
|
|
352
|
-
canMoveUp
|
|
353
|
-
? html`<sp-action-button
|
|
354
|
-
quiet
|
|
355
|
-
size="xs"
|
|
356
|
-
title="Move up"
|
|
357
|
-
@click=${(e: MouseEvent) => {
|
|
358
|
-
e.stopPropagation();
|
|
359
|
-
(e.currentTarget as HTMLElement).blur();
|
|
360
|
-
const pp = parentPath as JxPath;
|
|
361
|
-
transactDoc(activeTab.value!, (t) =>
|
|
362
|
-
mutateMoveNode(t, path, pp, idx - 1),
|
|
363
|
-
);
|
|
364
|
-
}}
|
|
365
|
-
>
|
|
366
|
-
<sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
|
|
367
|
-
</sp-action-button>`
|
|
368
|
-
: nothing
|
|
369
|
-
}
|
|
370
|
-
${
|
|
371
|
-
canMoveDown
|
|
372
|
-
? html`<sp-action-button
|
|
373
|
-
quiet
|
|
374
|
-
size="xs"
|
|
375
|
-
title="Move down"
|
|
376
|
-
@click=${(e: MouseEvent) => {
|
|
377
|
-
e.stopPropagation();
|
|
378
|
-
(e.currentTarget as HTMLElement).blur();
|
|
379
|
-
const pp = parentPath as JxPath;
|
|
380
|
-
transactDoc(activeTab.value!, (t) =>
|
|
381
|
-
mutateMoveNode(t, path, pp, idx + 2),
|
|
382
|
-
);
|
|
383
|
-
}}
|
|
384
|
-
>
|
|
385
|
-
<sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
|
|
386
|
-
</sp-action-button>`
|
|
387
|
-
: nothing
|
|
388
|
-
}
|
|
389
|
-
${
|
|
390
|
-
canMoveIn
|
|
391
|
-
? html`<sp-action-button
|
|
392
|
-
quiet
|
|
393
|
-
size="xs"
|
|
394
|
-
title="Move into previous sibling"
|
|
395
|
-
@click=${(e: MouseEvent) => {
|
|
396
|
-
e.stopPropagation();
|
|
397
|
-
(e.currentTarget as HTMLElement).blur();
|
|
398
|
-
const pp = parentPath as JxPath;
|
|
399
|
-
const prevPath = [...pp, "children", idx - 1];
|
|
400
|
-
const prev = getNodeAtPath(activeTab.value!.doc.document, prevPath);
|
|
401
|
-
const len = childList(prev).length;
|
|
402
|
-
transactDoc(activeTab.value!, (t) =>
|
|
403
|
-
mutateMoveNode(t, path, prevPath, len),
|
|
404
|
-
);
|
|
405
|
-
}}
|
|
406
|
-
>
|
|
407
|
-
<sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>
|
|
408
|
-
</sp-action-button>`
|
|
409
|
-
: nothing
|
|
410
|
-
}
|
|
411
|
-
${
|
|
412
|
-
canMoveOut
|
|
413
|
-
? html`<sp-action-button
|
|
414
|
-
quiet
|
|
415
|
-
size="xs"
|
|
416
|
-
title="Move out of parent"
|
|
417
|
-
@click=${(e: MouseEvent) => {
|
|
418
|
-
e.stopPropagation();
|
|
419
|
-
(e.currentTarget as HTMLElement).blur();
|
|
420
|
-
const gp = grandparentPath as JxPath;
|
|
421
|
-
const parentIdx = childIndex(parentPath!) as number;
|
|
422
|
-
transactDoc(activeTab.value!, (t) =>
|
|
423
|
-
mutateMoveNode(t, path, gp, parentIdx + 1),
|
|
424
|
-
);
|
|
425
|
-
}}
|
|
426
|
-
>
|
|
427
|
-
<sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>
|
|
428
|
-
</sp-action-button>`
|
|
429
|
-
: nothing
|
|
430
|
-
}
|
|
431
|
-
<sp-action-button
|
|
432
|
-
quiet
|
|
433
|
-
size="xs"
|
|
434
|
-
class="layer-delete"
|
|
435
|
-
title="Delete"
|
|
436
|
-
@click=${(e: MouseEvent) => {
|
|
437
|
-
e.stopPropagation();
|
|
438
|
-
transactDoc(activeTab.value!, (t) => mutateRemoveNode(t, path));
|
|
439
|
-
}}
|
|
440
|
-
>
|
|
441
|
-
<sp-icon-close slot="icon"></sp-icon-close>
|
|
442
|
-
</sp-action-button>
|
|
443
|
-
</span>
|
|
444
|
-
`
|
|
445
|
-
: nothing
|
|
446
|
-
}
|
|
447
|
-
</div>
|
|
448
|
-
`,
|
|
449
|
-
});
|
|
1102
|
+
</div>
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* @param {{ navigateToComponent: (path: string) => void; rerender: () => void }} ctx
|
|
1108
|
+
* @returns {import("lit-html").TemplateResult}
|
|
1109
|
+
*/
|
|
1110
|
+
export function renderLayersTemplate(ctx: {
|
|
1111
|
+
navigateToComponent: (path: string) => void;
|
|
1112
|
+
rerender: () => void;
|
|
1113
|
+
}) {
|
|
1114
|
+
const tab = activeTab.value;
|
|
1115
|
+
// The scroll watch outlives this call and must never repaint through a closure from an earlier
|
|
1116
|
+
// One — the Navigator's scheduler is the only thing that knows how to draw the Outline.
|
|
1117
|
+
_outlineRerender = ctx.rerender;
|
|
1118
|
+
|
|
1119
|
+
for (const fn of view.dndCleanups) {
|
|
1120
|
+
fn();
|
|
450
1121
|
}
|
|
1122
|
+
view.dndCleanups = [];
|
|
1123
|
+
|
|
1124
|
+
view._layersCollapsed ||= new Set();
|
|
1125
|
+
const collapsed = view._layersCollapsed;
|
|
1126
|
+
const registry = selectionCommandRegistry();
|
|
1127
|
+
|
|
1128
|
+
_outlineRows = buildOutlineRows(tab!.doc.document, tab?.doc.mode ?? "", collapsed);
|
|
1129
|
+
// The window is computed from the PREVIOUS render's tree element, because that is the only one
|
|
1130
|
+
// That exists while this template is being built. On the first paint of a session there is none
|
|
1131
|
+
// And `listWindow` answers "all of them" — which is exactly what the Outline did before it
|
|
1132
|
+
// Windowed, and what the panel's `afterRender` then measures in order to ask for a second pass.
|
|
1133
|
+
const range = listWindow(_outlineList, {
|
|
1134
|
+
count: _outlineRows.length,
|
|
1135
|
+
rowHeight: outlineRowHeight(),
|
|
1136
|
+
});
|
|
1137
|
+
const doc = { mode: tab?.doc.mode ?? "", selection: tab!.session.selection };
|
|
1138
|
+
const layerRows = _outlineRows
|
|
1139
|
+
.slice(range.start, range.end)
|
|
1140
|
+
.map((row) => ({ key: row.key, tpl: outlineRowTemplate(row, doc, collapsed, registry, ctx) }));
|
|
451
1141
|
|
|
452
1142
|
return html`
|
|
453
1143
|
<div class="layers-container" style="position:relative">
|
|
454
1144
|
<div
|
|
455
1145
|
class="layers-tree"
|
|
1146
|
+
role="tree"
|
|
1147
|
+
aria-label="Document outline"
|
|
1148
|
+
${ref((el) => {
|
|
1149
|
+
if (el) {
|
|
1150
|
+
afterTreeRender(el as HTMLElement);
|
|
1151
|
+
}
|
|
1152
|
+
})}
|
|
1153
|
+
@keydown=${(e: KeyboardEvent) => onTreeKeydown(e, collapsed, ctx.rerender)}
|
|
1154
|
+
@mouseover=${onTreeHover}
|
|
1155
|
+
@mouseleave=${clearHoverActions}
|
|
456
1156
|
@click=${(e: MouseEvent) => {
|
|
457
1157
|
const toggle = (e.target as HTMLElement).closest(".layer-toggle");
|
|
458
1158
|
if (!toggle) {
|
|
@@ -475,12 +1175,90 @@ export function renderLayersTemplate(ctx: {
|
|
|
475
1175
|
ctx.rerender();
|
|
476
1176
|
}}
|
|
477
1177
|
>
|
|
478
|
-
${
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
1178
|
+
${
|
|
1179
|
+
_outlineRows.length === 0
|
|
1180
|
+
? renderEmptyState({
|
|
1181
|
+
actions: [
|
|
1182
|
+
{
|
|
1183
|
+
label: "Add an element",
|
|
1184
|
+
run: () => {
|
|
1185
|
+
// `"insert"`, not `"blocks"`. The panel was renamed in P3.1 and this call
|
|
1186
|
+
// Kept the old id for three phases, so the one action an empty page offers
|
|
1187
|
+
// Landed the Navigator on "No Navigator panel is registered as blocks".
|
|
1188
|
+
// `setActivityTab` takes a `NavigatorPanelId` now, so this cannot recur.
|
|
1189
|
+
setActivityTab("insert");
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
],
|
|
1193
|
+
message: "This page is empty. Everything you add to it is listed here, in order.",
|
|
1194
|
+
})
|
|
1195
|
+
: html`
|
|
1196
|
+
<!-- The rows the window left above and below, as the height they would have
|
|
1197
|
+
occupied: the scrollbar stays the length of the whole document, and
|
|
1198
|
+
aria-hidden keeps two empty spacers out of a tree that owns treeitems. -->
|
|
1199
|
+
<div style="height:${range.padTop}px" aria-hidden="true"></div>
|
|
1200
|
+
${repeat(
|
|
1201
|
+
layerRows,
|
|
1202
|
+
(r) => r.key,
|
|
1203
|
+
(r) => r.tpl,
|
|
1204
|
+
)}
|
|
1205
|
+
<div style="height:${range.padBottom}px" aria-hidden="true"></div>
|
|
1206
|
+
`
|
|
1207
|
+
}
|
|
483
1208
|
</div>
|
|
484
1209
|
</div>
|
|
485
1210
|
`;
|
|
486
1211
|
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Keep the selected row on screen after a repaint.
|
|
1215
|
+
*
|
|
1216
|
+
* Two cases now, where there used to be one. If the row is drawn, it scrolls itself into view, as
|
|
1217
|
+
* it always has. If it is NOT — the canvas selected a node three thousand rows down, a jump from
|
|
1218
|
+
* Problems, a collaborator's edit — then the row the author is meant to see is precisely the one
|
|
1219
|
+
* with no element to call `scrollIntoView` on, and the reveal has to be arithmetic: scroll to where
|
|
1220
|
+
* the model says the row is, and let the scroll watch draw it. Silently doing nothing would be the
|
|
1221
|
+
* windowing bug that looks like a selection bug.
|
|
1222
|
+
*/
|
|
1223
|
+
function revealSelectedRow(host: HTMLElement): void {
|
|
1224
|
+
const drawn = host.querySelector(".layer-row.selected");
|
|
1225
|
+
if (drawn) {
|
|
1226
|
+
drawn.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
revealOutlineRow(outlineIndexOfPath(primarySelection(activeTab.value?.session.selection)));
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Contribute the Outline panel.
|
|
1234
|
+
*
|
|
1235
|
+
* `level: "document"` — it writes the open document's tree (reorder, rename, delete, duplicate).
|
|
1236
|
+
* "Outline" rather than "Layers" is §3.2 ③'s name for it; the id stays `layers` because that is
|
|
1237
|
+
* what `view.setActivity` and 26 screenshot steps address it by, and an id is not a label.
|
|
1238
|
+
*/
|
|
1239
|
+
export function registerLayersPanel(): void {
|
|
1240
|
+
registerPanel({
|
|
1241
|
+
id: "layers",
|
|
1242
|
+
title: "Outline",
|
|
1243
|
+
level: "document",
|
|
1244
|
+
dock: "navigator",
|
|
1245
|
+
icon: "sp-icon-layers",
|
|
1246
|
+
requiresDocument: "Open a page to see the elements it is built from.",
|
|
1247
|
+
render: (ctx) =>
|
|
1248
|
+
ctx.deps.getCanvasMode() === "stylebook"
|
|
1249
|
+
? renderStylebookLayersTemplate({
|
|
1250
|
+
selectStylebookTag,
|
|
1251
|
+
stylebookMeta,
|
|
1252
|
+
} as Parameters<typeof renderStylebookLayersTemplate>[0])
|
|
1253
|
+
: renderLayersTemplate({
|
|
1254
|
+
navigateToComponent: ctx.deps.navigateToComponent,
|
|
1255
|
+
rerender: ctx.rerender,
|
|
1256
|
+
}),
|
|
1257
|
+
afterRender: (ctx, host) => {
|
|
1258
|
+
if (ctx.deps.getCanvasMode() !== "stylebook") {
|
|
1259
|
+
ctx.deps.registerLayersDnD();
|
|
1260
|
+
}
|
|
1261
|
+
revealSelectedRow(host);
|
|
1262
|
+
},
|
|
1263
|
+
});
|
|
1264
|
+
}
|