@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,2962 @@
|
|
|
1
|
+
/* richclay global build. Source lives in src/. Run npm run build to regenerate. */
|
|
2
|
+
var RichClayBundle = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/browser-global.js
|
|
22
|
+
var browser_global_exports = {};
|
|
23
|
+
__export(browser_global_exports, {
|
|
24
|
+
default: () => browser_global_default
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// src/buttons.js
|
|
28
|
+
var S = 'stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"';
|
|
29
|
+
var STHIN = 'stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"';
|
|
30
|
+
var F = 'fill="currentColor"';
|
|
31
|
+
var svg = (inner, viewBox = "0 0 18 18") => `<svg viewBox="${viewBox}" width="18" height="18" aria-hidden="true" focusable="false">${inner}</svg>`;
|
|
32
|
+
var linkInner = `<line ${S} x1="7" x2="11" y1="7" y2="11"/><path ${S} d="M8.9,4.577a3.476,3.476,0,0,1,.36,4.679A3.476,3.476,0,0,1,4.577,8.9C3.185,7.5,2.035,6.4,4.217,4.217S7.5,3.185,8.9,4.577Z"/><path ${S} d="M13.423,9.1a3.476,3.476,0,0,0-4.679-.36,3.476,3.476,0,0,0,.36,4.679c1.392,1.392,2.5,2.542,4.679.36S14.815,10.5,13.423,9.1Z"/>`;
|
|
33
|
+
var icons = {
|
|
34
|
+
bold: svg(`<path ${S} d="M5,4H9.5A2.5,2.5,0,0,1,12,6.5v0A2.5,2.5,0,0,1,9.5,9H5A0,0,0,0,1,5,9V4A0,0,0,0,1,5,4Z"/><path ${S} d="M5,9h5.5A2.5,2.5,0,0,1,13,11.5v0A2.5,2.5,0,0,1,10.5,14H5a0,0,0,0,1,0,0V9A0,0,0,0,1,5,9Z"/>`),
|
|
35
|
+
italic: svg(`<line ${S} x1="7" x2="13" y1="4" y2="4"/><line ${S} x1="5" x2="11" y1="14" y2="14"/><line ${S} x1="8" x2="10" y1="14" y2="4"/>`),
|
|
36
|
+
underline: svg(`<path ${S} d="M5,3V9a4.012,4.012,0,0,0,4,4H9a4.012,4.012,0,0,0,4-4V3"/><rect ${F} height="1" rx="0.5" ry="0.5" width="12" x="3" y="15"/>`),
|
|
37
|
+
strike: svg(`<path ${F} d="M9.007,8C6.542,7.791,6,7.519,6,6.5,6,5.792,7.283,5,9,5c1.571,0,2.765.679,2.969,1.309a1,1,0,0,0,1.9-.617C13.356,4.106,11.354,3,9,3,6.2,3,4,4.538,4,6.5a3.2,3.2,0,0,0,.5,1.843Z"/><path ${F} d="M8.984,10C11.457,10.208,12,10.479,12,11.5c0,0.708-1.283,1.5-3,1.5-1.571,0-2.765-.679-2.969-1.309a1,1,0,1,0-1.9.617C4.644,13.894,6.646,15,9,15c2.8,0,5-1.538,5-3.5a3.2,3.2,0,0,0-.5-1.843Z"/><line x1="2" y1="9" x2="16" y2="9" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5"/>`),
|
|
38
|
+
link: svg(linkInner),
|
|
39
|
+
unlink: svg(`${linkInner}<line class="richclay-cut" x1="2.5" y1="15.5" x2="15.5" y2="2.5" stroke-linecap="round" stroke-width="3.5"/><line x1="4.06" y1="13.94" x2="13.94" y2="4.06" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5"/>`),
|
|
40
|
+
ul: svg(`<line ${S} x1="6" x2="15" y1="4" y2="4"/><line ${S} x1="6" x2="15" y1="9" y2="9"/><line ${S} x1="6" x2="15" y1="14" y2="14"/><line ${S} x1="3" x2="3" y1="4" y2="4"/><line ${S} x1="3" x2="3" y1="9" y2="9"/><line ${S} x1="3" x2="3" y1="14" y2="14"/>`),
|
|
41
|
+
ol: svg(`<line ${S} x1="7" x2="15" y1="4" y2="4"/><line ${S} x1="7" x2="15" y1="9" y2="9"/><line ${S} x1="7" x2="15" y1="14" y2="14"/><line ${STHIN} x1="2.5" x2="4.5" y1="5.5" y2="5.5"/><path ${F} d="M3.5,6A0.5,0.5,0,0,1,3,5.5V3.085l-0.276.138A0.5,0.5,0,0,1,2.053,3c-0.124-.247-0.023-0.324.224-0.447l1-.5A0.5,0.5,0,0,1,4,2.5v3A0.5,0.5,0,0,1,3.5,6Z"/><path ${STHIN} d="M4.5,10.5h-2c0-.234,1.85-1.076,1.85-2.234A0.959,0.959,0,0,0,2.5,8.156"/><path ${STHIN} d="M2.5,14.846a0.959,0.959,0,0,0,1.85-.109A0.7,0.7,0,0,0,3.75,14a0.688,0.688,0,0,0,.6-0.736,0.959,0.959,0,0,0-1.85-.109"/>`),
|
|
42
|
+
quote: svg(`<path ${S} d="M10 10.8182L9 10.8182C8.80222 10.8182 8.60888 10.7649 8.44443 10.665C8.27998 10.5651 8.15181 10.4231 8.07612 10.257C8.00043 10.0909 7.98063 9.90808 8.01922 9.73174C8.0578 9.55539 8.15304 9.39341 8.29289 9.26627C8.43275 9.13913 8.61093 9.05255 8.80491 9.01747C8.99889 8.98239 9.19996 9.00039 9.38268 9.0692C9.56541 9.13801 9.72159 9.25453 9.83147 9.40403C9.94135 9.55353 10 9.72929 10 9.90909L10 12.1818C10 12.664 9.78929 13.1265 9.41421 13.4675C9.03914 13.8084 8.53043 14 8 14"/><path ${S} d="M16 10.8182L15 10.8182C14.8022 10.8182 14.6089 10.7649 14.4444 10.665C14.28 10.5651 14.1518 10.4231 14.0761 10.257C14.0004 10.0909 13.9806 9.90808 14.0192 9.73174C14.0578 9.55539 14.153 9.39341 14.2929 9.26627C14.4327 9.13913 14.6109 9.05255 14.8049 9.01747C14.9989 8.98239 15.2 9.00039 15.3827 9.0692C15.5654 9.13801 15.7216 9.25453 15.8315 9.40403C15.9414 9.55353 16 9.72929 16 9.90909L16 12.1818C16 12.664 15.7893 13.1265 15.4142 13.4675C15.0391 13.8084 14.5304 14 14 14"/>`, "2 2 20 20"),
|
|
43
|
+
undo: svg(`<polyline ${S} points="6.5 4 3.5 7 6.5 10"/><path ${S} d="M3.5 7H10a3.5 3.5 0 0 1 0 7H7"/>`),
|
|
44
|
+
redo: svg(`<polyline ${S} points="11.5 4 14.5 7 11.5 10"/><path ${S} d="M14.5 7H8a3.5 3.5 0 0 0 0 7h3"/>`),
|
|
45
|
+
clear: svg(`<line ${S} x1="5" x2="13" y1="3" y2="3"/><line ${S} x1="6" x2="9.35" y1="12" y2="3"/><line ${S} x1="11" x2="15" y1="11" y2="15"/><line ${S} x1="15" x2="11" y1="11" y2="15"/><rect ${F} height="1" rx="0.5" ry="0.5" width="7" x="2" y="14"/>`),
|
|
46
|
+
code: svg(`<polyline ${S} points="5 7 3 9 5 11"/><polyline ${S} points="13 7 15 9 13 11"/><line ${S} x1="10" x2="8" y1="5" y2="13"/>`),
|
|
47
|
+
indent: svg(`<line ${S} x1="3" x2="15" y1="4" y2="4"/><line ${S} x1="8" x2="15" y1="9" y2="9"/><line ${S} x1="3" x2="15" y1="14" y2="14"/><polyline ${S} points="3 7 5.5 9 3 11"/>`),
|
|
48
|
+
outdent: svg(`<line ${S} x1="3" x2="15" y1="4" y2="4"/><line ${S} x1="8" x2="15" y1="9" y2="9"/><line ${S} x1="3" x2="15" y1="14" y2="14"/><polyline ${S} points="5.5 7 3 9 5.5 11"/>`),
|
|
49
|
+
blocks: svg(`<line ${S} x1="3" x2="15" y1="4" y2="4"/><line ${S} x1="3" x2="15" y1="9" y2="9"/><line ${S} x1="3" x2="10" y1="14" y2="14"/>`)
|
|
50
|
+
};
|
|
51
|
+
var blocksOnly = (editor) => editor.blocksStayOut();
|
|
52
|
+
var notInsideLink = (editor) => Boolean(editor.element.closest("a"));
|
|
53
|
+
var presets = {
|
|
54
|
+
minimal: ["bold", "italic", "link", "unorderedList"],
|
|
55
|
+
inline: [
|
|
56
|
+
"bold",
|
|
57
|
+
"italic",
|
|
58
|
+
"underline",
|
|
59
|
+
"strikethrough",
|
|
60
|
+
"code",
|
|
61
|
+
"link",
|
|
62
|
+
"unlink",
|
|
63
|
+
"undo",
|
|
64
|
+
"redo",
|
|
65
|
+
"clearFormatting"
|
|
66
|
+
],
|
|
67
|
+
standard: [
|
|
68
|
+
"blockMenu",
|
|
69
|
+
"bold",
|
|
70
|
+
"italic",
|
|
71
|
+
"underline",
|
|
72
|
+
"strikethrough",
|
|
73
|
+
"code",
|
|
74
|
+
"link",
|
|
75
|
+
"unlink",
|
|
76
|
+
"unorderedList",
|
|
77
|
+
"orderedList",
|
|
78
|
+
"quote",
|
|
79
|
+
"outdent",
|
|
80
|
+
"indent",
|
|
81
|
+
"undo",
|
|
82
|
+
"redo",
|
|
83
|
+
"clearFormatting"
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
function createDefaultRegistry() {
|
|
87
|
+
return new Map(defaultButtons.map((button) => [button.id, button]));
|
|
88
|
+
}
|
|
89
|
+
function isApplePlatform(win = globalThis) {
|
|
90
|
+
const nav = win?.navigator;
|
|
91
|
+
if (!nav) return false;
|
|
92
|
+
return /Mac|iPhone|iPad|iPod/.test(nav.platform || nav.userAgent || "");
|
|
93
|
+
}
|
|
94
|
+
function formatShortcut(shortcut, win = globalThis) {
|
|
95
|
+
if (!shortcut) return "";
|
|
96
|
+
return shortcut.replace("Mod", isApplePlatform(win) ? "Cmd" : "Ctrl");
|
|
97
|
+
}
|
|
98
|
+
var defaultButtons = [
|
|
99
|
+
toggle("bold", "Bold", icons.bold, "Mod+B", "inline", (editor) => editor.toggleFormat("B", "bold", "removeBold")),
|
|
100
|
+
toggle(
|
|
101
|
+
"italic",
|
|
102
|
+
"Italic",
|
|
103
|
+
icons.italic,
|
|
104
|
+
"Mod+I",
|
|
105
|
+
"inline",
|
|
106
|
+
(editor) => editor.toggleFormat("I", "italic", "removeItalic")
|
|
107
|
+
),
|
|
108
|
+
toggle(
|
|
109
|
+
"underline",
|
|
110
|
+
"Underline",
|
|
111
|
+
icons.underline,
|
|
112
|
+
"Mod+U",
|
|
113
|
+
"inline",
|
|
114
|
+
(editor) => editor.toggleFormat("U", "underline", "removeUnderline")
|
|
115
|
+
),
|
|
116
|
+
toggle(
|
|
117
|
+
"strikethrough",
|
|
118
|
+
"Strikethrough",
|
|
119
|
+
icons.strike,
|
|
120
|
+
"Mod+Shift+7",
|
|
121
|
+
"inline",
|
|
122
|
+
(editor) => editor.toggleFormat("S", "strikethrough", "removeStrikethrough")
|
|
123
|
+
),
|
|
124
|
+
// No shortcut. One keystroke turning a paragraph of prose into a code block is
|
|
125
|
+
// a sharp edge in an editor whose DOM is the saved file, and Mod+D also shadows
|
|
126
|
+
// the browser's own bookmark shortcut. The toolbar button stays.
|
|
127
|
+
toggle(
|
|
128
|
+
"code",
|
|
129
|
+
"Code",
|
|
130
|
+
icons.code,
|
|
131
|
+
null,
|
|
132
|
+
"inline",
|
|
133
|
+
(editor) => editor.toggleCode(),
|
|
134
|
+
(editor) => editor.selectionHasFormat("CODE") || editor.selectionHasFormat("PRE")
|
|
135
|
+
),
|
|
136
|
+
{
|
|
137
|
+
id: "link",
|
|
138
|
+
label: "Link",
|
|
139
|
+
ariaLabel: "Insert or edit link",
|
|
140
|
+
icon: icons.link,
|
|
141
|
+
group: "links",
|
|
142
|
+
shortcut: "Mod+K",
|
|
143
|
+
mutates: false,
|
|
144
|
+
isDisabled: notInsideLink,
|
|
145
|
+
run: (editor) => editor.openLinkDialog(),
|
|
146
|
+
isActive: (editor) => editor.selectionHasFormat("A")
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "unlink",
|
|
150
|
+
label: "Remove link",
|
|
151
|
+
ariaLabel: "Remove link",
|
|
152
|
+
icon: icons.unlink,
|
|
153
|
+
group: "links",
|
|
154
|
+
run: (editor) => editor.squire.removeLink()
|
|
155
|
+
},
|
|
156
|
+
toggle(
|
|
157
|
+
"unorderedList",
|
|
158
|
+
"Bulleted list",
|
|
159
|
+
icons.ul,
|
|
160
|
+
"Mod+Shift+8",
|
|
161
|
+
"lists",
|
|
162
|
+
(editor) => editor.toggleList("UL"),
|
|
163
|
+
(editor) => editor.pathHas("UL"),
|
|
164
|
+
blocksOnly
|
|
165
|
+
),
|
|
166
|
+
toggle(
|
|
167
|
+
"orderedList",
|
|
168
|
+
"Numbered list",
|
|
169
|
+
icons.ol,
|
|
170
|
+
"Mod+Shift+9",
|
|
171
|
+
"lists",
|
|
172
|
+
(editor) => editor.toggleList("OL"),
|
|
173
|
+
(editor) => editor.pathHas("OL"),
|
|
174
|
+
blocksOnly
|
|
175
|
+
),
|
|
176
|
+
toggle("quote", "Quote", icons.quote, null, "blocks", (editor) => {
|
|
177
|
+
if (editor.pathHas("BLOCKQUOTE")) return editor.squire.decreaseQuoteLevel();
|
|
178
|
+
return editor.squire.increaseQuoteLevel();
|
|
179
|
+
}, (editor) => editor.pathHas("BLOCKQUOTE"), blocksOnly),
|
|
180
|
+
{
|
|
181
|
+
id: "outdent",
|
|
182
|
+
label: "Outdent",
|
|
183
|
+
ariaLabel: "Decrease indent",
|
|
184
|
+
icon: icons.outdent,
|
|
185
|
+
group: "blocks",
|
|
186
|
+
shortcut: "Mod+[",
|
|
187
|
+
isDisabled: blocksOnly,
|
|
188
|
+
run: (editor) => editor.outdent()
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "indent",
|
|
192
|
+
label: "Indent",
|
|
193
|
+
ariaLabel: "Increase indent",
|
|
194
|
+
icon: icons.indent,
|
|
195
|
+
group: "blocks",
|
|
196
|
+
shortcut: "Mod+]",
|
|
197
|
+
isDisabled: blocksOnly,
|
|
198
|
+
run: (editor) => editor.indent()
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "undo",
|
|
202
|
+
label: "Undo",
|
|
203
|
+
ariaLabel: "Undo",
|
|
204
|
+
icon: icons.undo,
|
|
205
|
+
group: "history",
|
|
206
|
+
shortcut: "Mod+Z",
|
|
207
|
+
mutates: false,
|
|
208
|
+
run: (editor) => editor.squire.undo()
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "redo",
|
|
212
|
+
label: "Redo",
|
|
213
|
+
ariaLabel: "Redo",
|
|
214
|
+
icon: icons.redo,
|
|
215
|
+
group: "history",
|
|
216
|
+
shortcut: "Mod+Shift+Z",
|
|
217
|
+
mutates: false,
|
|
218
|
+
run: (editor) => editor.squire.redo()
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "clearFormatting",
|
|
222
|
+
label: "Clear formatting",
|
|
223
|
+
ariaLabel: "Clear formatting",
|
|
224
|
+
icon: icons.clear,
|
|
225
|
+
group: "cleanup",
|
|
226
|
+
run: (editor) => editor.squire.removeAllFormatting()
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "blockMenu",
|
|
230
|
+
type: "menu",
|
|
231
|
+
label: "Block style",
|
|
232
|
+
ariaLabel: "Choose block style",
|
|
233
|
+
icon: icons.blocks,
|
|
234
|
+
group: "blocks",
|
|
235
|
+
isDisabled: blocksOnly,
|
|
236
|
+
options: [
|
|
237
|
+
blockOption("Paragraph", "P"),
|
|
238
|
+
blockOption("Heading 1", "H1"),
|
|
239
|
+
blockOption("Heading 2", "H2"),
|
|
240
|
+
blockOption("Heading 3", "H3"),
|
|
241
|
+
{
|
|
242
|
+
label: "Quote",
|
|
243
|
+
value: "BLOCKQUOTE",
|
|
244
|
+
isDisabled: blocksOnly,
|
|
245
|
+
run: (editor) => editor.squire.increaseQuoteLevel(),
|
|
246
|
+
isActive: (editor) => editor.pathHas("BLOCKQUOTE")
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
label: "Code block",
|
|
250
|
+
value: "PRE",
|
|
251
|
+
isDisabled: blocksOnly,
|
|
252
|
+
run: (editor) => editor.setBlockType("PRE"),
|
|
253
|
+
isActive: (editor) => editor.pathHas("PRE")
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
];
|
|
258
|
+
function toggle(id, label, icon, shortcut, group, run, isActive = (editor) => editor.selectionHasFormat(labelTag(id)), isDisabled) {
|
|
259
|
+
return {
|
|
260
|
+
id,
|
|
261
|
+
label,
|
|
262
|
+
ariaLabel: label,
|
|
263
|
+
icon,
|
|
264
|
+
group,
|
|
265
|
+
shortcut,
|
|
266
|
+
isDisabled,
|
|
267
|
+
run,
|
|
268
|
+
isActive
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function labelTag(id) {
|
|
272
|
+
return {
|
|
273
|
+
bold: "B",
|
|
274
|
+
italic: "I",
|
|
275
|
+
underline: "U",
|
|
276
|
+
strikethrough: "S"
|
|
277
|
+
}[id];
|
|
278
|
+
}
|
|
279
|
+
function blockOption(label, tag) {
|
|
280
|
+
return {
|
|
281
|
+
label,
|
|
282
|
+
value: tag,
|
|
283
|
+
isDisabled: blocksOnly,
|
|
284
|
+
run: (editor) => editor.setBlockType(tag),
|
|
285
|
+
isActive: (editor) => editor.pathHas(tag)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/normalize.js
|
|
290
|
+
var INLINE_NODE_NAMES = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/;
|
|
291
|
+
var NOT_WHITESPACE = /[^ \t\r\n]/;
|
|
292
|
+
var ELEMENT_NODE = 1;
|
|
293
|
+
var TEXT_NODE = 3;
|
|
294
|
+
var COMMENT_NODE = 8;
|
|
295
|
+
var LEAF_NODE_NAMES = /* @__PURE__ */ new Set(["BR", "HR", "IFRAME", "IMG", "INPUT", "WBR"]);
|
|
296
|
+
var FOREIGN_INLINE_ROOTS = /* @__PURE__ */ new Set(["svg", "math"]);
|
|
297
|
+
function isInlineNode(node) {
|
|
298
|
+
const type = node.nodeType;
|
|
299
|
+
if (type === TEXT_NODE || type === COMMENT_NODE) return true;
|
|
300
|
+
if (type !== ELEMENT_NODE) return false;
|
|
301
|
+
if (FOREIGN_INLINE_ROOTS.has(node.nodeName)) return true;
|
|
302
|
+
if (!INLINE_NODE_NAMES.test(node.nodeName)) return false;
|
|
303
|
+
return Array.from(node.childNodes).every(isInlineNode);
|
|
304
|
+
}
|
|
305
|
+
function isBlockContainer(element) {
|
|
306
|
+
return Array.from(element.childNodes).some((node) => !isInlineNode(node));
|
|
307
|
+
}
|
|
308
|
+
var isBlockElement = (node) => node?.nodeType === ELEMENT_NODE && !FOREIGN_INLINE_ROOTS.has(node.nodeName) && !INLINE_NODE_NAMES.test(node.nodeName);
|
|
309
|
+
function flattenBlocks(parent, createBoundary) {
|
|
310
|
+
const children = Array.from(parent.childNodes);
|
|
311
|
+
let removed = 0;
|
|
312
|
+
children.forEach((child, index) => {
|
|
313
|
+
if (child.nodeType !== ELEMENT_NODE || FOREIGN_INLINE_ROOTS.has(child.nodeName)) return;
|
|
314
|
+
removed += flattenBlocks(child, createBoundary);
|
|
315
|
+
if (!isBlockElement(child)) return;
|
|
316
|
+
if (isBlockElement(children[index - 1])) parent.insertBefore(createBoundary(), child);
|
|
317
|
+
while (child.firstChild) parent.insertBefore(child.firstChild, child);
|
|
318
|
+
child.remove();
|
|
319
|
+
removed += 1;
|
|
320
|
+
});
|
|
321
|
+
return removed;
|
|
322
|
+
}
|
|
323
|
+
var TABLE_STRUCTURE = /* @__PURE__ */ new Set(["TABLE", "THEAD", "TBODY", "TFOOT", "TR", "COLGROUP"]);
|
|
324
|
+
function ejectsBlocks(root) {
|
|
325
|
+
return Boolean(root.closest?.("p")) || TABLE_STRUCTURE.has(root.nodeName);
|
|
326
|
+
}
|
|
327
|
+
var TEXT_LINE_ROOTS = /* @__PURE__ */ new Set([
|
|
328
|
+
"P",
|
|
329
|
+
"H1",
|
|
330
|
+
"H2",
|
|
331
|
+
"H3",
|
|
332
|
+
"H4",
|
|
333
|
+
"H5",
|
|
334
|
+
"H6",
|
|
335
|
+
"PRE",
|
|
336
|
+
"LABEL",
|
|
337
|
+
"LEGEND",
|
|
338
|
+
"LI",
|
|
339
|
+
"TD",
|
|
340
|
+
"TH",
|
|
341
|
+
"DT",
|
|
342
|
+
"DD",
|
|
343
|
+
"FIGCAPTION",
|
|
344
|
+
"SUMMARY",
|
|
345
|
+
"CAPTION"
|
|
346
|
+
]);
|
|
347
|
+
function keepsTextShape(root) {
|
|
348
|
+
return TEXT_LINE_ROOTS.has(root.nodeName) || isInlineTag(root);
|
|
349
|
+
}
|
|
350
|
+
var VOID_ROOTS = /* @__PURE__ */ new Set([
|
|
351
|
+
"AREA",
|
|
352
|
+
"BASE",
|
|
353
|
+
"BR",
|
|
354
|
+
"COL",
|
|
355
|
+
"EMBED",
|
|
356
|
+
"HR",
|
|
357
|
+
"IMG",
|
|
358
|
+
"INPUT",
|
|
359
|
+
"LINK",
|
|
360
|
+
"META",
|
|
361
|
+
"PARAM",
|
|
362
|
+
"SOURCE",
|
|
363
|
+
"TRACK",
|
|
364
|
+
"WBR"
|
|
365
|
+
]);
|
|
366
|
+
var RAW_TEXT_ROOTS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "TEXTAREA", "TITLE", "IFRAME", "NOSCRIPT", "XMP"]);
|
|
367
|
+
var HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
368
|
+
function isUnsupportedRootTag(root) {
|
|
369
|
+
return TABLE_STRUCTURE.has(root.nodeName) || VOID_ROOTS.has(root.nodeName) || RAW_TEXT_ROOTS.has(root.nodeName) || root.nodeName === "TEMPLATE" || Boolean(root.namespaceURI) && root.namespaceURI !== HTML_NS;
|
|
370
|
+
}
|
|
371
|
+
function unsupportedRootReason(root) {
|
|
372
|
+
if (TABLE_STRUCTURE.has(root.nodeName)) {
|
|
373
|
+
return "a table element cannot be an editable region. Move the editable attribute to a <td>, a <th>, or an element inside one.";
|
|
374
|
+
}
|
|
375
|
+
if (root.namespaceURI && root.namespaceURI !== HTML_NS) {
|
|
376
|
+
return "an SVG or MathML element cannot be an editable region: an edit puts an HTML block inside it, and the next page load moves that text out of the graphic entirely. Put a <foreignObject> in the SVG and mark an HTML element inside it instead.";
|
|
377
|
+
}
|
|
378
|
+
if (VOID_ROOTS.has(root.nodeName)) {
|
|
379
|
+
return "this element cannot contain anything, so there is nothing to edit. Move the editable attribute to an element that holds text.";
|
|
380
|
+
}
|
|
381
|
+
if (root.nodeName === "TEMPLATE") {
|
|
382
|
+
return "a <template> keeps its content out of the document, so edits to it are never saved.";
|
|
383
|
+
}
|
|
384
|
+
return "this element treats its content as plain text, so any edit would be saved as visible markup rather than formatting.";
|
|
385
|
+
}
|
|
386
|
+
function isInlineTag(node) {
|
|
387
|
+
return node.nodeType === ELEMENT_NODE && !FOREIGN_INLINE_ROOTS.has(node.nodeName) && INLINE_NODE_NAMES.test(node.nodeName);
|
|
388
|
+
}
|
|
389
|
+
function hasBlockDescendant(root) {
|
|
390
|
+
return Array.from(root.querySelectorAll("*")).some(isBlockElement);
|
|
391
|
+
}
|
|
392
|
+
function normalizeEditorRoot(root, options = {}) {
|
|
393
|
+
const {
|
|
394
|
+
blockTag = "DIV",
|
|
395
|
+
wrapBareRoot = false,
|
|
396
|
+
onBareRootWrapped,
|
|
397
|
+
blocksAllowed = !ejectsBlocks(root)
|
|
398
|
+
} = options;
|
|
399
|
+
dropFormattingWhitespace(root);
|
|
400
|
+
if (blocksAllowed) {
|
|
401
|
+
wrapStrayInlineChildren(root, blockTag, wrapBareRoot, onBareRootWrapped);
|
|
402
|
+
}
|
|
403
|
+
return root;
|
|
404
|
+
}
|
|
405
|
+
var needsWrapping = (node) => isInlineNode(node) && node.nodeType !== COMMENT_NODE;
|
|
406
|
+
var isAuthorContent = (node) => node.nodeType !== COMMENT_NODE && !(node.nodeType === TEXT_NODE && !NOT_WHITESPACE.test(node.nodeValue));
|
|
407
|
+
function editorRootNeedsNormalization(root, { wrapBareRoot = false, blocksAllowed = !ejectsBlocks(root) } = {}) {
|
|
408
|
+
if (!blocksAllowed) return false;
|
|
409
|
+
if (!isBlockContainer(root)) return wrapBareRoot;
|
|
410
|
+
return Array.from(root.childNodes).some(needsWrapping) || hasNestedFormattingWhitespace(root);
|
|
411
|
+
}
|
|
412
|
+
function hasNestedFormattingWhitespace(element) {
|
|
413
|
+
return Array.from(element.children).some((child) => {
|
|
414
|
+
if (!isBlockContainer(child)) return false;
|
|
415
|
+
const ownWhitespace = !preservesWhitespace(child) && Array.from(child.childNodes).some(
|
|
416
|
+
(node) => node.nodeType === TEXT_NODE && !NOT_WHITESPACE.test(node.nodeValue)
|
|
417
|
+
);
|
|
418
|
+
return ownWhitespace || hasNestedFormattingWhitespace(child);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function dropFormattingWhitespace(element) {
|
|
422
|
+
if (!isBlockContainer(element)) return;
|
|
423
|
+
if (!preservesWhitespace(element)) {
|
|
424
|
+
Array.from(element.childNodes).forEach((child) => {
|
|
425
|
+
if (child.nodeType === TEXT_NODE && !NOT_WHITESPACE.test(child.nodeValue)) child.remove();
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
Array.from(element.children).forEach(dropFormattingWhitespace);
|
|
429
|
+
}
|
|
430
|
+
function wrapStrayInlineChildren(root, blockTag, wrapBareRoot, onBareRootWrapped) {
|
|
431
|
+
if (!isBlockContainer(root)) {
|
|
432
|
+
if (!wrapBareRoot) return;
|
|
433
|
+
const wrapper = root.ownerDocument.createElement(blockTag);
|
|
434
|
+
if (!Array.from(root.childNodes).some(isAuthorContent)) {
|
|
435
|
+
wrapper.appendChild(root.ownerDocument.createElement("BR"));
|
|
436
|
+
root.appendChild(wrapper);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
while (root.firstChild) wrapper.appendChild(root.firstChild);
|
|
440
|
+
root.appendChild(wrapper);
|
|
441
|
+
onBareRootWrapped?.(root, wrapper);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
let run = [];
|
|
445
|
+
const flush = () => {
|
|
446
|
+
const nodes = run;
|
|
447
|
+
run = [];
|
|
448
|
+
if (!nodes.some((node) => node.nodeType !== COMMENT_NODE)) return;
|
|
449
|
+
const block = root.ownerDocument.createElement(blockTag);
|
|
450
|
+
root.insertBefore(block, nodes[0]);
|
|
451
|
+
nodes.forEach((node) => block.appendChild(node));
|
|
452
|
+
};
|
|
453
|
+
Array.from(root.childNodes).forEach((child) => {
|
|
454
|
+
if (isInlineNode(child)) run.push(child);
|
|
455
|
+
else flush();
|
|
456
|
+
});
|
|
457
|
+
flush();
|
|
458
|
+
}
|
|
459
|
+
function preservesWhitespace(element) {
|
|
460
|
+
return element.nodeName === "PRE";
|
|
461
|
+
}
|
|
462
|
+
function captureRange(root, range) {
|
|
463
|
+
return {
|
|
464
|
+
start: captureBoundary(root, range.startContainer, range.startOffset),
|
|
465
|
+
end: captureBoundary(root, range.endContainer, range.endOffset),
|
|
466
|
+
collapsed: range.collapsed
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function restoreRange(root, range, saved) {
|
|
470
|
+
applyBoundary(range, "setStart", root, saved.start);
|
|
471
|
+
if (saved.collapsed) range.collapse(true);
|
|
472
|
+
else applyBoundary(range, "setEnd", root, saved.end);
|
|
473
|
+
}
|
|
474
|
+
function captureBoundary(root, container, offset) {
|
|
475
|
+
if (!root.contains(container)) return { scope: root, after: [], before: [] };
|
|
476
|
+
if (container.nodeType === TEXT_NODE) {
|
|
477
|
+
if (!isDroppedWhitespace(container)) return { container, offset };
|
|
478
|
+
const scope = container.parentNode;
|
|
479
|
+
return siblingAnchor(scope, Array.from(scope.childNodes).indexOf(container));
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
container,
|
|
483
|
+
offset,
|
|
484
|
+
childCount: container.childNodes.length,
|
|
485
|
+
...siblingAnchor(container, offset)
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function siblingAnchor(scope, index) {
|
|
489
|
+
const children = Array.from(scope.childNodes);
|
|
490
|
+
const at = Math.max(0, index);
|
|
491
|
+
return { scope, after: children.slice(at), before: children.slice(0, at).reverse() };
|
|
492
|
+
}
|
|
493
|
+
function applyBoundary(range, method, root, boundary) {
|
|
494
|
+
const { container, childCount } = boundary;
|
|
495
|
+
if (container && root.contains(container)) {
|
|
496
|
+
if (childCount === void 0 || childCount === container.childNodes.length) {
|
|
497
|
+
range[method](container, Math.min(boundary.offset, nodeLength(container)));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const after = boundary.after?.find((node) => root.contains(node));
|
|
502
|
+
if (after) {
|
|
503
|
+
range[method](caretEdge(after, true), 0);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const before = boundary.before?.find((node) => root.contains(node));
|
|
507
|
+
if (before) {
|
|
508
|
+
const edge = caretEdge(before, false);
|
|
509
|
+
range[method](edge, nodeLength(edge));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const scope = boundary.scope && root.contains(boundary.scope) ? boundary.scope : root;
|
|
513
|
+
range[method](scope, 0);
|
|
514
|
+
}
|
|
515
|
+
function isDroppedWhitespace(node) {
|
|
516
|
+
return node.nodeType === TEXT_NODE && !NOT_WHITESPACE.test(node.nodeValue) && Boolean(node.parentNode) && node.parentNode.nodeType === ELEMENT_NODE && isBlockContainer(node.parentNode) && !preservesWhitespace(node.parentNode);
|
|
517
|
+
}
|
|
518
|
+
var isCaretHost = (node) => node.nodeType === ELEMENT_NODE || node.nodeType === TEXT_NODE;
|
|
519
|
+
function caretEdge(node, first = true) {
|
|
520
|
+
let current = node;
|
|
521
|
+
while (current.nodeType === ELEMENT_NODE && !FOREIGN_INLINE_ROOTS.has(current.nodeName)) {
|
|
522
|
+
const children = Array.from(current.childNodes).filter(isCaretHost);
|
|
523
|
+
const next = first ? children[0] : children[children.length - 1];
|
|
524
|
+
if (!next || LEAF_NODE_NAMES.has(next.nodeName)) break;
|
|
525
|
+
current = next;
|
|
526
|
+
}
|
|
527
|
+
return current;
|
|
528
|
+
}
|
|
529
|
+
function nodeLength(node) {
|
|
530
|
+
return node.nodeType === TEXT_NODE ? node.nodeValue.length : node.childNodes.length;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/hyperclay.js
|
|
534
|
+
var RICHCLAY_SELECTOR = "[data-richclay], [richclay], [editable], [clay-editable]";
|
|
535
|
+
var RICHCLAY_OPT_IN = "[data-richclay], [richclay], [clay-editable]";
|
|
536
|
+
var EDITABLE_ATTRS = ["editable", "clay-editable"];
|
|
537
|
+
var singleLineSelector = ':is([editable~="single-line"], [clay-editable~="single-line"])';
|
|
538
|
+
function isRichClayHost(el) {
|
|
539
|
+
if (!el || typeof el.matches !== "function") return false;
|
|
540
|
+
if (!el.tagName.includes("-")) return true;
|
|
541
|
+
return el.matches(RICHCLAY_OPT_IN);
|
|
542
|
+
}
|
|
543
|
+
var CHROME_SELECTOR = "[data-richclay-toolbar], [data-richclay-menu], [data-richclay-dialog], [data-richclay-live], [data-richclay-float]";
|
|
544
|
+
var runtimeClasses = [
|
|
545
|
+
"richclay-editor",
|
|
546
|
+
"richclay-inline",
|
|
547
|
+
"richclay-active",
|
|
548
|
+
"richclay-empty",
|
|
549
|
+
"richclay-focused"
|
|
550
|
+
];
|
|
551
|
+
var installedWindows = /* @__PURE__ */ new WeakSet();
|
|
552
|
+
var armedWindows = /* @__PURE__ */ new WeakSet();
|
|
553
|
+
var PLATFORM_READY = ["clay:ready", "hyperclay:ready"];
|
|
554
|
+
function platformReadyPromise(win) {
|
|
555
|
+
return [win.clay?.ready, win.hyperclay?.ready].find(
|
|
556
|
+
(ready) => ready && typeof ready.then === "function"
|
|
557
|
+
) || null;
|
|
558
|
+
}
|
|
559
|
+
function platformEditMode(win) {
|
|
560
|
+
if (typeof win.clay?.isEditMode === "boolean") return win.clay.isEditMode;
|
|
561
|
+
if (typeof win.hyperclay?.isEditMode === "boolean") return win.hyperclay.isEditMode;
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
function hasPlatformLifecycle(win) {
|
|
565
|
+
const modules = win.hyperclayModules;
|
|
566
|
+
const legacyHyperclayLoader = win.__hyperclayNoAutoExport === false && modules && typeof modules === "object" && typeof modules.nodeType !== "number";
|
|
567
|
+
return platformEditMode(win) !== null || Boolean(platformReadyPromise(win)) || Boolean(legacyHyperclayLoader);
|
|
568
|
+
}
|
|
569
|
+
function platformDocumentTransform(win) {
|
|
570
|
+
return win.clay?.addDocumentTransform || win.hyperclay?.beforeSave || null;
|
|
571
|
+
}
|
|
572
|
+
function platformSnapshotTransform(win) {
|
|
573
|
+
return win.clay?.onSnapshot || win.hyperclay?.onSnapshot || null;
|
|
574
|
+
}
|
|
575
|
+
var PRE_CONTAINMENT = {
|
|
576
|
+
boxSizing: "border-box",
|
|
577
|
+
minWidth: "100%",
|
|
578
|
+
overflow: "auto",
|
|
579
|
+
width: "0"
|
|
580
|
+
};
|
|
581
|
+
function shouldUseHyperclay(options = {}, win = window) {
|
|
582
|
+
if (options.hyperclay === false) return false;
|
|
583
|
+
if (options.hyperclay === true) return true;
|
|
584
|
+
return Boolean(hasPlatformLifecycle(win) || hasEditmodeSignal(win));
|
|
585
|
+
}
|
|
586
|
+
function isHyperclayEditMode(win = window) {
|
|
587
|
+
const fromQuery = readEditmodeParam(win);
|
|
588
|
+
if (fromQuery !== null) return fromQuery;
|
|
589
|
+
if (typeof win.__hyperclayEditMode === "boolean") {
|
|
590
|
+
return win.__hyperclayEditMode;
|
|
591
|
+
}
|
|
592
|
+
const fromPlatform = platformEditMode(win);
|
|
593
|
+
if (fromPlatform !== null) return fromPlatform;
|
|
594
|
+
return readEditmodeCookie(win);
|
|
595
|
+
}
|
|
596
|
+
function shouldActivateEditor(options = {}, win = window) {
|
|
597
|
+
if (options.readOnly) return false;
|
|
598
|
+
if (!shouldUseHyperclay(options, win)) return true;
|
|
599
|
+
return isHyperclayEditMode(win);
|
|
600
|
+
}
|
|
601
|
+
function parseEditableOptions(element) {
|
|
602
|
+
const name = EDITABLE_ATTRS.find((attr) => element.hasAttribute(attr));
|
|
603
|
+
if (!name) return null;
|
|
604
|
+
const tokens = new Set(
|
|
605
|
+
(element.getAttribute(name) || "").trim().split(/\s+/).filter(Boolean)
|
|
606
|
+
);
|
|
607
|
+
const options = {
|
|
608
|
+
inline: true,
|
|
609
|
+
singleLine: tokens.has("single-line"),
|
|
610
|
+
toolbarOnSelect: tokens.has("toolbar-on-select")
|
|
611
|
+
};
|
|
612
|
+
if (tokens.has("no-toolbar")) options.toolbar = false;
|
|
613
|
+
return options;
|
|
614
|
+
}
|
|
615
|
+
function installHyperclayBridge(win = window) {
|
|
616
|
+
if (installedWindows.has(win)) return;
|
|
617
|
+
const register = platformSnapshotTransform(win) || platformDocumentTransform(win);
|
|
618
|
+
if (typeof register !== "function") {
|
|
619
|
+
armBridgeRetry(win);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
register((docElem) => stripRichClayFromClone(docElem));
|
|
623
|
+
installedWindows.add(win);
|
|
624
|
+
}
|
|
625
|
+
function armBridgeRetry(win) {
|
|
626
|
+
if (armedWindows.has(win)) return;
|
|
627
|
+
const doc = win.document;
|
|
628
|
+
const ready = platformReadyPromise(win);
|
|
629
|
+
if (!doc && !ready) return;
|
|
630
|
+
armedWindows.add(win);
|
|
631
|
+
const retry = () => {
|
|
632
|
+
installHyperclayBridge(win);
|
|
633
|
+
if (installedWindows.has(win)) disarm();
|
|
634
|
+
};
|
|
635
|
+
const disarm = () => {
|
|
636
|
+
if (!doc) return;
|
|
637
|
+
PLATFORM_READY.forEach((name) => doc.removeEventListener(name, retry));
|
|
638
|
+
};
|
|
639
|
+
if (doc) PLATFORM_READY.forEach((name) => doc.addEventListener(name, retry));
|
|
640
|
+
if (ready) ready.then(retry, () => {
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
function stripRichClayFromClone(docElem) {
|
|
644
|
+
docElem.querySelectorAll?.(CHROME_SELECTOR).forEach((node) => node.remove());
|
|
645
|
+
docElem.querySelectorAll?.(RICHCLAY_SELECTOR).forEach((region) => {
|
|
646
|
+
if (region.getAttribute("data-richclay-active") !== "true") return;
|
|
647
|
+
removeRuntimeState(region, "save");
|
|
648
|
+
if (needsFlattening(region)) {
|
|
649
|
+
const doc = region.ownerDocument;
|
|
650
|
+
const singleLine = Boolean(region.matches?.(singleLineSelector));
|
|
651
|
+
flattenBlocks(
|
|
652
|
+
region,
|
|
653
|
+
() => singleLine ? doc.createTextNode(" ") : doc.createElement("br")
|
|
654
|
+
);
|
|
655
|
+
region.querySelectorAll(region.localName).forEach((nested) => {
|
|
656
|
+
while (nested.firstChild) nested.parentNode.insertBefore(nested.firstChild, nested);
|
|
657
|
+
nested.remove();
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
if (region.matches?.(singleLineSelector)) unwrapLoneSingleLineBlock(region);
|
|
661
|
+
region.querySelectorAll("#squire-selection-start, #squire-selection-end").forEach((node) => {
|
|
662
|
+
node.remove();
|
|
663
|
+
});
|
|
664
|
+
region.querySelectorAll(".squire-image-resize-container").forEach((node) => node.remove());
|
|
665
|
+
stripZeroWidthArtifacts(region);
|
|
666
|
+
const codeBlocks = (region.matches?.("pre") ? [region, ...region.querySelectorAll("pre")] : Array.from(region.querySelectorAll("pre"))).filter((pre) => pre.hasAttribute("data-richclay-pre"));
|
|
667
|
+
codeBlocks.forEach((pre) => {
|
|
668
|
+
Object.entries(PRE_CONTAINMENT).forEach(([property, value]) => {
|
|
669
|
+
if (!pre.style[property]) pre.style[property] = value;
|
|
670
|
+
});
|
|
671
|
+
pre.removeAttribute("data-richclay-pre");
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
docElem.querySelectorAll?.("#squire-selection-start, #squire-selection-end").forEach((node) => node.remove());
|
|
675
|
+
}
|
|
676
|
+
var BLOCK_SCOPE = "p, td, th, li, dl, dt, dd, table, blockquote, div, section, article, aside, main, header, footer, figure, figcaption";
|
|
677
|
+
var MEASURE_ATTR = "data-richclay-measure";
|
|
678
|
+
function ejectsOnReload(region) {
|
|
679
|
+
const doc = region.ownerDocument;
|
|
680
|
+
const scope = region.closest(BLOCK_SCOPE) || region;
|
|
681
|
+
region.setAttribute(MEASURE_ATTR, "");
|
|
682
|
+
const probe = doc.createElement("div");
|
|
683
|
+
try {
|
|
684
|
+
probe.innerHTML = scope.outerHTML;
|
|
685
|
+
} finally {
|
|
686
|
+
region.removeAttribute(MEASURE_ATTR);
|
|
687
|
+
}
|
|
688
|
+
const reparsed = probe.querySelector(`[${MEASURE_ATTR}]`);
|
|
689
|
+
return !reparsed || reparsed.textContent !== region.textContent;
|
|
690
|
+
}
|
|
691
|
+
var selfNests = (region) => Boolean(region.querySelector(region.localName));
|
|
692
|
+
var needsFlattening = (region) => Boolean(region.matches?.(singleLineSelector)) || (hasBlockDescendant(region) || selfNests(region)) && ejectsOnReload(region);
|
|
693
|
+
function removeRuntimeState(region, mode) {
|
|
694
|
+
const origin = region.getAttribute("data-richclay-runtime-contenteditable");
|
|
695
|
+
if (region.hasAttribute("contenteditable")) {
|
|
696
|
+
if (origin === "true") {
|
|
697
|
+
region.removeAttribute("contenteditable");
|
|
698
|
+
} else if (origin) {
|
|
699
|
+
if (mode === "destroy") {
|
|
700
|
+
region.setAttribute("contenteditable", origin);
|
|
701
|
+
} else {
|
|
702
|
+
region.setAttribute("inert-contenteditable", origin);
|
|
703
|
+
region.removeAttribute("contenteditable");
|
|
704
|
+
}
|
|
705
|
+
} else if (mode === "save") {
|
|
706
|
+
region.setAttribute("inert-contenteditable", region.getAttribute("contenteditable"));
|
|
707
|
+
region.removeAttribute("contenteditable");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
runtimeClasses.forEach((className) => region.classList.remove(className));
|
|
711
|
+
if (region.getAttribute("class") === "") region.removeAttribute("class");
|
|
712
|
+
removeRuntimeAttribute(region, "role", "data-richclay-runtime-role");
|
|
713
|
+
removeRuntimeAttribute(region, "aria-multiline", "data-richclay-runtime-aria-multiline");
|
|
714
|
+
removeRuntimeAttribute(region, "no-undo", "data-richclay-runtime-no-undo");
|
|
715
|
+
if (region.getAttribute("data-richclay-runtime-display") === "true") {
|
|
716
|
+
region.style.removeProperty("display");
|
|
717
|
+
if (region.getAttribute("style") === "") region.removeAttribute("style");
|
|
718
|
+
}
|
|
719
|
+
removeRuntimeDescribedBy(region);
|
|
720
|
+
region.removeAttribute("data-richclay-active");
|
|
721
|
+
region.removeAttribute("data-richclay-placeholder");
|
|
722
|
+
region.removeAttribute("data-richclay-pre");
|
|
723
|
+
region.removeAttribute("data-richclay-runtime-role");
|
|
724
|
+
region.removeAttribute("data-richclay-runtime-aria-multiline");
|
|
725
|
+
region.removeAttribute("data-richclay-runtime-no-undo");
|
|
726
|
+
region.removeAttribute("data-richclay-runtime-describedby");
|
|
727
|
+
region.removeAttribute("data-richclay-runtime-contenteditable");
|
|
728
|
+
region.removeAttribute("data-richclay-runtime-display");
|
|
729
|
+
if (region.getAttribute("data-richclay-runtime-marker") === "true") {
|
|
730
|
+
region.removeAttribute("data-richclay");
|
|
731
|
+
}
|
|
732
|
+
region.removeAttribute("data-richclay-runtime-marker");
|
|
733
|
+
}
|
|
734
|
+
function stripZeroWidthArtifacts(region) {
|
|
735
|
+
const zwsp = String.fromCharCode(8203);
|
|
736
|
+
const stack = [region];
|
|
737
|
+
while (stack.length) {
|
|
738
|
+
const node = stack.pop();
|
|
739
|
+
node.childNodes.forEach((child) => {
|
|
740
|
+
if (child.nodeType === 3) {
|
|
741
|
+
if (child.nodeValue.includes(zwsp)) {
|
|
742
|
+
child.nodeValue = child.nodeValue.split(zwsp).join("");
|
|
743
|
+
}
|
|
744
|
+
} else if (child.nodeType === 1) {
|
|
745
|
+
stack.push(child);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
region.querySelectorAll("b, i, u, s, em, strong, code, sub, sup, span").forEach((el) => {
|
|
750
|
+
if (el.children.length === 0 && (el.textContent || "") === "" && el.attributes.length === 0) {
|
|
751
|
+
el.remove();
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
function unwrapLoneSingleLineBlock(region) {
|
|
756
|
+
const meaningful = Array.from(region.childNodes).filter(
|
|
757
|
+
(node) => node.nodeType !== 3 || (node.nodeValue || "").trim() !== ""
|
|
758
|
+
);
|
|
759
|
+
if (meaningful.length !== 1) return;
|
|
760
|
+
const block = meaningful[0];
|
|
761
|
+
if (block.nodeType !== 1 || block.nodeName !== "P" || block.attributes.length > 0) return;
|
|
762
|
+
while (block.firstChild) region.insertBefore(block.firstChild, block);
|
|
763
|
+
block.remove();
|
|
764
|
+
}
|
|
765
|
+
function consumeInertContenteditable(element) {
|
|
766
|
+
if (!element.hasAttribute("inert-contenteditable")) return null;
|
|
767
|
+
let value = element.getAttribute("inert-contenteditable");
|
|
768
|
+
if (!["false", "plaintext-only"].includes(value)) value = "true";
|
|
769
|
+
element.setAttribute("contenteditable", value);
|
|
770
|
+
element.removeAttribute("inert-contenteditable");
|
|
771
|
+
if (value === "true") {
|
|
772
|
+
element.setAttribute("data-richclay-runtime-contenteditable", "true");
|
|
773
|
+
}
|
|
774
|
+
return value;
|
|
775
|
+
}
|
|
776
|
+
function markChrome(element) {
|
|
777
|
+
element.setAttribute("snapshot-remove", "");
|
|
778
|
+
element.setAttribute("no-watch", "");
|
|
779
|
+
return element;
|
|
780
|
+
}
|
|
781
|
+
function readEditmodeParam(win) {
|
|
782
|
+
try {
|
|
783
|
+
const params = new URLSearchParams(win.location?.search || "");
|
|
784
|
+
if (params.get("editmode") === "true") return true;
|
|
785
|
+
if (params.get("editmode") === "false") return false;
|
|
786
|
+
} catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
function hasEditmodeSignal(win) {
|
|
792
|
+
return readEditmodeParam(win) !== null || typeof win.__hyperclayEditMode === "boolean";
|
|
793
|
+
}
|
|
794
|
+
function readEditmodeCookie(win) {
|
|
795
|
+
const cookie = win.document?.cookie || "";
|
|
796
|
+
const match = cookie.match(/(?:^|;\s*)isAdminOfCurrentResource=([^;]*)/);
|
|
797
|
+
return Boolean(match && match[1] !== "");
|
|
798
|
+
}
|
|
799
|
+
function removeRuntimeAttribute(region, attribute, marker) {
|
|
800
|
+
if (!region.hasAttribute(marker)) return;
|
|
801
|
+
region.removeAttribute(attribute);
|
|
802
|
+
}
|
|
803
|
+
function removeRuntimeDescribedBy(region) {
|
|
804
|
+
const id = region.getAttribute("data-richclay-runtime-describedby");
|
|
805
|
+
if (!id) return;
|
|
806
|
+
const ids = (region.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean).filter((value) => value !== id);
|
|
807
|
+
if (ids.length) {
|
|
808
|
+
region.setAttribute("aria-describedby", ids.join(" "));
|
|
809
|
+
} else {
|
|
810
|
+
region.removeAttribute("aria-describedby");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/a11y.js
|
|
815
|
+
var liveId = 0;
|
|
816
|
+
function createLiveRegion(doc = document) {
|
|
817
|
+
const region = doc.createElement("div");
|
|
818
|
+
region.id = `richclay-live-${++liveId}`;
|
|
819
|
+
region.className = "richclay-sr-only";
|
|
820
|
+
region.setAttribute("aria-live", "polite");
|
|
821
|
+
region.setAttribute("aria-atomic", "true");
|
|
822
|
+
region.setAttribute("data-richclay-live", "");
|
|
823
|
+
markChrome(region);
|
|
824
|
+
doc.body.appendChild(region);
|
|
825
|
+
return region;
|
|
826
|
+
}
|
|
827
|
+
function announce(region, message) {
|
|
828
|
+
if (!region || !message) return;
|
|
829
|
+
region.textContent = "";
|
|
830
|
+
region.ownerDocument.defaultView.setTimeout(() => {
|
|
831
|
+
region.textContent = message;
|
|
832
|
+
}, 20);
|
|
833
|
+
}
|
|
834
|
+
function preservePointerSelection(event) {
|
|
835
|
+
event.preventDefault();
|
|
836
|
+
}
|
|
837
|
+
function cloneRange(range) {
|
|
838
|
+
try {
|
|
839
|
+
return range?.cloneRange?.() || null;
|
|
840
|
+
} catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function getSquireSelection(squire) {
|
|
845
|
+
try {
|
|
846
|
+
return cloneRange(squire?.getSelection?.());
|
|
847
|
+
} catch {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function restoreSquireSelection(squire, range) {
|
|
852
|
+
if (!squire || !range) return false;
|
|
853
|
+
try {
|
|
854
|
+
squire.setSelection(range);
|
|
855
|
+
return true;
|
|
856
|
+
} catch {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
function setRuntimeAttribute(element, attribute, value, marker) {
|
|
861
|
+
if (!element.hasAttribute(attribute)) {
|
|
862
|
+
element.setAttribute(marker, "true");
|
|
863
|
+
}
|
|
864
|
+
element.setAttribute(attribute, value);
|
|
865
|
+
}
|
|
866
|
+
function connectDescription(element, description) {
|
|
867
|
+
const ids = new Set((element.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean));
|
|
868
|
+
ids.add(description.id);
|
|
869
|
+
element.setAttribute("aria-describedby", Array.from(ids).join(" "));
|
|
870
|
+
element.setAttribute("data-richclay-runtime-describedby", description.id);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/toolbar.js
|
|
874
|
+
var toolbarId = 0;
|
|
875
|
+
var Toolbar = class {
|
|
876
|
+
constructor(editor, controls, options = {}) {
|
|
877
|
+
this.editor = editor;
|
|
878
|
+
this.controls = controls;
|
|
879
|
+
this.options = options;
|
|
880
|
+
this.id = ++toolbarId;
|
|
881
|
+
this.items = [];
|
|
882
|
+
this.menus = /* @__PURE__ */ new Map();
|
|
883
|
+
this.activeIndex = 0;
|
|
884
|
+
this.root = this.createRoot();
|
|
885
|
+
this.onRootKeydown = (event) => this.handleRootKeydown(event);
|
|
886
|
+
this.onRootClick = (event) => this.handleRootClick(event);
|
|
887
|
+
this.onRootPointerDown = (event) => this.handleRootPointerDown(event);
|
|
888
|
+
this.onDocumentPointerDown = (event) => this.handleDocumentPointerDown(event);
|
|
889
|
+
this.render();
|
|
890
|
+
this.root.addEventListener("keydown", this.onRootKeydown);
|
|
891
|
+
this.root.addEventListener("click", this.onRootClick);
|
|
892
|
+
this.root.addEventListener("mousedown", this.onRootPointerDown);
|
|
893
|
+
this.root.ownerDocument.addEventListener("mousedown", this.onDocumentPointerDown);
|
|
894
|
+
}
|
|
895
|
+
destroy() {
|
|
896
|
+
this.closeMenus();
|
|
897
|
+
this.root.removeEventListener("keydown", this.onRootKeydown);
|
|
898
|
+
this.root.removeEventListener("click", this.onRootClick);
|
|
899
|
+
this.root.removeEventListener("mousedown", this.onRootPointerDown);
|
|
900
|
+
this.root.ownerDocument.removeEventListener("mousedown", this.onDocumentPointerDown);
|
|
901
|
+
this.root.remove();
|
|
902
|
+
}
|
|
903
|
+
update() {
|
|
904
|
+
this.items.forEach((item) => {
|
|
905
|
+
const { def, button } = item;
|
|
906
|
+
if (typeof def.isActive === "function") {
|
|
907
|
+
const active = Boolean(def.isActive(this.editor));
|
|
908
|
+
button.setAttribute("aria-pressed", active ? "true" : "false");
|
|
909
|
+
button.classList.toggle("is-active", active);
|
|
910
|
+
}
|
|
911
|
+
if (typeof def.isDisabled === "function") {
|
|
912
|
+
button.disabled = Boolean(def.isDisabled(this.editor));
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
this.ensureSingleTabStop();
|
|
916
|
+
}
|
|
917
|
+
focusFirst() {
|
|
918
|
+
this.activeIndex = 0;
|
|
919
|
+
this.ensureSingleTabStop();
|
|
920
|
+
this.items[0]?.button.focus();
|
|
921
|
+
}
|
|
922
|
+
createRoot() {
|
|
923
|
+
const doc = this.editor.element.ownerDocument;
|
|
924
|
+
const root = doc.createElement("div");
|
|
925
|
+
root.className = "richclay-toolbar";
|
|
926
|
+
root.setAttribute("role", "toolbar");
|
|
927
|
+
root.setAttribute("aria-label", this.options.ariaLabel || "Rich text formatting");
|
|
928
|
+
root.setAttribute("data-richclay-toolbar", "");
|
|
929
|
+
markChrome(root);
|
|
930
|
+
const container = resolveContainer(this.options.toolbarContainer, doc);
|
|
931
|
+
if (container) {
|
|
932
|
+
container.appendChild(root);
|
|
933
|
+
} else {
|
|
934
|
+
this.editor.element.insertAdjacentElement("beforebegin", root);
|
|
935
|
+
}
|
|
936
|
+
return root;
|
|
937
|
+
}
|
|
938
|
+
render() {
|
|
939
|
+
this.root.textContent = "";
|
|
940
|
+
this.items = [];
|
|
941
|
+
this.menus.clear();
|
|
942
|
+
let lastGroup = null;
|
|
943
|
+
this.visibleControls().forEach((def, index) => {
|
|
944
|
+
if (def.type === "separator") {
|
|
945
|
+
this.root.appendChild(createSeparator(this.root.ownerDocument));
|
|
946
|
+
lastGroup = null;
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (lastGroup && def.group && def.group !== lastGroup) {
|
|
950
|
+
this.root.appendChild(createSeparator(this.root.ownerDocument));
|
|
951
|
+
}
|
|
952
|
+
lastGroup = def.group || lastGroup;
|
|
953
|
+
if (def.type === "menu") {
|
|
954
|
+
this.renderMenu(def, index);
|
|
955
|
+
} else {
|
|
956
|
+
this.renderButton(def, index);
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
this.ensureSingleTabStop();
|
|
960
|
+
this.update();
|
|
961
|
+
}
|
|
962
|
+
// A control disabled by the root is disabled for this editor's whole life, not
|
|
963
|
+
// for this selection, so it is left out of the toolbar rather than rendered
|
|
964
|
+
// greyed: a greyed button says "not right now" and invites clicking, an absent
|
|
965
|
+
// one just gives a simpler toolbar. It stays disabled at dispatch as well,
|
|
966
|
+
// because hiding a button stops neither its shortcut nor its menu item.
|
|
967
|
+
visibleControls() {
|
|
968
|
+
const kept = this.controls.filter(
|
|
969
|
+
(def) => def.type === "separator" || !def.isDisabled?.(this.editor)
|
|
970
|
+
);
|
|
971
|
+
const trimmed = [];
|
|
972
|
+
kept.forEach((def) => {
|
|
973
|
+
const previous = trimmed[trimmed.length - 1];
|
|
974
|
+
if (def.type === "separator" && (!previous || previous.type === "separator")) return;
|
|
975
|
+
trimmed.push(def);
|
|
976
|
+
});
|
|
977
|
+
while (trimmed[trimmed.length - 1]?.type === "separator") trimmed.pop();
|
|
978
|
+
return trimmed;
|
|
979
|
+
}
|
|
980
|
+
renderButton(def, index) {
|
|
981
|
+
const button = createToolbarButton(this.root.ownerDocument, def);
|
|
982
|
+
button.dataset.richclayControl = def.id;
|
|
983
|
+
button.dataset.richclayIndex = String(index);
|
|
984
|
+
if (typeof def.isActive === "function") {
|
|
985
|
+
button.setAttribute("aria-pressed", "false");
|
|
986
|
+
}
|
|
987
|
+
this.root.appendChild(button);
|
|
988
|
+
this.items.push({ def, button, type: "button" });
|
|
989
|
+
}
|
|
990
|
+
renderMenu(def, index) {
|
|
991
|
+
const doc = this.root.ownerDocument;
|
|
992
|
+
const button = createToolbarButton(doc, def);
|
|
993
|
+
const menuId = `richclay-menu-${this.id}-${def.id}`;
|
|
994
|
+
button.dataset.richclayControl = def.id;
|
|
995
|
+
button.dataset.richclayIndex = String(index);
|
|
996
|
+
button.setAttribute("aria-haspopup", "menu");
|
|
997
|
+
button.setAttribute("aria-expanded", "false");
|
|
998
|
+
button.setAttribute("aria-controls", menuId);
|
|
999
|
+
const menu = doc.createElement("div");
|
|
1000
|
+
menu.id = menuId;
|
|
1001
|
+
menu.className = "richclay-menu";
|
|
1002
|
+
menu.hidden = true;
|
|
1003
|
+
menu.setAttribute("role", "menu");
|
|
1004
|
+
menu.setAttribute("data-richclay-menu", "");
|
|
1005
|
+
markChrome(menu);
|
|
1006
|
+
def.options.forEach((option, optionIndex) => {
|
|
1007
|
+
const item = doc.createElement("button");
|
|
1008
|
+
item.type = "button";
|
|
1009
|
+
item.className = "richclay-menu-item";
|
|
1010
|
+
item.setAttribute("role", "menuitemradio");
|
|
1011
|
+
item.setAttribute("tabindex", "-1");
|
|
1012
|
+
item.dataset.richclayMenuItem = def.id;
|
|
1013
|
+
item.dataset.richclayOptionIndex = String(optionIndex);
|
|
1014
|
+
item.textContent = option.label;
|
|
1015
|
+
item.addEventListener("mousedown", preservePointerSelection);
|
|
1016
|
+
item.addEventListener("click", (event) => {
|
|
1017
|
+
event.preventDefault();
|
|
1018
|
+
this.chooseMenuItem(def, option, button);
|
|
1019
|
+
});
|
|
1020
|
+
item.addEventListener("keydown", (event) => this.handleMenuKeydown(event, def, button));
|
|
1021
|
+
menu.appendChild(item);
|
|
1022
|
+
});
|
|
1023
|
+
const wrap = doc.createElement("span");
|
|
1024
|
+
wrap.className = "richclay-menu-wrap";
|
|
1025
|
+
wrap.append(button, menu);
|
|
1026
|
+
this.root.appendChild(wrap);
|
|
1027
|
+
this.items.push({ def, button, type: "menu", menu });
|
|
1028
|
+
this.menus.set(def.id, { def, button, menu });
|
|
1029
|
+
}
|
|
1030
|
+
handleRootPointerDown(event) {
|
|
1031
|
+
const control = event.target.closest?.("[data-richclay-control]");
|
|
1032
|
+
if (!control || !this.root.contains(control)) return;
|
|
1033
|
+
preservePointerSelection(event);
|
|
1034
|
+
this.editor.saveSelection();
|
|
1035
|
+
}
|
|
1036
|
+
handleRootClick(event) {
|
|
1037
|
+
const button = event.target.closest?.("[data-richclay-control]");
|
|
1038
|
+
if (!button || !this.root.contains(button)) return;
|
|
1039
|
+
event.preventDefault();
|
|
1040
|
+
const item = this.items.find((candidate) => candidate.button === button);
|
|
1041
|
+
if (!item || button.disabled) return;
|
|
1042
|
+
this.activeIndex = this.items.indexOf(item);
|
|
1043
|
+
this.ensureSingleTabStop();
|
|
1044
|
+
if (item.type === "menu") {
|
|
1045
|
+
this.toggleMenu(item.def.id);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
this.closeMenus();
|
|
1049
|
+
this.editor.runControl(item.def);
|
|
1050
|
+
}
|
|
1051
|
+
handleRootKeydown(event) {
|
|
1052
|
+
const key = event.key;
|
|
1053
|
+
const navigation = {
|
|
1054
|
+
ArrowRight: 1,
|
|
1055
|
+
ArrowDown: 1,
|
|
1056
|
+
ArrowLeft: -1,
|
|
1057
|
+
ArrowUp: -1
|
|
1058
|
+
};
|
|
1059
|
+
if (key in navigation) {
|
|
1060
|
+
event.preventDefault();
|
|
1061
|
+
this.moveFocus(navigation[key]);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (key === "Home") {
|
|
1065
|
+
event.preventDefault();
|
|
1066
|
+
this.setFocusIndex(0);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (key === "End") {
|
|
1070
|
+
event.preventDefault();
|
|
1071
|
+
this.setFocusIndex(this.items.length - 1);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const current = this.items[this.activeIndex];
|
|
1075
|
+
if (!current) return;
|
|
1076
|
+
if ((key === "Enter" || key === " ") && current.type === "menu") {
|
|
1077
|
+
event.preventDefault();
|
|
1078
|
+
this.openMenu(current.def.id, "first");
|
|
1079
|
+
}
|
|
1080
|
+
if (key === "Escape") {
|
|
1081
|
+
const hadOpenMenu = Array.from(this.menus.values()).some((entry) => !entry.menu.hidden);
|
|
1082
|
+
this.closeMenus();
|
|
1083
|
+
if (hadOpenMenu) current.button.focus();
|
|
1084
|
+
else this.editor.focus();
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
handleMenuKeydown(event, def, trigger) {
|
|
1088
|
+
event.stopPropagation();
|
|
1089
|
+
const menu = this.menus.get(def.id)?.menu;
|
|
1090
|
+
const items = Array.from(menu.querySelectorAll(".richclay-menu-item"));
|
|
1091
|
+
const index = items.indexOf(event.currentTarget);
|
|
1092
|
+
let next = index;
|
|
1093
|
+
if (event.key === "ArrowDown") next = (index + 1) % items.length;
|
|
1094
|
+
if (event.key === "ArrowUp") next = (index - 1 + items.length) % items.length;
|
|
1095
|
+
if (event.key === "Home") next = 0;
|
|
1096
|
+
if (event.key === "End") next = items.length - 1;
|
|
1097
|
+
if (next !== index) {
|
|
1098
|
+
event.preventDefault();
|
|
1099
|
+
items[next].focus();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (event.key === "Escape") {
|
|
1103
|
+
event.preventDefault();
|
|
1104
|
+
this.closeMenus();
|
|
1105
|
+
trigger.focus();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1109
|
+
event.preventDefault();
|
|
1110
|
+
const option = def.options[index];
|
|
1111
|
+
this.chooseMenuItem(def, option, trigger);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
handleDocumentPointerDown(event) {
|
|
1115
|
+
if (this.root.contains(event.target)) return;
|
|
1116
|
+
this.closeMenus();
|
|
1117
|
+
}
|
|
1118
|
+
chooseMenuItem(def, option, trigger) {
|
|
1119
|
+
this.closeMenus();
|
|
1120
|
+
this.editor.runControl({
|
|
1121
|
+
...option,
|
|
1122
|
+
id: `${def.id}:${option.value}`,
|
|
1123
|
+
ariaLabel: option.label
|
|
1124
|
+
});
|
|
1125
|
+
trigger.focus();
|
|
1126
|
+
}
|
|
1127
|
+
toggleMenu(id) {
|
|
1128
|
+
const menu = this.menus.get(id);
|
|
1129
|
+
if (!menu) return;
|
|
1130
|
+
if (menu.menu.hidden) this.openMenu(id, "first");
|
|
1131
|
+
else this.closeMenus();
|
|
1132
|
+
}
|
|
1133
|
+
openMenu(id, focus = "first") {
|
|
1134
|
+
this.closeMenus(id);
|
|
1135
|
+
const entry = this.menus.get(id);
|
|
1136
|
+
if (!entry) return;
|
|
1137
|
+
entry.menu.hidden = false;
|
|
1138
|
+
entry.button.setAttribute("aria-expanded", "true");
|
|
1139
|
+
this.updateMenuState(entry);
|
|
1140
|
+
const items = Array.from(entry.menu.querySelectorAll(".richclay-menu-item"));
|
|
1141
|
+
const active = items.find((item) => item.getAttribute("aria-checked") === "true");
|
|
1142
|
+
const target = focus === "active" ? active || items[0] : items[0];
|
|
1143
|
+
target?.focus();
|
|
1144
|
+
}
|
|
1145
|
+
closeMenus(exceptId = null) {
|
|
1146
|
+
this.menus.forEach((entry, id) => {
|
|
1147
|
+
if (id === exceptId) return;
|
|
1148
|
+
entry.menu.hidden = true;
|
|
1149
|
+
entry.button.setAttribute("aria-expanded", "false");
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
updateMenuState(entry) {
|
|
1153
|
+
const items = Array.from(entry.menu.querySelectorAll(".richclay-menu-item"));
|
|
1154
|
+
items.forEach((item, index) => {
|
|
1155
|
+
const option = entry.def.options[index];
|
|
1156
|
+
const active = Boolean(option.isActive?.(this.editor));
|
|
1157
|
+
item.setAttribute("aria-checked", active ? "true" : "false");
|
|
1158
|
+
item.classList.toggle("is-active", active);
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
moveFocus(delta) {
|
|
1162
|
+
if (!this.items.length) return;
|
|
1163
|
+
let next = this.activeIndex;
|
|
1164
|
+
do {
|
|
1165
|
+
next = (next + delta + this.items.length) % this.items.length;
|
|
1166
|
+
} while (this.items[next].button.disabled && next !== this.activeIndex);
|
|
1167
|
+
this.setFocusIndex(next);
|
|
1168
|
+
}
|
|
1169
|
+
setFocusIndex(index) {
|
|
1170
|
+
this.activeIndex = Math.max(0, Math.min(index, this.items.length - 1));
|
|
1171
|
+
this.ensureSingleTabStop();
|
|
1172
|
+
this.items[this.activeIndex]?.button.focus();
|
|
1173
|
+
}
|
|
1174
|
+
ensureSingleTabStop() {
|
|
1175
|
+
const enabled = this.items.filter((item) => !item.button.disabled);
|
|
1176
|
+
if (!enabled.length) return;
|
|
1177
|
+
if (!enabled.includes(this.items[this.activeIndex])) {
|
|
1178
|
+
this.activeIndex = this.items.indexOf(enabled[0]);
|
|
1179
|
+
}
|
|
1180
|
+
this.items.forEach((item, index) => {
|
|
1181
|
+
item.button.tabIndex = index === this.activeIndex ? 0 : -1;
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
function createToolbarButton(doc, def) {
|
|
1186
|
+
const button = doc.createElement("button");
|
|
1187
|
+
button.type = "button";
|
|
1188
|
+
button.className = "richclay-button";
|
|
1189
|
+
const shortcut = formatShortcut(def.shortcut, doc.defaultView);
|
|
1190
|
+
const label = shortcut ? `${def.ariaLabel || def.label} (${shortcut})` : def.ariaLabel || def.label;
|
|
1191
|
+
button.setAttribute("aria-label", label);
|
|
1192
|
+
button.title = label;
|
|
1193
|
+
button.innerHTML = `${def.icon || ""}<span class="richclay-sr-only">${def.label}</span>`;
|
|
1194
|
+
return button;
|
|
1195
|
+
}
|
|
1196
|
+
function createSeparator(doc) {
|
|
1197
|
+
const separator = doc.createElement("span");
|
|
1198
|
+
separator.className = "richclay-separator";
|
|
1199
|
+
separator.setAttribute("role", "separator");
|
|
1200
|
+
separator.setAttribute("aria-orientation", "vertical");
|
|
1201
|
+
return separator;
|
|
1202
|
+
}
|
|
1203
|
+
function resolveContainer(container, doc) {
|
|
1204
|
+
if (!container) return null;
|
|
1205
|
+
if (typeof container === "string") return doc.querySelector(container);
|
|
1206
|
+
return container;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/toolbar-float.js
|
|
1210
|
+
var GAP = 16;
|
|
1211
|
+
var GAP_LADDER = [16, 8, 0];
|
|
1212
|
+
var VIEWPORT_INSET = 8;
|
|
1213
|
+
var PLACEMENT_SLACK = 4;
|
|
1214
|
+
function placeToolbar({ anchor, bar, rail, viewport, current = null }) {
|
|
1215
|
+
const slack = (mode) => current === mode ? PLACEMENT_SLACK : 0;
|
|
1216
|
+
if (anchor.bottom <= 0 || anchor.top >= viewport.height || anchor.right <= 0 || anchor.left >= viewport.width) {
|
|
1217
|
+
return { mode: "hidden", x: 0, y: 0 };
|
|
1218
|
+
}
|
|
1219
|
+
const clampX = (width) => Math.max(VIEWPORT_INSET, Math.min(anchor.left, viewport.width - width - VIEWPORT_INSET));
|
|
1220
|
+
const aboveY = anchor.top - GAP - bar.height;
|
|
1221
|
+
if (aboveY >= VIEWPORT_INSET - slack("above")) {
|
|
1222
|
+
return { mode: "above", x: clampX(bar.width), y: aboveY };
|
|
1223
|
+
}
|
|
1224
|
+
const belowY = anchor.bottom + GAP;
|
|
1225
|
+
if (belowY + bar.height <= viewport.height - VIEWPORT_INSET + slack("below")) {
|
|
1226
|
+
return { mode: "below", x: clampX(bar.width), y: belowY };
|
|
1227
|
+
}
|
|
1228
|
+
const rightSpace = viewport.width - anchor.right - VIEWPORT_INSET;
|
|
1229
|
+
const leftSpace = anchor.left - VIEWPORT_INSET;
|
|
1230
|
+
const sides = rightSpace >= leftSpace ? [["rail-right", rightSpace], ["rail-left", leftSpace]] : [["rail-left", leftSpace], ["rail-right", rightSpace]];
|
|
1231
|
+
for (const [mode, space] of sides) {
|
|
1232
|
+
for (const gap of GAP_LADDER) {
|
|
1233
|
+
if (rail.width + gap > space + slack(mode)) continue;
|
|
1234
|
+
const x = mode === "rail-right" ? Math.min(anchor.right + gap, viewport.width - rail.width - VIEWPORT_INSET) : Math.max(anchor.left - gap - rail.width, VIEWPORT_INSET);
|
|
1235
|
+
const maxY = Math.min(anchor.bottom, viewport.height - VIEWPORT_INSET) - rail.height;
|
|
1236
|
+
const y = Math.max(VIEWPORT_INSET, Math.min(Math.max(anchor.top, VIEWPORT_INSET), maxY));
|
|
1237
|
+
return { mode, x, y, gap };
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return { mode: "pinned", x: clampX(bar.width), y: VIEWPORT_INSET };
|
|
1241
|
+
}
|
|
1242
|
+
var FloatingToolbar = class {
|
|
1243
|
+
constructor(editor, controls) {
|
|
1244
|
+
this.editor = editor;
|
|
1245
|
+
this.mode = null;
|
|
1246
|
+
this.hidden = false;
|
|
1247
|
+
this.frame = 0;
|
|
1248
|
+
this.bar = { width: 0, height: 0 };
|
|
1249
|
+
this.rail = { width: 0, height: 0 };
|
|
1250
|
+
const doc = editor.element.ownerDocument;
|
|
1251
|
+
this.doc = doc;
|
|
1252
|
+
this.win = doc.defaultView || globalThis;
|
|
1253
|
+
this.root = doc.createElement("div");
|
|
1254
|
+
this.root.className = "richclay-float";
|
|
1255
|
+
this.root.setAttribute("data-richclay-float", "");
|
|
1256
|
+
this.root.setAttribute("save-remove", "");
|
|
1257
|
+
markChrome(this.root);
|
|
1258
|
+
doc.body.appendChild(this.root);
|
|
1259
|
+
this.toolbar = new Toolbar(editor, controls, { toolbarContainer: this.root });
|
|
1260
|
+
this.onScroll = () => this.schedule();
|
|
1261
|
+
this.onResize = () => {
|
|
1262
|
+
this.measure();
|
|
1263
|
+
this.schedule();
|
|
1264
|
+
};
|
|
1265
|
+
doc.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
|
|
1266
|
+
this.win.addEventListener("resize", this.onResize, { passive: true });
|
|
1267
|
+
this.win.visualViewport?.addEventListener("resize", this.onResize, { passive: true });
|
|
1268
|
+
this.win.visualViewport?.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1269
|
+
this.resizeObserver = typeof this.win.ResizeObserver === "function" ? new this.win.ResizeObserver(() => this.schedule()) : null;
|
|
1270
|
+
this.resizeObserver?.observe(editor.element);
|
|
1271
|
+
this.measure();
|
|
1272
|
+
this.reposition();
|
|
1273
|
+
}
|
|
1274
|
+
// Measure both orientations up front so the placement math can evaluate the
|
|
1275
|
+
// rail without first switching to it.
|
|
1276
|
+
measure() {
|
|
1277
|
+
const wasRail = this.root.classList.contains("richclay-float-rail");
|
|
1278
|
+
this.root.style.visibility = "hidden";
|
|
1279
|
+
this.root.classList.remove("richclay-float-rail");
|
|
1280
|
+
this.bar = { width: this.root.offsetWidth || 0, height: this.root.offsetHeight || 0 };
|
|
1281
|
+
this.root.classList.add("richclay-float-rail");
|
|
1282
|
+
this.rail = { width: this.root.offsetWidth || 0, height: this.root.offsetHeight || 0 };
|
|
1283
|
+
this.root.classList.toggle("richclay-float-rail", wasRail);
|
|
1284
|
+
this.root.style.visibility = "";
|
|
1285
|
+
}
|
|
1286
|
+
schedule() {
|
|
1287
|
+
if (!this.win.requestAnimationFrame) {
|
|
1288
|
+
this.reposition();
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (this.frame) return;
|
|
1292
|
+
this.frame = this.win.requestAnimationFrame(() => {
|
|
1293
|
+
this.frame = 0;
|
|
1294
|
+
this.reposition();
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
setVisible(visible) {
|
|
1298
|
+
this.hidden = !visible;
|
|
1299
|
+
this.reposition();
|
|
1300
|
+
}
|
|
1301
|
+
reposition() {
|
|
1302
|
+
if (this.hidden) {
|
|
1303
|
+
this.root.style.display = "none";
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const anchor = this.editor.element.getBoundingClientRect();
|
|
1307
|
+
const viewport = {
|
|
1308
|
+
width: this.win.visualViewport?.width ?? this.win.innerWidth,
|
|
1309
|
+
height: this.win.visualViewport?.height ?? this.win.innerHeight
|
|
1310
|
+
};
|
|
1311
|
+
const placement = placeToolbar({
|
|
1312
|
+
anchor,
|
|
1313
|
+
bar: this.bar,
|
|
1314
|
+
rail: this.rail,
|
|
1315
|
+
viewport,
|
|
1316
|
+
current: this.mode
|
|
1317
|
+
});
|
|
1318
|
+
this.mode = placement.mode;
|
|
1319
|
+
if (placement.mode === "hidden") {
|
|
1320
|
+
this.root.style.display = "none";
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
this.root.style.display = "";
|
|
1324
|
+
this.root.classList.toggle(
|
|
1325
|
+
"richclay-float-rail",
|
|
1326
|
+
placement.mode === "rail-left" || placement.mode === "rail-right"
|
|
1327
|
+
);
|
|
1328
|
+
this.root.classList.toggle("richclay-float-pinned", placement.mode === "pinned");
|
|
1329
|
+
this.root.style.transform = `translate(${Math.round(placement.x)}px, ${Math.round(placement.y)}px)`;
|
|
1330
|
+
}
|
|
1331
|
+
destroy() {
|
|
1332
|
+
if (this.frame) this.win.cancelAnimationFrame?.(this.frame);
|
|
1333
|
+
this.frame = 0;
|
|
1334
|
+
this.doc.removeEventListener("scroll", this.onScroll, { capture: true });
|
|
1335
|
+
this.win.removeEventListener("resize", this.onResize);
|
|
1336
|
+
this.win.visualViewport?.removeEventListener("resize", this.onResize);
|
|
1337
|
+
this.win.visualViewport?.removeEventListener("scroll", this.onScroll);
|
|
1338
|
+
this.resizeObserver?.disconnect();
|
|
1339
|
+
this.toolbar.destroy();
|
|
1340
|
+
this.root.remove();
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// src/sanitize.js
|
|
1345
|
+
var DEFAULT_SANITIZE_CONFIG = {
|
|
1346
|
+
ALLOWED_TAGS: [
|
|
1347
|
+
"a",
|
|
1348
|
+
"b",
|
|
1349
|
+
"blockquote",
|
|
1350
|
+
"br",
|
|
1351
|
+
"code",
|
|
1352
|
+
"div",
|
|
1353
|
+
"em",
|
|
1354
|
+
"h1",
|
|
1355
|
+
"h2",
|
|
1356
|
+
"h3",
|
|
1357
|
+
"h4",
|
|
1358
|
+
"h5",
|
|
1359
|
+
"h6",
|
|
1360
|
+
"i",
|
|
1361
|
+
"li",
|
|
1362
|
+
"ol",
|
|
1363
|
+
"p",
|
|
1364
|
+
"pre",
|
|
1365
|
+
"s",
|
|
1366
|
+
"span",
|
|
1367
|
+
"strong",
|
|
1368
|
+
"sub",
|
|
1369
|
+
"sup",
|
|
1370
|
+
"u",
|
|
1371
|
+
"ul"
|
|
1372
|
+
],
|
|
1373
|
+
ALLOWED_ATTR: ["aria-label", "href", "rel", "target", "title"],
|
|
1374
|
+
ALLOW_DATA_ATTR: false,
|
|
1375
|
+
ALLOW_ARIA_ATTR: true,
|
|
1376
|
+
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
|
|
1377
|
+
FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
|
|
1378
|
+
FORBID_ATTR: ["style", "srcset", "onerror", "onclick", "onload"]
|
|
1379
|
+
};
|
|
1380
|
+
function createSanitizer(config = {}, doc = document) {
|
|
1381
|
+
const purify = resolveDOMPurify(doc);
|
|
1382
|
+
return {
|
|
1383
|
+
sanitizeHTML(html) {
|
|
1384
|
+
return sanitizeHTML(html, config, doc, purify);
|
|
1385
|
+
},
|
|
1386
|
+
sanitizeElement(element) {
|
|
1387
|
+
return sanitizeElement(element, config, purify);
|
|
1388
|
+
},
|
|
1389
|
+
sanitizeToDOMFragment(html, editor) {
|
|
1390
|
+
const ownerDocument = editor?.getRoot?.().ownerDocument || doc;
|
|
1391
|
+
return sanitizeToDOMFragment(html, config, ownerDocument, purify);
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function sanitizeHTML(html, config = {}, doc = document, purify = resolveDOMPurify(doc)) {
|
|
1396
|
+
const template = doc.createElement("template");
|
|
1397
|
+
template.innerHTML = purify.sanitize(String(html || ""), mergeConfig(config));
|
|
1398
|
+
normalizeLinks(template.content);
|
|
1399
|
+
return template.innerHTML;
|
|
1400
|
+
}
|
|
1401
|
+
function sanitizeElement(element, config = {}, purify = resolveDOMPurify(element.ownerDocument)) {
|
|
1402
|
+
const doc = element.ownerDocument;
|
|
1403
|
+
const wrapper = doc.createElement("div");
|
|
1404
|
+
while (element.firstChild) wrapper.appendChild(element.firstChild);
|
|
1405
|
+
purify.sanitize(wrapper, {
|
|
1406
|
+
...mergeConfig(config),
|
|
1407
|
+
IN_PLACE: true
|
|
1408
|
+
});
|
|
1409
|
+
normalizeLinks(wrapper);
|
|
1410
|
+
while (wrapper.firstChild) element.appendChild(wrapper.firstChild);
|
|
1411
|
+
return element;
|
|
1412
|
+
}
|
|
1413
|
+
function sanitizeToDOMFragment(html, config = {}, doc = document, purify = resolveDOMPurify(doc)) {
|
|
1414
|
+
const fragment = purify.sanitize(String(html || ""), {
|
|
1415
|
+
...mergeConfig(config),
|
|
1416
|
+
RETURN_DOM_FRAGMENT: true
|
|
1417
|
+
});
|
|
1418
|
+
const clean = fragment?.ownerDocument === doc ? fragment : doc.importNode(fragment, true);
|
|
1419
|
+
normalizeLinks(clean);
|
|
1420
|
+
return clean || doc.createDocumentFragment();
|
|
1421
|
+
}
|
|
1422
|
+
function mergeConfig(config) {
|
|
1423
|
+
return { ...DEFAULT_SANITIZE_CONFIG, ...config };
|
|
1424
|
+
}
|
|
1425
|
+
var SAFE_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
1426
|
+
var URL_CONTROL_CHARS = new RegExp("[\\u0000-\\u0020\\u007F-\\u009F]", "g");
|
|
1427
|
+
function isSafeUrl(url) {
|
|
1428
|
+
const stripped = String(url == null ? "" : url).replace(URL_CONTROL_CHARS, "");
|
|
1429
|
+
const scheme = (stripped.match(/^([a-z][a-z0-9+.\-]*):/i) || [])[1];
|
|
1430
|
+
return !scheme || SAFE_URL_SCHEMES.has(scheme.toLowerCase());
|
|
1431
|
+
}
|
|
1432
|
+
function normalizeUrl(value) {
|
|
1433
|
+
const raw = String(value == null ? "" : value).trim();
|
|
1434
|
+
if (!raw || !isSafeUrl(raw)) return "";
|
|
1435
|
+
if (/^(?:https?:|mailto:|tel:)/i.test(raw)) return raw;
|
|
1436
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) return `mailto:${raw}`;
|
|
1437
|
+
if (/^(?:[#?]|\/|\.{1,2}\/)/.test(raw)) return raw;
|
|
1438
|
+
return `https://${raw}`;
|
|
1439
|
+
}
|
|
1440
|
+
function resolveDOMPurify(doc) {
|
|
1441
|
+
const supplied = globalThis.DOMPurify;
|
|
1442
|
+
if (supplied?.sanitize) return supplied;
|
|
1443
|
+
if (typeof supplied === "function") return supplied(doc.defaultView || globalThis);
|
|
1444
|
+
throw new Error(
|
|
1445
|
+
"RichClay requires DOMPurify. Load vendor/purify.min.js before richclay.js or provide globalThis.DOMPurify."
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
function normalizeLinks(root) {
|
|
1449
|
+
root.querySelectorAll?.("a[href]").forEach((link) => {
|
|
1450
|
+
const href = link.getAttribute("href") || "";
|
|
1451
|
+
if (/^\s*javascript:/i.test(href)) {
|
|
1452
|
+
link.removeAttribute("href");
|
|
1453
|
+
}
|
|
1454
|
+
if (link.getAttribute("target") === "_blank") {
|
|
1455
|
+
const rel = new Set((link.getAttribute("rel") || "").split(/\s+/).filter(Boolean));
|
|
1456
|
+
rel.add("noopener");
|
|
1457
|
+
rel.add("noreferrer");
|
|
1458
|
+
link.setAttribute("rel", Array.from(rel).join(" "));
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
var INLINE_SANITIZE_EXTENSIONS = {
|
|
1463
|
+
ADD_TAGS: ["img"],
|
|
1464
|
+
ADD_ATTR: ["class", "id", "src", "alt", "width", "height"]
|
|
1465
|
+
};
|
|
1466
|
+
function inlineSanitizeConfig(config = {}) {
|
|
1467
|
+
return {
|
|
1468
|
+
...config,
|
|
1469
|
+
ADD_TAGS: [.../* @__PURE__ */ new Set([...INLINE_SANITIZE_EXTENSIONS.ADD_TAGS, ...config.ADD_TAGS || []])],
|
|
1470
|
+
ADD_ATTR: [.../* @__PURE__ */ new Set([...INLINE_SANITIZE_EXTENSIONS.ADD_ATTR, ...config.ADD_ATTR || []])],
|
|
1471
|
+
ALLOW_DATA_ATTR: config.ALLOW_DATA_ATTR ?? true
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
var FLATTEN_SELECTOR = "p, div, h1, h2, h3, h4, h5, h6, blockquote, pre, ul, ol, li, figure, figcaption, table, thead, tbody, tr, td, th";
|
|
1475
|
+
function flattenFragmentToSingleLine(fragment) {
|
|
1476
|
+
const doc = fragment.ownerDocument || document;
|
|
1477
|
+
fragment.querySelectorAll?.("br").forEach((br) => br.replaceWith(doc.createTextNode(" ")));
|
|
1478
|
+
let block = fragment.querySelector?.(FLATTEN_SELECTOR);
|
|
1479
|
+
while (block) {
|
|
1480
|
+
const parent = block.parentNode;
|
|
1481
|
+
parent.insertBefore(doc.createTextNode(" "), block);
|
|
1482
|
+
while (block.firstChild) parent.insertBefore(block.firstChild, block);
|
|
1483
|
+
parent.insertBefore(doc.createTextNode(" "), block);
|
|
1484
|
+
block.remove();
|
|
1485
|
+
block = fragment.querySelector(FLATTEN_SELECTOR);
|
|
1486
|
+
}
|
|
1487
|
+
collapseFragmentWhitespace(fragment, doc);
|
|
1488
|
+
return fragment;
|
|
1489
|
+
}
|
|
1490
|
+
function collapseFragmentWhitespace(fragment, doc) {
|
|
1491
|
+
fragment.normalize?.();
|
|
1492
|
+
const walker = doc.createTreeWalker(
|
|
1493
|
+
fragment,
|
|
1494
|
+
4
|
|
1495
|
+
/* NodeFilter.SHOW_TEXT */
|
|
1496
|
+
);
|
|
1497
|
+
const nodes = [];
|
|
1498
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
1499
|
+
nodes.forEach((node) => {
|
|
1500
|
+
node.nodeValue = node.nodeValue.replace(/\s+/g, " ");
|
|
1501
|
+
});
|
|
1502
|
+
const first = fragment.firstChild;
|
|
1503
|
+
if (first?.nodeType === 3) first.nodeValue = first.nodeValue.replace(/^\s+/, "");
|
|
1504
|
+
const last = fragment.lastChild;
|
|
1505
|
+
if (last?.nodeType === 3) last.nodeValue = last.nodeValue.replace(/\s+$/, "");
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/styles.js
|
|
1509
|
+
var STYLE_ID = "richclay-styles";
|
|
1510
|
+
var styledDocs = /* @__PURE__ */ new WeakSet();
|
|
1511
|
+
var cssText = "";
|
|
1512
|
+
function setRichClayStyles(text) {
|
|
1513
|
+
cssText = text || "";
|
|
1514
|
+
}
|
|
1515
|
+
function ensureStyles(doc = document) {
|
|
1516
|
+
if (!doc || styledDocs.has(doc)) return;
|
|
1517
|
+
if (doc.getElementById(STYLE_ID)) {
|
|
1518
|
+
styledDocs.add(doc);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (!cssText) return;
|
|
1522
|
+
const style = doc.createElement("style");
|
|
1523
|
+
style.id = STYLE_ID;
|
|
1524
|
+
style.setAttribute("save-remove", "");
|
|
1525
|
+
style.setAttribute("save-ignore", "");
|
|
1526
|
+
style.textContent = cssText;
|
|
1527
|
+
(doc.head || doc.documentElement).appendChild(style);
|
|
1528
|
+
styledDocs.add(doc);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// src/richclay.js
|
|
1532
|
+
var instances = /* @__PURE__ */ new WeakMap();
|
|
1533
|
+
var autoInitWindows = /* @__PURE__ */ new WeakSet();
|
|
1534
|
+
var watchedWindows = /* @__PURE__ */ new WeakSet();
|
|
1535
|
+
var globalRegistry = createDefaultRegistry();
|
|
1536
|
+
var KEEP_FOCUS = Symbol("richclay-keep-focus");
|
|
1537
|
+
var ROOT_TRANSFER_EVENTS = ["cut", "paste", "drop"];
|
|
1538
|
+
var MODIFIER_ORDER = ["Alt", "Ctrl", "Meta", "Shift"];
|
|
1539
|
+
var dialogSeq = 0;
|
|
1540
|
+
var defaultOptions = {
|
|
1541
|
+
toolbar: "standard",
|
|
1542
|
+
toolbarContainer: null,
|
|
1543
|
+
sanitize: {},
|
|
1544
|
+
placeholder: "",
|
|
1545
|
+
readOnly: false,
|
|
1546
|
+
hyperclay: "auto",
|
|
1547
|
+
onChange: null,
|
|
1548
|
+
Squire: null,
|
|
1549
|
+
inline: false,
|
|
1550
|
+
singleLine: false,
|
|
1551
|
+
toolbarOnSelect: false
|
|
1552
|
+
};
|
|
1553
|
+
var RichClay = class _RichClay {
|
|
1554
|
+
static presets = presets;
|
|
1555
|
+
static selector = RICHCLAY_SELECTOR;
|
|
1556
|
+
constructor(element, options = {}) {
|
|
1557
|
+
if (!element) throw new Error("RichClay requires an element.");
|
|
1558
|
+
const existing = instances.get(element);
|
|
1559
|
+
if (existing) return existing;
|
|
1560
|
+
this.element = element;
|
|
1561
|
+
this.unsupported = conflictsWithExistingEditor(element) || refuseUnsupportedRoot(element);
|
|
1562
|
+
const derived = { ...parseEditableOptions(element), ...options };
|
|
1563
|
+
this.options = { ...defaultOptions, ...derived };
|
|
1564
|
+
if (this.options.singleLine && !("toolbar" in derived)) {
|
|
1565
|
+
this.options.toolbar = "inline";
|
|
1566
|
+
}
|
|
1567
|
+
this._blockTag = this.options.inline ? "DIV" : "P";
|
|
1568
|
+
this.registry = new Map(globalRegistry);
|
|
1569
|
+
this.toolbar = null;
|
|
1570
|
+
this.float = null;
|
|
1571
|
+
this._onToolbarKey = null;
|
|
1572
|
+
this._onFloatFocusOut = null;
|
|
1573
|
+
this._onDocPointerDown = null;
|
|
1574
|
+
this.liveRegion = null;
|
|
1575
|
+
this.description = null;
|
|
1576
|
+
this.dialog = null;
|
|
1577
|
+
this.savedSelection = null;
|
|
1578
|
+
this.path = "";
|
|
1579
|
+
this.active = false;
|
|
1580
|
+
this._squire = null;
|
|
1581
|
+
this._squireListeners = [];
|
|
1582
|
+
this._onBeforeInput = null;
|
|
1583
|
+
this._onRootBeforeInput = null;
|
|
1584
|
+
this._onRootKeydown = null;
|
|
1585
|
+
this._onRootTransfer = null;
|
|
1586
|
+
this._shortcutKeys = [];
|
|
1587
|
+
this._appleDeleteKeys = /* @__PURE__ */ new Set();
|
|
1588
|
+
this._authoredPres = /* @__PURE__ */ new WeakSet();
|
|
1589
|
+
this._warnedInlineBlock = false;
|
|
1590
|
+
this._onFocus = () => {
|
|
1591
|
+
this.element.classList.add("richclay-focused");
|
|
1592
|
+
if (this.options.inline) this.ensureFloatingToolbar();
|
|
1593
|
+
};
|
|
1594
|
+
this._onBlur = (event) => {
|
|
1595
|
+
this.element.classList.remove("richclay-focused");
|
|
1596
|
+
if (this.options.inline) this.scheduleFloatTeardown(event);
|
|
1597
|
+
};
|
|
1598
|
+
this.ensureMarker();
|
|
1599
|
+
this.hyperclay = shouldUseHyperclay(this.options, this.window);
|
|
1600
|
+
if (this.hyperclay) installHyperclayBridge(this.window);
|
|
1601
|
+
instances.set(element, this);
|
|
1602
|
+
this.sanitizeConfig = this.options.inline ? inlineSanitizeConfig(this.options.sanitize) : this.options.sanitize;
|
|
1603
|
+
this.sanitizer = createSanitizer(this.sanitizeConfig, this.element.ownerDocument);
|
|
1604
|
+
if (!this.options.inline) {
|
|
1605
|
+
sanitizeElement(this.element, this.sanitizeConfig);
|
|
1606
|
+
}
|
|
1607
|
+
if (shouldActivateEditor(this.options, this.window)) {
|
|
1608
|
+
this.activate();
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
get squire() {
|
|
1612
|
+
return this._squire;
|
|
1613
|
+
}
|
|
1614
|
+
get window() {
|
|
1615
|
+
return this.element.ownerDocument.defaultView || window;
|
|
1616
|
+
}
|
|
1617
|
+
static init(selector = RICHCLAY_SELECTOR, options = {}) {
|
|
1618
|
+
const guard = selector === RICHCLAY_SELECTOR ? isRichClayHost : () => true;
|
|
1619
|
+
const elements = resolveElements(selector).filter(guard).filter((element) => !conflictsWithExistingEditor(element));
|
|
1620
|
+
return elements.map((element) => new _RichClay(element, options));
|
|
1621
|
+
}
|
|
1622
|
+
static autoInit(win = typeof window !== "undefined" ? window : void 0) {
|
|
1623
|
+
if (!win || !win.document || autoInitWindows.has(win)) return;
|
|
1624
|
+
autoInitWindows.add(win);
|
|
1625
|
+
const run = () => {
|
|
1626
|
+
if (shouldUseHyperclay({}, win) && isHyperclayEditMode(win)) {
|
|
1627
|
+
_RichClay.init();
|
|
1628
|
+
_RichClay.watch(win);
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
if (win.document.readyState === "loading") {
|
|
1632
|
+
win.document.addEventListener("DOMContentLoaded", run, { once: true });
|
|
1633
|
+
} else {
|
|
1634
|
+
run();
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
static watch(win = typeof window !== "undefined" ? window : void 0, options = {}) {
|
|
1638
|
+
if (!win || !win.document || watchedWindows.has(win)) return;
|
|
1639
|
+
watchedWindows.add(win);
|
|
1640
|
+
const mount = (element) => {
|
|
1641
|
+
if (!isMountable(element)) return;
|
|
1642
|
+
if (!instances.has(element) && !conflictsWithExistingEditor(element)) new _RichClay(element, options);
|
|
1643
|
+
};
|
|
1644
|
+
const unmount = (element) => instances.get(element)?.destroy();
|
|
1645
|
+
const observer = new win.MutationObserver((records) => {
|
|
1646
|
+
records.forEach((record) => {
|
|
1647
|
+
if (record.type === "attributes") {
|
|
1648
|
+
const target = record.target;
|
|
1649
|
+
if (isMountable(target)) mount(target);
|
|
1650
|
+
else unmount(target);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
record.addedNodes.forEach((node) => {
|
|
1654
|
+
if (node.nodeType !== 1) return;
|
|
1655
|
+
if (node.matches?.(RICHCLAY_SELECTOR)) mount(node);
|
|
1656
|
+
node.querySelectorAll?.(RICHCLAY_SELECTOR).forEach(mount);
|
|
1657
|
+
});
|
|
1658
|
+
record.removedNodes.forEach((node) => {
|
|
1659
|
+
if (node.nodeType !== 1) return;
|
|
1660
|
+
const teardown = (el) => {
|
|
1661
|
+
if (!el.isConnected) instances.get(el)?.destroy();
|
|
1662
|
+
};
|
|
1663
|
+
if (node.matches?.(RICHCLAY_SELECTOR)) teardown(node);
|
|
1664
|
+
node.querySelectorAll?.(RICHCLAY_SELECTOR).forEach(teardown);
|
|
1665
|
+
});
|
|
1666
|
+
});
|
|
1667
|
+
});
|
|
1668
|
+
observer.observe(win.document.documentElement || win.document, {
|
|
1669
|
+
childList: true,
|
|
1670
|
+
subtree: true,
|
|
1671
|
+
attributes: true,
|
|
1672
|
+
attributeFilter: ["editable", "clay-editable", "data-richclay", "richclay"]
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
static registerButton(def) {
|
|
1676
|
+
validateButton(def);
|
|
1677
|
+
globalRegistry.set(def.id, def);
|
|
1678
|
+
return _RichClay;
|
|
1679
|
+
}
|
|
1680
|
+
static unregisterButton(id) {
|
|
1681
|
+
globalRegistry.delete(id);
|
|
1682
|
+
return _RichClay;
|
|
1683
|
+
}
|
|
1684
|
+
static stripFromClone(docElem) {
|
|
1685
|
+
stripRichClayFromClone(docElem);
|
|
1686
|
+
}
|
|
1687
|
+
// Remove richclay's runtime state from ONE element, addressed by element
|
|
1688
|
+
// rather than by instance.
|
|
1689
|
+
//
|
|
1690
|
+
// A host that grows a list by cloning a row clones an active region with it,
|
|
1691
|
+
// and the copy carries contenteditable, the marker and the runtime classes.
|
|
1692
|
+
// Usually there is no editor behind them and destroy() cannot reach it,
|
|
1693
|
+
// because destroy belongs to an instance and that clone has none. But a clone
|
|
1694
|
+
// carrying the author's own data-richclay is mountable, so the watcher can
|
|
1695
|
+
// have given it an instance before the host gets here: this cannot assume the
|
|
1696
|
+
// element is instanceless.
|
|
1697
|
+
static stripElement(element) {
|
|
1698
|
+
if (!element || typeof element.getAttribute !== "function") return;
|
|
1699
|
+
const existing = instances.get(element);
|
|
1700
|
+
if (existing) {
|
|
1701
|
+
existing.destroy();
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
removeRuntimeState(element, "destroy");
|
|
1705
|
+
}
|
|
1706
|
+
registerButton(def) {
|
|
1707
|
+
validateButton(def);
|
|
1708
|
+
this.registry.set(def.id, def);
|
|
1709
|
+
if (this.active) this.renderToolbar();
|
|
1710
|
+
return this;
|
|
1711
|
+
}
|
|
1712
|
+
unregisterButton(id) {
|
|
1713
|
+
this.registry.delete(id);
|
|
1714
|
+
if (this.active) this.renderToolbar();
|
|
1715
|
+
return this;
|
|
1716
|
+
}
|
|
1717
|
+
activate() {
|
|
1718
|
+
if (this.unsupported) return;
|
|
1719
|
+
if (this.active) return;
|
|
1720
|
+
this.active = true;
|
|
1721
|
+
this.applyInlineBox();
|
|
1722
|
+
ensureStyles(this.element.ownerDocument);
|
|
1723
|
+
consumeInertContenteditable(this.element);
|
|
1724
|
+
const initialNodes = this.options.inline ? Array.from(this.element.childNodes) : null;
|
|
1725
|
+
const initialHTML = this.options.inline ? null : this.element.innerHTML;
|
|
1726
|
+
const Squire = this.options.Squire || this.window.Squire || globalThis.Squire;
|
|
1727
|
+
if (!Squire) {
|
|
1728
|
+
throw new Error("RichClay requires Squire. Load vendor/squire.js before richclay.js.");
|
|
1729
|
+
}
|
|
1730
|
+
this.setupEditorAttributes();
|
|
1731
|
+
this.liveRegion = createLiveRegion(this.element.ownerDocument);
|
|
1732
|
+
this._squire = new Squire(this.element, {
|
|
1733
|
+
blockTag: this._blockTag,
|
|
1734
|
+
sanitizeToDOMFragment: (html, editor) => {
|
|
1735
|
+
const fragment = this.sanitizer.sanitizeToDOMFragment(html, editor);
|
|
1736
|
+
return this.options.singleLine ? flattenFragmentToSingleLine(fragment) : fragment;
|
|
1737
|
+
},
|
|
1738
|
+
didError: (error) => {
|
|
1739
|
+
console.error("RichClay/Squire error", error);
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
if (this.options.inline) {
|
|
1743
|
+
this.element.replaceChildren(...initialNodes);
|
|
1744
|
+
this.resetSquireUndoBaseline();
|
|
1745
|
+
} else {
|
|
1746
|
+
this._squire.setHTML(initialHTML);
|
|
1747
|
+
}
|
|
1748
|
+
this.bindSquire();
|
|
1749
|
+
if (this.options.singleLine) this.installSingleLineGuards();
|
|
1750
|
+
if (this.options.inline && !this.options.singleLine) this.installRootGuards();
|
|
1751
|
+
this.installAppleDeleteKeys();
|
|
1752
|
+
if (this.blocksStayOut()) this._squire._ensureBottomLine = () => {
|
|
1753
|
+
};
|
|
1754
|
+
this.maskSquireCodeShortcut();
|
|
1755
|
+
this.maskSquireBlockShortcuts();
|
|
1756
|
+
this.installShortcuts();
|
|
1757
|
+
if (this.options.inline) {
|
|
1758
|
+
this._onToolbarKey = (event) => {
|
|
1759
|
+
if (event.altKey && event.key === "F10") {
|
|
1760
|
+
event.preventDefault();
|
|
1761
|
+
this.ensureFloatingToolbar();
|
|
1762
|
+
this.float?.toolbar.focusFirst();
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
this.element.addEventListener("keydown", this._onToolbarKey);
|
|
1766
|
+
}
|
|
1767
|
+
if (this.blocksStayOut()) {
|
|
1768
|
+
const willPaste = (event) => {
|
|
1769
|
+
const fragment = event.detail?.fragment;
|
|
1770
|
+
if (!fragment) return;
|
|
1771
|
+
const doc = this.element.ownerDocument;
|
|
1772
|
+
flattenBlocks(
|
|
1773
|
+
fragment,
|
|
1774
|
+
() => this.options.singleLine ? doc.createTextNode(" ") : doc.createElement("br")
|
|
1775
|
+
);
|
|
1776
|
+
};
|
|
1777
|
+
this._squire.addEventListener("willPaste", willPaste);
|
|
1778
|
+
this._squireListeners.push(["willPaste", willPaste]);
|
|
1779
|
+
}
|
|
1780
|
+
this.renderToolbar();
|
|
1781
|
+
this.updatePlaceholder();
|
|
1782
|
+
this.warnOnBlockInInlineRegion();
|
|
1783
|
+
this.element.querySelectorAll("pre").forEach((pre) => this._authoredPres.add(pre));
|
|
1784
|
+
}
|
|
1785
|
+
// Live sync morphs the page against an incoming copy, and that copy is clean:
|
|
1786
|
+
// contenteditable, the marker and the runtime attributes all come back off an
|
|
1787
|
+
// element this editor is still pointing at. The instance is fine and Squire is
|
|
1788
|
+
// still bound; only the element's own state is gone, and activate() cannot put
|
|
1789
|
+
// it back because it returns early on an instance that never stopped being
|
|
1790
|
+
// active. Rebuilding is not an option for a host that adopted an editor the
|
|
1791
|
+
// author mounted, so this re-applies the state instead.
|
|
1792
|
+
//
|
|
1793
|
+
// The marker is in here because it is the one whose absence is fatal rather
|
|
1794
|
+
// than cosmetic: without it the watcher stops recognising the element, which
|
|
1795
|
+
// is the failure ensureMarker's own comment describes.
|
|
1796
|
+
//
|
|
1797
|
+
// Returns whether it had to do anything, so a caller can tell a repair from a
|
|
1798
|
+
// no-op without asking the same question twice.
|
|
1799
|
+
reattach() {
|
|
1800
|
+
if (this.unsupported || !this.active) return false;
|
|
1801
|
+
const intact = this.element.getAttribute("contenteditable") === "true" && this.element.getAttribute("data-richclay-active") === "true" && isMountable(this.element);
|
|
1802
|
+
if (intact) return false;
|
|
1803
|
+
this.ensureMarker();
|
|
1804
|
+
this.applyInlineBox();
|
|
1805
|
+
consumeInertContenteditable(this.element);
|
|
1806
|
+
this.setupEditorAttributes();
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
// A block inside an inline element renders as a run-on. This changes the box
|
|
1810
|
+
// and nothing else: whether a region survives a reload is decided by the
|
|
1811
|
+
// parser from tag names, before any CSS exists. Runtime only, so the author's
|
|
1812
|
+
// file keeps no style richclay put there.
|
|
1813
|
+
applyInlineBox() {
|
|
1814
|
+
if (isInlineTag(this.element) && !this.element.style.display) {
|
|
1815
|
+
this.element.style.display = "inline-block";
|
|
1816
|
+
this.element.setAttribute("data-richclay-runtime-display", "true");
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
getHTML() {
|
|
1820
|
+
return this._squire ? this._squire.getHTML() : this.element.innerHTML;
|
|
1821
|
+
}
|
|
1822
|
+
setHTML(html) {
|
|
1823
|
+
if (this._squire) {
|
|
1824
|
+
this._squire.setHTML(String(html || ""));
|
|
1825
|
+
} else {
|
|
1826
|
+
this.element.innerHTML = sanitizeHTML(
|
|
1827
|
+
html,
|
|
1828
|
+
this.sanitizeConfig,
|
|
1829
|
+
this.element.ownerDocument
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
this.updatePlaceholder();
|
|
1833
|
+
this.toolbar?.update();
|
|
1834
|
+
this.options.onChange?.(this.getHTML());
|
|
1835
|
+
return this;
|
|
1836
|
+
}
|
|
1837
|
+
focus() {
|
|
1838
|
+
if (this._squire) this._squire.focus();
|
|
1839
|
+
else this.element.focus();
|
|
1840
|
+
return this;
|
|
1841
|
+
}
|
|
1842
|
+
destroy() {
|
|
1843
|
+
this.closeLinkDialog();
|
|
1844
|
+
this.teardownFloatingToolbar();
|
|
1845
|
+
this.toolbar?.destroy();
|
|
1846
|
+
this.toolbar = null;
|
|
1847
|
+
if (this._onToolbarKey) {
|
|
1848
|
+
this.element.removeEventListener("keydown", this._onToolbarKey);
|
|
1849
|
+
this._onToolbarKey = null;
|
|
1850
|
+
}
|
|
1851
|
+
this.liveRegion?.remove();
|
|
1852
|
+
this.liveRegion = null;
|
|
1853
|
+
this.description?.remove();
|
|
1854
|
+
this.description = null;
|
|
1855
|
+
this._shortcutKeys.forEach((key) => this._squire?.setKeyHandler?.(key, null));
|
|
1856
|
+
this._squireListeners.forEach(([type, listener]) => {
|
|
1857
|
+
this._squire?.removeEventListener?.(type, listener);
|
|
1858
|
+
});
|
|
1859
|
+
this._squireListeners = [];
|
|
1860
|
+
this._shortcutKeys = [];
|
|
1861
|
+
if (this._onBeforeInput) {
|
|
1862
|
+
this.element.ownerDocument.removeEventListener("beforeinput", this._onBeforeInput, true);
|
|
1863
|
+
this._onBeforeInput = null;
|
|
1864
|
+
}
|
|
1865
|
+
const doc = this.element.ownerDocument;
|
|
1866
|
+
if (this._onRootBeforeInput) {
|
|
1867
|
+
doc.removeEventListener("beforeinput", this._onRootBeforeInput, true);
|
|
1868
|
+
this._onRootBeforeInput = null;
|
|
1869
|
+
}
|
|
1870
|
+
if (this._onRootKeydown) {
|
|
1871
|
+
doc.removeEventListener("keydown", this._onRootKeydown, true);
|
|
1872
|
+
this._onRootKeydown = null;
|
|
1873
|
+
}
|
|
1874
|
+
if (this._onRootTransfer) {
|
|
1875
|
+
ROOT_TRANSFER_EVENTS.forEach((type) => doc.removeEventListener(type, this._onRootTransfer, true));
|
|
1876
|
+
this._onRootTransfer = null;
|
|
1877
|
+
}
|
|
1878
|
+
this._squire?.destroy?.();
|
|
1879
|
+
this._squire = null;
|
|
1880
|
+
this.cleanupEditorAttributes();
|
|
1881
|
+
instances.delete(this.element);
|
|
1882
|
+
}
|
|
1883
|
+
saveSelection() {
|
|
1884
|
+
this.savedSelection = getSquireSelection(this._squire) || this.savedSelection;
|
|
1885
|
+
return this.savedSelection;
|
|
1886
|
+
}
|
|
1887
|
+
restoreSelection() {
|
|
1888
|
+
return restoreSquireSelection(this._squire, this.savedSelection);
|
|
1889
|
+
}
|
|
1890
|
+
// Squire's invariant is established on the first real edit rather than at
|
|
1891
|
+
// activation, so that opening a page in edit mode never rewrites it. These
|
|
1892
|
+
// listeners capture on the document, so the root is valid before Squire's own
|
|
1893
|
+
// handlers see the event. History inputs are skipped: repairing mid-undo would
|
|
1894
|
+
// fight the stack it is replaying.
|
|
1895
|
+
installRootGuards() {
|
|
1896
|
+
const doc = this.element.ownerDocument;
|
|
1897
|
+
const owns = (event) => this.element.contains(event.target);
|
|
1898
|
+
this._onRootBeforeInput = (event) => {
|
|
1899
|
+
if (!owns(event) || event.isComposing || /^history/.test(event.inputType || "")) return;
|
|
1900
|
+
this.ensureRootIsEditable();
|
|
1901
|
+
};
|
|
1902
|
+
this._onRootKeydown = (event) => {
|
|
1903
|
+
if (!owns(event) || event.defaultPrevented || event.isComposing) return;
|
|
1904
|
+
if (this.isEditingKey(event)) this.ensureRootIsEditable();
|
|
1905
|
+
};
|
|
1906
|
+
this._onRootTransfer = (event) => {
|
|
1907
|
+
if (owns(event)) this.ensureRootIsEditable();
|
|
1908
|
+
};
|
|
1909
|
+
doc.addEventListener("beforeinput", this._onRootBeforeInput, true);
|
|
1910
|
+
doc.addEventListener("keydown", this._onRootKeydown, true);
|
|
1911
|
+
ROOT_TRANSFER_EVENTS.forEach((type) => {
|
|
1912
|
+
doc.addEventListener(type, this._onRootTransfer, true);
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
// Re-checked rather than latched, because Hyperclay's live sync morphs new DOM
|
|
1916
|
+
// into the region long after activation and can reintroduce a loose text node.
|
|
1917
|
+
ensureRootIsEditable() {
|
|
1918
|
+
if (!this._squire || this.options.singleLine) return;
|
|
1919
|
+
const wrapBareRoot = this.options.inline;
|
|
1920
|
+
const blocksAllowed = !this.blocksStayOut();
|
|
1921
|
+
if (!editorRootNeedsNormalization(this.element, { wrapBareRoot, blocksAllowed })) return;
|
|
1922
|
+
const range = getSquireSelection(this._squire);
|
|
1923
|
+
const saved = range ? captureRange(this.element, range) : null;
|
|
1924
|
+
const repair = () => {
|
|
1925
|
+
normalizeEditorRoot(this.element, {
|
|
1926
|
+
blockTag: this._blockTag,
|
|
1927
|
+
wrapBareRoot,
|
|
1928
|
+
blocksAllowed,
|
|
1929
|
+
onBareRootWrapped: (root, wrapper) => {
|
|
1930
|
+
console.warn(
|
|
1931
|
+
`richclay: wrapped this region's loose text in a <div> so multi-line editing works. Wrap the content in your own block element to keep the markup you wrote, or use editable="single-line" if it is meant to be one line.`,
|
|
1932
|
+
root,
|
|
1933
|
+
wrapper
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
};
|
|
1938
|
+
if (typeof this._squire.modifyDocument === "function") this._squire.modifyDocument(repair);
|
|
1939
|
+
else repair();
|
|
1940
|
+
if (saved) {
|
|
1941
|
+
restoreRange(this.element, range, saved);
|
|
1942
|
+
restoreSquireSelection(this._squire, range);
|
|
1943
|
+
this.savedSelection = cloneRange(range);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
// Only keys that will actually edit. Repairing is not free on a root that has
|
|
1947
|
+
// not been repaired yet, which is every freshly opened page, so a blanket
|
|
1948
|
+
// "any single-character key" made Cmd+C and Cmd+S rewrite an untouched region.
|
|
1949
|
+
// The only modified keys that do edit are the ones this editor rebound itself,
|
|
1950
|
+
// which is why the set is asked rather than Squire's dispatch mirrored.
|
|
1951
|
+
isEditingKey(event) {
|
|
1952
|
+
if (["Backspace", "Delete", "Enter"].includes(event.key)) return true;
|
|
1953
|
+
if (event.key === "Tab") return this.caretIsInList();
|
|
1954
|
+
if (event.key.length !== 1) return false;
|
|
1955
|
+
if (!event.ctrlKey && !event.metaKey && !event.altKey) return true;
|
|
1956
|
+
return event.ctrlKey && !event.metaKey && !event.altKey && this._appleDeleteKeys.has(event.key.toLowerCase());
|
|
1957
|
+
}
|
|
1958
|
+
caretIsInList() {
|
|
1959
|
+
const range = this._squire?.getSelection();
|
|
1960
|
+
const container = range?.startContainer;
|
|
1961
|
+
if (!container) return false;
|
|
1962
|
+
const element = container.nodeType === 1 ? container : container.parentElement;
|
|
1963
|
+
return Boolean(element && this.element.contains(element) && element.closest("ul, ol"));
|
|
1964
|
+
}
|
|
1965
|
+
runControl(def) {
|
|
1966
|
+
if (!this._squire || typeof def.run !== "function") return;
|
|
1967
|
+
if (def.isDisabled?.(this)) return;
|
|
1968
|
+
this.restoreSelection();
|
|
1969
|
+
if (def.mutates !== false) this.ensureRootIsEditable();
|
|
1970
|
+
const result = def.run(this);
|
|
1971
|
+
this.element.querySelectorAll("pre:not([data-richclay-pre])").forEach((pre) => {
|
|
1972
|
+
if (!this._authoredPres.has(pre)) pre.setAttribute("data-richclay-pre", "");
|
|
1973
|
+
});
|
|
1974
|
+
if (def.mutates !== false) {
|
|
1975
|
+
this.ensureRootIsEditable();
|
|
1976
|
+
this.anchorSelectionInBlock();
|
|
1977
|
+
}
|
|
1978
|
+
this.saveSelection();
|
|
1979
|
+
this.toolbar?.update();
|
|
1980
|
+
this.updatePlaceholder();
|
|
1981
|
+
this.announceControl(def);
|
|
1982
|
+
if (result !== KEEP_FOCUS) {
|
|
1983
|
+
this.focus();
|
|
1984
|
+
}
|
|
1985
|
+
this.warnOnBlockInInlineRegion();
|
|
1986
|
+
}
|
|
1987
|
+
// Squire's modifyBlocks leaves the caret anchored on the root itself, between
|
|
1988
|
+
// blocks. From there getStartBlockOfRange finds no block, so the next block
|
|
1989
|
+
// command is a silent no-op — the "it stopped working" the user hit. Push a
|
|
1990
|
+
// collapsed caret back inside the block it is sitting in front of.
|
|
1991
|
+
anchorSelectionInBlock() {
|
|
1992
|
+
const range = getSquireSelection(this._squire);
|
|
1993
|
+
if (!range?.collapsed || range.startContainer !== this.element) return;
|
|
1994
|
+
const children = Array.from(this.element.childNodes);
|
|
1995
|
+
const atEnd = range.startOffset >= children.length;
|
|
1996
|
+
const child = atEnd ? children.filter(isCaretHost).pop() : children.slice(range.startOffset).find(isCaretHost) || children.slice(0, range.startOffset).filter(isCaretHost).pop();
|
|
1997
|
+
if (!child) return;
|
|
1998
|
+
const edge = caretEdge(child, !atEnd);
|
|
1999
|
+
range.setStart(edge, atEnd ? nodeLength(edge) : 0);
|
|
2000
|
+
range.collapse(true);
|
|
2001
|
+
restoreSquireSelection(this._squire, range);
|
|
2002
|
+
}
|
|
2003
|
+
selectionHasFormat(tag) {
|
|
2004
|
+
if (!tag || !this._squire?.hasFormat) return false;
|
|
2005
|
+
try {
|
|
2006
|
+
return this._squire.hasFormat(tag);
|
|
2007
|
+
} catch {
|
|
2008
|
+
return false;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
// Squire sets its path to "(selection)" whenever the selection spans more than
|
|
2012
|
+
// one node, so asking the path answered "no" for every multi-block selection.
|
|
2013
|
+
// Both boundaries must land in the same matching element, or a selection that
|
|
2014
|
+
// merely starts in a list would report as being in one.
|
|
2015
|
+
pathHas(tag) {
|
|
2016
|
+
const range = getSquireSelection(this._squire);
|
|
2017
|
+
if (!range) return false;
|
|
2018
|
+
const selector = tag.toLowerCase();
|
|
2019
|
+
const start = matchAtBoundary(this.element, range.startContainer, range.startOffset, true, selector);
|
|
2020
|
+
return Boolean(start) && start === matchAtBoundary(this.element, range.endContainer, range.endOffset, false, selector);
|
|
2021
|
+
}
|
|
2022
|
+
// Blocks stay out of this region for either of two reasons, and both are fixed
|
|
2023
|
+
// for the editor's life, which is why the toolbar can leave the controls out
|
|
2024
|
+
// rather than grey them. Either the parser would eject a block on the next page
|
|
2025
|
+
// load, or the author wrote the region as a line of text and richclay does not
|
|
2026
|
+
// rewrite what they wrote.
|
|
2027
|
+
blocksStayOut() {
|
|
2028
|
+
return this.options.singleLine || ejectsBlocks(this.element) || keepsTextShape(this.element);
|
|
2029
|
+
}
|
|
2030
|
+
// Advisory only: the markup is stable and saves correctly, it just will not
|
|
2031
|
+
// validate. Checked when the editor mounts and after any command, which covers
|
|
2032
|
+
// the ways an author actually produces one. Paste is deliberately not covered:
|
|
2033
|
+
// nothing hooks paste any more, and a console note is not worth reinstating a
|
|
2034
|
+
// hook that took three rounds to get wrong.
|
|
2035
|
+
warnOnBlockInInlineRegion() {
|
|
2036
|
+
if (this._warnedInlineBlock || !isInlineTag(this.element)) return;
|
|
2037
|
+
if (!hasBlockDescendant(this.element)) return;
|
|
2038
|
+
this._warnedInlineBlock = true;
|
|
2039
|
+
const tag = this.element.nodeName.toLowerCase();
|
|
2040
|
+
console.warn(
|
|
2041
|
+
`richclay: a block element inside <${tag} editable> is not valid HTML. It stays where it is and saves correctly, but a validator will flag it. Put the editable attribute on a block element if you want blocks here.`,
|
|
2042
|
+
this.element
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
toggleFormat(tag, addMethod, removeMethod) {
|
|
2046
|
+
if (this.selectionHasFormat(tag)) return this._squire[removeMethod]();
|
|
2047
|
+
return this._squire[addMethod]();
|
|
2048
|
+
}
|
|
2049
|
+
toggleList(tag) {
|
|
2050
|
+
if (this.pathHas(tag)) return this._squire.removeList();
|
|
2051
|
+
return tag === "UL" ? this._squire.makeUnorderedList() : this._squire.makeOrderedList();
|
|
2052
|
+
}
|
|
2053
|
+
indent() {
|
|
2054
|
+
if (this.pathHas("UL") || this.pathHas("OL")) return this._squire.increaseListLevel();
|
|
2055
|
+
return this._squire.increaseQuoteLevel();
|
|
2056
|
+
}
|
|
2057
|
+
outdent() {
|
|
2058
|
+
if (this.pathHas("UL") || this.pathHas("OL")) return this._squire.decreaseListLevel();
|
|
2059
|
+
return this._squire.decreaseQuoteLevel();
|
|
2060
|
+
}
|
|
2061
|
+
setBlockType(tag) {
|
|
2062
|
+
const target = tag.toUpperCase();
|
|
2063
|
+
if (target === "BLOCKQUOTE") return this._squire.increaseQuoteLevel();
|
|
2064
|
+
return this._squire.modifyBlocks((fragment) => {
|
|
2065
|
+
const doc = this.element.ownerDocument;
|
|
2066
|
+
const output = doc.createDocumentFragment();
|
|
2067
|
+
Array.from(fragment.childNodes).forEach((node) => {
|
|
2068
|
+
if (node.nodeType !== 1) {
|
|
2069
|
+
output.appendChild(node);
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (node.nodeName === target) {
|
|
2073
|
+
output.appendChild(node);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
const replacement = doc.createElement(target);
|
|
2077
|
+
while (node.firstChild) replacement.appendChild(node.firstChild);
|
|
2078
|
+
output.appendChild(replacement);
|
|
2079
|
+
});
|
|
2080
|
+
return output;
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
openLinkDialog() {
|
|
2084
|
+
this.saveSelection();
|
|
2085
|
+
this.closeLinkDialog();
|
|
2086
|
+
const doc = this.element.ownerDocument;
|
|
2087
|
+
const existing = this.currentLinkHref();
|
|
2088
|
+
const dialog = doc.createElement("form");
|
|
2089
|
+
const labelId = `richclay-link-label-${++dialogSeq}`;
|
|
2090
|
+
dialog.className = "richclay-dialog";
|
|
2091
|
+
dialog.setAttribute("role", "dialog");
|
|
2092
|
+
dialog.setAttribute("aria-modal", "true");
|
|
2093
|
+
dialog.setAttribute("aria-labelledby", labelId);
|
|
2094
|
+
dialog.setAttribute("data-richclay-dialog", "");
|
|
2095
|
+
markChrome(dialog);
|
|
2096
|
+
dialog.innerHTML = `
|
|
2097
|
+
<div class="richclay-dialog-title" id="${labelId}">${existing ? "Edit link" : "Insert link"}</div>
|
|
2098
|
+
<label class="richclay-field">
|
|
2099
|
+
<span>URL</span>
|
|
2100
|
+
<input class="richclay-input" name="url" type="text" inputmode="url" autocomplete="url" placeholder="https://example.com">
|
|
2101
|
+
</label>
|
|
2102
|
+
<div class="richclay-dialog-actions">
|
|
2103
|
+
<button type="button" class="richclay-secondary" data-richclay-cancel>Cancel</button>
|
|
2104
|
+
<button type="submit" class="richclay-primary">Apply</button>
|
|
2105
|
+
</div>
|
|
2106
|
+
`;
|
|
2107
|
+
const close = () => {
|
|
2108
|
+
this.closeLinkDialog();
|
|
2109
|
+
this.toolbar?.update();
|
|
2110
|
+
this.focus();
|
|
2111
|
+
};
|
|
2112
|
+
dialog.addEventListener("submit", (event) => {
|
|
2113
|
+
event.preventDefault();
|
|
2114
|
+
const data = new FormData(dialog);
|
|
2115
|
+
const href = normalizeUrl(data.get("url"));
|
|
2116
|
+
if (!href) {
|
|
2117
|
+
announce(this.liveRegion, "Enter a valid URL");
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
this.restoreSelection();
|
|
2121
|
+
const anchor = this.currentLinkElement();
|
|
2122
|
+
if (anchor) this.selectElement(anchor);
|
|
2123
|
+
this._squire.makeLink(href);
|
|
2124
|
+
announce(this.liveRegion, "Link applied");
|
|
2125
|
+
close();
|
|
2126
|
+
});
|
|
2127
|
+
dialog.querySelector("[data-richclay-cancel]").addEventListener("click", close);
|
|
2128
|
+
dialog.addEventListener("keydown", (event) => {
|
|
2129
|
+
if (event.key === "Escape") {
|
|
2130
|
+
event.preventDefault();
|
|
2131
|
+
close();
|
|
2132
|
+
} else if (event.key === "Tab") {
|
|
2133
|
+
trapTab(event, dialog);
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
const mount = this.toolbar?.root || this.element;
|
|
2137
|
+
mount.insertAdjacentElement("afterend", dialog);
|
|
2138
|
+
this.dialog = dialog;
|
|
2139
|
+
const input = dialog.querySelector("input");
|
|
2140
|
+
input.value = existing;
|
|
2141
|
+
input.focus();
|
|
2142
|
+
input.select();
|
|
2143
|
+
return KEEP_FOCUS;
|
|
2144
|
+
}
|
|
2145
|
+
currentLinkElement() {
|
|
2146
|
+
const range = getSquireSelection(this._squire);
|
|
2147
|
+
let node = range?.commonAncestorContainer || null;
|
|
2148
|
+
if (node && node.nodeType !== 1) node = node.parentElement;
|
|
2149
|
+
return node?.closest?.("a[href]") || null;
|
|
2150
|
+
}
|
|
2151
|
+
currentLinkHref() {
|
|
2152
|
+
const anchor = this.currentLinkElement();
|
|
2153
|
+
return anchor ? anchor.getAttribute("href") || "" : "";
|
|
2154
|
+
}
|
|
2155
|
+
selectElement(node) {
|
|
2156
|
+
const range = node.ownerDocument.createRange();
|
|
2157
|
+
range.selectNode(node);
|
|
2158
|
+
this._squire.setSelection(range);
|
|
2159
|
+
this.saveSelection();
|
|
2160
|
+
return range;
|
|
2161
|
+
}
|
|
2162
|
+
closeLinkDialog() {
|
|
2163
|
+
this.dialog?.remove();
|
|
2164
|
+
this.dialog = null;
|
|
2165
|
+
}
|
|
2166
|
+
ensureMarker() {
|
|
2167
|
+
if (!isMountable(this.element)) {
|
|
2168
|
+
this.element.setAttribute("data-richclay-runtime-marker", "true");
|
|
2169
|
+
this.element.setAttribute("data-richclay", "");
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
setupEditorAttributes() {
|
|
2173
|
+
this.element.classList.add(
|
|
2174
|
+
this.options.inline ? "richclay-inline" : "richclay-editor",
|
|
2175
|
+
"richclay-active"
|
|
2176
|
+
);
|
|
2177
|
+
this.element.setAttribute("data-richclay-active", "true");
|
|
2178
|
+
if (!this.element.hasAttribute("data-richclay-runtime-contenteditable")) {
|
|
2179
|
+
const original = this.element.getAttribute("contenteditable");
|
|
2180
|
+
if (original === null) {
|
|
2181
|
+
this.element.setAttribute("data-richclay-runtime-contenteditable", "true");
|
|
2182
|
+
} else if (original !== "true" && original !== "") {
|
|
2183
|
+
this.element.setAttribute("data-richclay-runtime-contenteditable", original);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
this.element.setAttribute("contenteditable", "true");
|
|
2187
|
+
if (!this.element.hasAttribute("role")) {
|
|
2188
|
+
setRuntimeAttribute(this.element, "role", "textbox", "data-richclay-runtime-role");
|
|
2189
|
+
}
|
|
2190
|
+
if (!this.element.hasAttribute("aria-multiline")) {
|
|
2191
|
+
setRuntimeAttribute(
|
|
2192
|
+
this.element,
|
|
2193
|
+
"aria-multiline",
|
|
2194
|
+
this.options.singleLine ? "false" : "true",
|
|
2195
|
+
"data-richclay-runtime-aria-multiline"
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
if (!this.element.hasAttribute("no-undo")) {
|
|
2199
|
+
setRuntimeAttribute(this.element, "no-undo", "", "data-richclay-runtime-no-undo");
|
|
2200
|
+
}
|
|
2201
|
+
if (this.options.placeholder) {
|
|
2202
|
+
this.element.setAttribute("data-richclay-placeholder", this.options.placeholder);
|
|
2203
|
+
if (!this.description) {
|
|
2204
|
+
this.description = this.element.ownerDocument.createElement("div");
|
|
2205
|
+
this.description.id = `richclay-placeholder-${++dialogSeq}`;
|
|
2206
|
+
this.description.className = "richclay-sr-only";
|
|
2207
|
+
this.description.textContent = this.options.placeholder;
|
|
2208
|
+
this.description.setAttribute("data-richclay-live", "");
|
|
2209
|
+
markChrome(this.description);
|
|
2210
|
+
}
|
|
2211
|
+
if (!this.description.isConnected) {
|
|
2212
|
+
this.element.insertAdjacentElement("afterend", this.description);
|
|
2213
|
+
}
|
|
2214
|
+
connectDescription(this.element, this.description);
|
|
2215
|
+
}
|
|
2216
|
+
this.element.addEventListener("focus", this._onFocus);
|
|
2217
|
+
this.element.addEventListener("blur", this._onBlur);
|
|
2218
|
+
}
|
|
2219
|
+
cleanupEditorAttributes() {
|
|
2220
|
+
removeRuntimeState(this.element, "destroy");
|
|
2221
|
+
this.element.removeEventListener("focus", this._onFocus);
|
|
2222
|
+
this.element.removeEventListener("blur", this._onBlur);
|
|
2223
|
+
}
|
|
2224
|
+
bindSquire() {
|
|
2225
|
+
const input = () => {
|
|
2226
|
+
this.saveSelection();
|
|
2227
|
+
this.updatePlaceholder();
|
|
2228
|
+
this.options.onChange?.(this.getHTML());
|
|
2229
|
+
this.toolbar?.update();
|
|
2230
|
+
};
|
|
2231
|
+
const pathChange = (event) => {
|
|
2232
|
+
this.path = event.detail?.path || this._squire.getPath?.() || "";
|
|
2233
|
+
this.saveSelection();
|
|
2234
|
+
this.toolbar?.update();
|
|
2235
|
+
};
|
|
2236
|
+
const selection = () => {
|
|
2237
|
+
this.saveSelection();
|
|
2238
|
+
this.toolbar?.update();
|
|
2239
|
+
if (this.options.toolbarOnSelect) this.updateFloatVisibility();
|
|
2240
|
+
};
|
|
2241
|
+
const undoState = () => this.toolbar?.update();
|
|
2242
|
+
[
|
|
2243
|
+
["input", input],
|
|
2244
|
+
["pathChange", pathChange],
|
|
2245
|
+
["select", selection],
|
|
2246
|
+
["cursor", selection],
|
|
2247
|
+
["undoStateChange", undoState]
|
|
2248
|
+
].forEach(([type, listener]) => {
|
|
2249
|
+
this._squire.addEventListener(type, listener);
|
|
2250
|
+
this._squireListeners.push([type, listener]);
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
renderToolbar() {
|
|
2254
|
+
if (!this.active || this.options.readOnly) return;
|
|
2255
|
+
if (this.options.inline) {
|
|
2256
|
+
if (this.float) {
|
|
2257
|
+
this.teardownFloatingToolbar();
|
|
2258
|
+
this.ensureFloatingToolbar();
|
|
2259
|
+
}
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
this.toolbar?.destroy();
|
|
2263
|
+
this.toolbar = null;
|
|
2264
|
+
const controls = this.resolveToolbarControls(this.options.toolbar);
|
|
2265
|
+
if (!controls.length) return;
|
|
2266
|
+
this.toolbar = new Toolbar(this, controls, {
|
|
2267
|
+
toolbarContainer: this.options.toolbarContainer
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
ensureFloatingToolbar() {
|
|
2271
|
+
if (this.float || !this.active || this.options.readOnly) return;
|
|
2272
|
+
const controls = this.resolveToolbarControls(this.options.toolbar);
|
|
2273
|
+
if (!controls.length) return;
|
|
2274
|
+
this.float = new FloatingToolbar(this, controls);
|
|
2275
|
+
this._onFloatFocusOut = (event) => this.scheduleFloatTeardown(event);
|
|
2276
|
+
this.float.root.addEventListener("focusout", this._onFloatFocusOut);
|
|
2277
|
+
this._onDocPointerDown = (event) => {
|
|
2278
|
+
if (this.ownsFocusTarget(event.target)) return;
|
|
2279
|
+
const doc = this.element.ownerDocument;
|
|
2280
|
+
if (doc.activeElement && this.ownsFocusTarget(doc.activeElement)) {
|
|
2281
|
+
doc.activeElement.blur?.();
|
|
2282
|
+
}
|
|
2283
|
+
this.teardownFloatingToolbar();
|
|
2284
|
+
};
|
|
2285
|
+
this.element.ownerDocument.addEventListener("pointerdown", this._onDocPointerDown, true);
|
|
2286
|
+
this.toolbar = this.float.toolbar;
|
|
2287
|
+
this.toolbar.update();
|
|
2288
|
+
if (this.options.toolbarOnSelect) this.updateFloatVisibility();
|
|
2289
|
+
}
|
|
2290
|
+
teardownFloatingToolbar() {
|
|
2291
|
+
if (!this.float) return;
|
|
2292
|
+
if (this._onFloatFocusOut) {
|
|
2293
|
+
this.float.root.removeEventListener("focusout", this._onFloatFocusOut);
|
|
2294
|
+
this._onFloatFocusOut = null;
|
|
2295
|
+
}
|
|
2296
|
+
if (this._onDocPointerDown) {
|
|
2297
|
+
this.element.ownerDocument.removeEventListener("pointerdown", this._onDocPointerDown, true);
|
|
2298
|
+
this._onDocPointerDown = null;
|
|
2299
|
+
}
|
|
2300
|
+
this.float.destroy();
|
|
2301
|
+
this.float = null;
|
|
2302
|
+
this.toolbar = null;
|
|
2303
|
+
}
|
|
2304
|
+
scheduleFloatTeardown(event) {
|
|
2305
|
+
const next = event?.relatedTarget;
|
|
2306
|
+
if (next && this.ownsFocusTarget(next)) return;
|
|
2307
|
+
const doc = this.element.ownerDocument;
|
|
2308
|
+
const win = doc.defaultView || globalThis;
|
|
2309
|
+
win.setTimeout(() => {
|
|
2310
|
+
const active = doc.activeElement;
|
|
2311
|
+
if (active && this.ownsFocusTarget(active)) return;
|
|
2312
|
+
this.teardownFloatingToolbar();
|
|
2313
|
+
}, 0);
|
|
2314
|
+
}
|
|
2315
|
+
ownsFocusTarget(node) {
|
|
2316
|
+
return node === this.element || this.element.contains(node) || Boolean(this.float?.root.contains(node)) || Boolean(this.dialog?.contains(node));
|
|
2317
|
+
}
|
|
2318
|
+
updateFloatVisibility() {
|
|
2319
|
+
if (!this.float) return;
|
|
2320
|
+
const range = this.savedSelection;
|
|
2321
|
+
this.float.setVisible(Boolean(range && !range.collapsed));
|
|
2322
|
+
}
|
|
2323
|
+
resolveToolbarControls(toolbar) {
|
|
2324
|
+
if (toolbar === false || toolbar === null || toolbar === "none") return [];
|
|
2325
|
+
const requested = Array.isArray(toolbar) ? toolbar : presets[toolbar] || presets.standard;
|
|
2326
|
+
return requested.map((item) => {
|
|
2327
|
+
if (typeof item === "string") {
|
|
2328
|
+
const def = this.registry.get(item);
|
|
2329
|
+
if (!def) throw new Error(`Unknown RichClay toolbar control: ${item}`);
|
|
2330
|
+
return def;
|
|
2331
|
+
}
|
|
2332
|
+
if (item?.type === "separator") return item;
|
|
2333
|
+
validateButton(item);
|
|
2334
|
+
return item;
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
installShortcuts() {
|
|
2338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2339
|
+
const registryDefs = [...this.registry.values()].filter(
|
|
2340
|
+
(def) => !this.options.singleLine || presets.inline.includes(def.id)
|
|
2341
|
+
);
|
|
2342
|
+
const defs = [...this.resolveToolbarControls(this.options.toolbar), ...registryDefs];
|
|
2343
|
+
defs.forEach((def) => {
|
|
2344
|
+
if (def.type === "menu" || def.type === "separator") return;
|
|
2345
|
+
if (!def.shortcut || !def.id || seen.has(def.id)) return;
|
|
2346
|
+
if (def.isDisabled?.(this)) {
|
|
2347
|
+
const masked = shortcutKey(def.shortcut, this.window);
|
|
2348
|
+
this._squire.setKeyHandler(masked, null);
|
|
2349
|
+
this._shortcutKeys.push(masked);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
seen.add(def.id);
|
|
2353
|
+
const key = shortcutKey(def.shortcut, this.window);
|
|
2354
|
+
this._squire.setKeyHandler(key, (squire, event) => {
|
|
2355
|
+
event.preventDefault();
|
|
2356
|
+
this.runControl(def);
|
|
2357
|
+
});
|
|
2358
|
+
this._shortcutKeys.push(key);
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
// macOS sends Ctrl+D and Ctrl+H to the browser's own forward/backward delete,
|
|
2362
|
+
// which merges two blocks by wrapping the moved text in a computed-style
|
|
2363
|
+
// <span> — permanent junk in an editor whose DOM is the saved document. Point
|
|
2364
|
+
// them at Squire's own handlers so they behave exactly like Delete and
|
|
2365
|
+
// Backspace. Reads Squire's key handler table; no-ops for other engines.
|
|
2366
|
+
// Squire binds Code to the platform modifier itself, on _keyHandlers' prototype,
|
|
2367
|
+
// so dropping richclay's own shortcut is not enough to retire it. An own
|
|
2368
|
+
// property of null shadows the inherited handler and the key falls through.
|
|
2369
|
+
maskSquireCodeShortcut() {
|
|
2370
|
+
const key = `${isApplePlatform(this.window) ? "Meta" : "Ctrl"}-d`;
|
|
2371
|
+
this._squire.setKeyHandler(key, null);
|
|
2372
|
+
this._shortcutKeys.push(key);
|
|
2373
|
+
}
|
|
2374
|
+
// Squire binds these on _keyHandlers' prototype, and richclay has no definition
|
|
2375
|
+
// carrying some of them, so filtering richclay's own shortcuts cannot reach
|
|
2376
|
+
// them: Mod+] kept building a <blockquote> in a <p editable> through every
|
|
2377
|
+
// round. An own property of null shadows the inherited handler and the key
|
|
2378
|
+
// falls through. The prefix must match the one Squire computed, which is why
|
|
2379
|
+
// the test harness has to agree about the platform.
|
|
2380
|
+
maskSquireBlockShortcuts() {
|
|
2381
|
+
if (!this.blocksStayOut()) return;
|
|
2382
|
+
const mod = isApplePlatform(this.window) ? "Meta" : "Ctrl";
|
|
2383
|
+
[`${mod}-]`, `${mod}-[`, `${mod}-Shift-8`, `${mod}-Shift-9`].forEach((key) => {
|
|
2384
|
+
this._squire.setKeyHandler(key, null);
|
|
2385
|
+
this._shortcutKeys.push(key);
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
// Squire's toggleCode() makes a block <pre> when the selection is collapsed,
|
|
2389
|
+
// which inside a heading or a paragraph root means a code block where no block
|
|
2390
|
+
// can legally go. Those regions get inline <code> instead.
|
|
2391
|
+
toggleCode() {
|
|
2392
|
+
if (!this.blocksStayOut()) return this._squire.toggleCode();
|
|
2393
|
+
if (this.selectionHasFormat("CODE")) return this._squire.changeFormat(null, { tag: "CODE" });
|
|
2394
|
+
return this._squire.changeFormat({ tag: "CODE" }, null);
|
|
2395
|
+
}
|
|
2396
|
+
installAppleDeleteKeys() {
|
|
2397
|
+
if (!isApplePlatform(this.window)) return;
|
|
2398
|
+
const handlers = this._squire._keyHandlers;
|
|
2399
|
+
if (!handlers) return;
|
|
2400
|
+
[["Ctrl-d", "Delete"], ["Ctrl-h", "Backspace"]].forEach(([key, native]) => {
|
|
2401
|
+
if (typeof handlers[native] !== "function") return;
|
|
2402
|
+
this._squire.setKeyHandler(key, handlers[native]);
|
|
2403
|
+
this._shortcutKeys.push(key);
|
|
2404
|
+
this._appleDeleteKeys.add(key.slice("Ctrl-".length));
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
installSingleLineGuards() {
|
|
2408
|
+
["Enter", "Shift-Enter"].forEach((key) => {
|
|
2409
|
+
this._squire.setKeyHandler(key, (squire, event) => event.preventDefault());
|
|
2410
|
+
this._shortcutKeys.push(key);
|
|
2411
|
+
});
|
|
2412
|
+
this._onBeforeInput = (event) => {
|
|
2413
|
+
if (!this.element.contains(event.target)) return;
|
|
2414
|
+
if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") {
|
|
2415
|
+
event.preventDefault();
|
|
2416
|
+
event.stopPropagation();
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
this.element.ownerDocument.addEventListener("beforeinput", this._onBeforeInput, true);
|
|
2420
|
+
}
|
|
2421
|
+
// The fidelity restore bypasses setHTML, so Squire's undo baseline is still
|
|
2422
|
+
// the empty document its constructor recorded; the first undo could wipe the
|
|
2423
|
+
// region. Rebase the stack on the restored content. This touches Squire
|
|
2424
|
+
// internals (property names survive its esbuild minify) and no-ops for other
|
|
2425
|
+
// engines, like the tests' FakeSquire.
|
|
2426
|
+
resetSquireUndoBaseline() {
|
|
2427
|
+
const squire = this._squire;
|
|
2428
|
+
if (!squire || !Array.isArray(squire._undoStack) || typeof squire.saveUndoState !== "function") {
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
squire._undoStack.length = 0;
|
|
2432
|
+
squire._undoStackLength = 0;
|
|
2433
|
+
squire._undoIndex = -1;
|
|
2434
|
+
squire._isInUndoState = false;
|
|
2435
|
+
squire.saveUndoState();
|
|
2436
|
+
}
|
|
2437
|
+
updatePlaceholder() {
|
|
2438
|
+
const text = (this.element.textContent || "").replace(/\u200B/g, "").trim();
|
|
2439
|
+
const hasMeaningfulElement = this.element.querySelector("img, video, audio, iframe, table");
|
|
2440
|
+
const isEmpty = !text && !hasMeaningfulElement;
|
|
2441
|
+
this.element.classList.toggle("richclay-empty", isEmpty);
|
|
2442
|
+
}
|
|
2443
|
+
announceControl(def) {
|
|
2444
|
+
if (!this.liveRegion) return;
|
|
2445
|
+
const label = def.ariaLabel || def.label;
|
|
2446
|
+
if (!label) return;
|
|
2447
|
+
if (typeof def.isActive === "function") {
|
|
2448
|
+
announce(this.liveRegion, `${label} ${def.isActive(this) ? "on" : "off"}`);
|
|
2449
|
+
} else {
|
|
2450
|
+
announce(this.liveRegion, `${label} applied`);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
function resolveElements(selector) {
|
|
2455
|
+
if (typeof selector === "string") {
|
|
2456
|
+
return Array.from(document.querySelectorAll(selector));
|
|
2457
|
+
}
|
|
2458
|
+
if (selector?.nodeType === 1) return [selector];
|
|
2459
|
+
return Array.from(selector || []);
|
|
2460
|
+
}
|
|
2461
|
+
function matchAtBoundary(root, container, offset, first, selector) {
|
|
2462
|
+
let node = container;
|
|
2463
|
+
if (node === root) {
|
|
2464
|
+
const children = Array.from(root.childNodes).filter(isCaretHost);
|
|
2465
|
+
if (!children.length) return null;
|
|
2466
|
+
const index = first ? Math.min(offset, children.length - 1) : Math.min(Math.max(offset - 1, 0), children.length - 1);
|
|
2467
|
+
node = caretEdge(children[index], first);
|
|
2468
|
+
}
|
|
2469
|
+
const element = node?.nodeType === 1 ? node : node?.parentElement;
|
|
2470
|
+
const match = element?.closest?.(selector);
|
|
2471
|
+
return match && root.contains(match) && match !== root ? match : null;
|
|
2472
|
+
}
|
|
2473
|
+
function closestConflictingAncestor(element) {
|
|
2474
|
+
let node = element.parentElement;
|
|
2475
|
+
while (node) {
|
|
2476
|
+
if (instances.has(node) || isMountable(node)) return node;
|
|
2477
|
+
node = node.parentElement;
|
|
2478
|
+
}
|
|
2479
|
+
return null;
|
|
2480
|
+
}
|
|
2481
|
+
function isMountable(element) {
|
|
2482
|
+
return Boolean(element.matches?.(RICHCLAY_SELECTOR)) && isRichClayHost(element);
|
|
2483
|
+
}
|
|
2484
|
+
function conflictsWithExistingEditor(element) {
|
|
2485
|
+
const host = closestConflictingAncestor(element);
|
|
2486
|
+
const nested = Array.from(element.querySelectorAll?.(RICHCLAY_SELECTOR) || []).find(
|
|
2487
|
+
(node) => instances.has(node)
|
|
2488
|
+
);
|
|
2489
|
+
const other = host || nested;
|
|
2490
|
+
if (!other) return false;
|
|
2491
|
+
console.warn(
|
|
2492
|
+
"richclay: nested editable regions are not supported, so this one was skipped. Remove the editable attribute from either it or the other region.",
|
|
2493
|
+
element,
|
|
2494
|
+
other
|
|
2495
|
+
);
|
|
2496
|
+
return true;
|
|
2497
|
+
}
|
|
2498
|
+
function refuseUnsupportedRoot(element) {
|
|
2499
|
+
if (!isUnsupportedRootTag(element)) return false;
|
|
2500
|
+
console.warn(`richclay: ${unsupportedRootReason(element)} This region was skipped.`, element);
|
|
2501
|
+
return true;
|
|
2502
|
+
}
|
|
2503
|
+
function validateButton(def) {
|
|
2504
|
+
if (!def || !def.id) throw new Error("RichClay button definitions require an id.");
|
|
2505
|
+
if (def.type !== "menu" && typeof def.run !== "function") {
|
|
2506
|
+
throw new Error(`RichClay button "${def.id}" requires a run(editor) function.`);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
function shortcutKey(shortcut, win) {
|
|
2510
|
+
const parts = shortcut.split("+");
|
|
2511
|
+
const keyPart = parts[parts.length - 1];
|
|
2512
|
+
const modifiers = new Set(parts.slice(0, -1).map(normalizeModifier));
|
|
2513
|
+
if (modifiers.delete("Mod")) modifiers.add(isApplePlatform(win) ? "Meta" : "Ctrl");
|
|
2514
|
+
const key = keyPart.length === 1 ? modifiers.has("Shift") ? keyPart.toUpperCase() : keyPart.toLowerCase() : keyPart;
|
|
2515
|
+
const prefix = MODIFIER_ORDER.filter((modifier) => modifiers.has(modifier)).join("-");
|
|
2516
|
+
return `${prefix ? `${prefix}-` : ""}${key}`;
|
|
2517
|
+
}
|
|
2518
|
+
function normalizeModifier(modifier) {
|
|
2519
|
+
return modifier === "Cmd" ? "Meta" : modifier;
|
|
2520
|
+
}
|
|
2521
|
+
function trapTab(event, container) {
|
|
2522
|
+
const focusables = container.querySelectorAll(
|
|
2523
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
2524
|
+
);
|
|
2525
|
+
if (!focusables.length) return;
|
|
2526
|
+
const first = focusables[0];
|
|
2527
|
+
const last = focusables[focusables.length - 1];
|
|
2528
|
+
const active = container.ownerDocument.activeElement;
|
|
2529
|
+
if (event.shiftKey && active === first) {
|
|
2530
|
+
event.preventDefault();
|
|
2531
|
+
last.focus();
|
|
2532
|
+
} else if (!event.shiftKey && active === last) {
|
|
2533
|
+
event.preventDefault();
|
|
2534
|
+
first.focus();
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// richclay.css
|
|
2539
|
+
var richclay_default = `:root {
|
|
2540
|
+
--richclay-surface: #ffffff;
|
|
2541
|
+
--richclay-surface-alt: #f6f7f9;
|
|
2542
|
+
--richclay-text: #0b0e14;
|
|
2543
|
+
--richclay-muted: #667085;
|
|
2544
|
+
--richclay-border: #c9ced8;
|
|
2545
|
+
--richclay-accent: #0f766e;
|
|
2546
|
+
--richclay-accent-ink: #ffffff;
|
|
2547
|
+
--richclay-focus: #2563eb;
|
|
2548
|
+
--richclay-danger: #b42318;
|
|
2549
|
+
--richclay-radius: 6px;
|
|
2550
|
+
--richclay-control-size: 34px;
|
|
2551
|
+
--richclay-shadow: 0 10px 24px rgb(15 23 42 / 0.16);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
@media (prefers-color-scheme: dark) {
|
|
2555
|
+
:root {
|
|
2556
|
+
--richclay-surface: #16181d;
|
|
2557
|
+
--richclay-surface-alt: #22252c;
|
|
2558
|
+
--richclay-text: #ffffff;
|
|
2559
|
+
--richclay-muted: #a3aab8;
|
|
2560
|
+
--richclay-border: #4a5260;
|
|
2561
|
+
--richclay-accent: #2dd4bf;
|
|
2562
|
+
--richclay-accent-ink: #05201d;
|
|
2563
|
+
--richclay-focus: #93c5fd;
|
|
2564
|
+
--richclay-shadow: 0 12px 28px rgb(0 0 0 / 0.35);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
.richclay-toolbar {
|
|
2569
|
+
align-items: center;
|
|
2570
|
+
background: var(--richclay-surface-alt);
|
|
2571
|
+
border: 1px solid var(--richclay-border);
|
|
2572
|
+
border-radius: var(--richclay-radius);
|
|
2573
|
+
color: var(--richclay-text);
|
|
2574
|
+
display: flex;
|
|
2575
|
+
flex-wrap: wrap;
|
|
2576
|
+
gap: 2px;
|
|
2577
|
+
margin-block: 0 6px;
|
|
2578
|
+
padding: 4px;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
/* Seamless card: in the default layout the toolbar sits directly above the
|
|
2582
|
+
editor. Fuse them into a single bordered card with one divider \u2014 the
|
|
2583
|
+
toolbar's bottom border \u2014 instead of two stacked, separated boxes. */
|
|
2584
|
+
.richclay-toolbar:has(+ .richclay-editor) {
|
|
2585
|
+
margin-block-end: 0;
|
|
2586
|
+
border-bottom-left-radius: 0;
|
|
2587
|
+
border-bottom-right-radius: 0;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
.richclay-toolbar + .richclay-editor {
|
|
2591
|
+
border-top: 0;
|
|
2592
|
+
border-top-left-radius: 0;
|
|
2593
|
+
border-top-right-radius: 0;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
/* In a fused card, draw the focus ring inside the body so it never crosses
|
|
2597
|
+
the toolbar divider. */
|
|
2598
|
+
.richclay-toolbar + .richclay-editor:focus-visible {
|
|
2599
|
+
outline-offset: -3px;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
.richclay-button {
|
|
2603
|
+
align-items: center;
|
|
2604
|
+
appearance: none;
|
|
2605
|
+
background: transparent;
|
|
2606
|
+
border: 1px solid transparent;
|
|
2607
|
+
border-radius: 5px;
|
|
2608
|
+
color: currentColor;
|
|
2609
|
+
display: inline-flex;
|
|
2610
|
+
height: var(--richclay-control-size);
|
|
2611
|
+
justify-content: center;
|
|
2612
|
+
margin: 0;
|
|
2613
|
+
min-width: var(--richclay-control-size);
|
|
2614
|
+
padding: 0;
|
|
2615
|
+
position: relative;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
.richclay-button:hover,
|
|
2619
|
+
.richclay-button[aria-expanded="true"],
|
|
2620
|
+
.richclay-menu-item:hover {
|
|
2621
|
+
background: var(--richclay-surface);
|
|
2622
|
+
border-color: var(--richclay-border);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
.richclay-button.is-active,
|
|
2626
|
+
.richclay-button[aria-pressed="true"],
|
|
2627
|
+
.richclay-menu-item.is-active {
|
|
2628
|
+
background: rgb(0 0 0 / 0.13);
|
|
2629
|
+
border-color: rgb(0 0 0 / 0.22);
|
|
2630
|
+
color: var(--richclay-text);
|
|
2631
|
+
box-shadow: inset 0 1px 3px rgb(0 0 0 / 0.26);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
.richclay-button:focus-visible,
|
|
2635
|
+
.richclay-menu-item:focus-visible,
|
|
2636
|
+
.richclay-editor:focus-visible,
|
|
2637
|
+
.richclay-input:focus-visible,
|
|
2638
|
+
.richclay-primary:focus-visible,
|
|
2639
|
+
.richclay-secondary:focus-visible {
|
|
2640
|
+
outline: 3px solid var(--richclay-focus);
|
|
2641
|
+
outline-offset: 2px;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
.richclay-button:disabled {
|
|
2645
|
+
color: var(--richclay-muted);
|
|
2646
|
+
cursor: not-allowed;
|
|
2647
|
+
opacity: 0.55;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
/* Remove-link icon: the standard link glyph with a diagonal cut painted in the
|
|
2651
|
+
button's own background colour, so the chain reads as severed in every state.
|
|
2652
|
+
The cut tracks the background the button shows: the toolbar surface at rest,
|
|
2653
|
+
the raised surface on hover. */
|
|
2654
|
+
.richclay-cut {
|
|
2655
|
+
stroke: var(--richclay-surface-alt);
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
.richclay-button:hover .richclay-cut,
|
|
2659
|
+
.richclay-button[aria-expanded="true"] .richclay-cut {
|
|
2660
|
+
stroke: var(--richclay-surface);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
.richclay-separator {
|
|
2664
|
+
align-self: stretch;
|
|
2665
|
+
background: var(--richclay-border);
|
|
2666
|
+
display: inline-block;
|
|
2667
|
+
margin: 5px 4px;
|
|
2668
|
+
width: 1px;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
.richclay-menu-wrap {
|
|
2672
|
+
display: inline-flex;
|
|
2673
|
+
position: relative;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
.richclay-menu {
|
|
2677
|
+
background: var(--richclay-surface);
|
|
2678
|
+
border: 1px solid var(--richclay-border);
|
|
2679
|
+
border-radius: var(--richclay-radius);
|
|
2680
|
+
box-shadow: var(--richclay-shadow);
|
|
2681
|
+
color: var(--richclay-text);
|
|
2682
|
+
display: grid;
|
|
2683
|
+
gap: 2px;
|
|
2684
|
+
inset-block-start: calc(100% + 4px);
|
|
2685
|
+
inset-inline-start: 0;
|
|
2686
|
+
min-width: 160px;
|
|
2687
|
+
padding: 4px;
|
|
2688
|
+
position: absolute;
|
|
2689
|
+
z-index: 1000;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
.richclay-menu[hidden] {
|
|
2693
|
+
display: none;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
.richclay-menu-item {
|
|
2697
|
+
appearance: none;
|
|
2698
|
+
background: transparent;
|
|
2699
|
+
border: 1px solid transparent;
|
|
2700
|
+
border-radius: 4px;
|
|
2701
|
+
color: inherit;
|
|
2702
|
+
font: inherit;
|
|
2703
|
+
min-height: 32px;
|
|
2704
|
+
padding: 5px 9px;
|
|
2705
|
+
text-align: start;
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
.richclay-editor {
|
|
2709
|
+
background: var(--richclay-surface);
|
|
2710
|
+
border: 1px solid var(--richclay-border);
|
|
2711
|
+
border-radius: var(--richclay-radius);
|
|
2712
|
+
color: var(--richclay-text);
|
|
2713
|
+
min-height: 9rem;
|
|
2714
|
+
/* Break a long unbreakable string (e.g. a bare URL) in prose rather than let
|
|
2715
|
+
it widen the editor. \`anywhere\` also lowers min-content, so the editor can't
|
|
2716
|
+
grow even as a flex/grid item. Inert on the code <pre> (white-space: pre). */
|
|
2717
|
+
overflow-wrap: anywhere;
|
|
2718
|
+
padding: 12px;
|
|
2719
|
+
position: relative;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
.richclay-editor.richclay-empty::before {
|
|
2723
|
+
color: var(--richclay-muted);
|
|
2724
|
+
content: attr(data-richclay-placeholder);
|
|
2725
|
+
inset-block-start: 12px;
|
|
2726
|
+
inset-inline-start: 12px;
|
|
2727
|
+
pointer-events: none;
|
|
2728
|
+
position: absolute;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
.richclay-editor p,
|
|
2732
|
+
.richclay-editor h1,
|
|
2733
|
+
.richclay-editor h2,
|
|
2734
|
+
.richclay-editor h3,
|
|
2735
|
+
.richclay-editor h4,
|
|
2736
|
+
.richclay-editor h5,
|
|
2737
|
+
.richclay-editor h6,
|
|
2738
|
+
.richclay-editor blockquote,
|
|
2739
|
+
.richclay-editor pre,
|
|
2740
|
+
.richclay-editor ul,
|
|
2741
|
+
.richclay-editor ol {
|
|
2742
|
+
margin-block: 0 0.75em;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
.richclay-editor > :last-child {
|
|
2746
|
+
margin-block-end: 0;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
/* Inline editors ([editable]) inherit the page's own styling. richclay adds
|
|
2750
|
+
only a caret, a discoverability outline, and empty-state affordances. */
|
|
2751
|
+
.richclay-inline {
|
|
2752
|
+
caret-color: currentColor;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
.richclay-inline:hover {
|
|
2756
|
+
outline: 1px dashed var(--richclay-border);
|
|
2757
|
+
outline-offset: 2px;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
.richclay-inline.richclay-focused,
|
|
2761
|
+
.richclay-inline:focus-visible {
|
|
2762
|
+
outline: 2px dashed var(--richclay-focus);
|
|
2763
|
+
outline-offset: 2px;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
.richclay-inline.richclay-empty {
|
|
2767
|
+
min-height: 1em;
|
|
2768
|
+
min-width: 1ch;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
.richclay-inline.richclay-empty::before {
|
|
2772
|
+
color: var(--richclay-muted);
|
|
2773
|
+
content: attr(data-richclay-placeholder);
|
|
2774
|
+
pointer-events: none;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
/* Floating toolbar shell for inline editors. Fixed-position and body-mounted:
|
|
2778
|
+
fixed elements never create scrollbars, and body mounting avoids transformed
|
|
2779
|
+
ancestors silently turning fixed into ancestor-relative. */
|
|
2780
|
+
.richclay-float {
|
|
2781
|
+
left: 0;
|
|
2782
|
+
position: fixed;
|
|
2783
|
+
top: 0;
|
|
2784
|
+
will-change: transform;
|
|
2785
|
+
z-index: 99999;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
.richclay-float .richclay-toolbar {
|
|
2789
|
+
box-shadow: var(--richclay-shadow);
|
|
2790
|
+
margin-block: 0;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
.richclay-float .richclay-dialog {
|
|
2794
|
+
margin-block: 6px 0;
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
/* Compact vertical rail for narrow margins. */
|
|
2798
|
+
.richclay-float-rail .richclay-toolbar {
|
|
2799
|
+
--richclay-control-size: 28px;
|
|
2800
|
+
flex-direction: column;
|
|
2801
|
+
flex-wrap: nowrap;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
.richclay-float-rail .richclay-toolbar svg {
|
|
2805
|
+
height: 15px;
|
|
2806
|
+
width: 15px;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
.richclay-float-rail .richclay-separator {
|
|
2810
|
+
align-self: stretch;
|
|
2811
|
+
height: 1px;
|
|
2812
|
+
margin: 4px 5px;
|
|
2813
|
+
width: auto;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
.richclay-float-rail .richclay-menu {
|
|
2817
|
+
inset-block-start: 0;
|
|
2818
|
+
inset-inline-start: calc(100% + 4px);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
.richclay-editor blockquote {
|
|
2822
|
+
border-inline-start: 3px solid var(--richclay-border);
|
|
2823
|
+
color: var(--richclay-muted);
|
|
2824
|
+
margin-inline: 0;
|
|
2825
|
+
padding-inline-start: 12px;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
/* Code blocks scroll horizontally inside the editor instead of widening it.
|
|
2829
|
+
\`width: 0; min-width: 100%\` zeroes the pre's intrinsic-width contribution so a
|
|
2830
|
+
long unbreakable line can't push an intrinsically-sized ancestor (flex/grid
|
|
2831
|
+
item, inline-block, table cell) wider, while still filling the editor and
|
|
2832
|
+
scrolling its own overflow. box-sizing keeps padding inside that 100%.
|
|
2833
|
+
Containment applies to inline editors too: \`white-space: pre\` never wraps, so
|
|
2834
|
+
without it one code block runs off the side of the page. Only the decoration
|
|
2835
|
+
below stays card-scoped, since inline mode keeps the page's own typography. */
|
|
2836
|
+
.richclay-editor pre,
|
|
2837
|
+
.richclay-inline pre {
|
|
2838
|
+
box-sizing: border-box;
|
|
2839
|
+
min-width: 100%;
|
|
2840
|
+
overflow: auto;
|
|
2841
|
+
width: 0;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
.richclay-editor pre {
|
|
2845
|
+
background: var(--richclay-surface-alt);
|
|
2846
|
+
border-radius: 4px;
|
|
2847
|
+
padding: 10px;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
.richclay-dialog {
|
|
2851
|
+
background: var(--richclay-surface);
|
|
2852
|
+
border: 1px solid var(--richclay-border);
|
|
2853
|
+
border-radius: var(--richclay-radius);
|
|
2854
|
+
box-shadow: var(--richclay-shadow);
|
|
2855
|
+
color: var(--richclay-text);
|
|
2856
|
+
display: grid;
|
|
2857
|
+
gap: 10px;
|
|
2858
|
+
margin-block: 6px;
|
|
2859
|
+
max-width: 420px;
|
|
2860
|
+
padding: 12px;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
.richclay-dialog-title {
|
|
2864
|
+
font-weight: 650;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
.richclay-field {
|
|
2868
|
+
display: grid;
|
|
2869
|
+
gap: 4px;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
.richclay-field span {
|
|
2873
|
+
color: var(--richclay-muted);
|
|
2874
|
+
font-size: 0.875rem;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
.richclay-input {
|
|
2878
|
+
background: var(--richclay-surface);
|
|
2879
|
+
border: 1px solid var(--richclay-border);
|
|
2880
|
+
border-radius: 4px;
|
|
2881
|
+
color: var(--richclay-text);
|
|
2882
|
+
font: inherit;
|
|
2883
|
+
min-height: 36px;
|
|
2884
|
+
padding: 6px 8px;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
.richclay-dialog-actions {
|
|
2888
|
+
display: flex;
|
|
2889
|
+
gap: 8px;
|
|
2890
|
+
justify-content: flex-end;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
.richclay-primary,
|
|
2894
|
+
.richclay-secondary {
|
|
2895
|
+
appearance: none;
|
|
2896
|
+
border: 1px solid var(--richclay-border);
|
|
2897
|
+
border-radius: 4px;
|
|
2898
|
+
font: inherit;
|
|
2899
|
+
min-height: 34px;
|
|
2900
|
+
padding: 5px 10px;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
.richclay-primary {
|
|
2904
|
+
background: var(--richclay-accent);
|
|
2905
|
+
border-color: var(--richclay-accent);
|
|
2906
|
+
color: var(--richclay-accent-ink);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
.richclay-secondary {
|
|
2910
|
+
background: var(--richclay-surface-alt);
|
|
2911
|
+
color: var(--richclay-text);
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
.richclay-sr-only {
|
|
2915
|
+
border: 0;
|
|
2916
|
+
clip: rect(0 0 0 0);
|
|
2917
|
+
clip-path: inset(50%);
|
|
2918
|
+
height: 1px;
|
|
2919
|
+
margin: -1px;
|
|
2920
|
+
overflow: hidden;
|
|
2921
|
+
padding: 0;
|
|
2922
|
+
position: absolute;
|
|
2923
|
+
white-space: nowrap;
|
|
2924
|
+
width: 1px;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
@media (forced-colors: active) {
|
|
2928
|
+
.richclay-toolbar,
|
|
2929
|
+
.richclay-editor,
|
|
2930
|
+
.richclay-menu,
|
|
2931
|
+
.richclay-dialog,
|
|
2932
|
+
.richclay-input {
|
|
2933
|
+
border-color: CanvasText;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
.richclay-button.is-active,
|
|
2937
|
+
.richclay-button[aria-pressed="true"],
|
|
2938
|
+
.richclay-menu-item.is-active,
|
|
2939
|
+
.richclay-primary {
|
|
2940
|
+
background: Highlight;
|
|
2941
|
+
color: HighlightText;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
.richclay-button:focus-visible,
|
|
2945
|
+
.richclay-menu-item:focus-visible,
|
|
2946
|
+
.richclay-editor:focus-visible,
|
|
2947
|
+
.richclay-input:focus-visible {
|
|
2948
|
+
outline-color: Highlight;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
.richclay-cut {
|
|
2952
|
+
stroke: Canvas;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
`;
|
|
2956
|
+
|
|
2957
|
+
// src/browser-global.js
|
|
2958
|
+
setRichClayStyles(richclay_default);
|
|
2959
|
+
var browser_global_default = RichClay;
|
|
2960
|
+
return __toCommonJS(browser_global_exports);
|
|
2961
|
+
})();
|
|
2962
|
+
window.RichClay = RichClayBundle.default;
|