@panphora/bevel 0.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/LICENSE +16 -0
- package/README.md +226 -0
- package/bevel-fonts.css +16 -0
- package/bevel.css +3075 -0
- package/bevel.js +2126 -0
- package/dist/react/components/activity-feed.d.ts +89 -0
- package/dist/react/components/activity-feed.js +54 -0
- package/dist/react/components/alert-dialog.d.ts +14 -0
- package/dist/react/components/alert-dialog.js +19 -0
- package/dist/react/components/app-shell.d.ts +98 -0
- package/dist/react/components/app-shell.js +86 -0
- package/dist/react/components/app-toolbar.d.ts +19 -0
- package/dist/react/components/app-toolbar.js +22 -0
- package/dist/react/components/avatar-group.d.ts +67 -0
- package/dist/react/components/avatar-group.js +49 -0
- package/dist/react/components/badge-and-pill.d.ts +34 -0
- package/dist/react/components/badge-and-pill.js +12 -0
- package/dist/react/components/breadcrumbs.d.ts +56 -0
- package/dist/react/components/breadcrumbs.js +44 -0
- package/dist/react/components/bulk-bar.d.ts +22 -0
- package/dist/react/components/bulk-bar.js +59 -0
- package/dist/react/components/button.d.ts +15 -0
- package/dist/react/components/button.js +20 -0
- package/dist/react/components/callout-and-hint.d.ts +17 -0
- package/dist/react/components/callout-and-hint.js +8 -0
- package/dist/react/components/checkbox.d.ts +5 -0
- package/dist/react/components/checkbox.js +13 -0
- package/dist/react/components/chip.d.ts +149 -0
- package/dist/react/components/chip.js +29 -0
- package/dist/react/components/code-block.d.ts +50 -0
- package/dist/react/components/code-block.js +77 -0
- package/dist/react/components/code-editor.d.ts +122 -0
- package/dist/react/components/code-editor.js +695 -0
- package/dist/react/components/column-manager.d.ts +28 -0
- package/dist/react/components/column-manager.js +85 -0
- package/dist/react/components/combobox.d.ts +32 -0
- package/dist/react/components/combobox.js +37 -0
- package/dist/react/components/data-grid.d.ts +108 -0
- package/dist/react/components/data-grid.js +159 -0
- package/dist/react/components/data-table.d.ts +2 -0
- package/dist/react/components/data-table.js +6 -0
- package/dist/react/components/date.d.ts +23 -0
- package/dist/react/components/date.js +11 -0
- package/dist/react/components/details.d.ts +13 -0
- package/dist/react/components/details.js +20 -0
- package/dist/react/components/dialog.d.ts +74 -0
- package/dist/react/components/dialog.js +31 -0
- package/dist/react/components/diff-view.d.ts +73 -0
- package/dist/react/components/diff-view.js +68 -0
- package/dist/react/components/documents.d.ts +70 -0
- package/dist/react/components/documents.js +10 -0
- package/dist/react/components/drawer.d.ts +45 -0
- package/dist/react/components/drawer.js +35 -0
- package/dist/react/components/editable-grid-cells.d.ts +32 -0
- package/dist/react/components/editable-grid-cells.js +116 -0
- package/dist/react/components/eyebrow-kicker-lede-pull-quote.d.ts +39 -0
- package/dist/react/components/eyebrow-kicker-lede-pull-quote.js +12 -0
- package/dist/react/components/field.d.ts +32 -0
- package/dist/react/components/field.js +27 -0
- package/dist/react/components/file-tree.d.ts +75 -0
- package/dist/react/components/file-tree.js +247 -0
- package/dist/react/components/file.d.ts +43 -0
- package/dist/react/components/file.js +73 -0
- package/dist/react/components/files-table.d.ts +174 -0
- package/dist/react/components/files-table.js +237 -0
- package/dist/react/components/filter-builder.d.ts +37 -0
- package/dist/react/components/filter-builder.js +109 -0
- package/dist/react/components/form-error-summary.d.ts +16 -0
- package/dist/react/components/form-error-summary.js +49 -0
- package/dist/react/components/icon-button.d.ts +8 -0
- package/dist/react/components/icon-button.js +14 -0
- package/dist/react/components/icon.d.ts +7 -0
- package/dist/react/components/icon.js +15 -0
- package/dist/react/components/image.d.ts +72 -0
- package/dist/react/components/image.js +24 -0
- package/dist/react/components/inline-edit.d.ts +70 -0
- package/dist/react/components/inline-edit.js +149 -0
- package/dist/react/components/input.d.ts +29 -0
- package/dist/react/components/input.js +20 -0
- package/dist/react/components/kanban-board.d.ts +140 -0
- package/dist/react/components/kanban-board.js +374 -0
- package/dist/react/components/layout.d.ts +51 -0
- package/dist/react/components/layout.js +28 -0
- package/dist/react/components/list.d.ts +28 -0
- package/dist/react/components/list.js +6 -0
- package/dist/react/components/masthead-and-verdict.d.ts +60 -0
- package/dist/react/components/masthead-and-verdict.js +8 -0
- package/dist/react/components/menu.d.ts +26 -0
- package/dist/react/components/menu.js +31 -0
- package/dist/react/components/multi-select-picker.d.ts +49 -0
- package/dist/react/components/multi-select-picker.js +20 -0
- package/dist/react/components/note.d.ts +33 -0
- package/dist/react/components/note.js +8 -0
- package/dist/react/components/number-field.d.ts +16 -0
- package/dist/react/components/number-field.js +17 -0
- package/dist/react/components/pagination.d.ts +45 -0
- package/dist/react/components/pagination.js +87 -0
- package/dist/react/components/persistent-action-banner.d.ts +71 -0
- package/dist/react/components/persistent-action-banner.js +39 -0
- package/dist/react/components/popconfirm.d.ts +42 -0
- package/dist/react/components/popconfirm.js +59 -0
- package/dist/react/components/popover.d.ts +16 -0
- package/dist/react/components/popover.js +21 -0
- package/dist/react/components/progress-and-gauge.d.ts +44 -0
- package/dist/react/components/progress-and-gauge.js +19 -0
- package/dist/react/components/radio.d.ts +7 -0
- package/dist/react/components/radio.js +13 -0
- package/dist/react/components/rich-text-toolbar.d.ts +26 -0
- package/dist/react/components/rich-text-toolbar.js +64 -0
- package/dist/react/components/rows-and-card.d.ts +88 -0
- package/dist/react/components/rows-and-card.js +10 -0
- package/dist/react/components/save-status-indicator.d.ts +11 -0
- package/dist/react/components/save-status-indicator.js +33 -0
- package/dist/react/components/segmented-control.d.ts +21 -0
- package/dist/react/components/segmented-control.js +15 -0
- package/dist/react/components/select.d.ts +29 -0
- package/dist/react/components/select.js +29 -0
- package/dist/react/components/selectable-cards.d.ts +30 -0
- package/dist/react/components/selectable-cards.js +29 -0
- package/dist/react/components/settings-rows.d.ts +51 -0
- package/dist/react/components/settings-rows.js +8 -0
- package/dist/react/components/sidebar-nav.d.ts +77 -0
- package/dist/react/components/sidebar-nav.js +41 -0
- package/dist/react/components/skeleton-and-empty.d.ts +51 -0
- package/dist/react/components/skeleton-and-empty.js +14 -0
- package/dist/react/components/slider.d.ts +19 -0
- package/dist/react/components/slider.js +17 -0
- package/dist/react/components/small-charts.d.ts +60 -0
- package/dist/react/components/small-charts.js +76 -0
- package/dist/react/components/sortable.d.ts +30 -0
- package/dist/react/components/sortable.js +85 -0
- package/dist/react/components/split-button.d.ts +38 -0
- package/dist/react/components/split-button.js +43 -0
- package/dist/react/components/split-panes.d.ts +35 -0
- package/dist/react/components/split-panes.js +102 -0
- package/dist/react/components/stat-tiles.d.ts +54 -0
- package/dist/react/components/stat-tiles.js +8 -0
- package/dist/react/components/steps.d.ts +11 -0
- package/dist/react/components/steps.js +13 -0
- package/dist/react/components/switch.d.ts +6 -0
- package/dist/react/components/switch.js +13 -0
- package/dist/react/components/table.d.ts +61 -0
- package/dist/react/components/table.js +6 -0
- package/dist/react/components/tabs.d.ts +7 -0
- package/dist/react/components/tabs.js +13 -0
- package/dist/react/components/tags.d.ts +33 -0
- package/dist/react/components/tags.js +83 -0
- package/dist/react/components/textarea.d.ts +10 -0
- package/dist/react/components/textarea.js +6 -0
- package/dist/react/components/toast.d.ts +35 -0
- package/dist/react/components/toast.js +90 -0
- package/dist/react/components/tooltip-and-key.d.ts +20 -0
- package/dist/react/components/tooltip-and-key.js +20 -0
- package/dist/react/components/tree-view.d.ts +34 -0
- package/dist/react/components/tree-view.js +118 -0
- package/dist/react/components/upload-dropzone.d.ts +92 -0
- package/dist/react/components/upload-dropzone.js +116 -0
- package/dist/react/components/wizard-shell.d.ts +78 -0
- package/dist/react/components/wizard-shell.js +176 -0
- package/dist/react/fonts.css.d.ts +1 -0
- package/dist/react/index.d.ts +76 -0
- package/dist/react/index.js +77 -0
- package/dist/react/integrations/quickcrop.d.ts +58 -0
- package/dist/react/integrations/quickcrop.js +137 -0
- package/dist/react/integrations/richclay.d.ts +43 -0
- package/dist/react/integrations/richclay.js +129 -0
- package/dist/react/internal/classes.d.ts +3 -0
- package/dist/react/internal/classes.js +10 -0
- package/dist/react/internal/close-mark.d.ts +1 -0
- package/dist/react/internal/close-mark.js +10 -0
- package/dist/react/internal/controllable-state.d.ts +9 -0
- package/dist/react/internal/controllable-state.js +22 -0
- package/dist/react/internal/drop-effect.d.ts +28 -0
- package/dist/react/internal/drop-effect.js +8 -0
- package/dist/react/internal/form-reset.d.ts +2 -0
- package/dist/react/internal/form-reset.js +22 -0
- package/dist/react/internal/format.d.ts +1 -0
- package/dist/react/internal/format.js +14 -0
- package/dist/react/internal/link.d.ts +2 -0
- package/dist/react/internal/link.js +8 -0
- package/dist/react/internal/part.d.ts +20 -0
- package/dist/react/internal/part.js +47 -0
- package/dist/react/internal/pointer-session.d.ts +8 -0
- package/dist/react/internal/pointer-session.js +60 -0
- package/dist/react/internal/refs.d.ts +2 -0
- package/dist/react/internal/refs.js +22 -0
- package/dist/react/internal/rich-text.d.ts +28 -0
- package/dist/react/internal/rich-text.js +4 -0
- package/dist/react/internal/table-context.d.ts +39 -0
- package/dist/react/internal/table-context.js +12 -0
- package/dist/react/internal/use-object-url.d.ts +1 -0
- package/dist/react/internal/use-object-url.js +15 -0
- package/dist/react/layout.css +274 -0
- package/dist/react/layout.css.d.ts +1 -0
- package/dist/react/models/filter.d.ts +31 -0
- package/dist/react/models/filter.js +79 -0
- package/dist/react/models/kanban.d.ts +51 -0
- package/dist/react/models/kanban.js +35 -0
- package/dist/react/models/reorder.d.ts +9 -0
- package/dist/react/models/reorder.js +23 -0
- package/dist/react/models/table.d.ts +85 -0
- package/dist/react/models/table.js +144 -0
- package/dist/react/models/tree.d.ts +16 -0
- package/dist/react/models/tree.js +26 -0
- package/dist/react/provider.d.ts +18 -0
- package/dist/react/provider.js +31 -0
- package/dist/react/recipes/context-hypercms.d.ts +17 -0
- package/dist/react/recipes/context-hypercms.js +71 -0
- package/dist/react/recipes/context-quickcrop.d.ts +8 -0
- package/dist/react/recipes/context-quickcrop.js +49 -0
- package/dist/react/theme.css +8004 -0
- package/dist/react/theme.css.d.ts +1 -0
- package/examples/README.md +34 -0
- package/examples/components/activity-feed.html +28 -0
- package/examples/components/app-shell.html +12 -0
- package/examples/components/app-toolbar.html +38 -0
- package/examples/components/avatar-group.html +27 -0
- package/examples/components/breadcrumbs.html +26 -0
- package/examples/components/bulk-bar.html +22 -0
- package/examples/components/code-block.html +12 -0
- package/examples/components/code-editor.html +25 -0
- package/examples/components/column-manager.html +9 -0
- package/examples/components/combobox.html +13 -0
- package/examples/components/context-hypercms.css +41 -0
- package/examples/components/context-hypercms.html +40 -0
- package/examples/components/context-hypercms.js +62 -0
- package/examples/components/context-quickcrop.html +137 -0
- package/examples/components/diff-view.html +9 -0
- package/examples/components/drawer.html +11 -0
- package/examples/components/editable-grid-cells.html +9 -0
- package/examples/components/file-browser.html +53 -0
- package/examples/components/file-tree.html +34 -0
- package/examples/components/files-table.html +80 -0
- package/examples/components/filter-builder.html +11 -0
- package/examples/components/form-error-summary.html +11 -0
- package/examples/components/index.html +51 -0
- package/examples/components/inline-edit.html +42 -0
- package/examples/components/kanban-board.html +42 -0
- package/examples/components/manifest.json +3279 -0
- package/examples/components/modal.html +11 -0
- package/examples/components/multi-select-picker.html +13 -0
- package/examples/components/pagination.html +32 -0
- package/examples/components/persistent-action-banner.html +23 -0
- package/examples/components/popconfirm.html +13 -0
- package/examples/components/popover-menu.html +13 -0
- package/examples/components/reference/baseline/DECISIONS.md +85 -0
- package/examples/components/reference/baseline/README.md +58 -0
- package/examples/components/reference/baseline/bevel-fonts.css +16 -0
- package/examples/components/reference/baseline/bevel.css +1339 -0
- package/examples/components/reference/baseline/bevel.js +496 -0
- package/examples/components/reference/baseline/fonts/LICENSE-ibm-plex-mono.txt +93 -0
- package/examples/components/reference/baseline/fonts/LICENSE-ibm-plex-sans.txt +93 -0
- package/examples/components/reference/baseline/fonts/LICENSE-newsreader.txt +93 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-400-italic.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/examples/components/reference/fable/IMPROVEMENTS.md +106 -0
- package/examples/components/reference/fable/PARENT-REPAIRS.md +17 -0
- package/examples/components/reference/fable/__pycache__/build_repairs.cpython-311.pyc +0 -0
- package/examples/components/reference/fable/__pycache__/kanban.cpython-311.pyc +0 -0
- package/examples/components/reference/fable/as-authored/IMPROVEMENTS-completed.md +106 -0
- package/examples/components/reference/fable/as-authored/base-astra.css +17 -0
- package/examples/components/reference/fable/as-authored/base-parent.css +416 -0
- package/examples/components/reference/fable/as-authored/build.py +858 -0
- package/examples/components/reference/fable/as-authored/fable.css +823 -0
- package/examples/components/reference/fable/as-authored/kanban-completed.css +235 -0
- package/examples/components/reference/fable/as-authored/kanban-completed.html +53 -0
- package/examples/components/reference/fable/as-authored/kanban-completed.py +212 -0
- package/examples/components/reference/fable/as-authored/kanban.py +207 -0
- package/examples/components/reference/fable/base-astra.css +17 -0
- package/examples/components/reference/fable/base-parent.css +416 -0
- package/examples/components/reference/fable/build.py +858 -0
- package/examples/components/reference/fable/build_repairs.py +25 -0
- package/examples/components/reference/fable/components/activity-feed.html +3 -0
- package/examples/components/reference/fable/components/app-toolbar.html +3 -0
- package/examples/components/reference/fable/components/avatar-group.html +4 -0
- package/examples/components/reference/fable/components/breadcrumbs.html +3 -0
- package/examples/components/reference/fable/components/code-block.html +9 -0
- package/examples/components/reference/fable/components/column-manager.html +3 -0
- package/examples/components/reference/fable/components/combobox.html +6 -0
- package/examples/components/reference/fable/components/context-hypercms.html +3 -0
- package/examples/components/reference/fable/components/context-quickcrop.html +4 -0
- package/examples/components/reference/fable/components/diff-view.html +13 -0
- package/examples/components/reference/fable/components/drawer.html +3 -0
- package/examples/components/reference/fable/components/editable-grid-cells.html +3 -0
- package/examples/components/reference/fable/components/filter-builder.html +3 -0
- package/examples/components/reference/fable/components/form-error-summary.html +4 -0
- package/examples/components/reference/fable/components/inline-edit.html +3 -0
- package/examples/components/reference/fable/components/kanban-board.html +53 -0
- package/examples/components/reference/fable/components/modal.html +3 -0
- package/examples/components/reference/fable/components/multi-select-picker.html +5 -0
- package/examples/components/reference/fable/components/pagination.html +3 -0
- package/examples/components/reference/fable/components/persistent-action-banner.html +4 -0
- package/examples/components/reference/fable/components/popconfirm.html +5 -0
- package/examples/components/reference/fable/components/popover-menu.html +3 -0
- package/examples/components/reference/fable/components/rich-text-toolbar.html +3 -0
- package/examples/components/reference/fable/components/save-status-indicator.html +2 -0
- package/examples/components/reference/fable/components/selectable-cards.html +3 -0
- package/examples/components/reference/fable/components/settings-rows.html +3 -0
- package/examples/components/reference/fable/components/sidebar-nav.html +4 -0
- package/examples/components/reference/fable/components/small-charts.html +5 -0
- package/examples/components/reference/fable/components/split-button.html +4 -0
- package/examples/components/reference/fable/components/split-panes.html +8 -0
- package/examples/components/reference/fable/components/stat-tiles.html +3 -0
- package/examples/components/reference/fable/components/toast-stack.html +5 -0
- package/examples/components/reference/fable/components/tree-view.html +4 -0
- package/examples/components/reference/fable/components/upload-dropzone.html +6 -0
- package/examples/components/reference/fable/components/wizard-shell.html +3 -0
- package/examples/components/reference/fable/coverage.json +414 -0
- package/examples/components/reference/fable/fable.css +823 -0
- package/examples/components/reference/fable/index.html +5 -0
- package/examples/components/reference/fable/kanban.css +237 -0
- package/examples/components/reference/fable/kanban.py +212 -0
- package/examples/components/reference/fable/parent-repairs.css +9 -0
- package/examples/components/reference/fixtures/data.json +102 -0
- package/examples/components/reference/fixtures/host.js +47 -0
- package/examples/components/reference/fixtures/static-host.js +39 -0
- package/examples/components/reference/fixtures/workspace.jpg +0 -0
- package/examples/components/reference/parent/CHANGES.md +179 -0
- package/examples/components/reference/parent/__pycache__/kanban.cpython-311.pyc +0 -0
- package/examples/components/reference/parent/base-astra.css +17 -0
- package/examples/components/reference/parent/base-parent.css +416 -0
- package/examples/components/reference/parent/build.py +148 -0
- package/examples/components/reference/parent/components/activity-feed.html +2 -0
- package/examples/components/reference/parent/components/app-toolbar.html +2 -0
- package/examples/components/reference/parent/components/avatar-group.html +4 -0
- package/examples/components/reference/parent/components/breadcrumbs.html +2 -0
- package/examples/components/reference/parent/components/code-block.html +8 -0
- package/examples/components/reference/parent/components/column-manager.html +2 -0
- package/examples/components/reference/parent/components/combobox.html +6 -0
- package/examples/components/reference/parent/components/context-hypercms.html +2 -0
- package/examples/components/reference/parent/components/context-quickcrop.html +4 -0
- package/examples/components/reference/parent/components/diff-view.html +4 -0
- package/examples/components/reference/parent/components/drawer.html +2 -0
- package/examples/components/reference/parent/components/editable-grid-cells.html +2 -0
- package/examples/components/reference/parent/components/filter-builder.html +2 -0
- package/examples/components/reference/parent/components/form-error-summary.html +2 -0
- package/examples/components/reference/parent/components/inline-edit.html +2 -0
- package/examples/components/reference/parent/components/kanban-board.html +84 -0
- package/examples/components/reference/parent/components/modal.html +2 -0
- package/examples/components/reference/parent/components/multi-select-picker.html +5 -0
- package/examples/components/reference/parent/components/pagination.html +2 -0
- package/examples/components/reference/parent/components/persistent-action-banner.html +4 -0
- package/examples/components/reference/parent/components/popconfirm.html +5 -0
- package/examples/components/reference/parent/components/popover-menu.html +2 -0
- package/examples/components/reference/parent/components/rich-text-toolbar.html +2 -0
- package/examples/components/reference/parent/components/save-status-indicator.html +2 -0
- package/examples/components/reference/parent/components/selectable-cards.html +2 -0
- package/examples/components/reference/parent/components/settings-rows.html +2 -0
- package/examples/components/reference/parent/components/sidebar-nav.html +4 -0
- package/examples/components/reference/parent/components/small-charts.html +2 -0
- package/examples/components/reference/parent/components/split-button.html +5 -0
- package/examples/components/reference/parent/components/split-panes.html +8 -0
- package/examples/components/reference/parent/components/stat-tiles.html +2 -0
- package/examples/components/reference/parent/components/toast-stack.html +5 -0
- package/examples/components/reference/parent/components/tree-view.html +4 -0
- package/examples/components/reference/parent/components/upload-dropzone.html +6 -0
- package/examples/components/reference/parent/components/wizard-shell.html +2 -0
- package/examples/components/reference/parent/coverage.json +414 -0
- package/examples/components/reference/parent/index.html +1 -0
- package/examples/components/reference/parent/kanban.css +114 -0
- package/examples/components/reference/parent/kanban.py +55 -0
- package/examples/components/reference/parent/refinement.css +53 -0
- package/examples/components/reference/selected/build.py +70 -0
- package/examples/components/reference/selected/components/activity-feed.html +3 -0
- package/examples/components/reference/selected/components/app-toolbar.html +3 -0
- package/examples/components/reference/selected/components/avatar-group.html +4 -0
- package/examples/components/reference/selected/components/breadcrumbs.html +3 -0
- package/examples/components/reference/selected/components/code-block.html +9 -0
- package/examples/components/reference/selected/components/column-manager.html +3 -0
- package/examples/components/reference/selected/components/combobox.html +6 -0
- package/examples/components/reference/selected/components/context-hypercms.html +3 -0
- package/examples/components/reference/selected/components/context-quickcrop.html +4 -0
- package/examples/components/reference/selected/components/diff-view.html +13 -0
- package/examples/components/reference/selected/components/drawer.html +3 -0
- package/examples/components/reference/selected/components/editable-grid-cells.html +3 -0
- package/examples/components/reference/selected/components/filter-builder.html +3 -0
- package/examples/components/reference/selected/components/form-error-summary.html +4 -0
- package/examples/components/reference/selected/components/inline-edit.html +3 -0
- package/examples/components/reference/selected/components/kanban-board.html +53 -0
- package/examples/components/reference/selected/components/modal.html +2 -0
- package/examples/components/reference/selected/components/multi-select-picker.html +5 -0
- package/examples/components/reference/selected/components/pagination.html +3 -0
- package/examples/components/reference/selected/components/persistent-action-banner.html +4 -0
- package/examples/components/reference/selected/components/popconfirm.html +5 -0
- package/examples/components/reference/selected/components/popover-menu.html +3 -0
- package/examples/components/reference/selected/components/rich-text-toolbar.html +3 -0
- package/examples/components/reference/selected/components/save-status-indicator.html +2 -0
- package/examples/components/reference/selected/components/selectable-cards.html +3 -0
- package/examples/components/reference/selected/components/settings-rows.html +3 -0
- package/examples/components/reference/selected/components/sidebar-nav.html +4 -0
- package/examples/components/reference/selected/components/small-charts.html +5 -0
- package/examples/components/reference/selected/components/split-button.html +4 -0
- package/examples/components/reference/selected/components/split-panes.html +8 -0
- package/examples/components/reference/selected/components/stat-tiles.html +3 -0
- package/examples/components/reference/selected/components/toast-stack.html +5 -0
- package/examples/components/reference/selected/components/tree-view.html +4 -0
- package/examples/components/reference/selected/components/upload-dropzone.html +6 -0
- package/examples/components/reference/selected/components/wizard-shell.html +3 -0
- package/examples/components/reference/selected/corrections.css +11 -0
- package/examples/components/reference/selected/coverage.json +413 -0
- package/examples/components/reference/selected/gallery-build.mjs +49 -0
- package/examples/components/reference/selected/gallery-spec.json +12 -0
- package/examples/components/reference/selected/index.html +1 -0
- package/examples/components/reference/selected/kanban-comparison.json +6 -0
- package/examples/components/reference/selected/manifest.json +476 -0
- package/examples/components/reference/sha256.json +142 -0
- package/examples/components/reference/sources/diagonal-close.svg +1 -0
- package/examples/components/reference/sources/hyperclay/public-assets/css/hyperclay-app-source.css +614 -0
- package/examples/components/reference/sources/hyperclay/public-assets/js/app.js +94 -0
- package/examples/components/reference/sources/hyperclayjs/src/ui/theModal.js +897 -0
- package/examples/components/reference/sources/hypercms/scripts/build-theme.js +203 -0
- package/examples/components/reference/sources/hypercms/src/anchor.js +24 -0
- package/examples/components/reference/sources/hypercms/src/apply-loop.js +167 -0
- package/examples/components/reference/sources/hypercms/src/diagnostics.js +44 -0
- package/examples/components/reference/sources/hypercms/src/enhance.js +173 -0
- package/examples/components/reference/sources/hypercms/src/events.js +955 -0
- package/examples/components/reference/sources/hypercms/src/form-builder.js +518 -0
- package/examples/components/reference/sources/hypercms/src/form-rules.js +208 -0
- package/examples/components/reference/sources/hypercms/src/hooks.js +100 -0
- package/examples/components/reference/sources/hypercms/src/hypercms-bundle.js +10 -0
- package/examples/components/reference/sources/hypercms/src/hypercms.js +347 -0
- package/examples/components/reference/sources/hypercms/src/morph.js +10 -0
- package/examples/components/reference/sources/hypercms/src/path.js +61 -0
- package/examples/components/reference/sources/hypercms/src/place.js +131 -0
- package/examples/components/reference/sources/hypercms/src/platform.js +131 -0
- package/examples/components/reference/sources/hypercms/src/refresh.js +87 -0
- package/examples/components/reference/sources/hypercms/src/richclay-bridge.js +140 -0
- package/examples/components/reference/sources/hypercms/src/row-identity.js +111 -0
- package/examples/components/reference/sources/hypercms/src/scaffold.js +10 -0
- package/examples/components/reference/sources/hypercms/src/session.js +303 -0
- package/examples/components/reference/sources/hypercms/src/shell.js +248 -0
- package/examples/components/reference/sources/hypercms/src/styles.css +298 -0
- package/examples/components/reference/sources/hypercms/src/targets.js +144 -0
- package/examples/components/reference/sources/hypercms/src/templates.js +656 -0
- package/examples/components/reference/sources/hypercms/src/theme/pixel-quiet.overrides.css +761 -0
- package/examples/components/reference/sources/hypercms/src/theme.generated.css +1772 -0
- package/examples/components/reference/sources/hypercms/src/toggle.js +666 -0
- package/examples/components/reference/sources/hypercms/src/unresolved.js +211 -0
- package/examples/components/reference/sources/hypercms/src/vendor/mirk.vendor.js +212 -0
- package/examples/components/reference/sources/hypercms/src/views/inline-layer.js +421 -0
- package/examples/components/reference/sources/hypercms/src/views/inline.js +1072 -0
- package/examples/components/reference/sources/hypercms/src/views/sidebar.js +97 -0
- package/examples/components/reference/sources/modal-reference.html +301 -0
- package/examples/components/reference/sources/quickcrop/quickcrop.css +124 -0
- package/examples/components/reference/sources/quickcrop/quickcrop.js +400 -0
- package/examples/components/reference/sources/richclay/richclay.css +416 -0
- package/examples/components/reference/sources/richclay/richclay.js +2962 -0
- package/examples/components/reference/sources/richclay/src/a11y.js +67 -0
- package/examples/components/reference/sources/richclay/src/browser-global.js +7 -0
- package/examples/components/reference/sources/richclay/src/buttons.js +254 -0
- package/examples/components/reference/sources/richclay/src/hyperclay.js +470 -0
- package/examples/components/reference/sources/richclay/src/normalize.js +380 -0
- package/examples/components/reference/sources/richclay/src/richclay.js +1356 -0
- package/examples/components/reference/sources/richclay/src/sanitize.js +189 -0
- package/examples/components/reference/sources/richclay/src/styles.js +23 -0
- package/examples/components/reference/sources/richclay/src/toolbar-float.js +175 -0
- package/examples/components/reference/sources/richclay/src/toolbar.js +394 -0
- package/examples/components/reference/sources/richclay/src/vendor-bundle.js +15 -0
- package/examples/components/reference/sources/richclay/vendor/DOMPURIFY-LICENSE +202 -0
- package/examples/components/reference/sources/richclay/vendor/DOMPURIFY-LICENSE-MPL +373 -0
- package/examples/components/reference/sources/richclay/vendor/SQUIRE-LICENSE +21 -0
- package/examples/components/reference/sources/richclay/vendor/purify.es.mjs +2249 -0
- package/examples/components/reference/sources/richclay/vendor/purify.min.js +3 -0
- package/examples/components/reference/sources/richclay/vendor/squire.js +22 -0
- package/examples/components/reference/sources/richclay/vendor/squire.mjs +22 -0
- package/examples/components/reference/sources/richclay/vendor/squire.provenance.json +19 -0
- package/examples/components/rich-text-toolbar.html +43 -0
- package/examples/components/save-status-indicator.html +11 -0
- package/examples/components/selectable-cards.html +9 -0
- package/examples/components/settings-rows.html +10 -0
- package/examples/components/sidebar-nav.html +9 -0
- package/examples/components/small-charts.html +21 -0
- package/examples/components/split-button.html +13 -0
- package/examples/components/split-panes-editor.html +6 -0
- package/examples/components/split-panes.html +9 -0
- package/examples/components/stat-tiles.html +23 -0
- package/examples/components/toast-stack.html +21 -0
- package/examples/components/tree-view.html +9 -0
- package/examples/components/upload-dropzone.html +88 -0
- package/examples/components/wizard-shell.html +50 -0
- package/examples/datastar/index.html +38 -0
- package/examples/datastar/server.mjs +31 -0
- package/examples/fixtures/field-guide.pdf +14 -0
- package/examples/fixtures/workspace.jpg +0 -0
- package/examples/setup-peers.mjs +35 -0
- package/fonts/LICENSE-ibm-plex-mono.txt +93 -0
- package/fonts/LICENSE-ibm-plex-sans.txt +93 -0
- package/fonts/LICENSE-newsreader.txt +93 -0
- package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-400-italic.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
- package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/icons/LICENSE +21 -0
- package/icons/house.json +71 -0
- package/icons/icons.d.ts +17 -0
- package/icons/icons.js +648 -0
- package/icons/icons.svg +126 -0
- package/icons/marks/build-tray.source.mjs +45 -0
- package/icons/marks/hyperclay-local.svg +11 -0
- package/icons/phosphor.json +53 -0
- package/icons/round.json +8 -0
- package/integrations/datastar.js +202 -0
- package/integrations/quickcrop.css +26 -0
- package/integrations/quickcrop.js +56 -0
- package/integrations/richclay.css +100 -0
- package/integrations/richclay.js +131 -0
- package/package.json +630 -0
- package/react/README.md +244 -0
- package/src/README.md +83 -0
- package/vendor/datastar/datastar-1.0.3.js +10 -0
- package/vendor/datastar/provenance.json +6 -0
- package/vendor/floating-ui/LICENSE-core +20 -0
- package/vendor/floating-ui/LICENSE-dom +20 -0
- package/vendor/floating-ui/floating-ui.core.umd.min.js +1 -0
- package/vendor/floating-ui/floating-ui.dom.umd.min.js +1 -0
- package/vendor/floating-ui/provenance.json +17 -0
- package/vendor/richclay/LICENSE +16 -0
- package/vendor/richclay/THIRD-PARTY-NOTICES.md +45 -0
- package/vendor/richclay/provenance.json +17 -0
- package/vendor/richclay/richclay.css +404 -0
- package/vendor/richclay/richclay.js +2940 -0
- package/vendor/richclay/vendor/DOMPURIFY-LICENSE +202 -0
- package/vendor/richclay/vendor/DOMPURIFY-LICENSE-MPL +373 -0
- package/vendor/richclay/vendor/SQUIRE-LICENSE +21 -0
- package/vendor/richclay/vendor/purify.min.js +3 -0
- package/vendor/richclay/vendor/squire.js +22 -0
- package/vendor/richclay/vendor/squire.provenance.json +30 -0
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDefaultRegistry,
|
|
3
|
+
isApplePlatform,
|
|
4
|
+
presets
|
|
5
|
+
} from "./buttons.js";
|
|
6
|
+
import {
|
|
7
|
+
captureRange,
|
|
8
|
+
caretEdge,
|
|
9
|
+
editorRootNeedsNormalization,
|
|
10
|
+
ejectsBlocks,
|
|
11
|
+
flattenBlocks,
|
|
12
|
+
hasBlockDescendant,
|
|
13
|
+
isCaretHost,
|
|
14
|
+
isInlineTag,
|
|
15
|
+
isUnsupportedRootTag,
|
|
16
|
+
keepsTextShape,
|
|
17
|
+
nodeLength,
|
|
18
|
+
normalizeEditorRoot,
|
|
19
|
+
restoreRange,
|
|
20
|
+
unsupportedRootReason
|
|
21
|
+
} from "./normalize.js";
|
|
22
|
+
import { Toolbar } from "./toolbar.js";
|
|
23
|
+
import { FloatingToolbar } from "./toolbar-float.js";
|
|
24
|
+
import {
|
|
25
|
+
createSanitizer,
|
|
26
|
+
flattenFragmentToSingleLine,
|
|
27
|
+
inlineSanitizeConfig,
|
|
28
|
+
normalizeUrl,
|
|
29
|
+
sanitizeHTML as sanitizeHTMLString,
|
|
30
|
+
sanitizeElement as sanitizeElementInPlace
|
|
31
|
+
} from "./sanitize.js";
|
|
32
|
+
import {
|
|
33
|
+
RICHCLAY_SELECTOR,
|
|
34
|
+
isRichClayHost,
|
|
35
|
+
consumeInertContenteditable,
|
|
36
|
+
installHyperclayBridge,
|
|
37
|
+
isHyperclayEditMode,
|
|
38
|
+
markChrome,
|
|
39
|
+
parseEditableOptions,
|
|
40
|
+
removeRuntimeState,
|
|
41
|
+
shouldActivateEditor,
|
|
42
|
+
shouldUseHyperclay,
|
|
43
|
+
stripRichClayFromClone
|
|
44
|
+
} from "./hyperclay.js";
|
|
45
|
+
import { ensureStyles } from "./styles.js";
|
|
46
|
+
import {
|
|
47
|
+
announce,
|
|
48
|
+
cloneRange,
|
|
49
|
+
connectDescription,
|
|
50
|
+
createLiveRegion,
|
|
51
|
+
getSquireSelection,
|
|
52
|
+
restoreSquireSelection,
|
|
53
|
+
setRuntimeAttribute
|
|
54
|
+
} from "./a11y.js";
|
|
55
|
+
|
|
56
|
+
const instances = new WeakMap();
|
|
57
|
+
const autoInitWindows = new WeakSet();
|
|
58
|
+
const watchedWindows = new WeakSet();
|
|
59
|
+
const globalRegistry = createDefaultRegistry();
|
|
60
|
+
const KEEP_FOCUS = Symbol("richclay-keep-focus");
|
|
61
|
+
const ROOT_TRANSFER_EVENTS = ["cut", "paste", "drop"];
|
|
62
|
+
const MODIFIER_ORDER = ["Alt", "Ctrl", "Meta", "Shift"];
|
|
63
|
+
let dialogSeq = 0;
|
|
64
|
+
|
|
65
|
+
const defaultOptions = {
|
|
66
|
+
toolbar: "standard",
|
|
67
|
+
toolbarContainer: null,
|
|
68
|
+
sanitize: {},
|
|
69
|
+
placeholder: "",
|
|
70
|
+
readOnly: false,
|
|
71
|
+
hyperclay: "auto",
|
|
72
|
+
onChange: null,
|
|
73
|
+
Squire: null,
|
|
74
|
+
inline: false,
|
|
75
|
+
singleLine: false,
|
|
76
|
+
toolbarOnSelect: false
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default class RichClay {
|
|
80
|
+
static presets = presets;
|
|
81
|
+
static selector = RICHCLAY_SELECTOR;
|
|
82
|
+
|
|
83
|
+
constructor(element, options = {}) {
|
|
84
|
+
if (!element) throw new Error("RichClay requires an element.");
|
|
85
|
+
const existing = instances.get(element);
|
|
86
|
+
if (existing) return existing;
|
|
87
|
+
|
|
88
|
+
this.element = element;
|
|
89
|
+
this.unsupported = conflictsWithExistingEditor(element) || refuseUnsupportedRoot(element);
|
|
90
|
+
const derived = { ...parseEditableOptions(element), ...options };
|
|
91
|
+
this.options = { ...defaultOptions, ...derived };
|
|
92
|
+
if (this.options.singleLine && !("toolbar" in derived)) {
|
|
93
|
+
this.options.toolbar = "inline";
|
|
94
|
+
}
|
|
95
|
+
// Squire's _ensureBottomLine() appends a fresh default block whenever the
|
|
96
|
+
// root's last element child differs from blockTag, and it runs from every
|
|
97
|
+
// Backspace and Delete. With "P", any inline region ending in an <h2>, a
|
|
98
|
+
// <ul>, or the repair's own wrapper gained a permanent empty margined
|
|
99
|
+
// paragraph on the first delete. DIV also matches what Squire's fixContainer
|
|
100
|
+
// produces and carries no margins, which is what inline mode needs.
|
|
101
|
+
this._blockTag = this.options.inline ? "DIV" : "P";
|
|
102
|
+
this.registry = new Map(globalRegistry);
|
|
103
|
+
this.toolbar = null;
|
|
104
|
+
this.float = null;
|
|
105
|
+
this._onToolbarKey = null;
|
|
106
|
+
this._onFloatFocusOut = null;
|
|
107
|
+
this._onDocPointerDown = null;
|
|
108
|
+
this.liveRegion = null;
|
|
109
|
+
this.description = null;
|
|
110
|
+
this.dialog = null;
|
|
111
|
+
this.savedSelection = null;
|
|
112
|
+
this.path = "";
|
|
113
|
+
this.active = false;
|
|
114
|
+
this._squire = null;
|
|
115
|
+
this._squireListeners = [];
|
|
116
|
+
this._onBeforeInput = null;
|
|
117
|
+
this._onRootBeforeInput = null;
|
|
118
|
+
this._onRootKeydown = null;
|
|
119
|
+
this._onRootTransfer = null;
|
|
120
|
+
this._shortcutKeys = [];
|
|
121
|
+
this._appleDeleteKeys = new Set();
|
|
122
|
+
this._authoredPres = new WeakSet();
|
|
123
|
+
this._warnedInlineBlock = false;
|
|
124
|
+
this._onFocus = () => {
|
|
125
|
+
this.element.classList.add("richclay-focused");
|
|
126
|
+
if (this.options.inline) this.ensureFloatingToolbar();
|
|
127
|
+
};
|
|
128
|
+
this._onBlur = event => {
|
|
129
|
+
this.element.classList.remove("richclay-focused");
|
|
130
|
+
if (this.options.inline) this.scheduleFloatTeardown(event);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
this.ensureMarker();
|
|
134
|
+
this.hyperclay = shouldUseHyperclay(this.options, this.window);
|
|
135
|
+
if (this.hyperclay) installHyperclayBridge(this.window);
|
|
136
|
+
|
|
137
|
+
instances.set(element, this);
|
|
138
|
+
|
|
139
|
+
this.sanitizeConfig = this.options.inline
|
|
140
|
+
? inlineSanitizeConfig(this.options.sanitize)
|
|
141
|
+
: this.options.sanitize;
|
|
142
|
+
this.sanitizer = createSanitizer(this.sanitizeConfig, this.element.ownerDocument);
|
|
143
|
+
if (!this.options.inline) {
|
|
144
|
+
// Card editors sanitize their region up front. Inline editors must not:
|
|
145
|
+
// the content is the page's own live DOM, and rewriting it on activation
|
|
146
|
+
// would destroy author markup (classes, images, data attributes).
|
|
147
|
+
sanitizeElementInPlace(this.element, this.sanitizeConfig);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (shouldActivateEditor(this.options, this.window)) {
|
|
151
|
+
this.activate();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get squire() {
|
|
156
|
+
return this._squire;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get window() {
|
|
160
|
+
return this.element.ownerDocument.defaultView || window;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static init(selector = RICHCLAY_SELECTOR, options = {}) {
|
|
164
|
+
// A caller who names its own hosts means them, custom elements included; the
|
|
165
|
+
// custom-element guard only applies to the default catch-all selector.
|
|
166
|
+
const guard = selector === RICHCLAY_SELECTOR ? isRichClayHost : () => true;
|
|
167
|
+
const elements = resolveElements(selector)
|
|
168
|
+
.filter(guard)
|
|
169
|
+
.filter(element => !conflictsWithExistingEditor(element));
|
|
170
|
+
return elements.map(element => new RichClay(element, options));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static autoInit(win = typeof window !== "undefined" ? window : undefined) {
|
|
174
|
+
if (!win || !win.document || autoInitWindows.has(win)) return;
|
|
175
|
+
autoInitWindows.add(win);
|
|
176
|
+
const run = () => {
|
|
177
|
+
if (shouldUseHyperclay({}, win) && isHyperclayEditMode(win)) {
|
|
178
|
+
RichClay.init();
|
|
179
|
+
RichClay.watch(win);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
if (win.document.readyState === "loading") {
|
|
183
|
+
win.document.addEventListener("DOMContentLoaded", run, { once: true });
|
|
184
|
+
} else {
|
|
185
|
+
run();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static watch(win = typeof window !== "undefined" ? window : undefined, options = {}) {
|
|
190
|
+
if (!win || !win.document || watchedWindows.has(win)) return;
|
|
191
|
+
watchedWindows.add(win);
|
|
192
|
+
|
|
193
|
+
const mount = element => {
|
|
194
|
+
if (!isMountable(element)) return;
|
|
195
|
+
if (!instances.has(element) && !conflictsWithExistingEditor(element)) new RichClay(element, options);
|
|
196
|
+
};
|
|
197
|
+
const unmount = element => instances.get(element)?.destroy();
|
|
198
|
+
|
|
199
|
+
const observer = new win.MutationObserver(records => {
|
|
200
|
+
records.forEach(record => {
|
|
201
|
+
if (record.type === "attributes") {
|
|
202
|
+
const target = record.target;
|
|
203
|
+
// Matching the selector is not enough to keep an editor alive. A custom
|
|
204
|
+
// element carrying a bare `editable` matches it and is still refused by
|
|
205
|
+
// the host guard, so deciding by the selector alone left such an element
|
|
206
|
+
// mounted forever with its runtime chrome saved into the file.
|
|
207
|
+
if (isMountable(target)) mount(target);
|
|
208
|
+
else unmount(target);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
record.addedNodes.forEach(node => {
|
|
212
|
+
if (node.nodeType !== 1) return;
|
|
213
|
+
if (node.matches?.(RICHCLAY_SELECTOR)) mount(node);
|
|
214
|
+
node.querySelectorAll?.(RICHCLAY_SELECTOR).forEach(mount);
|
|
215
|
+
});
|
|
216
|
+
record.removedNodes.forEach(node => {
|
|
217
|
+
if (node.nodeType !== 1) return;
|
|
218
|
+
// A node that is still connected was moved, not removed; keep its
|
|
219
|
+
// editor alive.
|
|
220
|
+
const teardown = el => {
|
|
221
|
+
if (!el.isConnected) instances.get(el)?.destroy();
|
|
222
|
+
};
|
|
223
|
+
if (node.matches?.(RICHCLAY_SELECTOR)) teardown(node);
|
|
224
|
+
node.querySelectorAll?.(RICHCLAY_SELECTOR).forEach(teardown);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
observer.observe(win.document.documentElement || win.document, {
|
|
230
|
+
childList: true,
|
|
231
|
+
subtree: true,
|
|
232
|
+
attributes: true,
|
|
233
|
+
attributeFilter: ["editable", "clay-editable", "data-richclay", "richclay"]
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static registerButton(def) {
|
|
238
|
+
validateButton(def);
|
|
239
|
+
globalRegistry.set(def.id, def);
|
|
240
|
+
return RichClay;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
static unregisterButton(id) {
|
|
244
|
+
globalRegistry.delete(id);
|
|
245
|
+
return RichClay;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static stripFromClone(docElem) {
|
|
249
|
+
stripRichClayFromClone(docElem);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Remove richclay's runtime state from ONE element, addressed by element
|
|
253
|
+
// rather than by instance.
|
|
254
|
+
//
|
|
255
|
+
// A host that grows a list by cloning a row clones an active region with it,
|
|
256
|
+
// and the copy carries contenteditable, the marker and the runtime classes.
|
|
257
|
+
// Usually there is no editor behind them and destroy() cannot reach it,
|
|
258
|
+
// because destroy belongs to an instance and that clone has none. But a clone
|
|
259
|
+
// carrying the author's own data-richclay is mountable, so the watcher can
|
|
260
|
+
// have given it an instance before the host gets here: this cannot assume the
|
|
261
|
+
// element is instanceless.
|
|
262
|
+
static stripElement(element) {
|
|
263
|
+
if (!element || typeof element.getAttribute !== "function") return;
|
|
264
|
+
// A watcher can have mounted an instance on the clone before the host got
|
|
265
|
+
// here, and then this is not the instanceless orphan described above:
|
|
266
|
+
// stripping the attributes while the instance stays in the cache leaves an
|
|
267
|
+
// editor reporting active on an element with no contenteditable, and every
|
|
268
|
+
// later constructor call hands that same dead instance back. destroy() is
|
|
269
|
+
// the same cleanup and it clears the cache too.
|
|
270
|
+
const existing = instances.get(element);
|
|
271
|
+
if (existing) {
|
|
272
|
+
existing.destroy();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
removeRuntimeState(element, "destroy");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
registerButton(def) {
|
|
279
|
+
validateButton(def);
|
|
280
|
+
this.registry.set(def.id, def);
|
|
281
|
+
if (this.active) this.renderToolbar();
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
unregisterButton(id) {
|
|
286
|
+
this.registry.delete(id);
|
|
287
|
+
if (this.active) this.renderToolbar();
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
activate() {
|
|
292
|
+
if (this.unsupported) return;
|
|
293
|
+
if (this.active) return;
|
|
294
|
+
|
|
295
|
+
this.active = true;
|
|
296
|
+
this.applyInlineBox();
|
|
297
|
+
ensureStyles(this.element.ownerDocument);
|
|
298
|
+
consumeInertContenteditable(this.element);
|
|
299
|
+
|
|
300
|
+
// Captured before Squire's constructor wipes the root. Inline mode keeps the
|
|
301
|
+
// live nodes rather than a serialized string, so activation cannot lose
|
|
302
|
+
// anything an innerHTML round trip would drop (form state, node identity).
|
|
303
|
+
const initialNodes = this.options.inline ? Array.from(this.element.childNodes) : null;
|
|
304
|
+
const initialHTML = this.options.inline ? null : this.element.innerHTML;
|
|
305
|
+
const Squire = this.options.Squire || this.window.Squire || globalThis.Squire;
|
|
306
|
+
if (!Squire) {
|
|
307
|
+
throw new Error("RichClay requires Squire. Load vendor/squire.js before richclay.js.");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this.setupEditorAttributes();
|
|
311
|
+
this.liveRegion = createLiveRegion(this.element.ownerDocument);
|
|
312
|
+
|
|
313
|
+
this._squire = new Squire(this.element, {
|
|
314
|
+
blockTag: this._blockTag,
|
|
315
|
+
sanitizeToDOMFragment: (html, editor) => {
|
|
316
|
+
const fragment = this.sanitizer.sanitizeToDOMFragment(html, editor);
|
|
317
|
+
return this.options.singleLine ? flattenFragmentToSingleLine(fragment) : fragment;
|
|
318
|
+
},
|
|
319
|
+
didError: error => {
|
|
320
|
+
console.error("RichClay/Squire error", error);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
if (this.options.inline) {
|
|
324
|
+
// Fidelity-first: Squire's constructor wiped the root; put the captured
|
|
325
|
+
// nodes back verbatim. Running them through setHTML would sanitize and
|
|
326
|
+
// re-wrap the page's own content into blocks on mere activation.
|
|
327
|
+
//
|
|
328
|
+
// The structural repair Squire needs is deliberately NOT done here. Merely
|
|
329
|
+
// opening a page in edit mode must leave it byte-identical, or a Hyperclay
|
|
330
|
+
// page with autosave on writes itself to disk for being looked at. It runs
|
|
331
|
+
// on the first real edit instead — see ensureRootIsEditable.
|
|
332
|
+
this.element.replaceChildren(...initialNodes);
|
|
333
|
+
this.resetSquireUndoBaseline();
|
|
334
|
+
} else {
|
|
335
|
+
this._squire.setHTML(initialHTML);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.bindSquire();
|
|
339
|
+
if (this.options.singleLine) this.installSingleLineGuards();
|
|
340
|
+
if (this.options.inline && !this.options.singleLine) this.installRootGuards();
|
|
341
|
+
// Before installShortcuts, so an explicitly declared Ctrl+ shortcut wins.
|
|
342
|
+
this.installAppleDeleteKeys();
|
|
343
|
+
// Squire appends a default block whenever the root's last element child is not
|
|
344
|
+
// its blockTag, from Backspace over a selection, cut, and paste. Where blocks
|
|
345
|
+
// stay out that is a block nobody asked for, and the save hook is not enough on
|
|
346
|
+
// its own: flattening the block leaves its <br> behind, the next session adds
|
|
347
|
+
// another, and the file grows a blank line per edit. Prevention, because the
|
|
348
|
+
// cleanup has a residue.
|
|
349
|
+
if (this.blocksStayOut()) this._squire._ensureBottomLine = () => {};
|
|
350
|
+
this.maskSquireCodeShortcut();
|
|
351
|
+
this.maskSquireBlockShortcuts();
|
|
352
|
+
this.installShortcuts();
|
|
353
|
+
if (this.options.inline) {
|
|
354
|
+
this._onToolbarKey = event => {
|
|
355
|
+
if (event.altKey && event.key === "F10") {
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
this.ensureFloatingToolbar();
|
|
358
|
+
this.float?.toolbar.focusFirst();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
this.element.addEventListener("keydown", this._onToolbarKey);
|
|
362
|
+
}
|
|
363
|
+
// Squire's paste path runs fixContainer over the pasted fragment, so pasting
|
|
364
|
+
// even two plain lines into a <h2 editable> lands a <div> the author never
|
|
365
|
+
// wrote. Flatten it where it arrives: this is the same promise the rest of the
|
|
366
|
+
// editor keeps, that richclay never manufactures a block in a region the
|
|
367
|
+
// author wrote as a line of text.
|
|
368
|
+
if (this.blocksStayOut()) {
|
|
369
|
+
const willPaste = event => {
|
|
370
|
+
const fragment = event.detail?.fragment;
|
|
371
|
+
if (!fragment) return;
|
|
372
|
+
const doc = this.element.ownerDocument;
|
|
373
|
+
flattenBlocks(fragment, () =>
|
|
374
|
+
this.options.singleLine ? doc.createTextNode(" ") : doc.createElement("br")
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
this._squire.addEventListener("willPaste", willPaste);
|
|
378
|
+
this._squireListeners.push(["willPaste", willPaste]);
|
|
379
|
+
}
|
|
380
|
+
this.renderToolbar();
|
|
381
|
+
this.updatePlaceholder();
|
|
382
|
+
this.warnOnBlockInInlineRegion();
|
|
383
|
+
// Whatever <pre> elements exist before the first command are the author's.
|
|
384
|
+
this.element.querySelectorAll("pre").forEach(pre => this._authoredPres.add(pre));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Live sync morphs the page against an incoming copy, and that copy is clean:
|
|
388
|
+
// contenteditable, the marker and the runtime attributes all come back off an
|
|
389
|
+
// element this editor is still pointing at. The instance is fine and Squire is
|
|
390
|
+
// still bound; only the element's own state is gone, and activate() cannot put
|
|
391
|
+
// it back because it returns early on an instance that never stopped being
|
|
392
|
+
// active. Rebuilding is not an option for a host that adopted an editor the
|
|
393
|
+
// author mounted, so this re-applies the state instead.
|
|
394
|
+
//
|
|
395
|
+
// The marker is in here because it is the one whose absence is fatal rather
|
|
396
|
+
// than cosmetic: without it the watcher stops recognising the element, which
|
|
397
|
+
// is the failure ensureMarker's own comment describes.
|
|
398
|
+
//
|
|
399
|
+
// Returns whether it had to do anything, so a caller can tell a repair from a
|
|
400
|
+
// no-op without asking the same question twice.
|
|
401
|
+
reattach() {
|
|
402
|
+
if (this.unsupported || !this.active) return false;
|
|
403
|
+
// isMountable is the question ensureMarker asks, so this is "would the
|
|
404
|
+
// watcher still adopt this element", not a guess at which attributes matter.
|
|
405
|
+
const intact =
|
|
406
|
+
this.element.getAttribute("contenteditable") === "true" &&
|
|
407
|
+
this.element.getAttribute("data-richclay-active") === "true" &&
|
|
408
|
+
isMountable(this.element);
|
|
409
|
+
if (intact) return false;
|
|
410
|
+
this.ensureMarker();
|
|
411
|
+
this.applyInlineBox();
|
|
412
|
+
consumeInertContenteditable(this.element);
|
|
413
|
+
this.setupEditorAttributes();
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// A block inside an inline element renders as a run-on. This changes the box
|
|
418
|
+
// and nothing else: whether a region survives a reload is decided by the
|
|
419
|
+
// parser from tag names, before any CSS exists. Runtime only, so the author's
|
|
420
|
+
// file keeps no style richclay put there.
|
|
421
|
+
applyInlineBox() {
|
|
422
|
+
if (isInlineTag(this.element) && !this.element.style.display) {
|
|
423
|
+
this.element.style.display = "inline-block";
|
|
424
|
+
this.element.setAttribute("data-richclay-runtime-display", "true");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
getHTML() {
|
|
429
|
+
return this._squire ? this._squire.getHTML() : this.element.innerHTML;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
setHTML(html) {
|
|
433
|
+
if (this._squire) {
|
|
434
|
+
this._squire.setHTML(String(html || ""));
|
|
435
|
+
} else {
|
|
436
|
+
this.element.innerHTML = sanitizeHTMLString(
|
|
437
|
+
html,
|
|
438
|
+
this.sanitizeConfig,
|
|
439
|
+
this.element.ownerDocument
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
this.updatePlaceholder();
|
|
443
|
+
this.toolbar?.update();
|
|
444
|
+
this.options.onChange?.(this.getHTML());
|
|
445
|
+
return this;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
focus() {
|
|
449
|
+
if (this._squire) this._squire.focus();
|
|
450
|
+
else this.element.focus();
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
destroy() {
|
|
455
|
+
this.closeLinkDialog();
|
|
456
|
+
this.teardownFloatingToolbar();
|
|
457
|
+
this.toolbar?.destroy();
|
|
458
|
+
this.toolbar = null;
|
|
459
|
+
if (this._onToolbarKey) {
|
|
460
|
+
this.element.removeEventListener("keydown", this._onToolbarKey);
|
|
461
|
+
this._onToolbarKey = null;
|
|
462
|
+
}
|
|
463
|
+
this.liveRegion?.remove();
|
|
464
|
+
this.liveRegion = null;
|
|
465
|
+
this.description?.remove();
|
|
466
|
+
this.description = null;
|
|
467
|
+
|
|
468
|
+
this._shortcutKeys.forEach(key => this._squire?.setKeyHandler?.(key, null));
|
|
469
|
+
this._squireListeners.forEach(([type, listener]) => {
|
|
470
|
+
this._squire?.removeEventListener?.(type, listener);
|
|
471
|
+
});
|
|
472
|
+
this._squireListeners = [];
|
|
473
|
+
this._shortcutKeys = [];
|
|
474
|
+
|
|
475
|
+
if (this._onBeforeInput) {
|
|
476
|
+
this.element.ownerDocument.removeEventListener("beforeinput", this._onBeforeInput, true);
|
|
477
|
+
this._onBeforeInput = null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const doc = this.element.ownerDocument;
|
|
481
|
+
if (this._onRootBeforeInput) {
|
|
482
|
+
doc.removeEventListener("beforeinput", this._onRootBeforeInput, true);
|
|
483
|
+
this._onRootBeforeInput = null;
|
|
484
|
+
}
|
|
485
|
+
if (this._onRootKeydown) {
|
|
486
|
+
doc.removeEventListener("keydown", this._onRootKeydown, true);
|
|
487
|
+
this._onRootKeydown = null;
|
|
488
|
+
}
|
|
489
|
+
if (this._onRootTransfer) {
|
|
490
|
+
ROOT_TRANSFER_EVENTS.forEach(type => doc.removeEventListener(type, this._onRootTransfer, true));
|
|
491
|
+
this._onRootTransfer = null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
this._squire?.destroy?.();
|
|
495
|
+
this._squire = null;
|
|
496
|
+
this.cleanupEditorAttributes();
|
|
497
|
+
|
|
498
|
+
instances.delete(this.element);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
saveSelection() {
|
|
502
|
+
this.savedSelection = getSquireSelection(this._squire) || this.savedSelection;
|
|
503
|
+
return this.savedSelection;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
restoreSelection() {
|
|
507
|
+
return restoreSquireSelection(this._squire, this.savedSelection);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Squire's invariant is established on the first real edit rather than at
|
|
511
|
+
// activation, so that opening a page in edit mode never rewrites it. These
|
|
512
|
+
// listeners capture on the document, so the root is valid before Squire's own
|
|
513
|
+
// handlers see the event. History inputs are skipped: repairing mid-undo would
|
|
514
|
+
// fight the stack it is replaying.
|
|
515
|
+
installRootGuards() {
|
|
516
|
+
const doc = this.element.ownerDocument;
|
|
517
|
+
const owns = event => this.element.contains(event.target);
|
|
518
|
+
|
|
519
|
+
this._onRootBeforeInput = event => {
|
|
520
|
+
// Repairing mid-composition mutates the DOM under the IME and moves the
|
|
521
|
+
// selection out from under it; repairing mid-undo fights the stack being
|
|
522
|
+
// replayed.
|
|
523
|
+
if (!owns(event) || event.isComposing || /^history/.test(event.inputType || "")) return;
|
|
524
|
+
this.ensureRootIsEditable();
|
|
525
|
+
};
|
|
526
|
+
this._onRootKeydown = event => {
|
|
527
|
+
if (!owns(event) || event.defaultPrevented || event.isComposing) return;
|
|
528
|
+
if (this.isEditingKey(event)) this.ensureRootIsEditable();
|
|
529
|
+
};
|
|
530
|
+
this._onRootTransfer = event => {
|
|
531
|
+
if (owns(event)) this.ensureRootIsEditable();
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
doc.addEventListener("beforeinput", this._onRootBeforeInput, true);
|
|
535
|
+
doc.addEventListener("keydown", this._onRootKeydown, true);
|
|
536
|
+
ROOT_TRANSFER_EVENTS.forEach(type => {
|
|
537
|
+
doc.addEventListener(type, this._onRootTransfer, true);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Re-checked rather than latched, because Hyperclay's live sync morphs new DOM
|
|
542
|
+
// into the region long after activation and can reintroduce a loose text node.
|
|
543
|
+
ensureRootIsEditable() {
|
|
544
|
+
if (!this._squire || this.options.singleLine) return;
|
|
545
|
+
const wrapBareRoot = this.options.inline;
|
|
546
|
+
const blocksAllowed = !this.blocksStayOut();
|
|
547
|
+
if (!editorRootNeedsNormalization(this.element, { wrapBareRoot, blocksAllowed })) return;
|
|
548
|
+
|
|
549
|
+
const range = getSquireSelection(this._squire);
|
|
550
|
+
const saved = range ? captureRange(this.element, range) : null;
|
|
551
|
+
const repair = () => {
|
|
552
|
+
normalizeEditorRoot(this.element, {
|
|
553
|
+
blockTag: this._blockTag,
|
|
554
|
+
wrapBareRoot,
|
|
555
|
+
blocksAllowed,
|
|
556
|
+
onBareRootWrapped: (root, wrapper) => {
|
|
557
|
+
console.warn(
|
|
558
|
+
"richclay: wrapped this region's loose text in a <div> so multi-line editing " +
|
|
559
|
+
"works. Wrap the content in your own block element to keep the markup you wrote, " +
|
|
560
|
+
'or use editable="single-line" if it is meant to be one line.',
|
|
561
|
+
root,
|
|
562
|
+
wrapper
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// Through modifyDocument so the repair is not recorded as a change of its
|
|
569
|
+
// own: it folds into the first edit's diff, which is what makes undo restore
|
|
570
|
+
// the byte-identical pre-repair source instead of stepping back into a
|
|
571
|
+
// half-repaired state.
|
|
572
|
+
if (typeof this._squire.modifyDocument === "function") this._squire.modifyDocument(repair);
|
|
573
|
+
else repair();
|
|
574
|
+
|
|
575
|
+
if (saved) {
|
|
576
|
+
restoreRange(this.element, range, saved);
|
|
577
|
+
restoreSquireSelection(this._squire, range);
|
|
578
|
+
this.savedSelection = cloneRange(range);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Only keys that will actually edit. Repairing is not free on a root that has
|
|
583
|
+
// not been repaired yet, which is every freshly opened page, so a blanket
|
|
584
|
+
// "any single-character key" made Cmd+C and Cmd+S rewrite an untouched region.
|
|
585
|
+
// The only modified keys that do edit are the ones this editor rebound itself,
|
|
586
|
+
// which is why the set is asked rather than Squire's dispatch mirrored.
|
|
587
|
+
isEditingKey(event) {
|
|
588
|
+
if (["Backspace", "Delete", "Enter"].includes(event.key)) return true;
|
|
589
|
+
// Squire acts on Tab only inside a list. Anywhere else it edits nothing, and
|
|
590
|
+
// treating it as an editing key made merely tabbing out of a fresh region run
|
|
591
|
+
// the repair and strip the author's source indentation.
|
|
592
|
+
if (event.key === "Tab") return this.caretIsInList();
|
|
593
|
+
if (event.key.length !== 1) return false;
|
|
594
|
+
if (!event.ctrlKey && !event.metaKey && !event.altKey) return true;
|
|
595
|
+
return (
|
|
596
|
+
event.ctrlKey && !event.metaKey && !event.altKey &&
|
|
597
|
+
this._appleDeleteKeys.has(event.key.toLowerCase())
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
caretIsInList() {
|
|
602
|
+
const range = this._squire?.getSelection();
|
|
603
|
+
const container = range?.startContainer;
|
|
604
|
+
if (!container) return false;
|
|
605
|
+
const element = container.nodeType === 1 ? container : container.parentElement;
|
|
606
|
+
return Boolean(element && this.element.contains(element) && element.closest("ul, ol"));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
runControl(def) {
|
|
610
|
+
if (!this._squire || typeof def.run !== "function") return;
|
|
611
|
+
// Enforced here, not only where the toolbar renders: shortcuts and menu items
|
|
612
|
+
// reach this same path, and the block commands they carry are exactly the
|
|
613
|
+
// ones that put a <blockquote> inside a <p editable>.
|
|
614
|
+
if (def.isDisabled?.(this)) return;
|
|
615
|
+
this.restoreSelection();
|
|
616
|
+
// Before the command, not only after. A toolbar click can be the first
|
|
617
|
+
// interaction with a page and no root guard fires for it, so a block command
|
|
618
|
+
// ran against the raw root: over two source-indented paragraphs, one list
|
|
619
|
+
// command produced two separate <ul>s. Both halves are block-valid, so the
|
|
620
|
+
// heal below cannot undo it.
|
|
621
|
+
if (def.mutates !== false) this.ensureRootIsEditable();
|
|
622
|
+
const result = def.run(this);
|
|
623
|
+
// Stamp any <pre> this command just created, so the save hook can contain it
|
|
624
|
+
// without touching one the author wrote themselves.
|
|
625
|
+
this.element.querySelectorAll("pre:not([data-richclay-pre])").forEach(pre => {
|
|
626
|
+
if (!this._authoredPres.has(pre)) pre.setAttribute("data-richclay-pre", "");
|
|
627
|
+
});
|
|
628
|
+
// After it too. Squire's removeCode() splices an emptied <pre> out onto the
|
|
629
|
+
// root, and a caret parked on the orphan makes every later block command a
|
|
630
|
+
// silent no-op.
|
|
631
|
+
if (def.mutates !== false) {
|
|
632
|
+
this.ensureRootIsEditable();
|
|
633
|
+
this.anchorSelectionInBlock();
|
|
634
|
+
}
|
|
635
|
+
this.saveSelection();
|
|
636
|
+
this.toolbar?.update();
|
|
637
|
+
this.updatePlaceholder();
|
|
638
|
+
this.announceControl(def);
|
|
639
|
+
|
|
640
|
+
if (result !== KEEP_FOCUS) {
|
|
641
|
+
this.focus();
|
|
642
|
+
}
|
|
643
|
+
this.warnOnBlockInInlineRegion();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Squire's modifyBlocks leaves the caret anchored on the root itself, between
|
|
647
|
+
// blocks. From there getStartBlockOfRange finds no block, so the next block
|
|
648
|
+
// command is a silent no-op — the "it stopped working" the user hit. Push a
|
|
649
|
+
// collapsed caret back inside the block it is sitting in front of.
|
|
650
|
+
anchorSelectionInBlock() {
|
|
651
|
+
const range = getSquireSelection(this._squire);
|
|
652
|
+
if (!range?.collapsed || range.startContainer !== this.element) return;
|
|
653
|
+
|
|
654
|
+
const children = Array.from(this.element.childNodes);
|
|
655
|
+
const atEnd = range.startOffset >= children.length;
|
|
656
|
+
// Comments are skipped: a caret set inside a comment node is not a text
|
|
657
|
+
// position, and the region's markers are comments.
|
|
658
|
+
const child = atEnd
|
|
659
|
+
? children.filter(isCaretHost).pop()
|
|
660
|
+
: children.slice(range.startOffset).find(isCaretHost) ||
|
|
661
|
+
children.slice(0, range.startOffset).filter(isCaretHost).pop();
|
|
662
|
+
if (!child) return;
|
|
663
|
+
|
|
664
|
+
// At the end of the region the caret belongs at the end of the last block,
|
|
665
|
+
// not thrown back to the start of the line it was sitting after.
|
|
666
|
+
const edge = caretEdge(child, !atEnd);
|
|
667
|
+
range.setStart(edge, atEnd ? nodeLength(edge) : 0);
|
|
668
|
+
range.collapse(true);
|
|
669
|
+
restoreSquireSelection(this._squire, range);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
selectionHasFormat(tag) {
|
|
673
|
+
if (!tag || !this._squire?.hasFormat) return false;
|
|
674
|
+
try {
|
|
675
|
+
return this._squire.hasFormat(tag);
|
|
676
|
+
} catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Squire sets its path to "(selection)" whenever the selection spans more than
|
|
682
|
+
// one node, so asking the path answered "no" for every multi-block selection.
|
|
683
|
+
// Both boundaries must land in the same matching element, or a selection that
|
|
684
|
+
// merely starts in a list would report as being in one.
|
|
685
|
+
pathHas(tag) {
|
|
686
|
+
const range = getSquireSelection(this._squire);
|
|
687
|
+
if (!range) return false;
|
|
688
|
+
const selector = tag.toLowerCase();
|
|
689
|
+
const start = matchAtBoundary(this.element, range.startContainer, range.startOffset, true, selector);
|
|
690
|
+
return Boolean(start) &&
|
|
691
|
+
start === matchAtBoundary(this.element, range.endContainer, range.endOffset, false, selector);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Blocks stay out of this region for either of two reasons, and both are fixed
|
|
695
|
+
// for the editor's life, which is why the toolbar can leave the controls out
|
|
696
|
+
// rather than grey them. Either the parser would eject a block on the next page
|
|
697
|
+
// load, or the author wrote the region as a line of text and richclay does not
|
|
698
|
+
// rewrite what they wrote.
|
|
699
|
+
blocksStayOut() {
|
|
700
|
+
return (
|
|
701
|
+
this.options.singleLine ||
|
|
702
|
+
ejectsBlocks(this.element) ||
|
|
703
|
+
keepsTextShape(this.element)
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Advisory only: the markup is stable and saves correctly, it just will not
|
|
708
|
+
// validate. Checked when the editor mounts and after any command, which covers
|
|
709
|
+
// the ways an author actually produces one. Paste is deliberately not covered:
|
|
710
|
+
// nothing hooks paste any more, and a console note is not worth reinstating a
|
|
711
|
+
// hook that took three rounds to get wrong.
|
|
712
|
+
warnOnBlockInInlineRegion() {
|
|
713
|
+
if (this._warnedInlineBlock || !isInlineTag(this.element)) return;
|
|
714
|
+
if (!hasBlockDescendant(this.element)) return;
|
|
715
|
+
this._warnedInlineBlock = true;
|
|
716
|
+
const tag = this.element.nodeName.toLowerCase();
|
|
717
|
+
console.warn(
|
|
718
|
+
`richclay: a block element inside <${tag} editable> is not valid HTML. It stays where it is ` +
|
|
719
|
+
"and saves correctly, but a validator will flag it. Put the editable attribute on a block " +
|
|
720
|
+
"element if you want blocks here.",
|
|
721
|
+
this.element
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
toggleFormat(tag, addMethod, removeMethod) {
|
|
726
|
+
if (this.selectionHasFormat(tag)) return this._squire[removeMethod]();
|
|
727
|
+
return this._squire[addMethod]();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
toggleList(tag) {
|
|
731
|
+
if (this.pathHas(tag)) return this._squire.removeList();
|
|
732
|
+
return tag === "UL" ? this._squire.makeUnorderedList() : this._squire.makeOrderedList();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
indent() {
|
|
736
|
+
if (this.pathHas("UL") || this.pathHas("OL")) return this._squire.increaseListLevel();
|
|
737
|
+
return this._squire.increaseQuoteLevel();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
outdent() {
|
|
741
|
+
if (this.pathHas("UL") || this.pathHas("OL")) return this._squire.decreaseListLevel();
|
|
742
|
+
return this._squire.decreaseQuoteLevel();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
setBlockType(tag) {
|
|
746
|
+
const target = tag.toUpperCase();
|
|
747
|
+
if (target === "BLOCKQUOTE") return this._squire.increaseQuoteLevel();
|
|
748
|
+
|
|
749
|
+
return this._squire.modifyBlocks(fragment => {
|
|
750
|
+
const doc = this.element.ownerDocument;
|
|
751
|
+
const output = doc.createDocumentFragment();
|
|
752
|
+
|
|
753
|
+
Array.from(fragment.childNodes).forEach(node => {
|
|
754
|
+
if (node.nodeType !== 1) {
|
|
755
|
+
output.appendChild(node);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (node.nodeName === target) {
|
|
760
|
+
output.appendChild(node);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const replacement = doc.createElement(target);
|
|
765
|
+
while (node.firstChild) replacement.appendChild(node.firstChild);
|
|
766
|
+
output.appendChild(replacement);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
return output;
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
openLinkDialog() {
|
|
774
|
+
this.saveSelection();
|
|
775
|
+
this.closeLinkDialog();
|
|
776
|
+
|
|
777
|
+
const doc = this.element.ownerDocument;
|
|
778
|
+
const existing = this.currentLinkHref();
|
|
779
|
+
const dialog = doc.createElement("form");
|
|
780
|
+
const labelId = `richclay-link-label-${++dialogSeq}`;
|
|
781
|
+
dialog.className = "richclay-dialog";
|
|
782
|
+
dialog.setAttribute("role", "dialog");
|
|
783
|
+
dialog.setAttribute("aria-modal", "true");
|
|
784
|
+
dialog.setAttribute("aria-labelledby", labelId);
|
|
785
|
+
dialog.setAttribute("data-richclay-dialog", "");
|
|
786
|
+
markChrome(dialog);
|
|
787
|
+
dialog.innerHTML = `
|
|
788
|
+
<div class="richclay-dialog-title" id="${labelId}">${existing ? "Edit link" : "Insert link"}</div>
|
|
789
|
+
<label class="richclay-field">
|
|
790
|
+
<span>URL</span>
|
|
791
|
+
<input class="richclay-input" name="url" type="text" inputmode="url" autocomplete="url" placeholder="https://example.com">
|
|
792
|
+
</label>
|
|
793
|
+
<div class="richclay-dialog-actions">
|
|
794
|
+
<button type="button" class="richclay-secondary" data-richclay-cancel>Cancel</button>
|
|
795
|
+
<button type="submit" class="richclay-primary">Apply</button>
|
|
796
|
+
</div>
|
|
797
|
+
`;
|
|
798
|
+
|
|
799
|
+
const close = () => {
|
|
800
|
+
this.closeLinkDialog();
|
|
801
|
+
this.toolbar?.update();
|
|
802
|
+
this.focus();
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
dialog.addEventListener("submit", event => {
|
|
806
|
+
event.preventDefault();
|
|
807
|
+
const data = new FormData(dialog);
|
|
808
|
+
const href = normalizeUrl(data.get("url"));
|
|
809
|
+
if (!href) {
|
|
810
|
+
announce(this.liveRegion, "Enter a valid URL");
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
this.restoreSelection();
|
|
814
|
+
const anchor = this.currentLinkElement();
|
|
815
|
+
if (anchor) this.selectElement(anchor);
|
|
816
|
+
this._squire.makeLink(href);
|
|
817
|
+
announce(this.liveRegion, "Link applied");
|
|
818
|
+
close();
|
|
819
|
+
});
|
|
820
|
+
dialog.querySelector("[data-richclay-cancel]").addEventListener("click", close);
|
|
821
|
+
dialog.addEventListener("keydown", event => {
|
|
822
|
+
if (event.key === "Escape") {
|
|
823
|
+
event.preventDefault();
|
|
824
|
+
close();
|
|
825
|
+
} else if (event.key === "Tab") {
|
|
826
|
+
trapTab(event, dialog);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
const mount = this.toolbar?.root || this.element;
|
|
831
|
+
mount.insertAdjacentElement("afterend", dialog);
|
|
832
|
+
this.dialog = dialog;
|
|
833
|
+
|
|
834
|
+
const input = dialog.querySelector("input");
|
|
835
|
+
input.value = existing;
|
|
836
|
+
input.focus();
|
|
837
|
+
input.select();
|
|
838
|
+
return KEEP_FOCUS;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
currentLinkElement() {
|
|
842
|
+
const range = getSquireSelection(this._squire);
|
|
843
|
+
let node = range?.commonAncestorContainer || null;
|
|
844
|
+
if (node && node.nodeType !== 1) node = node.parentElement;
|
|
845
|
+
return node?.closest?.("a[href]") || null;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
currentLinkHref() {
|
|
849
|
+
const anchor = this.currentLinkElement();
|
|
850
|
+
return anchor ? anchor.getAttribute("href") || "" : "";
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
selectElement(node) {
|
|
854
|
+
const range = node.ownerDocument.createRange();
|
|
855
|
+
range.selectNode(node);
|
|
856
|
+
this._squire.setSelection(range);
|
|
857
|
+
this.saveSelection();
|
|
858
|
+
return range;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
closeLinkDialog() {
|
|
862
|
+
this.dialog?.remove();
|
|
863
|
+
this.dialog = null;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
ensureMarker() {
|
|
867
|
+
// The marker exists so an element mounted through an explicit selector is
|
|
868
|
+
// still recognized on the next pass. What decides that is not whether the
|
|
869
|
+
// element matches the selector but whether the watcher would adopt it again: a
|
|
870
|
+
// custom element carrying a bare `editable` matches and is still refused by the
|
|
871
|
+
// host guard, so with no marker its editor died on the first node replacement
|
|
872
|
+
// and never came back. Stamping makes the guard pass, so the two agree.
|
|
873
|
+
if (!isMountable(this.element)) {
|
|
874
|
+
// Record that WE invented this marker. Without it, cleanup cannot tell an
|
|
875
|
+
// element the author marked from one richclay stamped, so it either leaves
|
|
876
|
+
// ours behind forever (the element stays adoptable on every later load,
|
|
877
|
+
// and the attribute reaches the saved file) or strips theirs (silently
|
|
878
|
+
// breaking a region they asked for). Same provenance idea as
|
|
879
|
+
// data-richclay-runtime-contenteditable just below.
|
|
880
|
+
this.element.setAttribute("data-richclay-runtime-marker", "true");
|
|
881
|
+
this.element.setAttribute("data-richclay", "");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
setupEditorAttributes() {
|
|
886
|
+
this.element.classList.add(
|
|
887
|
+
this.options.inline ? "richclay-inline" : "richclay-editor",
|
|
888
|
+
"richclay-active"
|
|
889
|
+
);
|
|
890
|
+
this.element.setAttribute("data-richclay-active", "true");
|
|
891
|
+
|
|
892
|
+
if (!this.element.hasAttribute("data-richclay-runtime-contenteditable")) {
|
|
893
|
+
const original = this.element.getAttribute("contenteditable");
|
|
894
|
+
if (original === null) {
|
|
895
|
+
this.element.setAttribute("data-richclay-runtime-contenteditable", "true");
|
|
896
|
+
} else if (original !== "true" && original !== "") {
|
|
897
|
+
// A bare contenteditable attribute is the "true" state; only meaningful
|
|
898
|
+
// non-default values are recorded for restore.
|
|
899
|
+
this.element.setAttribute("data-richclay-runtime-contenteditable", original);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
this.element.setAttribute("contenteditable", "true");
|
|
903
|
+
|
|
904
|
+
if (!this.element.hasAttribute("role")) {
|
|
905
|
+
setRuntimeAttribute(this.element, "role", "textbox", "data-richclay-runtime-role");
|
|
906
|
+
}
|
|
907
|
+
if (!this.element.hasAttribute("aria-multiline")) {
|
|
908
|
+
setRuntimeAttribute(
|
|
909
|
+
this.element,
|
|
910
|
+
"aria-multiline",
|
|
911
|
+
this.options.singleLine ? "false" : "true",
|
|
912
|
+
"data-richclay-runtime-aria-multiline"
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Squire owns this region's undo stack. Mark the region so Hyperclay's
|
|
917
|
+
// optional page-level undo (hyper-undo) defers to Squire instead of fighting
|
|
918
|
+
// it. Runtime-only: stripped on save, re-applied on the next edit-mode load.
|
|
919
|
+
if (!this.element.hasAttribute("no-undo")) {
|
|
920
|
+
setRuntimeAttribute(this.element, "no-undo", "", "data-richclay-runtime-no-undo");
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (this.options.placeholder) {
|
|
924
|
+
this.element.setAttribute("data-richclay-placeholder", this.options.placeholder);
|
|
925
|
+
// One description for the life of the instance. A second call that built
|
|
926
|
+
// another would orphan the first: destroy() removes only the node this
|
|
927
|
+
// points at, and removeRuntimeDescribedBy clears only the id named by the
|
|
928
|
+
// runtime attribute, both of which the second call has overwritten. The
|
|
929
|
+
// original node and a dangling aria-describedby would reach the saved page.
|
|
930
|
+
// A morph can take the node itself away, so a detached one is put back
|
|
931
|
+
// rather than replaced, which keeps its id and the wiring that names it.
|
|
932
|
+
if (!this.description) {
|
|
933
|
+
this.description = this.element.ownerDocument.createElement("div");
|
|
934
|
+
this.description.id = `richclay-placeholder-${++dialogSeq}`;
|
|
935
|
+
this.description.className = "richclay-sr-only";
|
|
936
|
+
this.description.textContent = this.options.placeholder;
|
|
937
|
+
this.description.setAttribute("data-richclay-live", "");
|
|
938
|
+
markChrome(this.description);
|
|
939
|
+
}
|
|
940
|
+
if (!this.description.isConnected) {
|
|
941
|
+
this.element.insertAdjacentElement("afterend", this.description);
|
|
942
|
+
}
|
|
943
|
+
// Outside the branch above, because a morph can strip the element's own
|
|
944
|
+
// aria-describedby while leaving this node connected, and then the hint is
|
|
945
|
+
// gone with nothing to notice it. connectDescription builds its id list
|
|
946
|
+
// from a Set, so naming the same description twice costs nothing.
|
|
947
|
+
connectDescription(this.element, this.description);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
this.element.addEventListener("focus", this._onFocus);
|
|
951
|
+
this.element.addEventListener("blur", this._onBlur);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
cleanupEditorAttributes() {
|
|
955
|
+
removeRuntimeState(this.element, "destroy");
|
|
956
|
+
this.element.removeEventListener("focus", this._onFocus);
|
|
957
|
+
this.element.removeEventListener("blur", this._onBlur);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
bindSquire() {
|
|
961
|
+
const input = () => {
|
|
962
|
+
this.saveSelection();
|
|
963
|
+
this.updatePlaceholder();
|
|
964
|
+
this.options.onChange?.(this.getHTML());
|
|
965
|
+
this.toolbar?.update();
|
|
966
|
+
};
|
|
967
|
+
const pathChange = event => {
|
|
968
|
+
this.path = event.detail?.path || this._squire.getPath?.() || "";
|
|
969
|
+
this.saveSelection();
|
|
970
|
+
this.toolbar?.update();
|
|
971
|
+
};
|
|
972
|
+
const selection = () => {
|
|
973
|
+
this.saveSelection();
|
|
974
|
+
this.toolbar?.update();
|
|
975
|
+
if (this.options.toolbarOnSelect) this.updateFloatVisibility();
|
|
976
|
+
};
|
|
977
|
+
const undoState = () => this.toolbar?.update();
|
|
978
|
+
|
|
979
|
+
[
|
|
980
|
+
["input", input],
|
|
981
|
+
["pathChange", pathChange],
|
|
982
|
+
["select", selection],
|
|
983
|
+
["cursor", selection],
|
|
984
|
+
["undoStateChange", undoState]
|
|
985
|
+
].forEach(([type, listener]) => {
|
|
986
|
+
this._squire.addEventListener(type, listener);
|
|
987
|
+
this._squireListeners.push([type, listener]);
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
renderToolbar() {
|
|
992
|
+
if (!this.active || this.options.readOnly) return;
|
|
993
|
+
if (this.options.inline) {
|
|
994
|
+
// Inline toolbars are floating and focus-scoped; if one is open, rebuild
|
|
995
|
+
// it so registry changes show up immediately.
|
|
996
|
+
if (this.float) {
|
|
997
|
+
this.teardownFloatingToolbar();
|
|
998
|
+
this.ensureFloatingToolbar();
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
this.toolbar?.destroy();
|
|
1003
|
+
this.toolbar = null;
|
|
1004
|
+
const controls = this.resolveToolbarControls(this.options.toolbar);
|
|
1005
|
+
if (!controls.length) return;
|
|
1006
|
+
this.toolbar = new Toolbar(this, controls, {
|
|
1007
|
+
toolbarContainer: this.options.toolbarContainer
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
ensureFloatingToolbar() {
|
|
1012
|
+
if (this.float || !this.active || this.options.readOnly) return;
|
|
1013
|
+
const controls = this.resolveToolbarControls(this.options.toolbar);
|
|
1014
|
+
if (!controls.length) return;
|
|
1015
|
+
this.float = new FloatingToolbar(this, controls);
|
|
1016
|
+
// Blur on the editor doesn't fire when focus later leaves the toolbar or
|
|
1017
|
+
// link dialog directly (Alt+F10 then Tab away), so the shell watches its
|
|
1018
|
+
// own focusout too.
|
|
1019
|
+
this._onFloatFocusOut = event => this.scheduleFloatTeardown(event);
|
|
1020
|
+
this.float.root.addEventListener("focusout", this._onFloatFocusOut);
|
|
1021
|
+
// A press on non-focusable page chrome never blurs a contenteditable, so
|
|
1022
|
+
// blur/focusout alone can't dismiss the float. Watch the document
|
|
1023
|
+
// (capture, so stopped events still count) and dismiss on any pointer
|
|
1024
|
+
// press the editor doesn't own, releasing focus with it.
|
|
1025
|
+
this._onDocPointerDown = event => {
|
|
1026
|
+
if (this.ownsFocusTarget(event.target)) return;
|
|
1027
|
+
const doc = this.element.ownerDocument;
|
|
1028
|
+
if (doc.activeElement && this.ownsFocusTarget(doc.activeElement)) {
|
|
1029
|
+
doc.activeElement.blur?.();
|
|
1030
|
+
}
|
|
1031
|
+
this.teardownFloatingToolbar();
|
|
1032
|
+
};
|
|
1033
|
+
this.element.ownerDocument.addEventListener("pointerdown", this._onDocPointerDown, true);
|
|
1034
|
+
this.toolbar = this.float.toolbar;
|
|
1035
|
+
this.toolbar.update();
|
|
1036
|
+
if (this.options.toolbarOnSelect) this.updateFloatVisibility();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
teardownFloatingToolbar() {
|
|
1040
|
+
if (!this.float) return;
|
|
1041
|
+
if (this._onFloatFocusOut) {
|
|
1042
|
+
this.float.root.removeEventListener("focusout", this._onFloatFocusOut);
|
|
1043
|
+
this._onFloatFocusOut = null;
|
|
1044
|
+
}
|
|
1045
|
+
if (this._onDocPointerDown) {
|
|
1046
|
+
this.element.ownerDocument.removeEventListener("pointerdown", this._onDocPointerDown, true);
|
|
1047
|
+
this._onDocPointerDown = null;
|
|
1048
|
+
}
|
|
1049
|
+
this.float.destroy();
|
|
1050
|
+
this.float = null;
|
|
1051
|
+
this.toolbar = null;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
scheduleFloatTeardown(event) {
|
|
1055
|
+
const next = event?.relatedTarget;
|
|
1056
|
+
if (next && this.ownsFocusTarget(next)) return;
|
|
1057
|
+
const doc = this.element.ownerDocument;
|
|
1058
|
+
const win = doc.defaultView || globalThis;
|
|
1059
|
+
win.setTimeout(() => {
|
|
1060
|
+
const active = doc.activeElement;
|
|
1061
|
+
if (active && this.ownsFocusTarget(active)) return;
|
|
1062
|
+
this.teardownFloatingToolbar();
|
|
1063
|
+
}, 0);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
ownsFocusTarget(node) {
|
|
1067
|
+
return (
|
|
1068
|
+
node === this.element ||
|
|
1069
|
+
this.element.contains(node) ||
|
|
1070
|
+
Boolean(this.float?.root.contains(node)) ||
|
|
1071
|
+
Boolean(this.dialog?.contains(node))
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
updateFloatVisibility() {
|
|
1076
|
+
if (!this.float) return;
|
|
1077
|
+
const range = this.savedSelection;
|
|
1078
|
+
this.float.setVisible(Boolean(range && !range.collapsed));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
resolveToolbarControls(toolbar) {
|
|
1082
|
+
if (toolbar === false || toolbar === null || toolbar === "none") return [];
|
|
1083
|
+
const requested = Array.isArray(toolbar) ? toolbar : presets[toolbar] || presets.standard;
|
|
1084
|
+
|
|
1085
|
+
return requested.map(item => {
|
|
1086
|
+
if (typeof item === "string") {
|
|
1087
|
+
const def = this.registry.get(item);
|
|
1088
|
+
if (!def) throw new Error(`Unknown RichClay toolbar control: ${item}`);
|
|
1089
|
+
return def;
|
|
1090
|
+
}
|
|
1091
|
+
if (item?.type === "separator") return item;
|
|
1092
|
+
validateButton(item);
|
|
1093
|
+
return item;
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
installShortcuts() {
|
|
1098
|
+
// Shortcuts are a capability of the registry plus this instance's toolbar
|
|
1099
|
+
// definitions, not of the visible toolbar: toolbar:false keeps shortcuts,
|
|
1100
|
+
// and inline toolbar defs win over registry defs with the same id.
|
|
1101
|
+
const seen = new Set();
|
|
1102
|
+
// Single-line editors must never gain block commands: registry defaults are
|
|
1103
|
+
// filtered to the inline preset (explicit toolbar defs still always bind).
|
|
1104
|
+
const registryDefs = [...this.registry.values()].filter(
|
|
1105
|
+
def => !this.options.singleLine || presets.inline.includes(def.id)
|
|
1106
|
+
);
|
|
1107
|
+
const defs = [...this.resolveToolbarControls(this.options.toolbar), ...registryDefs];
|
|
1108
|
+
defs.forEach(def => {
|
|
1109
|
+
if (def.type === "menu" || def.type === "separator") return;
|
|
1110
|
+
if (!def.shortcut || !def.id || seen.has(def.id)) return;
|
|
1111
|
+
// isDisabled is a property of the root, not of the selection, so a control
|
|
1112
|
+
// disabled here is disabled for this editor's whole life. Masked rather than
|
|
1113
|
+
// skipped, for the same reason as maskSquireBlockShortcuts: not installing
|
|
1114
|
+
// ours is what leaves Squire's own handler in charge.
|
|
1115
|
+
if (def.isDisabled?.(this)) {
|
|
1116
|
+
const masked = shortcutKey(def.shortcut, this.window);
|
|
1117
|
+
this._squire.setKeyHandler(masked, null);
|
|
1118
|
+
this._shortcutKeys.push(masked);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
seen.add(def.id);
|
|
1122
|
+
const key = shortcutKey(def.shortcut, this.window);
|
|
1123
|
+
this._squire.setKeyHandler(key, (squire, event) => {
|
|
1124
|
+
event.preventDefault();
|
|
1125
|
+
this.runControl(def);
|
|
1126
|
+
});
|
|
1127
|
+
this._shortcutKeys.push(key);
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// macOS sends Ctrl+D and Ctrl+H to the browser's own forward/backward delete,
|
|
1132
|
+
// which merges two blocks by wrapping the moved text in a computed-style
|
|
1133
|
+
// <span> — permanent junk in an editor whose DOM is the saved document. Point
|
|
1134
|
+
// them at Squire's own handlers so they behave exactly like Delete and
|
|
1135
|
+
// Backspace. Reads Squire's key handler table; no-ops for other engines.
|
|
1136
|
+
// Squire binds Code to the platform modifier itself, on _keyHandlers' prototype,
|
|
1137
|
+
// so dropping richclay's own shortcut is not enough to retire it. An own
|
|
1138
|
+
// property of null shadows the inherited handler and the key falls through.
|
|
1139
|
+
maskSquireCodeShortcut() {
|
|
1140
|
+
const key = `${isApplePlatform(this.window) ? "Meta" : "Ctrl"}-d`;
|
|
1141
|
+
this._squire.setKeyHandler(key, null);
|
|
1142
|
+
this._shortcutKeys.push(key);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Squire binds these on _keyHandlers' prototype, and richclay has no definition
|
|
1146
|
+
// carrying some of them, so filtering richclay's own shortcuts cannot reach
|
|
1147
|
+
// them: Mod+] kept building a <blockquote> in a <p editable> through every
|
|
1148
|
+
// round. An own property of null shadows the inherited handler and the key
|
|
1149
|
+
// falls through. The prefix must match the one Squire computed, which is why
|
|
1150
|
+
// the test harness has to agree about the platform.
|
|
1151
|
+
maskSquireBlockShortcuts() {
|
|
1152
|
+
if (!this.blocksStayOut()) return;
|
|
1153
|
+
const mod = isApplePlatform(this.window) ? "Meta" : "Ctrl";
|
|
1154
|
+
[`${mod}-]`, `${mod}-[`, `${mod}-Shift-8`, `${mod}-Shift-9`].forEach(key => {
|
|
1155
|
+
this._squire.setKeyHandler(key, null);
|
|
1156
|
+
this._shortcutKeys.push(key);
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Squire's toggleCode() makes a block <pre> when the selection is collapsed,
|
|
1161
|
+
// which inside a heading or a paragraph root means a code block where no block
|
|
1162
|
+
// can legally go. Those regions get inline <code> instead.
|
|
1163
|
+
toggleCode() {
|
|
1164
|
+
if (!this.blocksStayOut()) return this._squire.toggleCode();
|
|
1165
|
+
if (this.selectionHasFormat("CODE")) return this._squire.changeFormat(null, { tag: "CODE" });
|
|
1166
|
+
return this._squire.changeFormat({ tag: "CODE" }, null);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
installAppleDeleteKeys() {
|
|
1170
|
+
if (!isApplePlatform(this.window)) return;
|
|
1171
|
+
const handlers = this._squire._keyHandlers;
|
|
1172
|
+
if (!handlers) return;
|
|
1173
|
+
|
|
1174
|
+
[["Ctrl-d", "Delete"], ["Ctrl-h", "Backspace"]].forEach(([key, native]) => {
|
|
1175
|
+
if (typeof handlers[native] !== "function") return;
|
|
1176
|
+
this._squire.setKeyHandler(key, handlers[native]);
|
|
1177
|
+
this._shortcutKeys.push(key);
|
|
1178
|
+
this._appleDeleteKeys.add(key.slice("Ctrl-".length));
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
installSingleLineGuards() {
|
|
1183
|
+
["Enter", "Shift-Enter"].forEach(key => {
|
|
1184
|
+
this._squire.setKeyHandler(key, (squire, event) => event.preventDefault());
|
|
1185
|
+
this._shortcutKeys.push(key);
|
|
1186
|
+
});
|
|
1187
|
+
// Belt for input paths that bypass keydown (IME confirm, mobile keyboards):
|
|
1188
|
+
// cancel paragraph/line-break insertions before Squire's own handler runs.
|
|
1189
|
+
this._onBeforeInput = event => {
|
|
1190
|
+
if (!this.element.contains(event.target)) return;
|
|
1191
|
+
if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") {
|
|
1192
|
+
event.preventDefault();
|
|
1193
|
+
event.stopPropagation();
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
this.element.ownerDocument.addEventListener("beforeinput", this._onBeforeInput, true);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// The fidelity restore bypasses setHTML, so Squire's undo baseline is still
|
|
1200
|
+
// the empty document its constructor recorded; the first undo could wipe the
|
|
1201
|
+
// region. Rebase the stack on the restored content. This touches Squire
|
|
1202
|
+
// internals (property names survive its esbuild minify) and no-ops for other
|
|
1203
|
+
// engines, like the tests' FakeSquire.
|
|
1204
|
+
resetSquireUndoBaseline() {
|
|
1205
|
+
const squire = this._squire;
|
|
1206
|
+
if (!squire || !Array.isArray(squire._undoStack) || typeof squire.saveUndoState !== "function") {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
squire._undoStack.length = 0;
|
|
1210
|
+
squire._undoStackLength = 0;
|
|
1211
|
+
squire._undoIndex = -1;
|
|
1212
|
+
squire._isInUndoState = false;
|
|
1213
|
+
squire.saveUndoState();
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
updatePlaceholder() {
|
|
1217
|
+
const text = (this.element.textContent || "").replace(/\u200B/g, "").trim();
|
|
1218
|
+
const hasMeaningfulElement = this.element.querySelector("img, video, audio, iframe, table");
|
|
1219
|
+
const isEmpty = !text && !hasMeaningfulElement;
|
|
1220
|
+
this.element.classList.toggle("richclay-empty", isEmpty);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
announceControl(def) {
|
|
1224
|
+
if (!this.liveRegion) return;
|
|
1225
|
+
const label = def.ariaLabel || def.label;
|
|
1226
|
+
if (!label) return;
|
|
1227
|
+
|
|
1228
|
+
if (typeof def.isActive === "function") {
|
|
1229
|
+
announce(this.liveRegion, `${label} ${def.isActive(this) ? "on" : "off"}`);
|
|
1230
|
+
} else {
|
|
1231
|
+
announce(this.liveRegion, `${label} applied`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
export { presets };
|
|
1237
|
+
|
|
1238
|
+
function resolveElements(selector) {
|
|
1239
|
+
if (typeof selector === "string") {
|
|
1240
|
+
return Array.from(document.querySelectorAll(selector));
|
|
1241
|
+
}
|
|
1242
|
+
if (selector?.nodeType === 1) return [selector];
|
|
1243
|
+
return Array.from(selector || []);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// A selection anchored on the root itself, which is what Select All produces, has
|
|
1247
|
+
// no ancestor chain to walk, so the boundary is first resolved down to the node it
|
|
1248
|
+
// actually points at. Type selectors are ASCII case-insensitive in HTML documents
|
|
1249
|
+
// but jsdom's closest() is not, hence the lowercased selector.
|
|
1250
|
+
function matchAtBoundary(root, container, offset, first, selector) {
|
|
1251
|
+
let node = container;
|
|
1252
|
+
if (node === root) {
|
|
1253
|
+
const children = Array.from(root.childNodes).filter(isCaretHost);
|
|
1254
|
+
if (!children.length) return null;
|
|
1255
|
+
const index = first
|
|
1256
|
+
? Math.min(offset, children.length - 1)
|
|
1257
|
+
: Math.min(Math.max(offset - 1, 0), children.length - 1);
|
|
1258
|
+
node = caretEdge(children[index], first);
|
|
1259
|
+
}
|
|
1260
|
+
const element = node?.nodeType === 1 ? node : node?.parentElement;
|
|
1261
|
+
const match = element?.closest?.(selector);
|
|
1262
|
+
return match && root.contains(match) && match !== root ? match : null;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// An ancestor conflicts when it holds a live editor, or when it would get one.
|
|
1266
|
+
// closest() cannot answer the second half on its own: it stops at the nearest
|
|
1267
|
+
// element matching the selector, and a custom element the host guard refuses
|
|
1268
|
+
// matches it while never becoming an editor, which made <my-grid editable> block a
|
|
1269
|
+
// genuine <h2 editable> inside it and left the page with no editors at all.
|
|
1270
|
+
function closestConflictingAncestor(element) {
|
|
1271
|
+
let node = element.parentElement;
|
|
1272
|
+
while (node) {
|
|
1273
|
+
if (instances.has(node) || isMountable(node)) return node;
|
|
1274
|
+
node = node.parentElement;
|
|
1275
|
+
}
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Two Squire instances mutating one subtree is undefined: the outer editor's
|
|
1280
|
+
// repair restructures the inner one's content behind its back, inside the outer's
|
|
1281
|
+
// modifyDocument, so the inner instance never sees it. Checked in both directions,
|
|
1282
|
+
// because the editors can be constructed in either order.
|
|
1283
|
+
// The single question every mount path asks: would the default watcher adopt this?
|
|
1284
|
+
function isMountable(element) {
|
|
1285
|
+
return Boolean(element.matches?.(RICHCLAY_SELECTOR)) && isRichClayHost(element);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function conflictsWithExistingEditor(element) {
|
|
1289
|
+
const host = closestConflictingAncestor(element);
|
|
1290
|
+
const nested = Array.from(element.querySelectorAll?.(RICHCLAY_SELECTOR) || []).find(node =>
|
|
1291
|
+
instances.has(node)
|
|
1292
|
+
);
|
|
1293
|
+
const other = host || nested;
|
|
1294
|
+
if (!other) return false;
|
|
1295
|
+
console.warn(
|
|
1296
|
+
"richclay: nested editable regions are not supported, so this one was skipped. " +
|
|
1297
|
+
"Remove the editable attribute from either it or the other region.",
|
|
1298
|
+
element,
|
|
1299
|
+
other
|
|
1300
|
+
);
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function refuseUnsupportedRoot(element) {
|
|
1305
|
+
if (!isUnsupportedRootTag(element)) return false;
|
|
1306
|
+
console.warn(`richclay: ${unsupportedRootReason(element)} This region was skipped.`, element);
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function validateButton(def) {
|
|
1311
|
+
if (!def || !def.id) throw new Error("RichClay button definitions require an id.");
|
|
1312
|
+
if (def.type !== "menu" && typeof def.run !== "function") {
|
|
1313
|
+
throw new Error(`RichClay button "${def.id}" requires a run(editor) function.`);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Squire looks a handler up by "Alt-Ctrl-Meta-Shift-<key>", in that order, and
|
|
1318
|
+
// binds Mod to Meta on Apple platforms and to Ctrl elsewhere. richclay used to
|
|
1319
|
+
// bind both, which on macOS swallowed the Emacs bindings every text field has:
|
|
1320
|
+
// Ctrl+D (delete forward), Ctrl+K (kill line), Ctrl+B, Ctrl+U, Ctrl+I.
|
|
1321
|
+
function shortcutKey(shortcut, win) {
|
|
1322
|
+
const parts = shortcut.split("+");
|
|
1323
|
+
const keyPart = parts[parts.length - 1];
|
|
1324
|
+
const modifiers = new Set(parts.slice(0, -1).map(normalizeModifier));
|
|
1325
|
+
if (modifiers.delete("Mod")) modifiers.add(isApplePlatform(win) ? "Meta" : "Ctrl");
|
|
1326
|
+
|
|
1327
|
+
const key =
|
|
1328
|
+
keyPart.length === 1
|
|
1329
|
+
? modifiers.has("Shift")
|
|
1330
|
+
? keyPart.toUpperCase()
|
|
1331
|
+
: keyPart.toLowerCase()
|
|
1332
|
+
: keyPart;
|
|
1333
|
+
const prefix = MODIFIER_ORDER.filter(modifier => modifiers.has(modifier)).join("-");
|
|
1334
|
+
return `${prefix ? `${prefix}-` : ""}${key}`;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function normalizeModifier(modifier) {
|
|
1338
|
+
return modifier === "Cmd" ? "Meta" : modifier;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function trapTab(event, container) {
|
|
1342
|
+
const focusables = container.querySelectorAll(
|
|
1343
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
1344
|
+
);
|
|
1345
|
+
if (!focusables.length) return;
|
|
1346
|
+
const first = focusables[0];
|
|
1347
|
+
const last = focusables[focusables.length - 1];
|
|
1348
|
+
const active = container.ownerDocument.activeElement;
|
|
1349
|
+
if (event.shiftKey && active === first) {
|
|
1350
|
+
event.preventDefault();
|
|
1351
|
+
last.focus();
|
|
1352
|
+
} else if (!event.shiftKey && active === last) {
|
|
1353
|
+
event.preventDefault();
|
|
1354
|
+
first.focus();
|
|
1355
|
+
}
|
|
1356
|
+
}
|