@jxsuite/studio 2.0.1 → 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-799jpvhm.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-799jpvhm.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-8jxwts2p.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-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-gs6kqdt1.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-esrewxap.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-v7jkjqng.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-jm0z592w.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 +21080 -32585
- 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 +7 -0
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +221 -245
- 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 -53
- 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 +30 -1
- 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 +7 -1
- 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-qr3drkkw.js +0 -531
- package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
- package/dist/chunks/files-j8a1t06k.js +0 -50
- package/dist/chunks/files-j8a1t06k.js.map +0 -9
- package/dist/chunks/git-panel-xtq5z81x.js +0 -28
- package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
- package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
- package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
- package/dist/chunks/settings-modal-17ttr96g.js +0 -29
- package/dist/chunks/settings-modal-17ttr96g.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-0xgs7xrs.js +0 -429
- package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
- package/dist/chunks/studio-4d8hzcz5.js +0 -3427
- package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-jm0z592w.js.map +0 -115
- package/dist/chunks/studio-m2t4mdg5.js +0 -1854
- package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
- package/dist/chunks/studio-n4p11dfy.js +0 -853
- package/dist/chunks/studio-n4p11dfy.js.map +0 -12
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-sjh1wt1a.js +0 -3744
- package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
- package/dist/chunks/studio-v7jkjqng.js.map +0 -46
- 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-yfcstdzy.js +0 -399
- package/dist/chunks/studio-yfcstdzy.js.map +0 -12
- package/dist/chunks/studio-zp7jncs8.js +0 -469
- package/dist/chunks/studio-zp7jncs8.js.map +0 -14
- 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-8jxwts2p.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-esrewxap.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,33 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Block action bar —
|
|
4
|
-
*
|
|
3
|
+
* Block action bar — the floating toolbar above the selected element.
|
|
4
|
+
*
|
|
5
|
+
* The bar is a RENDERING of the command registry (plan §3.2 region ⑩, §5.5): its verb cluster is
|
|
6
|
+
* `registry.forPlacement("blockbar")`, sliced at {@link BLOCKBAR_MAX_ITEMS} with the remainder
|
|
7
|
+
* behind a `⋮` overflow menu. Every button's accessible name is its record's `title` and its
|
|
8
|
+
* tooltip carries `keymap.formatBinding(id)`, so no action is named twice.
|
|
9
|
+
*
|
|
10
|
+
* `studio-ui-guidelines.md` §8.6 is normative about the shape: **ONE shape.** The bar does not
|
|
11
|
+
* rearrange itself when the author starts typing, and a control that cannot act is DISABLED, not
|
|
12
|
+
* removed — a toolbar whose buttons move under the cursor is worse than one with a greyed button.
|
|
13
|
+
* That is why the parent selector, the drag handle and every verb render unconditionally and take
|
|
14
|
+
* their disabled state (and its one-sentence reason) from the record's own `enablement`.
|
|
15
|
+
*
|
|
16
|
+
* This module is also the single definition site for the structural selection verbs — move
|
|
17
|
+
* up/down/in/out and the component pair — because their implementations live here.
|
|
18
|
+
* {@link registerSelectionCommands} is what a host bootstrap calls to put them in the app-wide
|
|
19
|
+
* registry; until one exists, {@link selectionCommandRegistry} builds the surface's own.
|
|
5
20
|
*/
|
|
6
21
|
|
|
7
22
|
import { html, render as litRender, nothing } from "lit-html";
|
|
23
|
+
import { displayTagName } from "@jxsuite/schema/guards";
|
|
8
24
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
9
25
|
import { ref } from "lit-html/directives/ref.js";
|
|
10
26
|
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
11
27
|
import { disableNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview";
|
|
12
28
|
|
|
13
29
|
import {
|
|
14
|
-
|
|
30
|
+
VOID_ELEMENTS,
|
|
15
31
|
childIndex,
|
|
16
32
|
childList,
|
|
17
33
|
getNodeAtPath,
|
|
@@ -19,28 +35,53 @@ import {
|
|
|
19
35
|
parentElementPath,
|
|
20
36
|
} from "../store";
|
|
21
37
|
import { activeTab } from "../workspace/workspace";
|
|
22
|
-
import {
|
|
38
|
+
import { activeCanvasSurface, stageContaining } from "../canvas/canvas-surface";
|
|
39
|
+
import { primarySelection, structuralBatch } from "../tabs/selection";
|
|
40
|
+
import {
|
|
41
|
+
mutateDuplicateNodes,
|
|
42
|
+
mutateMoveNode,
|
|
43
|
+
mutateRemoveNodes,
|
|
44
|
+
mutateUpdateProperty,
|
|
45
|
+
transactDoc,
|
|
46
|
+
} from "../tabs/transact";
|
|
23
47
|
import { view } from "../view";
|
|
24
|
-
import { getInlineActions } from "../editor/inline-edit";
|
|
25
|
-
import type { InlineAction } from "../editor/inline-edit";
|
|
48
|
+
import { getInlineActions, isInlineElement } from "../editor/inline-edit";
|
|
26
49
|
import { buildMergeTags, buildRepeaterTagsFromFields } from "../editor/merge-tags";
|
|
27
50
|
import { findEnclosingRepeater, resolveRepeaterItemFields } from "../editor/repeater-scope";
|
|
28
51
|
import { projectState } from "../state";
|
|
29
52
|
import { componentRegistry } from "../files/components";
|
|
30
53
|
import { convertToComponent } from "../editor/convert-to-component";
|
|
31
54
|
import { getEditBarAnchorRect, getEditSnapshot, postApplyFormat } from "../canvas/iframe-host";
|
|
32
|
-
import { getLayerSlot, isModalOpen } from "../ui/layers";
|
|
55
|
+
import { getLayerSlot, isModalOpen, renderPopover } from "../ui/layers";
|
|
33
56
|
import { showSlashMenu } from "../editor/slash-menu";
|
|
34
57
|
import { getConvertTargets } from "../editor/convert-targets";
|
|
35
58
|
import { rectOf } from "../utils/geometry";
|
|
59
|
+
import { createCommandRegistry } from "../commands/registry";
|
|
60
|
+
import { editorKindForMode, makeContext } from "../commands/context";
|
|
61
|
+
import { defaultCommands, noopCommandDeps } from "../commands/defaults";
|
|
36
62
|
|
|
63
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
64
|
+
import type { CommandContext } from "../commands/context";
|
|
65
|
+
import type { CommandDeps } from "../commands/defaults";
|
|
37
66
|
import type { ApplyFormatIntent } from "../canvas/iframe-protocol";
|
|
38
67
|
import type { JxPath } from "../state";
|
|
68
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
39
69
|
import type { TemplateResult } from "lit-html";
|
|
40
70
|
import type { SlashCommand } from "../editor/convert-targets.js";
|
|
41
71
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The plain format commands — everything an action button posts except link/insertData.
|
|
74
|
+
*
|
|
75
|
+
* This read `Extract<ApplyFormatIntent, { command: "bold" }>["command"]`, which resolves to
|
|
76
|
+
* `never`: `Extract` distributes, and the intent member whose `command` is `"bold" | "italic" | …`
|
|
77
|
+
* is not assignable to `{ command: "bold" }`. Every use of the alias was therefore an unchecked
|
|
78
|
+
* cast, and the compiler agreed to all of them. Subtracting the two members that carry extra fields
|
|
79
|
+
* is the shape that actually names the seven.
|
|
80
|
+
*/
|
|
81
|
+
type FormatCommand = Exclude<
|
|
82
|
+
Extract<ApplyFormatIntent, { command: string }>["command"],
|
|
83
|
+
"link" | "insertData"
|
|
84
|
+
>;
|
|
44
85
|
|
|
45
86
|
/**
|
|
46
87
|
* @type {{
|
|
@@ -67,7 +108,13 @@ export function initBlockActionBar(ctx: {
|
|
|
67
108
|
}) {
|
|
68
109
|
_ctx = ctx;
|
|
69
110
|
if (!_formatShortcutBound) {
|
|
70
|
-
|
|
111
|
+
/* The format chords are gone from here. They were a `keydown` switch on ⌘B/⌘I/⌘`/⌘K that ran
|
|
112
|
+
BESIDE the registry's dispatcher on the same document, with its own modal check and its own
|
|
113
|
+
early return — a second keyboard authority for four verbs that now hold their chords as
|
|
114
|
+
records ({@link formatCommands}). Leaving it in place would fire ⌘K twice, and the second
|
|
115
|
+
call re-mounts the link popover over the URL field the first one opened. */
|
|
116
|
+
// ⌥↑ is the keyboard way INTO the bar. The audit found it unreachable without a mouse.
|
|
117
|
+
document.addEventListener("keydown", handleBlockBarEntryKey);
|
|
71
118
|
// `scroll` doesn't bubble but IS deliverable capture-phase at the document — one app-lifetime
|
|
72
119
|
// Listener covers .content-edit-canvas (edit mode), window scrolls, and any future scroll
|
|
73
120
|
// Container, keeping the position:fixed bar tracking its (scrolling) anchor.
|
|
@@ -93,7 +140,7 @@ export function onCanvasScroll(e: Event): void {
|
|
|
93
140
|
if (!_ctx || _linkPopoverOpen) {
|
|
94
141
|
return;
|
|
95
142
|
}
|
|
96
|
-
if (
|
|
143
|
+
if (activeTab.value?.session.selection.length === 0) {
|
|
97
144
|
return;
|
|
98
145
|
}
|
|
99
146
|
const mode = _ctx.getCanvasMode();
|
|
@@ -103,8 +150,7 @@ export function onCanvasScroll(e: Event): void {
|
|
|
103
150
|
// Only canvas-area scrolls (or the window itself) move the anchor; left/right panel scrolling
|
|
104
151
|
// Never does.
|
|
105
152
|
const isCanvasScroll =
|
|
106
|
-
e.target === document ||
|
|
107
|
-
(canvasWrap != null && e.target instanceof Node && canvasWrap.contains(e.target));
|
|
153
|
+
e.target === document || (e.target instanceof Node && stageContaining(e.target) !== null);
|
|
108
154
|
if (!isCanvasScroll || _scrollRafPending) {
|
|
109
155
|
return;
|
|
110
156
|
}
|
|
@@ -151,8 +197,15 @@ function barPosition(anchor: {
|
|
|
151
197
|
top: number;
|
|
152
198
|
height: number;
|
|
153
199
|
}): { left: number; top: number } | null {
|
|
154
|
-
|
|
155
|
-
|
|
200
|
+
/* Clipped against the stage the CARET is on. The bar anchors to one selection in one document,
|
|
201
|
+
and the bar is one element, so "is the anchor still on screen" is a question about ONE pane's
|
|
202
|
+
stage — and the pane is the FOCUSED one, because that is where the caret is. This asked
|
|
203
|
+
"the stage showing the active tab", which was the same answer only while a tab could be on
|
|
204
|
+
screen in one place; with a document displayed in two panes it picked whichever came first in
|
|
205
|
+
the grid, and clipped the bar against a stage the author is not typing in. */
|
|
206
|
+
const stage = activeCanvasSurface().wrap;
|
|
207
|
+
if (stage) {
|
|
208
|
+
const wrap = rectOf(stage);
|
|
156
209
|
if (anchor.top + anchor.height < wrap.top || anchor.top > wrap.bottom) {
|
|
157
210
|
return null;
|
|
158
211
|
}
|
|
@@ -176,46 +229,589 @@ function clampBarToWindow(bar: HTMLElement): void {
|
|
|
176
229
|
}
|
|
177
230
|
|
|
178
231
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
232
|
+
* The inline-format vocabulary, as records.
|
|
233
|
+
*
|
|
234
|
+
* **What this replaces, and what it revives.** Bold, Italic, Code and Link had chords printed in
|
|
235
|
+
* three places — a tooltip, a spec table, a data file's `shortcut: "Cmd+B"` string that is wrong on
|
|
236
|
+
* every Windows machine — and lived in a hand-written `keydown` switch that returned early whenever
|
|
237
|
+
* focus was inside the canvas iframe, which is exactly when a caret exists. Inside the frame
|
|
238
|
+
* `iframe-keys.ts` refused to forward them "because the engine handles them", and the engine did
|
|
239
|
+
* not: the parent's `editor/inline-edit.ts` keydown listener is attached to the BLOCK while the
|
|
240
|
+
* editing host is the canvas container, and `canvas/editable-actions.ts` rejects the browser's own
|
|
241
|
+
* `formatBold` because Jx owns its markup. So ⌘B in the canvas did nothing at all, in an app whose
|
|
242
|
+
* toolbar advertised it.
|
|
243
|
+
*
|
|
244
|
+
* They are the case UX-REDESIGN-PLAN §5.1 uses to justify two fields, so they are written the way
|
|
245
|
+
* it says: `level: "selection"` — a range inside the selected node is still the selection, and a
|
|
246
|
+
* fifth `range` level would demand a fifth region — with `keyScope: "caret"`, which is what makes
|
|
247
|
+
* the chord live only where a caret is. They were, until this change, the only capability in Studio
|
|
248
|
+
* with a chord and no record.
|
|
249
|
+
*
|
|
250
|
+
* **The set is the schema's, the verbs are the registry's.** `data/elements-meta.json` says which
|
|
251
|
+
* of the eight a given tag accepts (`$inlineActions`), so the bar draws four on an `<h1>` and eight
|
|
252
|
+
* on a `<p>`; what each one is CALLED, which chord it holds and what it does are on the record.
|
|
253
|
+
* That is the same division P5.3 made for value sources — caps derive from the schema, behaviour
|
|
254
|
+
* does not.
|
|
255
|
+
*
|
|
256
|
+
* `when` is {@link CommandContext.caret.inCanvas} and not `caret.active`: the caret stack is live
|
|
257
|
+
* in every parent-realm text field too, and ⌘K firing there would re-mount the link popover over
|
|
258
|
+
* the URL the author is typing into it.
|
|
259
|
+
*/
|
|
260
|
+
export function formatCommands(): AnyCommand[] {
|
|
261
|
+
const record = (
|
|
262
|
+
command: FormatCommand,
|
|
263
|
+
title: string,
|
|
264
|
+
icon: string,
|
|
265
|
+
keybinding?: string,
|
|
266
|
+
): AnyCommand => ({
|
|
267
|
+
category: "Edit",
|
|
268
|
+
group: "6_format",
|
|
269
|
+
icon,
|
|
270
|
+
id: `format.${command}`,
|
|
271
|
+
...(keybinding === undefined ? {} : { keybinding }),
|
|
272
|
+
keyScope: "caret",
|
|
273
|
+
level: "selection",
|
|
274
|
+
menus: ["blockbar/format", "palette"],
|
|
275
|
+
requires: "a text caret in the canvas",
|
|
276
|
+
run: () => {
|
|
277
|
+
postApplyFormat({ command });
|
|
278
|
+
},
|
|
279
|
+
title,
|
|
280
|
+
undo: "document",
|
|
281
|
+
when: (ctx: CommandContext) => ctx.caret.inCanvas,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return [
|
|
285
|
+
record("bold", "Bold", "sp-icon-text-bold", "mod+b"),
|
|
286
|
+
record("italic", "Italic", "sp-icon-text-italic", "mod+i"),
|
|
287
|
+
record("underline", "Underline", "sp-icon-text-underline", "mod+u"),
|
|
288
|
+
record("strikethrough", "Strikethrough", "sp-icon-text-strikethrough"),
|
|
289
|
+
record("superscript", "Superscript", "sp-icon-text-superscript"),
|
|
290
|
+
record("subscript", "Subscript", "sp-icon-text-subscript"),
|
|
291
|
+
record("code", "Code", "sp-icon-code", "mod+`"),
|
|
292
|
+
{
|
|
293
|
+
category: "Edit",
|
|
294
|
+
group: "6_format",
|
|
295
|
+
icon: "sp-icon-link",
|
|
296
|
+
id: "format.link",
|
|
297
|
+
/* ⌘K, at `caret` scope, beside `palette.open`'s global ⌘K. Two scopes may hold one chord —
|
|
298
|
+
that is what a shadowing ladder is — and the caret's wins only while a caret is in the
|
|
299
|
+
page. `commands/registry.ts`'s dispatcher takes the narrowest AVAILABLE claimant, so with
|
|
300
|
+
focus in a panel field (where this record's `when` is false) ⌘K still opens the palette. */
|
|
301
|
+
keybinding: "mod+k",
|
|
302
|
+
keyScope: "caret",
|
|
303
|
+
level: "selection",
|
|
304
|
+
menus: ["blockbar/format", "palette"],
|
|
305
|
+
requires: "a text caret in the canvas",
|
|
306
|
+
run: () => {
|
|
307
|
+
openLinkPopoverFromShortcut();
|
|
308
|
+
},
|
|
309
|
+
title: "Link…",
|
|
310
|
+
undo: "document",
|
|
311
|
+
when: (ctx: CommandContext) => ctx.caret.inCanvas,
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── The selection command layer ─────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
/** Enough of a tab for the structural verbs. Mirrors `activeTab.value`'s non-null type. */
|
|
319
|
+
type ActiveTab = NonNullable<typeof activeTab.value>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The node the selection verbs act on, when it is not simply the selection.
|
|
323
|
+
*
|
|
324
|
+
* The block action bar's target IS the selection. An Outline row's target is the ROW'S node —
|
|
325
|
+
* `PLACEMENT_MATRIX["outline/row"]` says so ("row actions act on the row's node"), and a hovered
|
|
326
|
+
* row is not the selected one. Rather than teach every record a second argument, the surface
|
|
327
|
+
* declares the target for the duration of one synchronous render or one click
|
|
328
|
+
* ({@link withCommandTarget}); the records keep reading one function.
|
|
329
|
+
*/
|
|
330
|
+
let _commandTarget: JxPath | null = null;
|
|
331
|
+
|
|
332
|
+
/** The path the selection verbs currently address: an explicit target, else the selection. */
|
|
333
|
+
export function commandTargetPath(): JxPath | null {
|
|
334
|
+
return _commandTarget ?? primarySelection(activeTab.value?.session.selection);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The paths a BATCH verb addresses — Delete and Duplicate, the two that are meaningful on a set.
|
|
339
|
+
*
|
|
340
|
+
* An explicit target wins outright and is always exactly one path:
|
|
341
|
+
* `PLACEMENT_MATRIX["outline/row"]` says a row action acts on the row's node, and a hovered row is
|
|
342
|
+
* not the selection. With no explicit target this is the whole selection, so `length === 1` hands
|
|
343
|
+
* back the single path the verbs have always received.
|
|
344
|
+
*
|
|
345
|
+
* The move verbs deliberately do NOT use this. "Move six nodes up one slot" has no single answer
|
|
346
|
+
* when they are not siblings, and every one of them is `childIndex` arithmetic against a parent
|
|
347
|
+
* that the previous move just renumbered.
|
|
348
|
+
*
|
|
349
|
+
* @returns {JxPath[]}
|
|
350
|
+
*/
|
|
351
|
+
export function commandTargetPaths(): JxPath[] {
|
|
352
|
+
if (_commandTarget) {
|
|
353
|
+
return [_commandTarget];
|
|
354
|
+
}
|
|
355
|
+
return activeTab.value?.session.selection ?? [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Run `fn` with `path` as the command target. Synchronous — the target never outlives the call. */
|
|
359
|
+
export function withCommandTarget<T>(path: JxPath, fn: () => T): T {
|
|
360
|
+
const previous = _commandTarget;
|
|
361
|
+
_commandTarget = path;
|
|
362
|
+
try {
|
|
363
|
+
return fn();
|
|
364
|
+
} finally {
|
|
365
|
+
_commandTarget = previous;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Whether `node` is an instance of a registered project component. */
|
|
370
|
+
function isComponentInstanceNode(node: JxMutableNode | null | undefined): boolean {
|
|
371
|
+
const tag = node?.tagName;
|
|
372
|
+
const literal = displayTagName(tag);
|
|
373
|
+
return Boolean(literal.includes("-") && componentRegistry.some((c) => c.tagName === literal));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The {@link CommandContext} the selection verbs are evaluated against.
|
|
378
|
+
*
|
|
379
|
+
* Only the groups these records read are populated; everything else keeps `emptyContext`'s honest
|
|
380
|
+
* cold-start value. `isRoot` is `parentElementPath(path) === null`, which is the same test in both
|
|
381
|
+
* document modes: a content root sits at `[]`, an element root at `[]` too, and every movable node
|
|
382
|
+
* has a `[…, "children", i]` tail.
|
|
383
|
+
*/
|
|
384
|
+
export function selectionCommandContext(): CommandContext {
|
|
385
|
+
const tab = activeTab.value;
|
|
386
|
+
const path = commandTargetPath();
|
|
387
|
+
const node = tab && path ? getNodeAtPath(tab.doc.document, path) : null;
|
|
388
|
+
return makeContext({
|
|
389
|
+
/* The caret, as this surface knows it. `getEditSnapshot().editing` IS "a caret session is live
|
|
390
|
+
in the canvas" — the same fact `commands/live-context.ts` derives from the bridge for the
|
|
391
|
+
app-wide registry — and the format records are gated on it, so without this line the bar
|
|
392
|
+
would draw its own format cluster permanently hidden. `active` and `inCanvas` are the same
|
|
393
|
+
answer here because this surface has no parent-realm text field to confuse it with. */
|
|
394
|
+
caret: { active: getEditSnapshot().editing, inCanvas: getEditSnapshot().editing },
|
|
395
|
+
// The editor kind is part of the answer, not a default: `makeContext` fills it with "none",
|
|
396
|
+
// Which now reads as "not a canvas" and would hide the bar's own verbs from itself.
|
|
397
|
+
editor: { kind: tab ? editorKindForMode(tab.session.ui.canvasMode) : "none" },
|
|
398
|
+
document: { open: Boolean(tab) },
|
|
399
|
+
selection: {
|
|
400
|
+
count: path ? 1 : 0,
|
|
401
|
+
isComponentInstance: isComponentInstanceNode(node),
|
|
402
|
+
isRoot: path ? parentElementPath(path) === null : false,
|
|
403
|
+
kind: displayTagName(node?.tagName),
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The structural facts a move verb needs. `null` when the target cannot be moved at all. */
|
|
409
|
+
interface StructuralTarget {
|
|
410
|
+
tab: ActiveTab;
|
|
411
|
+
path: JxPath;
|
|
412
|
+
index: number;
|
|
413
|
+
parentPath: JxPath;
|
|
414
|
+
siblings: (JxMutableNode | string)[];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Resolve the current command target to its parent/index/siblings, or `null` if it has none. */
|
|
418
|
+
function structuralTarget(path: JxPath | null = commandTargetPath()): StructuralTarget | null {
|
|
419
|
+
const tab = activeTab.value;
|
|
420
|
+
if (!tab || !path) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
/* THE SAME CONJUNCT `hasSelection` CARRIES, and for the same reason it gives. Four movers gate on
|
|
424
|
+
this function and none of them asked what editor is open, while their peers Delete and
|
|
425
|
+
Duplicate did — so with Project Settings focused the Outline row menu dropped Delete and
|
|
426
|
+
Duplicate and still rendered Move Up / Move Down / Move Into Previous / Move Out of Parent.
|
|
427
|
+
Those four `transactDoc` element splices straight into `project.json` through the transaction
|
|
428
|
+
log and then rewrite `session.selection`; the write lands and is saved. One surface, two rules,
|
|
429
|
+
and the loose ones were the mutating majority. Asked once, here, because all four share it. */
|
|
430
|
+
if (editorKindForMode(tab.session.ui.canvasMode) !== "canvas") {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
const index = childIndex(path);
|
|
434
|
+
const parentPath = parentElementPath(path);
|
|
435
|
+
if (typeof index !== "number" || !parentPath) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const parentNode = getNodeAtPath(tab.doc.document, parentPath);
|
|
439
|
+
if (!parentNode) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return { index, parentPath, path, siblings: childList(parentNode), tab };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Whether `node` can accept a dropped-in sibling — the precondition for "move into previous".
|
|
183
447
|
*
|
|
184
|
-
*
|
|
448
|
+
* A void element cannot; an element whose only children are inline (a paragraph's `<em>`) is a text
|
|
449
|
+
* block, not a container, and nesting a block inside it produces invalid markup.
|
|
185
450
|
*/
|
|
186
|
-
|
|
187
|
-
if (!
|
|
451
|
+
function isContainerNode(node: JxMutableNode | string | null | undefined): boolean {
|
|
452
|
+
if (!node || typeof node !== "object") {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
if (VOID_ELEMENTS.has((displayTagName(node.tagName) || "div").toLowerCase())) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
const { children } = node;
|
|
459
|
+
if (!children) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
if (!Array.isArray(children)) {
|
|
463
|
+
return (children as unknown as Record<string, unknown>).$prototype === "Array";
|
|
464
|
+
}
|
|
465
|
+
return (
|
|
466
|
+
children.length === 0 ||
|
|
467
|
+
children.some((c) => typeof c === "object" && c !== null && !isInlineElement(c, node))
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function canMoveUp(): boolean {
|
|
472
|
+
const target = structuralTarget();
|
|
473
|
+
return target !== null && target.index > 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function canMoveDown(): boolean {
|
|
477
|
+
const target = structuralTarget();
|
|
478
|
+
return target !== null && target.index < target.siblings.length - 1;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function canMoveIn(): boolean {
|
|
482
|
+
const target = structuralTarget();
|
|
483
|
+
return target !== null && target.index > 0 && isContainerNode(target.siblings[target.index - 1]);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function canMoveOut(): boolean {
|
|
487
|
+
const target = structuralTarget();
|
|
488
|
+
return (
|
|
489
|
+
target !== null &&
|
|
490
|
+
parentElementPath(target.parentPath) !== null &&
|
|
491
|
+
typeof childIndex(target.parentPath) === "number"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Move the target up one slot among its siblings, keeping it selected. */
|
|
496
|
+
function moveTargetUp(): void {
|
|
497
|
+
const target = structuralTarget();
|
|
498
|
+
if (!target || target.index === 0) {
|
|
188
499
|
return;
|
|
189
500
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
501
|
+
const { index, parentPath, path, tab } = target;
|
|
502
|
+
transactDoc(tab, (t) => mutateMoveNode(t, path, parentPath, index - 1));
|
|
503
|
+
tab.session.selection = [[...parentPath, "children", index - 1]];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Move the target down one slot among its siblings, keeping it selected. */
|
|
507
|
+
function moveTargetDown(): void {
|
|
508
|
+
const target = structuralTarget();
|
|
509
|
+
if (!target || target.index >= target.siblings.length - 1) {
|
|
193
510
|
return;
|
|
194
511
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
512
|
+
const { index, parentPath, path, tab } = target;
|
|
513
|
+
// `index + 2`: mutateMoveNode removes before it inserts, so a same-parent move down needs the
|
|
514
|
+
// Post-removal index of the slot AFTER the next sibling.
|
|
515
|
+
transactDoc(tab, (t) => mutateMoveNode(t, path, parentPath, index + 2));
|
|
516
|
+
tab.session.selection = [[...parentPath, "children", index + 1]];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Append the target to its previous sibling, making it that element's last child. */
|
|
520
|
+
function moveTargetIn(): void {
|
|
521
|
+
const target = structuralTarget();
|
|
522
|
+
if (!canMoveIn() || !target) {
|
|
198
523
|
return;
|
|
199
524
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
openLinkPopoverFromShortcut();
|
|
525
|
+
const { index, parentPath, path, tab } = target;
|
|
526
|
+
const prevPath = [...parentPath, "children", index - 1];
|
|
527
|
+
const { length } = childList(getNodeAtPath(tab.doc.document, prevPath));
|
|
528
|
+
transactDoc(tab, (t) => mutateMoveNode(t, path, prevPath, length));
|
|
529
|
+
tab.session.selection = [[...prevPath, "children", length]];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Lift the target out of its parent, landing it directly after that parent. */
|
|
533
|
+
function moveTargetOut(): void {
|
|
534
|
+
const target = structuralTarget();
|
|
535
|
+
if (!target) {
|
|
536
|
+
return;
|
|
213
537
|
}
|
|
538
|
+
const grandparent = parentElementPath(target.parentPath);
|
|
539
|
+
const parentIndex = childIndex(target.parentPath);
|
|
540
|
+
if (!grandparent || typeof parentIndex !== "number") {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
transactDoc(target.tab, (t) => mutateMoveNode(t, target.path, grandparent, parentIndex + 1));
|
|
544
|
+
target.tab.session.selection = [[...grandparent, "children", parentIndex + 1]];
|
|
214
545
|
}
|
|
215
546
|
|
|
216
|
-
/**
|
|
217
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Delete every targeted node, leaving the primary's parent selected (never a dangling path).
|
|
549
|
+
*
|
|
550
|
+
* One transaction, therefore one undo step — a six-node delete that produced six history entries
|
|
551
|
+
* would make ⌘Z a rebuild rather than a reversal.
|
|
552
|
+
*
|
|
553
|
+
* **A delete that removes nothing moves nothing.** The selection jump is the visible half of this
|
|
554
|
+
* verb, and running it over an unchanged document is the app telling the author something happened
|
|
555
|
+
* when nothing did. There are two ways to remove nothing, and each is checked on its own side of
|
|
556
|
+
* the transaction:
|
|
557
|
+
*
|
|
558
|
+
* - `mutateRemoveNodes` removes exactly {@link structuralBatch}, which drops every path that names no
|
|
559
|
+
* splice coordinate — the document element, a repeater's map template, a `$switch` case. A target
|
|
560
|
+
* made only of those filters out entirely, and calling `transactDoc` anyway would ALSO mark the
|
|
561
|
+
* tab dirty and push an undo step that reverses nothing. So an empty batch returns before the
|
|
562
|
+
* transaction rather than after it.
|
|
563
|
+
* - `transactDoc` itself declines while a peer holds source-canonical, and says so by leaving the
|
|
564
|
+
* document root reference — which it otherwise always replaces — exactly as it found it.
|
|
565
|
+
*/
|
|
566
|
+
function deleteTarget(): void {
|
|
567
|
+
const tab = activeTab.value;
|
|
568
|
+
const paths = commandTargetPaths();
|
|
569
|
+
const path = commandTargetPath();
|
|
570
|
+
// An empty target and a target of nothing-spliceable are the same answer: `structuralBatch([])`
|
|
571
|
+
// Is `[]`, so one test covers both.
|
|
572
|
+
if (!tab || !path || structuralBatch(paths).length === 0) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const documentBefore = tab.doc.document;
|
|
576
|
+
transactDoc(tab, (t) => mutateRemoveNodes(t, paths));
|
|
577
|
+
if (tab.doc.document === documentBefore) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const parentPath = parentElementPath(path);
|
|
581
|
+
tab.session.selection = parentPath ? [parentPath] : [];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Duplicate every targeted node in one transaction, selecting the copies. */
|
|
585
|
+
function duplicateTarget(): void {
|
|
586
|
+
const tab = activeTab.value;
|
|
587
|
+
const paths = commandTargetPaths();
|
|
588
|
+
if (!tab || paths.length === 0) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
transactDoc(tab, (t) => mutateDuplicateNodes(t, paths));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Select the target's parent element. */
|
|
595
|
+
function selectParentOfTarget(): void {
|
|
596
|
+
const tab = activeTab.value;
|
|
597
|
+
const path = commandTargetPath();
|
|
598
|
+
const parentPath = path ? parentElementPath(path) : null;
|
|
599
|
+
if (tab && parentPath) {
|
|
600
|
+
tab.session.selection = [parentPath];
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** What the structural selection verbs need that this module does not own. */
|
|
605
|
+
export interface SelectionCommandDeps {
|
|
606
|
+
/** Open the component definition behind the selected instance. */
|
|
607
|
+
navigateToComponent: (path: string) => void;
|
|
608
|
+
/** Lift the selection into a new component file. */
|
|
609
|
+
convertToComponent: () => void;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** A target exists at all — the `when` every selection verb shares. */
|
|
613
|
+
const hasTarget = (ctx: CommandContext) => ctx.selection.count > 0;
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* The structural selection verbs, defined once.
|
|
617
|
+
*
|
|
618
|
+
* `group` keys are ordering keys, not categories: the four moves sort ahead of `3_structure`
|
|
619
|
+
* (Duplicate, from `commands/defaults.ts`) and `9_danger` (Delete, likewise), which is the order
|
|
620
|
+
* the bar and the Outline rows both render in.
|
|
621
|
+
*
|
|
622
|
+
* No `keybinding` is claimed here on purpose. The chord table is `editor/shortcuts.ts`'s port (plan
|
|
623
|
+
* P2 workstream 4); claiming chords from a panel would decide that port's outcome by accident. The
|
|
624
|
+
* buttons still print the chords of the records that DO carry them.
|
|
625
|
+
*/
|
|
626
|
+
export function registerSelectionCommands(
|
|
627
|
+
registry: CommandRegistry,
|
|
628
|
+
deps: SelectionCommandDeps,
|
|
629
|
+
): void {
|
|
630
|
+
registry.registerAll([
|
|
631
|
+
{
|
|
632
|
+
category: "Selection",
|
|
633
|
+
enablement: canMoveUp,
|
|
634
|
+
group: "1_move_1",
|
|
635
|
+
icon: "sp-icon-arrow-up",
|
|
636
|
+
id: "selection.moveUp",
|
|
637
|
+
keyScope: "canvas",
|
|
638
|
+
level: "selection",
|
|
639
|
+
menus: ["blockbar", "outline/row"],
|
|
640
|
+
requires: "an element with a sibling above it",
|
|
641
|
+
run: () => moveTargetUp(),
|
|
642
|
+
title: "Move Up",
|
|
643
|
+
undo: "document",
|
|
644
|
+
when: hasTarget,
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
category: "Selection",
|
|
648
|
+
enablement: canMoveDown,
|
|
649
|
+
group: "1_move_2",
|
|
650
|
+
icon: "sp-icon-arrow-down",
|
|
651
|
+
id: "selection.moveDown",
|
|
652
|
+
keyScope: "canvas",
|
|
653
|
+
level: "selection",
|
|
654
|
+
menus: ["blockbar", "outline/row"],
|
|
655
|
+
requires: "an element with a sibling below it",
|
|
656
|
+
run: () => moveTargetDown(),
|
|
657
|
+
title: "Move Down",
|
|
658
|
+
undo: "document",
|
|
659
|
+
when: hasTarget,
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
category: "Selection",
|
|
663
|
+
enablement: canMoveIn,
|
|
664
|
+
group: "1_move_3",
|
|
665
|
+
icon: "sp-icon-arrow-right",
|
|
666
|
+
id: "selection.moveIn",
|
|
667
|
+
keyScope: "canvas",
|
|
668
|
+
level: "selection",
|
|
669
|
+
menus: ["outline/row"],
|
|
670
|
+
requires: "a container directly above the element",
|
|
671
|
+
run: () => moveTargetIn(),
|
|
672
|
+
title: "Move Into Previous",
|
|
673
|
+
undo: "document",
|
|
674
|
+
when: hasTarget,
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
category: "Selection",
|
|
678
|
+
enablement: canMoveOut,
|
|
679
|
+
group: "1_move_4",
|
|
680
|
+
icon: "sp-icon-arrow-left",
|
|
681
|
+
id: "selection.moveOut",
|
|
682
|
+
keyScope: "canvas",
|
|
683
|
+
level: "selection",
|
|
684
|
+
menus: ["outline/row"],
|
|
685
|
+
requires: "an element nested inside another",
|
|
686
|
+
run: () => moveTargetOut(),
|
|
687
|
+
title: "Move Out of Parent",
|
|
688
|
+
undo: "document",
|
|
689
|
+
when: hasTarget,
|
|
690
|
+
},
|
|
691
|
+
// Convert / Edit are one slot with two states, not two optional buttons: `when` splits them on
|
|
692
|
+
// "is this a component instance", so exactly one of the pair renders for any selection and the
|
|
693
|
+
// Bar keeps its shape.
|
|
694
|
+
{
|
|
695
|
+
category: "Selection",
|
|
696
|
+
// …and the same conjunct here: Convert to Component starts a project-level flow, so it must
|
|
697
|
+
// Not be reachable from a config node drawn as a layer tree.
|
|
698
|
+
enablement: (ctx) =>
|
|
699
|
+
Boolean(ctx.selection.kind) && !ctx.selection.isRoot && ctx.editor.kind === "canvas",
|
|
700
|
+
group: "4_component",
|
|
701
|
+
icon: "sp-icon-box",
|
|
702
|
+
id: "selection.convertToComponent",
|
|
703
|
+
level: "selection",
|
|
704
|
+
// Three surfaces, ONE record. `editor/context-menu.ts` carried a second `convertToComponent`
|
|
705
|
+
// With the same title and a different `when`, invisible to every registry but its own popover.
|
|
706
|
+
menus: ["blockbar", "context/element", "palette"],
|
|
707
|
+
requires: "an element that is not the document root",
|
|
708
|
+
// Deliberately NOT awaited: `convertToComponent()` resolves when the human answers the name
|
|
709
|
+
// Dialog, and a command whose promise waits on a person is a command nothing automated can
|
|
710
|
+
// Call. `run()` means "start this flow", the same as `settings.open` and `project.new`.
|
|
711
|
+
run: () => {
|
|
712
|
+
void deps.convertToComponent();
|
|
713
|
+
},
|
|
714
|
+
title: "Convert to Component",
|
|
715
|
+
undo: "project",
|
|
716
|
+
when: (ctx) => ctx.selection.count > 0 && !ctx.selection.isComponentInstance,
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
category: "Selection",
|
|
720
|
+
group: "4_component",
|
|
721
|
+
icon: "sp-icon-edit",
|
|
722
|
+
id: "selection.editComponent",
|
|
723
|
+
level: "selection",
|
|
724
|
+
menus: ["blockbar", "context/element", "palette"],
|
|
725
|
+
requires: "a component instance",
|
|
726
|
+
run: () => {
|
|
727
|
+
const node = componentOfTarget();
|
|
728
|
+
if (node) {
|
|
729
|
+
deps.navigateToComponent(node);
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
title: "Edit Component",
|
|
733
|
+
undo: "none",
|
|
734
|
+
when: (ctx) => ctx.selection.isComponentInstance,
|
|
735
|
+
},
|
|
736
|
+
] satisfies AnyCommand[]);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** The component file behind the current target, when it is an instance. */
|
|
740
|
+
function componentOfTarget(): string | null {
|
|
741
|
+
const tab = activeTab.value;
|
|
742
|
+
const path = commandTargetPath();
|
|
743
|
+
const node = tab && path ? getNodeAtPath(tab.doc.document, path) : null;
|
|
744
|
+
const entry = componentRegistry.find((c) => c.tagName === node?.tagName);
|
|
745
|
+
return entry?.path ?? null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** The registry the two selection surfaces render. Injected by a host, or built on first use. */
|
|
749
|
+
let _registry: CommandRegistry | null = null;
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Hand the surfaces the app-wide registry (or `null` to drop back to the surface's own).
|
|
753
|
+
*
|
|
754
|
+
* A host that injects one is responsible for two things: registering
|
|
755
|
+
* {@link registerSelectionCommands}, and building its `getContext` so it honours
|
|
756
|
+
* {@link commandTargetPath} — otherwise an Outline row's buttons report the SELECTION's enablement
|
|
757
|
+
* rather than the row's.
|
|
758
|
+
*/
|
|
759
|
+
export function useCommandRegistry(registry: CommandRegistry | null): void {
|
|
760
|
+
_registry = registry;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* The registry the block action bar and the Outline rows render.
|
|
765
|
+
*
|
|
766
|
+
* Until a host bootstrap injects one there is no app-wide registry, so this builds the surface's
|
|
767
|
+
* own: the SELECTION-level slice of `commands/defaults.ts` (which is where Delete, Duplicate and
|
|
768
|
+
* Select Parent are defined, with `blockbar` / `outline/row` already declared) plus the structural
|
|
769
|
+
* verbs above. The other levels are filtered out rather than stubbed — Save is not this surface's
|
|
770
|
+
* to own, and a registry holding a no-op `file.save` would be a lie.
|
|
771
|
+
*/
|
|
772
|
+
export function selectionCommandRegistry(): CommandRegistry {
|
|
773
|
+
if (_registry) {
|
|
774
|
+
return _registry;
|
|
775
|
+
}
|
|
776
|
+
const registry = createCommandRegistry({ getContext: selectionCommandContext });
|
|
777
|
+
const deps: CommandDeps = {
|
|
778
|
+
...noopCommandDeps(),
|
|
779
|
+
deleteSelection: deleteTarget,
|
|
780
|
+
duplicateSelection: duplicateTarget,
|
|
781
|
+
selectParent: selectParentOfTarget,
|
|
782
|
+
};
|
|
783
|
+
registry.registerAll(defaultCommands(deps).filter((command) => command.level === "selection"));
|
|
784
|
+
registerSelectionCommands(registry, {
|
|
785
|
+
convertToComponent: () => convertToComponent(),
|
|
786
|
+
navigateToComponent: (path) => _ctx?.navigateToComponent(path),
|
|
787
|
+
});
|
|
788
|
+
// The format cluster renders from records like every other button on this bar, so the surface's
|
|
789
|
+
// Own registry has to hold them — otherwise `keymap.formatBinding("format.bold")` has nothing to
|
|
790
|
+
// Format and the tooltip goes back to the hardcoded `Cmd+B` this change deleted.
|
|
791
|
+
registry.registerAll(formatCommands());
|
|
792
|
+
_registry = registry;
|
|
793
|
+
return registry;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** How many verb buttons the bar renders before the rest fold into `⋮` (plan §3.2 ⑩). */
|
|
797
|
+
export const BLOCKBAR_MAX_ITEMS = 5;
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Icons for command records, keyed by the record's own `icon`.
|
|
801
|
+
*
|
|
802
|
+
* A record with no icon renders its title as text rather than an unlabelled blank: the surface is
|
|
803
|
+
* allowed to choose how a name is drawn, never what it is.
|
|
804
|
+
*/
|
|
805
|
+
const commandIconMap: Record<string, TemplateResult> = {
|
|
806
|
+
"sp-icon-arrow-down": html`<sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>`,
|
|
807
|
+
"sp-icon-arrow-left": html`<sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>`,
|
|
808
|
+
"sp-icon-arrow-right": html`<sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>`,
|
|
809
|
+
"sp-icon-arrow-up": html`<sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>`,
|
|
810
|
+
"sp-icon-box": html`<sp-icon-box slot="icon" size="xs"></sp-icon-box>`,
|
|
218
811
|
"sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
|
|
812
|
+
"sp-icon-delete": html`<sp-icon-delete slot="icon"></sp-icon-delete>`,
|
|
813
|
+
"sp-icon-duplicate": html`<sp-icon-duplicate slot="icon"></sp-icon-duplicate>`,
|
|
814
|
+
"sp-icon-edit": html`<sp-icon-edit slot="icon" size="xs"></sp-icon-edit>`,
|
|
219
815
|
"sp-icon-link": html`<sp-icon-link slot="icon"></sp-icon-link>`,
|
|
220
816
|
"sp-icon-text-bold": html`<sp-icon-text-bold slot="icon"></sp-icon-text-bold>`,
|
|
221
817
|
"sp-icon-text-italic": html`<sp-icon-text-italic slot="icon"></sp-icon-text-italic>`,
|
|
@@ -227,7 +823,44 @@ const formatIconMap = {
|
|
|
227
823
|
slot="icon"
|
|
228
824
|
></sp-icon-text-superscript>`,
|
|
229
825
|
"sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
|
|
230
|
-
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
/** The record's icon, or its title drawn as a compact label. */
|
|
829
|
+
export function commandIcon(command: AnyCommand): TemplateResult {
|
|
830
|
+
return (
|
|
831
|
+
(command.icon ? commandIconMap[command.icon] : undefined) ??
|
|
832
|
+
html`<span class="cmd-label">${command.title}</span>`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* The tooltip: the chord when the control can act, the `requires` sentence when it cannot.
|
|
838
|
+
*
|
|
839
|
+
* Both strings come from the record. The accessible name stays the bare `title` either way, so a
|
|
840
|
+
* screen reader announces "Delete", not "Delete, requires an element selection".
|
|
841
|
+
*/
|
|
842
|
+
export function commandTooltip(registry: CommandRegistry, command: AnyCommand): string {
|
|
843
|
+
const reason = registry.disabledReason(command.id);
|
|
844
|
+
if (reason !== undefined) {
|
|
845
|
+
return `${command.title} — requires ${reason}`;
|
|
846
|
+
}
|
|
847
|
+
const chord = registry.keymap.formatBinding(command.id);
|
|
848
|
+
return chord ? `${command.title} (${chord})` : command.title;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Run a command, swallowing the refusal a disabled control should never have produced. */
|
|
852
|
+
export function runCommand(registry: CommandRegistry, id: string, target?: JxPath | null): void {
|
|
853
|
+
const invoke = () => {
|
|
854
|
+
if (registry.isEnabled(id)) {
|
|
855
|
+
void registry.run(id);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
if (target) {
|
|
859
|
+
withCommandTarget(target, invoke);
|
|
860
|
+
} else {
|
|
861
|
+
invoke();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
231
864
|
|
|
232
865
|
/**
|
|
233
866
|
* Prevent the bar from stealing focus from contenteditable
|
|
@@ -267,39 +900,34 @@ function onTagBadgeClick(e: MouseEvent, targets: SlashCommand[], selection: JxPa
|
|
|
267
900
|
}
|
|
268
901
|
|
|
269
902
|
/**
|
|
270
|
-
*
|
|
271
|
-
* every other command posts an `applyFormat` intent across the bridge.
|
|
903
|
+
* The parent selector.
|
|
272
904
|
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
905
|
+
* It renders the `selection.selectParent` record — its name, its chord and its handler — but as
|
|
906
|
+
* fixed chrome rather than from `forPlacement("blockbar")`, because that record does not declare
|
|
907
|
+
* the placement. Its disabled state is "the selection has no parent", which the record's own
|
|
908
|
+
* `enablement` (a bare "there is a selection") cannot express yet.
|
|
275
909
|
*/
|
|
276
|
-
function
|
|
277
|
-
e.stopPropagation();
|
|
278
|
-
if (action.command === "link") {
|
|
279
|
-
showLinkPopover((e.target as HTMLElement).closest("sp-action-button") as HTMLElement);
|
|
280
|
-
} else if (action.command) {
|
|
281
|
-
postApplyFormat({ command: action.command as FormatCommand });
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function renderParentSelector() {
|
|
910
|
+
function renderParentButton(registry: CommandRegistry) {
|
|
286
911
|
const tab = activeTab.value;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
const parentNode = getNodeAtPath(tab.doc.document, pPath);
|
|
912
|
+
const selection = primarySelection(tab?.session.selection);
|
|
913
|
+
const parentPath = selection ? parentElementPath(selection) : null;
|
|
914
|
+
const parentNode = tab && parentPath ? getNodeAtPath(tab.doc.document, parentPath) : null;
|
|
915
|
+
const command = registry.get("selection.selectParent");
|
|
916
|
+
const name = command?.title ?? "Select Parent";
|
|
917
|
+
const chord = command ? registry.keymap.formatBinding(command.id) : undefined;
|
|
918
|
+
const label = parentNode ? `Select parent: ${nodeLabel(parentNode)}` : name;
|
|
295
919
|
return html`
|
|
296
920
|
<sp-action-button
|
|
297
921
|
size="xs"
|
|
298
922
|
quiet
|
|
299
|
-
|
|
923
|
+
data-toolbar-item
|
|
924
|
+
tabindex="-1"
|
|
925
|
+
aria-label=${label}
|
|
926
|
+
title=${chord ? `${label} (${chord})` : label}
|
|
927
|
+
?disabled=${!parentPath}
|
|
300
928
|
@click=${(e: MouseEvent) => {
|
|
301
929
|
e.stopPropagation();
|
|
302
|
-
|
|
930
|
+
runCommand(registry, "selection.selectParent");
|
|
303
931
|
}}
|
|
304
932
|
>
|
|
305
933
|
<sp-icon-back slot="icon"></sp-icon-back>
|
|
@@ -307,42 +935,217 @@ function renderParentSelector() {
|
|
|
307
935
|
`;
|
|
308
936
|
}
|
|
309
937
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
938
|
+
/** One verb, rendered from its record. */
|
|
939
|
+
function renderCommandButton(registry: CommandRegistry, command: AnyCommand) {
|
|
940
|
+
const disabled = registry.disabledReason(command.id) !== undefined;
|
|
941
|
+
return html`<sp-action-button
|
|
942
|
+
size="xs"
|
|
943
|
+
quiet
|
|
944
|
+
class=${command.destructive ? "bar-cmd bar-cmd--danger" : "bar-cmd"}
|
|
945
|
+
data-toolbar-item
|
|
946
|
+
data-command=${command.id}
|
|
947
|
+
tabindex="-1"
|
|
948
|
+
aria-label=${command.title}
|
|
949
|
+
title=${commandTooltip(registry, command)}
|
|
950
|
+
?disabled=${disabled}
|
|
951
|
+
@mousedown=${(e: MouseEvent) => e.preventDefault()}
|
|
952
|
+
@click=${(e: MouseEvent) => {
|
|
953
|
+
e.stopPropagation();
|
|
954
|
+
runCommand(registry, command.id);
|
|
955
|
+
}}
|
|
956
|
+
>${commandIcon(command)}</sp-action-button
|
|
957
|
+
>`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** The `⋮` button and the menu of everything past {@link BLOCKBAR_MAX_ITEMS}. */
|
|
961
|
+
function renderOverflowButton(registry: CommandRegistry, commands: readonly AnyCommand[]) {
|
|
962
|
+
return html`<sp-action-button
|
|
963
|
+
size="xs"
|
|
964
|
+
quiet
|
|
965
|
+
class="bar-overflow"
|
|
966
|
+
data-toolbar-item
|
|
967
|
+
tabindex="-1"
|
|
968
|
+
aria-label="More block actions"
|
|
969
|
+
aria-haspopup="menu"
|
|
970
|
+
title="More block actions"
|
|
971
|
+
@mousedown=${(e: MouseEvent) => e.preventDefault()}
|
|
972
|
+
@click=${(e: MouseEvent) => {
|
|
973
|
+
e.stopPropagation();
|
|
974
|
+
showCommandOverflow(e.currentTarget as HTMLElement, registry, commands);
|
|
975
|
+
}}
|
|
976
|
+
>
|
|
977
|
+
<sp-icon-more slot="icon"></sp-icon-more>
|
|
978
|
+
</sp-action-button>`;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** The open overflow menu, so a second press (or a re-render) closes rather than stacks it. */
|
|
982
|
+
let _overflowHandle: { dismiss: () => void; host: HTMLElement } | null = null;
|
|
983
|
+
|
|
984
|
+
/** Close the block action bar's `⋮` menu if it is open. */
|
|
985
|
+
export function dismissBlockBarOverflow(): void {
|
|
986
|
+
_overflowHandle?.dismiss();
|
|
987
|
+
_overflowHandle = null;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Show the `⋮` menu under `anchor`. Rows carry the same names, chords and refusals as the buttons.
|
|
992
|
+
*
|
|
993
|
+
* Shared by the block action bar and the Outline rows — one menu is open at a time, so `target`
|
|
994
|
+
* (the Outline row's node) is captured per row and re-applied when a row is chosen.
|
|
995
|
+
*/
|
|
996
|
+
export function showCommandOverflow(
|
|
997
|
+
anchor: HTMLElement,
|
|
998
|
+
registry: CommandRegistry,
|
|
999
|
+
commands: readonly AnyCommand[],
|
|
1000
|
+
target: JxPath | null = null,
|
|
1001
|
+
): void {
|
|
1002
|
+
dismissBlockBarOverflow();
|
|
1003
|
+
const rect = rectOf(anchor);
|
|
1004
|
+
// Resolve every row's name / chord / refusal against the TARGET, not the selection, before the
|
|
1005
|
+
// Template is built — lit evaluates these eagerly, so the wrap has to cover the whole projection.
|
|
1006
|
+
const project = () =>
|
|
1007
|
+
commands.map((command) => ({
|
|
1008
|
+
chord: registry.keymap.formatBinding(command.id),
|
|
1009
|
+
destructive: command.destructive === true,
|
|
1010
|
+
disabled: registry.disabledReason(command.id) !== undefined,
|
|
1011
|
+
id: command.id,
|
|
1012
|
+
title: command.title,
|
|
1013
|
+
tooltip: commandTooltip(registry, command),
|
|
1014
|
+
}));
|
|
1015
|
+
const rows = target ? withCommandTarget(target, project) : project();
|
|
1016
|
+
_overflowHandle = renderPopover(
|
|
1017
|
+
html`<sp-popover
|
|
1018
|
+
open
|
|
1019
|
+
class="bar-overflow-menu"
|
|
1020
|
+
style=${styleMap({
|
|
1021
|
+
left: `${rect.left}px`,
|
|
1022
|
+
position: "fixed",
|
|
1023
|
+
top: `${rect.bottom + 4}px`,
|
|
1024
|
+
zIndex: "101",
|
|
1025
|
+
})}
|
|
342
1026
|
>
|
|
343
|
-
<sp-
|
|
344
|
-
|
|
345
|
-
|
|
1027
|
+
<sp-menu>
|
|
1028
|
+
${rows.map(
|
|
1029
|
+
(row) => html`<sp-menu-item
|
|
1030
|
+
?disabled=${row.disabled}
|
|
1031
|
+
data-command=${row.id}
|
|
1032
|
+
title=${row.tooltip}
|
|
1033
|
+
style=${row.destructive ? "color: var(--danger)" : nothing}
|
|
1034
|
+
@click=${() => {
|
|
1035
|
+
dismissBlockBarOverflow();
|
|
1036
|
+
runCommand(registry, row.id, target);
|
|
1037
|
+
}}
|
|
1038
|
+
>${row.title}${
|
|
1039
|
+
row.chord ? html`<kbd slot="value" class="cmd-chord">${row.chord}</kbd>` : nothing
|
|
1040
|
+
}</sp-menu-item
|
|
1041
|
+
>`,
|
|
1042
|
+
)}
|
|
1043
|
+
</sp-menu>
|
|
1044
|
+
</sp-popover>`,
|
|
1045
|
+
{
|
|
1046
|
+
dismissOnOutsideClick: true,
|
|
1047
|
+
onDismiss: () => {
|
|
1048
|
+
_overflowHandle = null;
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ─── Toolbar keyboard model ──────────────────────────────────────────────────
|
|
1055
|
+
|
|
1056
|
+
/** The bar's focusable items, in DOM order, skipping the ones that cannot act. */
|
|
1057
|
+
function toolbarItems(bar: HTMLElement): HTMLElement[] {
|
|
1058
|
+
return [...bar.querySelectorAll<HTMLElement>("[data-toolbar-item]")].filter(
|
|
1059
|
+
(el) => !el.hasAttribute("disabled") && el.getAttribute("aria-disabled") !== "true",
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Apply the roving tabindex: exactly one item is in the tab order at a time.
|
|
1065
|
+
*
|
|
1066
|
+
* Called after every render, so the survivor is whichever item still holds focus and otherwise the
|
|
1067
|
+
* first — a toolbar that resets to its first control on every keystroke is not navigable.
|
|
1068
|
+
*/
|
|
1069
|
+
export function applyRovingTabindex(bar: HTMLElement): void {
|
|
1070
|
+
const items = toolbarItems(bar);
|
|
1071
|
+
const focused = items.findIndex(
|
|
1072
|
+
(el) => el === document.activeElement || el.contains(document.activeElement),
|
|
1073
|
+
);
|
|
1074
|
+
const active = focused === -1 ? 0 : focused;
|
|
1075
|
+
for (const [index, el] of items.entries()) {
|
|
1076
|
+
el.tabIndex = index === active ? 0 : -1;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Move focus to item `index` (wrapping), and make it the one in the tab order. */
|
|
1081
|
+
function focusToolbarItem(bar: HTMLElement, index: number): void {
|
|
1082
|
+
const items = toolbarItems(bar);
|
|
1083
|
+
if (items.length === 0) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const wrapped = ((index % items.length) + items.length) % items.length;
|
|
1087
|
+
for (const [i, el] of items.entries()) {
|
|
1088
|
+
el.tabIndex = i === wrapped ? 0 : -1;
|
|
1089
|
+
}
|
|
1090
|
+
items[wrapped]!.focus();
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Put the keyboard back where it came from — the canvas iframe, else its wrapper. */
|
|
1094
|
+
function returnFocusToCanvas(): void {
|
|
1095
|
+
// The FOCUSED pane's stage: the keyboard came from there and is going back there. Resolving it
|
|
1096
|
+
// From the active tab answered with whichever pane displays that document first.
|
|
1097
|
+
const stage = activeCanvasSurface().wrap;
|
|
1098
|
+
const iframe = stage?.querySelector<HTMLElement>("iframe.jx-canvas-iframe");
|
|
1099
|
+
(iframe ?? stage)?.focus();
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** `role="toolbar"` navigation: ←/→ between items, Home/End to the ends, Esc back to the canvas. */
|
|
1103
|
+
export function onToolbarKeydown(e: KeyboardEvent): void {
|
|
1104
|
+
const bar = e.currentTarget as HTMLElement;
|
|
1105
|
+
const items = toolbarItems(bar);
|
|
1106
|
+
const current = items.findIndex(
|
|
1107
|
+
(el) => el === document.activeElement || el.contains(document.activeElement),
|
|
1108
|
+
);
|
|
1109
|
+
if (e.key === "ArrowRight") {
|
|
1110
|
+
e.preventDefault();
|
|
1111
|
+
focusToolbarItem(bar, current + 1);
|
|
1112
|
+
} else if (e.key === "ArrowLeft") {
|
|
1113
|
+
e.preventDefault();
|
|
1114
|
+
focusToolbarItem(bar, current - 1);
|
|
1115
|
+
} else if (e.key === "Home") {
|
|
1116
|
+
e.preventDefault();
|
|
1117
|
+
focusToolbarItem(bar, 0);
|
|
1118
|
+
} else if (e.key === "End") {
|
|
1119
|
+
e.preventDefault();
|
|
1120
|
+
focusToolbarItem(bar, items.length - 1);
|
|
1121
|
+
} else if (e.key === "Escape") {
|
|
1122
|
+
e.preventDefault();
|
|
1123
|
+
dismissBlockBarOverflow();
|
|
1124
|
+
returnFocusToCanvas();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* ⌥↑ enters the bar from the canvas.
|
|
1130
|
+
*
|
|
1131
|
+
* Bound on the PARENT document, so it fires whenever the parent chrome owns focus. A press from
|
|
1132
|
+
* inside the canvas iframe does not arrive: `canvas/iframe-keys.ts` forwards bare navigation keys
|
|
1133
|
+
* only when the target is not editable, and the canvas root is permanently `contenteditable`.
|
|
1134
|
+
* Exported so a test can dispatch it directly.
|
|
1135
|
+
*/
|
|
1136
|
+
export function handleBlockBarEntryKey(e: KeyboardEvent): void {
|
|
1137
|
+
if (e.key !== "ArrowUp" || !e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (isModalOpen()) {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const bar = view.blockActionBarEl?.querySelector<HTMLElement>(".block-action-bar");
|
|
1144
|
+
if (!bar) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
e.preventDefault();
|
|
1148
|
+
focusToolbarItem(bar, 0);
|
|
346
1149
|
}
|
|
347
1150
|
|
|
348
1151
|
/**
|
|
@@ -364,7 +1167,7 @@ function onMergeTagClick(e: MouseEvent) {
|
|
|
364
1167
|
// When the caret sits inside a repeater, offer its local scope (item/index + item fields). There is
|
|
365
1168
|
// No live `$map` in edit mode — the perimeter renders one glyph template — so fields resolve
|
|
366
1169
|
// Parent-side from schema, keyed off the selected element's doc path (which carries a `map` segment).
|
|
367
|
-
const selPath = getEditSnapshot().snapshot?.path ?? tab?.session.selection;
|
|
1170
|
+
const selPath = getEditSnapshot().snapshot?.path ?? primarySelection(tab?.session.selection);
|
|
368
1171
|
const arrayNode = tab && selPath ? findEnclosingRepeater(tab.doc.document, selPath) : null;
|
|
369
1172
|
if (arrayNode) {
|
|
370
1173
|
const tokens = resolveRepeaterItemFields(
|
|
@@ -397,11 +1200,90 @@ export function dismissLinkPopover() {
|
|
|
397
1200
|
|
|
398
1201
|
/** Dismiss the block action bar. */
|
|
399
1202
|
export function dismissBlockActionBar() {
|
|
1203
|
+
/* A dismiss is the bar's STRUCTURAL teardown — `canvas/canvas-render.ts` calls it when a stage is
|
|
1204
|
+
torn down and again on a mode transition — so it also drops any suppression. The bar it would
|
|
1205
|
+
otherwise be holding down does not exist any more; what comes back is a different one. */
|
|
1206
|
+
_suppressedFor = null;
|
|
1207
|
+
dismissBlockBarOverflow();
|
|
400
1208
|
if (view.blockActionBarEl) {
|
|
401
1209
|
litRender(nothing, view.blockActionBarEl);
|
|
402
1210
|
}
|
|
403
1211
|
}
|
|
404
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* The document + selection the bar is currently suppressed FOR, or null while it is free to draw.
|
|
1215
|
+
*
|
|
1216
|
+
* **Why a key and not a boolean.** The bar is `position: fixed` and clamped into the window, so it
|
|
1217
|
+
* can sit over the Document Header card, the pane context bar and the docks; an author working in
|
|
1218
|
+
* the Inspector's Logic tab reported it "blocking a part of the interface that the user needs to
|
|
1219
|
+
* utilize". Hiding it on a chrome pointerdown is half an answer — {@link dismissBlockActionBar}
|
|
1220
|
+
* alone would flash back on the very next repaint, because the bar is re-rendered from the
|
|
1221
|
+
* `toolbarRefresh` seam on every selection snapshot and from `renderOnly("overlays")` on zoom/pan.
|
|
1222
|
+
* The key is what makes the state end by itself: a render for a DIFFERENT selection is the click
|
|
1223
|
+
* that hid the bar having also chosen a new target (an Outline row, a Navigator entry), and the bar
|
|
1224
|
+
* belongs over the new one. {@link releaseBlockActionBar} is the other door, for the click that
|
|
1225
|
+
* chose the same target again.
|
|
1226
|
+
*
|
|
1227
|
+
* Nothing outside this module reads it. "Is the bar suppressed" is not a fact any other surface may
|
|
1228
|
+
* branch on — it is one panel declining to draw itself, and a second reader would make it a mode.
|
|
1229
|
+
*/
|
|
1230
|
+
let _suppressedFor: { key: string } | null = null;
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* What the bar is suppressed FOR: a path, in a document.
|
|
1234
|
+
*
|
|
1235
|
+
* The tab is half the key because `["children",0]` names a node in every document there has ever
|
|
1236
|
+
* been. Keyed on the path alone, an author who suppressed the bar and then switched tabs — itself a
|
|
1237
|
+
* chrome click — would find it hidden over the new document's first block, which they never clicked
|
|
1238
|
+
* away from. `JSON.stringify` rather than `join` for the same reason one level down: the document
|
|
1239
|
+
* root is `[]` and "nothing selected" is `null`, and those two must not compare equal.
|
|
1240
|
+
*/
|
|
1241
|
+
function suppressionKey(tab: ActiveTab | null, path: JxPath | null): string {
|
|
1242
|
+
return JSON.stringify([tab?.id ?? null, path]);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Hide the bar until the canvas is in play again — the author pressed a pointer down in parent
|
|
1247
|
+
* chrome, which is a surface the bar may be sitting on top of.
|
|
1248
|
+
*
|
|
1249
|
+
* **The selection is deliberately untouched.** The Inspector edits the selected node, so a click
|
|
1250
|
+
* into it is how the author edits the thing they selected; clearing the selection here would make
|
|
1251
|
+
* the panel they just reached for go blank. Only the bar's own rendering is suspended.
|
|
1252
|
+
*
|
|
1253
|
+
* The `⋮` menu and the link popover go with it, because both are ANCHORED to the bar — the overflow
|
|
1254
|
+
* under its `⋮` button, the popover under its Link button. Left open they would float over the
|
|
1255
|
+
* chrome with nothing under them to belong to, which is the same defect one level down. Neither can
|
|
1256
|
+
* be the cause of this call: {@link isEditChromeTarget} exempts both.
|
|
1257
|
+
*/
|
|
1258
|
+
export function suppressBlockActionBar(): void {
|
|
1259
|
+
// Clears any earlier key as its first act; this selection's replaces it below.
|
|
1260
|
+
dismissBlockActionBar();
|
|
1261
|
+
if (_linkPopoverOpen) {
|
|
1262
|
+
dismissLinkPopover();
|
|
1263
|
+
}
|
|
1264
|
+
const tab = activeTab.value;
|
|
1265
|
+
_suppressedFor = { key: suppressionKey(tab, primarySelection(tab?.session.selection)) };
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Let the bar draw again — a pointer went down in a canvas frame, so the canvas is the surface the
|
|
1270
|
+
* author is working on.
|
|
1271
|
+
*
|
|
1272
|
+
* This is the door the SELECTION cannot open: clicking the already-selected element posts the same
|
|
1273
|
+
* path back, so the key comparison in {@link renderBlockActionBar} still matches and the bar would
|
|
1274
|
+
* stay hidden for as long as the author kept clicking the block they are editing.
|
|
1275
|
+
*
|
|
1276
|
+
* A no-op when nothing is suppressed, so the common case — every canvas pointerdown, suppressed or
|
|
1277
|
+
* not — costs a null check rather than a render.
|
|
1278
|
+
*/
|
|
1279
|
+
export function releaseBlockActionBar(): void {
|
|
1280
|
+
if (!_suppressedFor) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
_suppressedFor = null;
|
|
1284
|
+
renderBlockActionBar();
|
|
1285
|
+
}
|
|
1286
|
+
|
|
405
1287
|
/**
|
|
406
1288
|
* Whether the link popover is open. A snapshot-driven {@link renderBlockActionBar} must NOT
|
|
407
1289
|
* re-render (and so re-mount) the open popover — typing the URL would re-create the field and lose
|
|
@@ -428,6 +1310,7 @@ export function isEditChromeTarget(target: EventTarget | null): boolean {
|
|
|
428
1310
|
}
|
|
429
1311
|
const roots = [
|
|
430
1312
|
view.blockActionBarEl,
|
|
1313
|
+
_overflowHandle?.host ?? null,
|
|
431
1314
|
getLayerSlot("popover", "link-popover"),
|
|
432
1315
|
getLayerSlot("popover", "slash-menu"),
|
|
433
1316
|
];
|
|
@@ -522,47 +1405,17 @@ function showLinkPopover(anchorBtn: HTMLElement) {
|
|
|
522
1405
|
*/
|
|
523
1406
|
export function openLinkPopoverFromShortcut(): void {
|
|
524
1407
|
const bar = view.blockActionBarEl?.querySelector(".block-action-bar") as HTMLElement | null;
|
|
1408
|
+
/* By RECORD ID, not by rendered text. This queried `sp-action-button[title^="Link"]`, which is
|
|
1409
|
+
the addressing §13's first rule bans in the screenshot manifest — a match on a title the app
|
|
1410
|
+
DERIVES (it now reads "Link… (⌘K)", and on Windows it reads something else again). The id is
|
|
1411
|
+
the input the app accepts. */
|
|
525
1412
|
const linkBtn =
|
|
526
|
-
(bar?.querySelector('
|
|
1413
|
+
(bar?.querySelector('[data-command-id="format.link"]') as HTMLElement | null) ?? bar;
|
|
527
1414
|
if (linkBtn) {
|
|
528
1415
|
showLinkPopover(linkBtn);
|
|
529
1416
|
}
|
|
530
1417
|
}
|
|
531
1418
|
|
|
532
|
-
/** Move the selected node up (swap with previous sibling). */
|
|
533
|
-
function moveSelectionUp() {
|
|
534
|
-
const tab = activeTab.value;
|
|
535
|
-
if (!tab?.session.selection || tab.session.selection.length < 2) {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
const sel = tab.session.selection;
|
|
539
|
-
const idx = childIndex(sel) as number;
|
|
540
|
-
if (idx <= 0) {
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
const pPath = parentElementPath(sel) as JxPath;
|
|
544
|
-
transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx - 1));
|
|
545
|
-
tab.session.selection = [...pPath, "children", idx - 1];
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/** Move the selected node down (swap with next sibling). */
|
|
549
|
-
function moveSelectionDown() {
|
|
550
|
-
const tab = activeTab.value;
|
|
551
|
-
if (!tab?.session.selection || tab.session.selection.length < 2) {
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
const sel = tab.session.selection;
|
|
555
|
-
const idx = childIndex(sel) as number;
|
|
556
|
-
const pPath = parentElementPath(sel) as JxPath;
|
|
557
|
-
const parentNode = getNodeAtPath(tab.doc.document, pPath);
|
|
558
|
-
const siblings = childList(parentNode);
|
|
559
|
-
if (idx >= siblings.length - 1) {
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx + 2));
|
|
563
|
-
tab.session.selection = [...pPath, "children", idx + 1];
|
|
564
|
-
}
|
|
565
|
-
|
|
566
1419
|
/** Render the unified block action bar above the selected element. */
|
|
567
1420
|
export function renderBlockActionBar() {
|
|
568
1421
|
if (!_ctx) {
|
|
@@ -572,6 +1425,19 @@ export function renderBlockActionBar() {
|
|
|
572
1425
|
view.blockActionBarEl = getLayerSlot("popover", "block-action-bar");
|
|
573
1426
|
}
|
|
574
1427
|
|
|
1428
|
+
/* A snapshot-driven refresh must not re-mount an open link popover — it would re-create the URL
|
|
1429
|
+
field and lose the caret — so this pass is skipped entirely.
|
|
1430
|
+
|
|
1431
|
+
It sits AHEAD of the drag release below, because it is the one early return that LEAVES THE BAR
|
|
1432
|
+
UP. Releasing and then returning without re-rendering left the ⠿ handle on screen and inert, and
|
|
1433
|
+
nothing re-installs it: `dismissLinkPopover` does not re-render the bar. Any repaint reaches
|
|
1434
|
+
here — `applyTransform` → `renderOnly("overlays")` → this — so a pan, a zoom or a pane resize
|
|
1435
|
+
with the Link popover open silently killed dragging until the next selection change. Every
|
|
1436
|
+
other early return draws `nothing`, so releasing before them is right. */
|
|
1437
|
+
if (_linkPopoverOpen) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
575
1441
|
if (view.selDragCleanup) {
|
|
576
1442
|
view.selDragCleanup();
|
|
577
1443
|
view.selDragCleanup = null;
|
|
@@ -580,18 +1446,25 @@ export function renderBlockActionBar() {
|
|
|
580
1446
|
const tab = activeTab.value;
|
|
581
1447
|
const canvasMode = _ctx.getCanvasMode();
|
|
582
1448
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
1449
|
+
const selection = primarySelection(tab?.session.selection);
|
|
1450
|
+
|
|
1451
|
+
/* Suppressed by a chrome pointerdown, and this pass is the first of the two doors out of it (see
|
|
1452
|
+
{@link _suppressedFor}). A different selection means the click that hid the bar also chose a new
|
|
1453
|
+
target, so the suppression is over; the same selection keeps it, which is what stops the next
|
|
1454
|
+
snapshot- or overlay-driven repaint from flashing the bar back under the author's cursor. */
|
|
1455
|
+
if (_suppressedFor) {
|
|
1456
|
+
if (_suppressedFor.key === suppressionKey(tab, selection)) {
|
|
1457
|
+
litRender(nothing, view.blockActionBarEl);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
_suppressedFor = null;
|
|
586
1461
|
}
|
|
587
1462
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (_linkPopoverOpen) {
|
|
1463
|
+
if (!tab || !selection || (canvasMode !== "design" && canvasMode !== "edit")) {
|
|
1464
|
+
litRender(nothing, view.blockActionBarEl);
|
|
591
1465
|
return;
|
|
592
1466
|
}
|
|
593
1467
|
|
|
594
|
-
const { selection } = tab.session;
|
|
595
1468
|
const node = getNodeAtPath(tab.doc.document, selection);
|
|
596
1469
|
if (!node) {
|
|
597
1470
|
litRender(nothing, view.blockActionBarEl);
|
|
@@ -607,7 +1480,7 @@ export function renderBlockActionBar() {
|
|
|
607
1480
|
return;
|
|
608
1481
|
}
|
|
609
1482
|
|
|
610
|
-
const tag = (node.tagName
|
|
1483
|
+
const tag = (displayTagName(node.tagName) || "div").toLowerCase();
|
|
611
1484
|
|
|
612
1485
|
// Inline format state, sourced from the iframe's selection snapshot.
|
|
613
1486
|
const { editingProp, snapshot } = getEditSnapshot();
|
|
@@ -631,7 +1504,7 @@ export function renderBlockActionBar() {
|
|
|
631
1504
|
|
|
632
1505
|
// Conversion targets for badge click
|
|
633
1506
|
const isComponent =
|
|
634
|
-
node.tagName
|
|
1507
|
+
displayTagName(node.tagName).includes("-") &&
|
|
635
1508
|
componentRegistry.some((/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName);
|
|
636
1509
|
const children = childList(node);
|
|
637
1510
|
const isEmpty =
|
|
@@ -644,96 +1517,98 @@ export function renderBlockActionBar() {
|
|
|
644
1517
|
const convertTargets = !isComponent && !isRepeater ? getConvertTargets(tag, isEmpty) : [];
|
|
645
1518
|
const badgeInteractive = convertTargets.length > 0;
|
|
646
1519
|
|
|
1520
|
+
// The verb cluster, sliced at the cap. `forPlacement` already dropped the records whose `when` is
|
|
1521
|
+
// False and sorted the rest by group; everything past the cap keeps its name and its chord in the
|
|
1522
|
+
// `⋮` menu rather than being silently unavailable.
|
|
1523
|
+
const registry = selectionCommandRegistry();
|
|
1524
|
+
/* The SET is the schema's, the VERBS are the registry's (see {@link formatCommands}).
|
|
1525
|
+
`$inlineActions` says which of the eight this tag accepts and in what order — four on an `<h1>`,
|
|
1526
|
+
eight on a `<p>` — and each one's name, icon, chord and behaviour come off its record. An action
|
|
1527
|
+
the registry has no record for is dropped rather than drawn as a button that runs nothing, so
|
|
1528
|
+
adding a ninth verb to the data file without a record shows up as a missing button and as a red
|
|
1529
|
+
`tests/block-action-bar.test.ts`, not as a silent no-op. */
|
|
1530
|
+
const formatButtons = actions.flatMap((action) => {
|
|
1531
|
+
const command = registry.get(`format.${action.command}`);
|
|
1532
|
+
return command ? [{ action, command }] : [];
|
|
1533
|
+
});
|
|
1534
|
+
const placed = registry.forPlacement("blockbar");
|
|
1535
|
+
const shown = placed.slice(0, BLOCKBAR_MAX_ITEMS);
|
|
1536
|
+
const overflow = placed.slice(BLOCKBAR_MAX_ITEMS);
|
|
1537
|
+
// The handle is chrome, not a verb, so it renders on every selection; only a node that actually
|
|
1538
|
+
// Sits at a child index can be dragged, and at the root it is a disabled affordance rather than a
|
|
1539
|
+
// Missing one (§8.6: ONE shape).
|
|
1540
|
+
const canDragSelection = structuralTarget(selection) !== null;
|
|
1541
|
+
|
|
647
1542
|
litRender(
|
|
648
1543
|
html`
|
|
649
1544
|
<div
|
|
650
1545
|
class="block-action-bar"
|
|
1546
|
+
data-jx-region="overlay.menu:block-action-bar"
|
|
1547
|
+
role="toolbar"
|
|
1548
|
+
aria-label="Block actions"
|
|
1549
|
+
aria-orientation="horizontal"
|
|
651
1550
|
style=${styleMap({ left: `${pos.left}px`, top: `${pos.top}px` })}
|
|
652
1551
|
@mousedown=${onBarMousedown}
|
|
1552
|
+
@keydown=${onToolbarKeydown}
|
|
653
1553
|
>
|
|
654
|
-
${
|
|
1554
|
+
${renderParentButton(registry)}
|
|
655
1555
|
|
|
656
1556
|
<span
|
|
657
1557
|
class="bar-tag${badgeInteractive ? " bar-tag--interactive" : ""}"
|
|
1558
|
+
role=${badgeInteractive ? "button" : nothing}
|
|
1559
|
+
data-toolbar-item=${badgeInteractive ? "" : nothing}
|
|
1560
|
+
tabindex=${badgeInteractive ? "-1" : nothing}
|
|
1561
|
+
title=${badgeInteractive ? "Change element type" : nothing}
|
|
658
1562
|
@click=${
|
|
659
1563
|
badgeInteractive
|
|
660
1564
|
? (e: MouseEvent) => onTagBadgeClick(e, convertTargets, selection)
|
|
661
1565
|
: nothing
|
|
662
1566
|
}
|
|
663
|
-
>${isRepeater ? nodeLabel(node) : node.$id || (node.tagName
|
|
1567
|
+
>${isRepeater ? nodeLabel(node) : node.$id || displayTagName(node.tagName) || "div"}${
|
|
664
1568
|
editingProp ? ` · ${editingProp}` : ""
|
|
665
1569
|
}</span
|
|
666
1570
|
>
|
|
667
1571
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
${
|
|
707
|
-
|
|
708
|
-
? (() => {
|
|
709
|
-
const isComp =
|
|
710
|
-
node.tagName.includes("-") &&
|
|
711
|
-
componentRegistry.some(
|
|
712
|
-
(/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName,
|
|
713
|
-
);
|
|
714
|
-
if (isComp) {
|
|
715
|
-
const comp = componentRegistry.find(
|
|
716
|
-
(/** @type {{ tagName: string; path: string }} */ c) =>
|
|
717
|
-
c.tagName === node.tagName,
|
|
718
|
-
);
|
|
719
|
-
return html`<sp-action-button
|
|
720
|
-
size="xs"
|
|
721
|
-
quiet
|
|
722
|
-
title="Edit Component"
|
|
723
|
-
@click=${() => _ctx?.navigateToComponent(comp?.path as string)}
|
|
724
|
-
><sp-icon-edit slot="icon" size="xs"></sp-icon-edit
|
|
725
|
-
></sp-action-button>`;
|
|
726
|
-
}
|
|
727
|
-
return html`<sp-action-button
|
|
728
|
-
size="xs"
|
|
729
|
-
quiet
|
|
730
|
-
title="Convert to Component"
|
|
731
|
-
@click=${() => convertToComponent()}
|
|
732
|
-
><sp-icon-box slot="icon" size="xs"></sp-icon-box
|
|
733
|
-
></sp-action-button>`;
|
|
734
|
-
})()
|
|
735
|
-
: nothing
|
|
736
|
-
}
|
|
1572
|
+
<span
|
|
1573
|
+
class="bar-drag-handle${canDragSelection ? "" : " bar-drag-handle--disabled"}"
|
|
1574
|
+
title=${canDragSelection ? "Drag to reorder" : "Drag to reorder — the document root cannot move"}
|
|
1575
|
+
aria-disabled=${canDragSelection ? nothing : "true"}
|
|
1576
|
+
data-toolbar-item
|
|
1577
|
+
tabindex="-1"
|
|
1578
|
+
${ref((handleEl) => {
|
|
1579
|
+
if (!handleEl || !canDragSelection) {
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
/* No release here: {@link renderBlockActionBar} already released at the top of this
|
|
1583
|
+
pass, and that one — not this — is the load-bearing copy, because it also runs on
|
|
1584
|
+
the passes that return before `litRender` and never reach this ref at all. A second
|
|
1585
|
+
release here could only ever see null, which is why it was dead. */
|
|
1586
|
+
view.selDragCleanup = draggable({
|
|
1587
|
+
element: handleEl as HTMLElement,
|
|
1588
|
+
getInitialData: () => ({
|
|
1589
|
+
// Snapshot the selection: the live array is a Vue reactive proxy, which
|
|
1590
|
+
// Structured clone rejects when the src crosses postMessage (DataCloneError
|
|
1591
|
+
// Killed the whole handle drag), and a live reference would also mutate the
|
|
1592
|
+
// Retained srcData if the selection changed mid-drag.
|
|
1593
|
+
path: [...(primarySelection(activeTab.value?.session.selection) ?? [])],
|
|
1594
|
+
type: "tree-node",
|
|
1595
|
+
}),
|
|
1596
|
+
onGenerateDragPreview: ({
|
|
1597
|
+
nativeSetDragImage,
|
|
1598
|
+
}: {
|
|
1599
|
+
nativeSetDragImage: ((image: Element, x: number, y: number) => void) | null;
|
|
1600
|
+
}) => {
|
|
1601
|
+
// Suppress the native drag image; the cross-frame ghost is the drag affordance.
|
|
1602
|
+
disableNativeDragPreview({ nativeSetDragImage });
|
|
1603
|
+
},
|
|
1604
|
+
});
|
|
1605
|
+
})}
|
|
1606
|
+
>⠿</span
|
|
1607
|
+
>
|
|
1608
|
+
|
|
1609
|
+
<sp-divider size="s" vertical></sp-divider>
|
|
1610
|
+
${shown.map((command) => renderCommandButton(registry, command))}
|
|
1611
|
+
${overflow.length > 0 ? renderOverflowButton(registry, overflow) : nothing}
|
|
737
1612
|
${
|
|
738
1613
|
showFormat
|
|
739
1614
|
? html`
|
|
@@ -745,17 +1620,24 @@ export function renderBlockActionBar() {
|
|
|
745
1620
|
selects="multiple"
|
|
746
1621
|
selected=${activeValues.length > 0 ? JSON.stringify(activeValues) : nothing}
|
|
747
1622
|
>
|
|
748
|
-
${
|
|
749
|
-
(action) => html`
|
|
1623
|
+
${formatButtons.map(
|
|
1624
|
+
({ action, command }) => html`
|
|
750
1625
|
<sp-action-button
|
|
751
1626
|
size="xs"
|
|
752
1627
|
value=${action.tag}
|
|
753
|
-
|
|
1628
|
+
data-command-id=${command.id}
|
|
1629
|
+
data-toolbar-item
|
|
1630
|
+
tabindex="-1"
|
|
1631
|
+
aria-label=${command.title}
|
|
1632
|
+
title=${commandTooltip(registry, command)}
|
|
754
1633
|
?disabled=${formatDisabled && action.command !== "link"}
|
|
755
1634
|
@mousedown=${(e: MouseEvent) => e.preventDefault()}
|
|
756
|
-
@click=${(e: MouseEvent) =>
|
|
1635
|
+
@click=${(e: MouseEvent) => {
|
|
1636
|
+
e.stopPropagation();
|
|
1637
|
+
runCommand(registry, command.id);
|
|
1638
|
+
}}
|
|
757
1639
|
>
|
|
758
|
-
${
|
|
1640
|
+
${commandIcon(command)}
|
|
759
1641
|
</sp-action-button>
|
|
760
1642
|
`,
|
|
761
1643
|
)}
|
|
@@ -763,6 +1645,10 @@ export function renderBlockActionBar() {
|
|
|
763
1645
|
<sp-action-button
|
|
764
1646
|
size="xs"
|
|
765
1647
|
quiet
|
|
1648
|
+
data-toolbar-item
|
|
1649
|
+
data-jx-region="overlay.menu:block-action-bar/insertData"
|
|
1650
|
+
tabindex="-1"
|
|
1651
|
+
aria-label="Insert data"
|
|
766
1652
|
title="Insert data"
|
|
767
1653
|
@mousedown=${(e: MouseEvent) => e.preventDefault()}
|
|
768
1654
|
@click=${onMergeTagClick}
|
|
@@ -777,6 +1663,11 @@ export function renderBlockActionBar() {
|
|
|
777
1663
|
view.blockActionBarEl,
|
|
778
1664
|
);
|
|
779
1665
|
|
|
1666
|
+
const rendered = view.blockActionBarEl.querySelector<HTMLElement>(".block-action-bar");
|
|
1667
|
+
if (rendered) {
|
|
1668
|
+
applyRovingTabindex(rendered);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
780
1671
|
// Post-render side effects
|
|
781
1672
|
requestAnimationFrame(() => {
|
|
782
1673
|
const bar = view.blockActionBarEl?.firstElementChild as HTMLElement | null;
|