@shelamkoff/rector 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +17 -0
- package/README.md +23 -0
- package/THIRD_PARTY_LICENSES.txt +298 -0
- package/dist/I18n.d.ts +1 -0
- package/dist/core/Block.d.ts +110 -0
- package/dist/core/Block.js +390 -0
- package/dist/core/BlockAnimator.d.ts +39 -0
- package/dist/core/BlockAnimator.js +106 -0
- package/dist/core/BlockManager.d.ts +163 -0
- package/dist/core/BlockManager.js +640 -0
- package/dist/core/BlockOperations.d.ts +67 -0
- package/dist/core/BlockOperations.js +238 -0
- package/dist/core/ChangeNotifier.d.ts +15 -0
- package/dist/core/ChangeNotifier.js +55 -0
- package/dist/core/CommandDispatcher.d.ts +52 -0
- package/dist/core/CommandDispatcher.js +208 -0
- package/dist/core/CrossBlockSelection.d.ts +30 -0
- package/dist/core/CrossBlockSelection.js +74 -0
- package/dist/core/Diagnostics.d.ts +23 -0
- package/dist/core/Diagnostics.js +62 -0
- package/dist/core/DocumentSchema.d.ts +29 -0
- package/dist/core/DocumentSchema.js +147 -0
- package/dist/core/DocumentSnapshotStore.d.ts +34 -0
- package/dist/core/DocumentSnapshotStore.js +153 -0
- package/dist/core/DragManager.d.ts +17 -0
- package/dist/core/DragManager.js +223 -0
- package/dist/core/EditorFacade.d.ts +154 -0
- package/dist/core/EditorFacade.js +361 -0
- package/dist/core/EditorHolderOwnership.d.ts +12 -0
- package/dist/core/EditorHolderOwnership.js +28 -0
- package/dist/core/I18n.d.ts +147 -0
- package/dist/core/I18n.js +296 -0
- package/dist/core/InlinePatternMatcher.d.ts +26 -0
- package/dist/core/InlinePatternMatcher.js +267 -0
- package/dist/core/InlinePluginRegistry.d.ts +55 -0
- package/dist/core/InlinePluginRegistry.js +128 -0
- package/dist/core/KeyboardManager.d.ts +17 -0
- package/dist/core/KeyboardManager.js +231 -0
- package/dist/core/LifecycleScope.d.ts +19 -0
- package/dist/core/LifecycleScope.js +41 -0
- package/dist/core/MouseSelectionManager.d.ts +30 -0
- package/dist/core/MouseSelectionManager.js +307 -0
- package/dist/core/PluginOwnership.d.ts +9 -0
- package/dist/core/PluginOwnership.js +40 -0
- package/dist/core/PopupManager.d.ts +50 -0
- package/dist/core/PopupManager.js +183 -0
- package/dist/core/PreservedBlockPlugin.d.ts +11 -0
- package/dist/core/PreservedBlockPlugin.js +42 -0
- package/dist/core/PublicEditorApi.d.ts +82 -0
- package/dist/core/PublicEditorApi.js +158 -0
- package/dist/core/SelectionManager.d.ts +50 -0
- package/dist/core/SelectionManager.js +205 -0
- package/dist/core/ShortcutRegistry.d.ts +29 -0
- package/dist/core/ShortcutRegistry.js +106 -0
- package/dist/core/SlashCommands.d.ts +34 -0
- package/dist/core/SlashCommands.js +483 -0
- package/dist/core/StyleInjector.d.ts +1 -0
- package/dist/core/StyleInjector.js +1 -0
- package/dist/core/Tooltip.d.ts +17 -0
- package/dist/core/Tooltip.js +60 -0
- package/dist/core/TriggerManager.d.ts +17 -0
- package/dist/core/TriggerManager.js +159 -0
- package/dist/core/TypeSelector.d.ts +31 -0
- package/dist/core/TypeSelector.js +464 -0
- package/dist/core/UndoManager.d.ts +72 -0
- package/dist/core/UndoManager.js +383 -0
- package/dist/core/block-settings-menu/BlockActions.d.ts +81 -0
- package/dist/core/block-settings-menu/BlockActions.js +233 -0
- package/dist/core/block-settings-menu/BlockSettingsMenu.d.ts +46 -0
- package/dist/core/block-settings-menu/BlockSettingsMenu.js +205 -0
- package/dist/core/block-settings-menu/MenuBuilder.d.ts +43 -0
- package/dist/core/block-settings-menu/MenuBuilder.js +203 -0
- package/dist/core/block-settings-menu/MenuPositioner.d.ts +22 -0
- package/dist/core/block-settings-menu/MenuPositioner.js +79 -0
- package/dist/core/clipboard/Clipboard.d.ts +47 -0
- package/dist/core/clipboard/Clipboard.js +653 -0
- package/dist/core/clipboard/CrossBlockEditor.d.ts +43 -0
- package/dist/core/clipboard/CrossBlockEditor.js +198 -0
- package/dist/core/clipboard/PasteRouter.d.ts +27 -0
- package/dist/core/clipboard/PasteRouter.js +86 -0
- package/dist/core/clipboard/pasteInsert.d.ts +35 -0
- package/dist/core/clipboard/pasteInsert.js +215 -0
- package/dist/core/clipboard/pasteUtils.d.ts +25 -0
- package/dist/core/clipboard/pasteUtils.js +98 -0
- package/dist/core/config.d.ts +77 -0
- package/dist/core/config.js +100 -0
- package/dist/core/constants.d.ts +23 -0
- package/dist/core/constants.js +30 -0
- package/dist/core/crossBlockConvert.d.ts +38 -0
- package/dist/core/crossBlockConvert.js +253 -0
- package/dist/core/dom.d.ts +35 -0
- package/dist/core/dom.js +65 -0
- package/dist/core/editorEvents.d.ts +89 -0
- package/dist/core/editorEvents.js +60 -0
- package/dist/core/hydrateInlinePlugins.d.ts +9 -0
- package/dist/core/hydrateInlinePlugins.js +36 -0
- package/dist/core/icons.d.ts +10 -0
- package/dist/core/icons.js +21 -0
- package/dist/core/index.d.ts +59 -0
- package/dist/core/index.js +703 -0
- package/dist/core/inline-toolbar/ActionsPanel.d.ts +61 -0
- package/dist/core/inline-toolbar/ActionsPanel.js +132 -0
- package/dist/core/inline-toolbar/InlinePositioner.d.ts +20 -0
- package/dist/core/inline-toolbar/InlinePositioner.js +63 -0
- package/dist/core/inline-toolbar/InlineToolbar.d.ts +45 -0
- package/dist/core/inline-toolbar/InlineToolbar.js +456 -0
- package/dist/core/inline-toolbar/PluginControlsSlot.d.ts +52 -0
- package/dist/core/inline-toolbar/PluginControlsSlot.js +118 -0
- package/dist/core/inline-toolbar/SelectionTracker.d.ts +63 -0
- package/dist/core/inline-toolbar/SelectionTracker.js +173 -0
- package/dist/core/inlinePluginInsert.d.ts +13 -0
- package/dist/core/inlinePluginInsert.js +56 -0
- package/dist/core/locale/en.d.ts +35 -0
- package/dist/core/locale/en.js +37 -0
- package/dist/core/locale/ru.d.ts +35 -0
- package/dist/core/locale/ru.js +37 -0
- package/dist/core/menuKeyboardNav.d.ts +15 -0
- package/dist/core/menuKeyboardNav.js +52 -0
- package/dist/core/sanitize.d.ts +2 -0
- package/dist/core/sanitize.js +6 -0
- package/dist/core/splitConvert.d.ts +46 -0
- package/dist/core/splitConvert.js +190 -0
- package/dist/core/textOffset.d.ts +46 -0
- package/dist/core/textOffset.js +180 -0
- package/dist/core/themes/dark.css +30 -0
- package/dist/core/themes/light.css +33 -0
- package/dist/core/themes/variables.css +1170 -0
- package/dist/core/toolbar/OffcanvasRoot.d.ts +37 -0
- package/dist/core/toolbar/OffcanvasRoot.js +127 -0
- package/dist/core/toolbar/Toolbar.d.ts +70 -0
- package/dist/core/toolbar/Toolbar.js +432 -0
- package/dist/core/toolbar/ToolbarPositioner.d.ts +60 -0
- package/dist/core/toolbar/ToolbarPositioner.js +188 -0
- package/dist/core/toolbar/ToolboxBuilder.d.ts +44 -0
- package/dist/core/toolbar/ToolboxBuilder.js +260 -0
- package/dist/core/types.d.ts +747 -0
- package/dist/core/uid.d.ts +6 -0
- package/dist/core/uid.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/inline-plugins/README.md +35 -0
- package/dist/inline-plugins/README.ru.md +35 -0
- package/dist/inline-plugins/color/README.md +61 -0
- package/dist/inline-plugins/color/README.ru.md +61 -0
- package/dist/inline-plugins/color.d.ts +8 -0
- package/dist/inline-plugins/color.js +184 -0
- package/dist/inline-plugins/locale/en.d.ts +7 -0
- package/dist/inline-plugins/locale/en.js +6 -0
- package/dist/inline-plugins/locale/ru.d.ts +7 -0
- package/dist/inline-plugins/locale/ru.js +6 -0
- package/dist/inline-plugins/mention/README.md +97 -0
- package/dist/inline-plugins/mention/README.ru.md +97 -0
- package/dist/inline-plugins/mention/index.d.ts +110 -0
- package/dist/inline-plugins/mention/index.js +1897 -0
- package/dist/inline-plugins/mention/styles.css +183 -0
- package/dist/inline-plugins/mention/widget.d.ts +16 -0
- package/dist/inline-plugins/mention/widget.js +75 -0
- package/dist/inline-tools/align.d.ts +14 -0
- package/dist/inline-tools/align.js +120 -0
- package/dist/inline-tools/bold.d.ts +8 -0
- package/dist/inline-tools/bold.js +12 -0
- package/dist/inline-tools/caseTransform.d.ts +8 -0
- package/dist/inline-tools/caseTransform.js +87 -0
- package/dist/inline-tools/clearFormatting.d.ts +8 -0
- package/dist/inline-tools/clearFormatting.js +74 -0
- package/dist/inline-tools/code.d.ts +8 -0
- package/dist/inline-tools/code.js +12 -0
- package/dist/inline-tools/colorPicker.d.ts +7 -0
- package/dist/inline-tools/colorPicker.js +271 -0
- package/dist/inline-tools/defaults.d.ts +10 -0
- package/dist/inline-tools/defaults.js +57 -0
- package/dist/inline-tools/fontSize.d.ts +7 -0
- package/dist/inline-tools/fontSize.js +524 -0
- package/dist/inline-tools/italic.d.ts +8 -0
- package/dist/inline-tools/italic.js +12 -0
- package/dist/inline-tools/link.d.ts +12 -0
- package/dist/inline-tools/link.js +251 -0
- package/dist/inline-tools/marker.d.ts +8 -0
- package/dist/inline-tools/marker.js +12 -0
- package/dist/inline-tools/scriptTool.d.ts +11 -0
- package/dist/inline-tools/scriptTool.js +149 -0
- package/dist/inline-tools/strikethrough.d.ts +8 -0
- package/dist/inline-tools/strikethrough.js +12 -0
- package/dist/inline-tools/utils.d.ts +220 -0
- package/dist/inline-tools/utils.js +992 -0
- package/dist/locale/en.d.ts +311 -0
- package/dist/locale/en.js +52 -0
- package/dist/locale/ru.d.ts +315 -0
- package/dist/locale/ru.js +52 -0
- package/dist/plugins/BlockPluginAbstract.d.ts +82 -0
- package/dist/plugins/BlockPluginAbstract.js +111 -0
- package/dist/plugins/README.md +44 -0
- package/dist/plugins/README.ru.md +40 -0
- package/dist/plugins/async.d.ts +35 -0
- package/dist/plugins/async.js +79 -0
- package/dist/plugins/attaches/README.md +86 -0
- package/dist/plugins/attaches/README.ru.md +87 -0
- package/dist/plugins/attaches/attaches.css +374 -0
- package/dist/plugins/attaches/index.d.ts +133 -0
- package/dist/plugins/attaches/index.js +921 -0
- package/dist/plugins/attaches/locale/en.d.ts +23 -0
- package/dist/plugins/attaches/locale/en.js +19 -0
- package/dist/plugins/attaches/locale/keys.d.ts +5 -0
- package/dist/plugins/attaches/locale/ru.d.ts +25 -0
- package/dist/plugins/attaches/locale/ru.js +19 -0
- package/dist/plugins/carousel/README.md +108 -0
- package/dist/plugins/carousel/README.ru.md +109 -0
- package/dist/plugins/carousel/carousel.css +198 -0
- package/dist/plugins/carousel/icons.d.ts +12 -0
- package/dist/plugins/carousel/icons.js +13 -0
- package/dist/plugins/carousel/index.d.ts +137 -0
- package/dist/plugins/carousel/index.js +859 -0
- package/dist/plugins/carousel/locale/en.d.ts +41 -0
- package/dist/plugins/carousel/locale/en.js +40 -0
- package/dist/plugins/carousel/locale/keys.d.ts +2 -0
- package/dist/plugins/carousel/locale/ru.d.ts +41 -0
- package/dist/plugins/carousel/locale/ru.js +40 -0
- package/dist/plugins/carousel/state.d.ts +29 -0
- package/dist/plugins/carousel/state.js +42 -0
- package/dist/plugins/checklist/README.md +54 -0
- package/dist/plugins/checklist/README.ru.md +55 -0
- package/dist/plugins/checklist/checklist.css +80 -0
- package/dist/plugins/checklist/index.d.ts +75 -0
- package/dist/plugins/checklist/index.js +407 -0
- package/dist/plugins/checklist/locale/en.d.ts +5 -0
- package/dist/plugins/checklist/locale/en.js +4 -0
- package/dist/plugins/checklist/locale/keys.d.ts +5 -0
- package/dist/plugins/checklist/locale/ru.d.ts +5 -0
- package/dist/plugins/checklist/locale/ru.js +4 -0
- package/dist/plugins/checklist/mapTextFields.d.ts +1 -0
- package/dist/plugins/checklist/mapTextFields.js +1 -0
- package/dist/plugins/code/README.md +59 -0
- package/dist/plugins/code/README.ru.md +60 -0
- package/dist/plugins/code/code.css +360 -0
- package/dist/plugins/code/index.d.ts +109 -0
- package/dist/plugins/code/index.js +1012 -0
- package/dist/plugins/code/locale/en.d.ts +11 -0
- package/dist/plugins/code/locale/en.js +10 -0
- package/dist/plugins/code/locale/keys.d.ts +5 -0
- package/dist/plugins/code/locale/ru.d.ts +11 -0
- package/dist/plugins/code/locale/ru.js +10 -0
- package/dist/plugins/columns/README.md +55 -0
- package/dist/plugins/columns/README.ru.md +56 -0
- package/dist/plugins/columns/columns.css +103 -0
- package/dist/plugins/columns/index.d.ts +58 -0
- package/dist/plugins/columns/index.js +236 -0
- package/dist/plugins/columns/locale/en.d.ts +6 -0
- package/dist/plugins/columns/locale/en.js +5 -0
- package/dist/plugins/columns/locale/keys.d.ts +5 -0
- package/dist/plugins/columns/locale/ru.d.ts +6 -0
- package/dist/plugins/columns/locale/ru.js +5 -0
- package/dist/plugins/delimiter/README.md +48 -0
- package/dist/plugins/delimiter/README.ru.md +49 -0
- package/dist/plugins/delimiter/delimiter.css +22 -0
- package/dist/plugins/delimiter/index.d.ts +38 -0
- package/dist/plugins/delimiter/index.js +69 -0
- package/dist/plugins/delimiter/locale/en.d.ts +4 -0
- package/dist/plugins/delimiter/locale/en.js +3 -0
- package/dist/plugins/delimiter/locale/keys.d.ts +5 -0
- package/dist/plugins/delimiter/locale/ru.d.ts +4 -0
- package/dist/plugins/delimiter/locale/ru.js +3 -0
- package/dist/plugins/embed/README.md +81 -0
- package/dist/plugins/embed/README.ru.md +82 -0
- package/dist/plugins/embed/embed.css +558 -0
- package/dist/plugins/embed/index.d.ts +271 -0
- package/dist/plugins/embed/index.js +967 -0
- package/dist/plugins/embed/locale/en.d.ts +20 -0
- package/dist/plugins/embed/locale/en.js +19 -0
- package/dist/plugins/embed/locale/keys.d.ts +5 -0
- package/dist/plugins/embed/locale/ru.d.ts +20 -0
- package/dist/plugins/embed/locale/ru.js +19 -0
- package/dist/plugins/embed/player.d.ts +1 -0
- package/dist/plugins/embed/player.js +1 -0
- package/dist/plugins/gallery/README.md +92 -0
- package/dist/plugins/gallery/README.ru.md +93 -0
- package/dist/plugins/gallery/css.d.ts +52 -0
- package/dist/plugins/gallery/css.js +67 -0
- package/dist/plugins/gallery/gallery.css +860 -0
- package/dist/plugins/gallery/icons.d.ts +13 -0
- package/dist/plugins/gallery/icons.js +45 -0
- package/dist/plugins/gallery/index.d.ts +126 -0
- package/dist/plugins/gallery/index.js +355 -0
- package/dist/plugins/gallery/layout.d.ts +35 -0
- package/dist/plugins/gallery/layout.js +79 -0
- package/dist/plugins/gallery/locale/en.d.ts +36 -0
- package/dist/plugins/gallery/locale/en.js +35 -0
- package/dist/plugins/gallery/locale/keys.d.ts +5 -0
- package/dist/plugins/gallery/locale/ru.d.ts +36 -0
- package/dist/plugins/gallery/locale/ru.js +35 -0
- package/dist/plugins/gallery/settings.d.ts +25 -0
- package/dist/plugins/gallery/settings.js +199 -0
- package/dist/plugins/gallery/slot.d.ts +73 -0
- package/dist/plugins/gallery/slot.js +260 -0
- package/dist/plugins/gallery/state.d.ts +67 -0
- package/dist/plugins/gallery/state.js +133 -0
- package/dist/plugins/gallery/styles.d.ts +10 -0
- package/dist/plugins/gallery/styles.js +24 -0
- package/dist/plugins/gallery/uploader.d.ts +31 -0
- package/dist/plugins/gallery/uploader.js +111 -0
- package/dist/plugins/gallery/view-empty.d.ts +41 -0
- package/dist/plugins/gallery/view-empty.js +56 -0
- package/dist/plugins/gallery/view-filled.d.ts +55 -0
- package/dist/plugins/gallery/view-filled.js +353 -0
- package/dist/plugins/heading/HeadingLevelSelect.d.ts +16 -0
- package/dist/plugins/heading/HeadingLevelSelect.js +183 -0
- package/dist/plugins/heading/README.md +58 -0
- package/dist/plugins/heading/README.ru.md +59 -0
- package/dist/plugins/heading/heading.css +76 -0
- package/dist/plugins/heading/index.d.ts +124 -0
- package/dist/plugins/heading/index.js +273 -0
- package/dist/plugins/heading/locale/en.d.ts +11 -0
- package/dist/plugins/heading/locale/en.js +10 -0
- package/dist/plugins/heading/locale/keys.d.ts +5 -0
- package/dist/plugins/heading/locale/ru.d.ts +11 -0
- package/dist/plugins/heading/locale/ru.js +10 -0
- package/dist/plugins/heading/mapTextFields.d.ts +1 -0
- package/dist/plugins/heading/mapTextFields.js +1 -0
- package/dist/plugins/image/README.md +93 -0
- package/dist/plugins/image/README.ru.md +94 -0
- package/dist/plugins/image/css.d.ts +47 -0
- package/dist/plugins/image/css.js +47 -0
- package/dist/plugins/image/icons.d.ts +13 -0
- package/dist/plugins/image/icons.js +26 -0
- package/dist/plugins/image/image.css +559 -0
- package/dist/plugins/image/index.d.ts +129 -0
- package/dist/plugins/image/index.js +346 -0
- package/dist/plugins/image/locale/en.d.ts +39 -0
- package/dist/plugins/image/locale/en.js +38 -0
- package/dist/plugins/image/locale/keys.d.ts +5 -0
- package/dist/plugins/image/locale/ru.d.ts +39 -0
- package/dist/plugins/image/locale/ru.js +38 -0
- package/dist/plugins/image/settings.d.ts +23 -0
- package/dist/plugins/image/settings.js +347 -0
- package/dist/plugins/image/state.d.ts +86 -0
- package/dist/plugins/image/state.js +151 -0
- package/dist/plugins/image/styles.d.ts +21 -0
- package/dist/plugins/image/styles.js +58 -0
- package/dist/plugins/image/uploader.d.ts +31 -0
- package/dist/plugins/image/uploader.js +92 -0
- package/dist/plugins/image/view-empty.d.ts +44 -0
- package/dist/plugins/image/view-empty.js +61 -0
- package/dist/plugins/image/view-filled.d.ts +49 -0
- package/dist/plugins/image/view-filled.js +282 -0
- package/dist/plugins/index.d.ts +21 -0
- package/dist/plugins/index.js +22 -0
- package/dist/plugins/link-preview/README.md +69 -0
- package/dist/plugins/link-preview/README.ru.md +70 -0
- package/dist/plugins/link-preview/index.d.ts +169 -0
- package/dist/plugins/link-preview/index.js +698 -0
- package/dist/plugins/link-preview/link-preview.css +254 -0
- package/dist/plugins/link-preview/locale/en.d.ts +14 -0
- package/dist/plugins/link-preview/locale/en.js +13 -0
- package/dist/plugins/link-preview/locale/keys.d.ts +5 -0
- package/dist/plugins/link-preview/locale/ru.d.ts +14 -0
- package/dist/plugins/link-preview/locale/ru.js +13 -0
- package/dist/plugins/list/README.md +59 -0
- package/dist/plugins/list/README.ru.md +60 -0
- package/dist/plugins/list/index.d.ts +134 -0
- package/dist/plugins/list/index.js +566 -0
- package/dist/plugins/list/list.css +57 -0
- package/dist/plugins/list/locale/en.d.ts +5 -0
- package/dist/plugins/list/locale/en.js +4 -0
- package/dist/plugins/list/locale/keys.d.ts +5 -0
- package/dist/plugins/list/locale/ru.d.ts +5 -0
- package/dist/plugins/list/locale/ru.js +4 -0
- package/dist/plugins/list/mapTextFields.d.ts +1 -0
- package/dist/plugins/list/mapTextFields.js +1 -0
- package/dist/plugins/paragraph/README.md +57 -0
- package/dist/plugins/paragraph/README.ru.md +58 -0
- package/dist/plugins/paragraph/index.d.ts +112 -0
- package/dist/plugins/paragraph/index.js +158 -0
- package/dist/plugins/paragraph/locale/en.d.ts +5 -0
- package/dist/plugins/paragraph/locale/en.js +4 -0
- package/dist/plugins/paragraph/locale/keys.d.ts +5 -0
- package/dist/plugins/paragraph/locale/ru.d.ts +5 -0
- package/dist/plugins/paragraph/locale/ru.js +4 -0
- package/dist/plugins/paragraph/mapTextFields.d.ts +1 -0
- package/dist/plugins/paragraph/mapTextFields.js +1 -0
- package/dist/plugins/paragraph/paragraph.css +56 -0
- package/dist/plugins/person/README.md +72 -0
- package/dist/plugins/person/README.ru.md +73 -0
- package/dist/plugins/person/index.d.ts +189 -0
- package/dist/plugins/person/index.js +777 -0
- package/dist/plugins/person/locale/en.d.ts +18 -0
- package/dist/plugins/person/locale/en.js +17 -0
- package/dist/plugins/person/locale/keys.d.ts +5 -0
- package/dist/plugins/person/locale/ru.d.ts +18 -0
- package/dist/plugins/person/locale/ru.js +17 -0
- package/dist/plugins/person/person.css +431 -0
- package/dist/plugins/person/socialResolver.d.ts +18 -0
- package/dist/plugins/person/socialResolver.js +69 -0
- package/dist/plugins/poll/README.md +96 -0
- package/dist/plugins/poll/README.ru.md +97 -0
- package/dist/plugins/poll/index.d.ts +134 -0
- package/dist/plugins/poll/index.js +825 -0
- package/dist/plugins/poll/locale/en.d.ts +23 -0
- package/dist/plugins/poll/locale/en.js +22 -0
- package/dist/plugins/poll/locale/keys.d.ts +5 -0
- package/dist/plugins/poll/locale/ru.d.ts +23 -0
- package/dist/plugins/poll/locale/ru.js +22 -0
- package/dist/plugins/poll/poll.css +400 -0
- package/dist/plugins/quote/README.md +55 -0
- package/dist/plugins/quote/README.ru.md +56 -0
- package/dist/plugins/quote/index.d.ts +81 -0
- package/dist/plugins/quote/index.js +165 -0
- package/dist/plugins/quote/locale/en.d.ts +6 -0
- package/dist/plugins/quote/locale/en.js +5 -0
- package/dist/plugins/quote/locale/keys.d.ts +5 -0
- package/dist/plugins/quote/locale/ru.d.ts +6 -0
- package/dist/plugins/quote/locale/ru.js +5 -0
- package/dist/plugins/quote/mapTextFields.d.ts +1 -0
- package/dist/plugins/quote/mapTextFields.js +1 -0
- package/dist/plugins/quote/quote.css +72 -0
- package/dist/plugins/raw/README.md +54 -0
- package/dist/plugins/raw/README.ru.md +55 -0
- package/dist/plugins/raw/index.d.ts +62 -0
- package/dist/plugins/raw/index.js +227 -0
- package/dist/plugins/raw/locale/en.d.ts +7 -0
- package/dist/plugins/raw/locale/en.js +6 -0
- package/dist/plugins/raw/locale/keys.d.ts +5 -0
- package/dist/plugins/raw/locale/ru.d.ts +7 -0
- package/dist/plugins/raw/locale/ru.js +6 -0
- package/dist/plugins/raw/raw.css +127 -0
- package/dist/plugins/shared/actionBar.d.ts +19 -0
- package/dist/plugins/shared/actionBar.js +35 -0
- package/dist/plugins/shared/dropzone.d.ts +106 -0
- package/dist/plugins/shared/dropzone.js +107 -0
- package/dist/plugins/shared/fileInput.d.ts +44 -0
- package/dist/plugins/shared/fileInput.js +62 -0
- package/dist/plugins/spoiler/README.md +55 -0
- package/dist/plugins/spoiler/README.ru.md +56 -0
- package/dist/plugins/spoiler/index.d.ts +61 -0
- package/dist/plugins/spoiler/index.js +169 -0
- package/dist/plugins/spoiler/locale/en.d.ts +7 -0
- package/dist/plugins/spoiler/locale/en.js +6 -0
- package/dist/plugins/spoiler/locale/keys.d.ts +5 -0
- package/dist/plugins/spoiler/locale/ru.d.ts +7 -0
- package/dist/plugins/spoiler/locale/ru.js +6 -0
- package/dist/plugins/spoiler/spoiler.css +101 -0
- package/dist/plugins/table/README.md +55 -0
- package/dist/plugins/table/README.ru.md +56 -0
- package/dist/plugins/table/index.d.ts +85 -0
- package/dist/plugins/table/index.js +399 -0
- package/dist/plugins/table/locale/en.d.ts +9 -0
- package/dist/plugins/table/locale/en.js +8 -0
- package/dist/plugins/table/locale/keys.d.ts +5 -0
- package/dist/plugins/table/locale/ru.d.ts +9 -0
- package/dist/plugins/table/locale/ru.js +8 -0
- package/dist/plugins/table/table.css +58 -0
- package/dist/plugins/toggle/README.md +56 -0
- package/dist/plugins/toggle/README.ru.md +57 -0
- package/dist/plugins/toggle/index.d.ts +69 -0
- package/dist/plugins/toggle/index.js +191 -0
- package/dist/plugins/toggle/locale/en.d.ts +7 -0
- package/dist/plugins/toggle/locale/en.js +6 -0
- package/dist/plugins/toggle/locale/keys.d.ts +5 -0
- package/dist/plugins/toggle/locale/ru.d.ts +7 -0
- package/dist/plugins/toggle/locale/ru.js +6 -0
- package/dist/plugins/toggle/toggle.css +101 -0
- package/dist/plugins/warning/README.md +55 -0
- package/dist/plugins/warning/README.ru.md +56 -0
- package/dist/plugins/warning/index.d.ts +66 -0
- package/dist/plugins/warning/index.js +167 -0
- package/dist/plugins/warning/locale/en.d.ts +6 -0
- package/dist/plugins/warning/locale/en.js +5 -0
- package/dist/plugins/warning/locale/keys.d.ts +5 -0
- package/dist/plugins/warning/locale/ru.d.ts +6 -0
- package/dist/plugins/warning/locale/ru.js +5 -0
- package/dist/plugins/warning/warning.css +63 -0
- package/dist/renderer/async.d.ts +1 -0
- package/dist/renderer/async.js +7 -0
- package/dist/renderer/errors.d.ts +27 -0
- package/dist/renderer/errors.js +48 -0
- package/dist/renderer/index.d.ts +82 -0
- package/dist/renderer/index.js +413 -0
- package/dist/renderer/inline.d.ts +5 -0
- package/dist/renderer/inline.js +61 -0
- package/dist/renderer/locale/en.d.ts +35 -0
- package/dist/renderer/locale/en.js +29 -0
- package/dist/renderer/locale/ru.d.ts +37 -0
- package/dist/renderer/locale/ru.js +29 -0
- package/dist/renderer/renderers/README.md +7 -0
- package/dist/renderer/renderers/README.ru.md +5 -0
- package/dist/renderer/renderers/async.d.ts +51 -0
- package/dist/renderer/renderers/async.js +99 -0
- package/dist/renderer/renderers/attaches/README.md +48 -0
- package/dist/renderer/renderers/attaches/README.ru.md +48 -0
- package/dist/renderer/renderers/attaches/index.d.ts +37 -0
- package/dist/renderer/renderers/attaches/index.js +451 -0
- package/dist/renderer/renderers/attaches/locale/en.d.ts +11 -0
- package/dist/renderer/renderers/attaches/locale/en.js +7 -0
- package/dist/renderer/renderers/attaches/locale/ru.d.ts +13 -0
- package/dist/renderer/renderers/attaches/locale/ru.js +7 -0
- package/dist/renderer/renderers/attaches/styles.css +196 -0
- package/dist/renderer/renderers/carousel/README.md +43 -0
- package/dist/renderer/renderers/carousel/README.ru.md +43 -0
- package/dist/renderer/renderers/carousel/index.d.ts +7 -0
- package/dist/renderer/renderers/carousel/index.js +110 -0
- package/dist/renderer/renderers/carousel/styles.css +7 -0
- package/dist/renderer/renderers/checklist/README.md +33 -0
- package/dist/renderer/renderers/checklist/README.ru.md +33 -0
- package/dist/renderer/renderers/checklist/index.d.ts +7 -0
- package/dist/renderer/renderers/checklist/index.js +55 -0
- package/dist/renderer/renderers/checklist/styles.css +54 -0
- package/dist/renderer/renderers/code/README.md +33 -0
- package/dist/renderer/renderers/code/README.ru.md +33 -0
- package/dist/renderer/renderers/code/index.d.ts +9 -0
- package/dist/renderer/renderers/code/index.js +137 -0
- package/dist/renderer/renderers/code/locale/en.d.ts +5 -0
- package/dist/renderer/renderers/code/locale/en.js +4 -0
- package/dist/renderer/renderers/code/locale/ru.d.ts +5 -0
- package/dist/renderer/renderers/code/locale/ru.js +4 -0
- package/dist/renderer/renderers/code/styles.css +330 -0
- package/dist/renderer/renderers/columns/README.md +33 -0
- package/dist/renderer/renderers/columns/README.ru.md +33 -0
- package/dist/renderer/renderers/columns/index.d.ts +7 -0
- package/dist/renderer/renderers/columns/index.js +53 -0
- package/dist/renderer/renderers/columns/styles.css +5 -0
- package/dist/renderer/renderers/delimiter/README.md +33 -0
- package/dist/renderer/renderers/delimiter/README.ru.md +33 -0
- package/dist/renderer/renderers/delimiter/index.d.ts +7 -0
- package/dist/renderer/renderers/delimiter/index.js +28 -0
- package/dist/renderer/renderers/delimiter/styles.css +12 -0
- package/dist/renderer/renderers/embed/README.md +35 -0
- package/dist/renderer/renderers/embed/README.ru.md +35 -0
- package/dist/renderer/renderers/embed/index.d.ts +8 -0
- package/dist/renderer/renderers/embed/index.js +66 -0
- package/dist/renderer/renderers/embed/styles.css +217 -0
- package/dist/renderer/renderers/gallery/README.md +41 -0
- package/dist/renderer/renderers/gallery/README.ru.md +41 -0
- package/dist/renderer/renderers/gallery/index.d.ts +8 -0
- package/dist/renderer/renderers/gallery/index.js +329 -0
- package/dist/renderer/renderers/gallery/styles.css +419 -0
- package/dist/renderer/renderers/heading/README.md +33 -0
- package/dist/renderer/renderers/heading/README.ru.md +33 -0
- package/dist/renderer/renderers/heading/index.d.ts +8 -0
- package/dist/renderer/renderers/heading/index.js +38 -0
- package/dist/renderer/renderers/heading/styles.css +26 -0
- package/dist/renderer/renderers/image/README.md +42 -0
- package/dist/renderer/renderers/image/README.ru.md +42 -0
- package/dist/renderer/renderers/image/index.d.ts +7 -0
- package/dist/renderer/renderers/image/index.js +93 -0
- package/dist/renderer/renderers/image/styles.css +40 -0
- package/dist/renderer/renderers/index.d.ts +47 -0
- package/dist/renderer/renderers/index.js +125 -0
- package/dist/renderer/renderers/link-preview/README.md +43 -0
- package/dist/renderer/renderers/link-preview/README.ru.md +43 -0
- package/dist/renderer/renderers/link-preview/index.d.ts +9 -0
- package/dist/renderer/renderers/link-preview/index.js +118 -0
- package/dist/renderer/renderers/link-preview/styles.css +246 -0
- package/dist/renderer/renderers/list/README.md +33 -0
- package/dist/renderer/renderers/list/README.ru.md +33 -0
- package/dist/renderer/renderers/list/index.d.ts +8 -0
- package/dist/renderer/renderers/list/index.js +42 -0
- package/dist/renderer/renderers/list/styles.css +20 -0
- package/dist/renderer/renderers/paragraph/README.md +33 -0
- package/dist/renderer/renderers/paragraph/README.ru.md +33 -0
- package/dist/renderer/renderers/paragraph/index.d.ts +7 -0
- package/dist/renderer/renderers/paragraph/index.js +35 -0
- package/dist/renderer/renderers/paragraph/styles.css +3 -0
- package/dist/renderer/renderers/person/README.md +44 -0
- package/dist/renderer/renderers/person/README.ru.md +44 -0
- package/dist/renderer/renderers/person/index.d.ts +7 -0
- package/dist/renderer/renderers/person/index.js +213 -0
- package/dist/renderer/renderers/person/locale/en.d.ts +6 -0
- package/dist/renderer/renderers/person/locale/en.js +5 -0
- package/dist/renderer/renderers/person/locale/ru.d.ts +6 -0
- package/dist/renderer/renderers/person/locale/ru.js +5 -0
- package/dist/renderer/renderers/person/styles.css +47 -0
- package/dist/renderer/renderers/poll/README.md +68 -0
- package/dist/renderer/renderers/poll/README.ru.md +68 -0
- package/dist/renderer/renderers/poll/index.d.ts +9 -0
- package/dist/renderer/renderers/poll/index.js +339 -0
- package/dist/renderer/renderers/poll/styles.css +148 -0
- package/dist/renderer/renderers/quote/README.md +33 -0
- package/dist/renderer/renderers/quote/README.ru.md +33 -0
- package/dist/renderer/renderers/quote/index.d.ts +7 -0
- package/dist/renderer/renderers/quote/index.js +59 -0
- package/dist/renderer/renderers/quote/styles.css +43 -0
- package/dist/renderer/renderers/raw/README.md +33 -0
- package/dist/renderer/renderers/raw/README.ru.md +33 -0
- package/dist/renderer/renderers/raw/index.d.ts +7 -0
- package/dist/renderer/renderers/raw/index.js +27 -0
- package/dist/renderer/renderers/spoiler/README.md +33 -0
- package/dist/renderer/renderers/spoiler/README.ru.md +33 -0
- package/dist/renderer/renderers/spoiler/index.d.ts +9 -0
- package/dist/renderer/renderers/spoiler/index.js +84 -0
- package/dist/renderer/renderers/spoiler/locale/en.d.ts +4 -0
- package/dist/renderer/renderers/spoiler/locale/en.js +3 -0
- package/dist/renderer/renderers/spoiler/locale/ru.d.ts +4 -0
- package/dist/renderer/renderers/spoiler/locale/ru.js +3 -0
- package/dist/renderer/renderers/spoiler/styles.css +61 -0
- package/dist/renderer/renderers/table/README.md +33 -0
- package/dist/renderer/renderers/table/README.ru.md +33 -0
- package/dist/renderer/renderers/table/index.d.ts +7 -0
- package/dist/renderer/renderers/table/index.js +83 -0
- package/dist/renderer/renderers/table/styles.css +34 -0
- package/dist/renderer/renderers/toggle/README.md +33 -0
- package/dist/renderer/renderers/toggle/README.ru.md +33 -0
- package/dist/renderer/renderers/toggle/index.d.ts +7 -0
- package/dist/renderer/renderers/toggle/index.js +59 -0
- package/dist/renderer/renderers/toggle/styles.css +49 -0
- package/dist/renderer/renderers/warning/README.md +33 -0
- package/dist/renderer/renderers/warning/README.ru.md +33 -0
- package/dist/renderer/renderers/warning/index.d.ts +7 -0
- package/dist/renderer/renderers/warning/index.js +62 -0
- package/dist/renderer/renderers/warning/styles.css +41 -0
- package/dist/renderer/styles/base.css +168 -0
- package/dist/renderer/types.d.ts +464 -0
- package/dist/shared/blockDataNormalizers.d.ts +14 -0
- package/dist/shared/blockDataNormalizers.js +235 -0
- package/dist/shared/blockDataValidators.d.ts +81 -0
- package/dist/shared/blockDataValidators.js +313 -0
- package/dist/shared/blockTypes.d.ts +2 -0
- package/dist/shared/blockTypes.js +26 -0
- package/dist/shared/carouselData.d.ts +34 -0
- package/dist/shared/carouselData.js +111 -0
- package/dist/shared/cloneEditorData.d.ts +11 -0
- package/dist/shared/cloneEditorData.js +20 -0
- package/dist/shared/documentTypes.d.ts +35 -0
- package/dist/shared/embedPlayer.d.ts +42 -0
- package/dist/shared/embedPlayer.js +141 -0
- package/dist/shared/fileUtils.d.ts +33 -0
- package/dist/shared/fileUtils.js +109 -0
- package/dist/shared/highlightRuntime.d.ts +38 -0
- package/dist/shared/highlightRuntime.js +75 -0
- package/dist/shared/inlineMarshal.d.ts +82 -0
- package/dist/shared/inlineMarshal.js +215 -0
- package/dist/shared/jsonData.d.ts +11 -0
- package/dist/shared/jsonData.js +53 -0
- package/dist/shared/localeTypes.d.ts +11 -0
- package/dist/shared/mapTextFields.d.ts +89 -0
- package/dist/shared/mapTextFields.js +119 -0
- package/dist/shared/pollData.d.ts +90 -0
- package/dist/shared/pollData.js +231 -0
- package/dist/shared/resolvePath.d.ts +8 -0
- package/dist/shared/resolvePath.js +10 -0
- package/dist/shared/runtime/dompurify.js +2424 -0
- package/dist/shared/runtime/highlightBundle.js +10 -0
- package/dist/shared/runtime/jszip.js +13 -0
- package/dist/shared/sanitize/allowlist.d.ts +21 -0
- package/dist/shared/sanitize/allowlist.js +40 -0
- package/dist/shared/sanitize/escapeHtml.d.ts +6 -0
- package/dist/shared/sanitize/escapeHtml.js +11 -0
- package/dist/shared/sanitize/index.d.ts +8 -0
- package/dist/shared/sanitize/index.js +8 -0
- package/dist/shared/sanitize/parseInline.d.ts +11 -0
- package/dist/shared/sanitize/parseInline.js +25 -0
- package/dist/shared/sanitize/sanitizeHtml.d.ts +8 -0
- package/dist/shared/sanitize/sanitizeHtml.js +19 -0
- package/dist/shared/sanitize/sanitizeRawHtml.d.ts +15 -0
- package/dist/shared/sanitize/sanitizeRawHtml.js +162 -0
- package/dist/shared/sanitize/sanitizeStyle.d.ts +9 -0
- package/dist/shared/sanitize/sanitizeStyle.js +39 -0
- package/dist/shared/sanitize/sanitizeUrl.d.ts +28 -0
- package/dist/shared/sanitize/sanitizeUrl.js +86 -0
- package/dist/shared/sanitize/walker.d.ts +15 -0
- package/dist/shared/sanitize/walker.js +98 -0
- package/dist/shared/styleRegistry.d.ts +4 -0
- package/dist/shared/styleRegistry.js +38 -0
- package/dist/shared/textFormat.d.ts +32 -0
- package/dist/shared/textFormat.js +48 -0
- package/dist/shared/zipRuntime.d.ts +19 -0
- package/dist/shared/zipRuntime.js +54 -0
- package/dist/styles/editor.css +3 -0
- package/dist/types.d.ts +1 -0
- package/package.json +149 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
import { BLOCK_CLASS } from '../core/constants.js'
|
|
2
|
+
import { CrossBlockSelection } from '../core/CrossBlockSelection.js'
|
|
3
|
+
import { closestBlock, el } from '../core/dom.js'
|
|
4
|
+
import { createSvgIcon, ICON_BACK } from '../core/icons.js'
|
|
5
|
+
import { getTextOffset, editableTextWalker } from '../core/textOffset.js'
|
|
6
|
+
|
|
7
|
+
/** SVG markup for the standard back-navigation icon used in action panels. */
|
|
8
|
+
export { ICON_BACK }
|
|
9
|
+
|
|
10
|
+
/** SVG markup for the bold formatting control. */
|
|
11
|
+
export const ICON_BOLD = createSvgIcon('<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6z"/><path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7"/>')
|
|
12
|
+
/** SVG markup for the italic formatting control. */
|
|
13
|
+
export const ICON_ITALIC = createSvgIcon('<path d="M11 5l6 0"/><path d="M7 19l6 0"/><path d="M14 5l-4 14"/>')
|
|
14
|
+
/** SVG markup for the link formatting control. */
|
|
15
|
+
export const ICON_LINK = createSvgIcon('<path d="M9 15l6 -6"/><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464"/><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463"/>')
|
|
16
|
+
/** SVG markup for the inline-code formatting control. */
|
|
17
|
+
export const ICON_CODE = createSvgIcon('<path d="M7 8l-4 4l4 4"/><path d="M17 8l4 4l-4 4"/>')
|
|
18
|
+
/** SVG markup for the text-highlight formatting control. */
|
|
19
|
+
export const ICON_HIGHLIGHT = createSvgIcon('<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"/><path d="M12.5 5.5l4 4"/><path d="M4.5 13.5l4 4"/><path d="M21 15l-2 2l-4 -4l2 -2z"/>')
|
|
20
|
+
/** SVG markup for the strikethrough formatting control. */
|
|
21
|
+
export const ICON_STRIKETHROUGH = createSvgIcon('<path d="M5 12l14 0"/><path d="M16 6.5a4 2 0 0 0 -4 -1.5h-1a3.5 3.5 0 0 0 0 7h2a3.5 3.5 0 0 1 0 7h-1.5a4 2 0 0 1 -4 -1.5"/>')
|
|
22
|
+
/** SVG markup for the left-alignment control. */
|
|
23
|
+
export const ICON_ALIGN_LEFT = createSvgIcon('<path d="M4 6l16 0"/><path d="M4 12l10 0"/><path d="M4 18l14 0"/>')
|
|
24
|
+
/** SVG markup for the center-alignment control. */
|
|
25
|
+
export const ICON_ALIGN_CENTER = createSvgIcon('<path d="M4 6l16 0"/><path d="M7 12l10 0"/><path d="M5 18l14 0"/>')
|
|
26
|
+
/** SVG markup for the right-alignment control. */
|
|
27
|
+
export const ICON_ALIGN_RIGHT = createSvgIcon('<path d="M4 6l16 0"/><path d="M10 12l10 0"/><path d="M6 18l14 0"/>')
|
|
28
|
+
/** SVG markup for the justified-alignment control. */
|
|
29
|
+
export const ICON_ALIGN_JUSTIFY = createSvgIcon('<path d="M4 6l16 0"/><path d="M4 12l16 0"/><path d="M4 18l16 0"/>')
|
|
30
|
+
/** SVG markup for the remove-link control. */
|
|
31
|
+
export const ICON_UNLINK = createSvgIcon('<path d="M9 15l3 -3m2 -2l1 -1"/><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464"/><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463"/><path d="M3 3l18 18"/>')
|
|
32
|
+
/** SVG markup for confirmation controls. */
|
|
33
|
+
export const ICON_CHECK = createSvgIcon('<path d="M5 12l5 5l10 -10"/>')
|
|
34
|
+
/** SVG markup for the clear-formatting control. */
|
|
35
|
+
export const ICON_CLEAR = createSvgIcon('<path d="M17 15l4 4m0 -4l-4 4"/><path d="M7 6v-1h11v1"/><path d="M7 19l4 0"/><path d="M13 5l-4 14"/>')
|
|
36
|
+
|
|
37
|
+
/** Inline tag selector for empty-tag cleanup. */
|
|
38
|
+
const INLINE_TAGS_SELECTOR = 'b, i, s, em, strong, u, mark, code, span, a, sup, sub'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Like `element.closest(selector)`, but stops at the contenteditable boundary.
|
|
42
|
+
* Prevents matching elements outside the editor (e.g. a `<span>` in the page layout).
|
|
43
|
+
* @param {HTMLElement | null} el
|
|
44
|
+
* @param {string} selector
|
|
45
|
+
* @returns {HTMLElement | null}
|
|
46
|
+
*/
|
|
47
|
+
function closestInEditable(el, selector) {
|
|
48
|
+
while (el) {
|
|
49
|
+
if (el.matches(selector)) return el
|
|
50
|
+
if (el.getAttribute('contenteditable') === 'true') return null
|
|
51
|
+
el = el.parentElement
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Remove empty inline wrapper tags and normalize text nodes.
|
|
58
|
+
* Called after any DOM mutation that unwraps/removes inline formatting.
|
|
59
|
+
* @param {Node | null} parent
|
|
60
|
+
* @returns {void}
|
|
61
|
+
*/
|
|
62
|
+
export function removeEmptyInlineTags(parent) {
|
|
63
|
+
if (!parent || parent.nodeType !== Node.ELEMENT_NODE) return
|
|
64
|
+
const empties = /** @type {HTMLElement} */ (parent).querySelectorAll(INLINE_TAGS_SELECTOR)
|
|
65
|
+
for (const el of empties) {
|
|
66
|
+
if (!el.textContent && !el.querySelector('img, br')) el.remove()
|
|
67
|
+
}
|
|
68
|
+
parent.normalize()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Merge adjacent sibling elements with the same tag name.
|
|
73
|
+
* E.g. `<b>a</b><b>b</b>` → `<b>ab</b>`.
|
|
74
|
+
* Only merges direct siblings; does not recurse into children.
|
|
75
|
+
* @param {Node | null} parent
|
|
76
|
+
* @param {string} tagName — lowercase tag name to merge
|
|
77
|
+
*/
|
|
78
|
+
function mergeAdjacentTags(parent, tagName) {
|
|
79
|
+
if (!parent || parent.nodeType !== Node.ELEMENT_NODE) return
|
|
80
|
+
const upper = tagName.toUpperCase()
|
|
81
|
+
// Normalize first so empty text nodes between elements are removed
|
|
82
|
+
parent.normalize()
|
|
83
|
+
/** @type {Node | null} */
|
|
84
|
+
let node = parent.firstChild
|
|
85
|
+
while (node) {
|
|
86
|
+
const next = node.nextSibling
|
|
87
|
+
// Only merge if two same-tag elements are truly DOM-adjacent (no text nodes between them)
|
|
88
|
+
if (node.nodeType === Node.ELEMENT_NODE
|
|
89
|
+
&& next?.nodeType === Node.ELEMENT_NODE
|
|
90
|
+
&& /** @type {Element} */ (node).tagName === upper
|
|
91
|
+
&& /** @type {Element} */ (next).tagName === upper
|
|
92
|
+
&& haveSameAttributes(
|
|
93
|
+
/** @type {Element} */ (node),
|
|
94
|
+
/** @type {Element} */ (next),
|
|
95
|
+
)) {
|
|
96
|
+
while (next.firstChild) node.appendChild(next.firstChild)
|
|
97
|
+
next.remove()
|
|
98
|
+
// Don't advance — merged node may have a new adjacent sibling
|
|
99
|
+
} else {
|
|
100
|
+
node = next
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
parent.normalize()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Compare element attributes without depending on their source order.
|
|
108
|
+
* Wrappers with different classes, data or inline styles must not be merged.
|
|
109
|
+
* @param {Element} left
|
|
110
|
+
* @param {Element} right
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function haveSameAttributes(left, right) {
|
|
114
|
+
if (left.attributes.length !== right.attributes.length) return false
|
|
115
|
+
for (const attribute of left.attributes) {
|
|
116
|
+
if (right.getAttribute(attribute.name) !== attribute.value) return false
|
|
117
|
+
}
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if content already has a given tag in its wrapper chain.
|
|
123
|
+
* extractContents clones partially-covered inline elements (DOM spec),
|
|
124
|
+
* so we need to detect these to avoid double-wrapping.
|
|
125
|
+
*
|
|
126
|
+
* Handles both single-child chains (`<i><b>text</b></i>`) and
|
|
127
|
+
* multi-child fragments (`<i>a</i><s>b</s>`) — the latter can occur
|
|
128
|
+
* when extractContents splits a parent that had multiple children.
|
|
129
|
+
*
|
|
130
|
+
* @param {Node} content — text node, element, or DocumentFragment
|
|
131
|
+
* @param {string} tag — lowercase tag name to look for
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
function hasWrapperTag(content, tag) {
|
|
135
|
+
let node = content
|
|
136
|
+
// Unwrap single-child DocumentFragments
|
|
137
|
+
while (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && node.childNodes.length === 1) {
|
|
138
|
+
node = /** @type {Node} */ (node.firstChild)
|
|
139
|
+
}
|
|
140
|
+
// For multi-child fragments: the tag is considered present if the
|
|
141
|
+
// fragment itself IS the unwrapped content (no single wrapper around it).
|
|
142
|
+
// We only need to detect a wrapper that surrounds ALL children.
|
|
143
|
+
// Walk down single-child element chain looking for the tag.
|
|
144
|
+
while (node.nodeType === Node.ELEMENT_NODE) {
|
|
145
|
+
if (/** @type {Element} */ (node).tagName.toLowerCase() === tag) return true
|
|
146
|
+
// Only descend if there's exactly one element child (a wrapper chain)
|
|
147
|
+
const children = /** @type {Element} */ (node).children
|
|
148
|
+
if (children.length !== 1) break
|
|
149
|
+
node = children[0]
|
|
150
|
+
}
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Wrap content in an inline wrapper tag, but only if the content isn't
|
|
156
|
+
* already wrapped in the same tag (which happens when extractContents
|
|
157
|
+
* clones partially-covered elements per DOM spec).
|
|
158
|
+
*
|
|
159
|
+
* For multi-child fragments (e.g. `<i>a</i><s>b</s>`), wraps each
|
|
160
|
+
* top-level child individually if it lacks the wrapper, avoiding
|
|
161
|
+
* double-wrapping children that already have the tag from extractContents.
|
|
162
|
+
*
|
|
163
|
+
* @param {Node} content — text node, element, or DocumentFragment
|
|
164
|
+
* @param {HTMLElement} wrapperTemplate — cloned empty element to use as wrapper
|
|
165
|
+
* @returns {Node}
|
|
166
|
+
*/
|
|
167
|
+
function wrapIfNeeded(content, wrapperTemplate) {
|
|
168
|
+
const tag = wrapperTemplate.tagName.toLowerCase()
|
|
169
|
+
|
|
170
|
+
// Already has the wrapper (single-child chain) — skip
|
|
171
|
+
if (hasWrapperTag(content, tag)) return content
|
|
172
|
+
|
|
173
|
+
// Multi-child fragment: extractContents already cloned partially-covered
|
|
174
|
+
// wrappers for each child individually (DOM spec). Adding another wrapper
|
|
175
|
+
// around the whole fragment would double-wrap children that don't need it.
|
|
176
|
+
// Leave multi-child fragments as-is — the per-child clones are correct.
|
|
177
|
+
if (content.nodeType === Node.DOCUMENT_FRAGMENT_NODE && content.childNodes.length > 1) {
|
|
178
|
+
return content
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const w = /** @type {HTMLElement} */ (wrapperTemplate.cloneNode(false))
|
|
182
|
+
w.appendChild(content)
|
|
183
|
+
return w
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Wrap or unwrap a selection with an inline tag.
|
|
188
|
+
* @param {string} tagName
|
|
189
|
+
* @param {Range} range
|
|
190
|
+
* @param {(element: HTMLElement) => boolean} [matches]
|
|
191
|
+
* @returns {void}
|
|
192
|
+
*/
|
|
193
|
+
export function toggleTag(tagName, range, matches = () => true) {
|
|
194
|
+
const sel = window.getSelection()
|
|
195
|
+
if (!sel || !range) return
|
|
196
|
+
|
|
197
|
+
const parent = range.commonAncestorContainer
|
|
198
|
+
const container = parent.nodeType === Node.ELEMENT_NODE
|
|
199
|
+
? /** @type {HTMLElement} */ (parent)
|
|
200
|
+
: parent.parentElement
|
|
201
|
+
|
|
202
|
+
// Case 1: Selection is entirely INSIDE an existing tag — unwrap
|
|
203
|
+
let ancestor = closestInEditable(container, tagName)
|
|
204
|
+
while (ancestor && !matches(ancestor)) {
|
|
205
|
+
ancestor = closestInEditable(ancestor.parentElement, tagName)
|
|
206
|
+
}
|
|
207
|
+
if (ancestor) {
|
|
208
|
+
// Check if selection covers the full ancestor content via character offsets
|
|
209
|
+
// (compareBoundaryPoints is unreliable for mixed text/element boundaries)
|
|
210
|
+
const startOff = getTextOffset(ancestor, range.startContainer, range.startOffset)
|
|
211
|
+
const endOff = getTextOffset(ancestor, range.endContainer, range.endOffset)
|
|
212
|
+
const totalLen = ancestor.textContent?.length ?? 0
|
|
213
|
+
const coversStart = startOff === 0
|
|
214
|
+
const coversEnd = endOff >= totalLen
|
|
215
|
+
|
|
216
|
+
if (coversStart && coversEnd) {
|
|
217
|
+
// Full unwrap — selection covers entire ancestor
|
|
218
|
+
const parentNode = ancestor.parentNode
|
|
219
|
+
const firstChild = ancestor.firstChild
|
|
220
|
+
const lastChild = ancestor.lastChild
|
|
221
|
+
while (ancestor.firstChild) {
|
|
222
|
+
parentNode?.insertBefore(ancestor.firstChild, ancestor)
|
|
223
|
+
}
|
|
224
|
+
ancestor.remove()
|
|
225
|
+
if (firstChild && lastChild) {
|
|
226
|
+
try {
|
|
227
|
+
sel.removeAllRanges()
|
|
228
|
+
const newRange = document.createRange()
|
|
229
|
+
newRange.setStartBefore(firstChild)
|
|
230
|
+
newRange.setEndAfter(lastChild)
|
|
231
|
+
sel.addRange(newRange)
|
|
232
|
+
} catch { /* detached */ }
|
|
233
|
+
}
|
|
234
|
+
removeEmptyInlineTags(parentNode)
|
|
235
|
+
} else {
|
|
236
|
+
// Partial unwrap — only remove tag from selected portion
|
|
237
|
+
// Preserves inner inline wrappers (e.g. removing <b> keeps <i>)
|
|
238
|
+
// Result: <b><i>before</i></b><i>selected</i><b><i>after</i></b>
|
|
239
|
+
const parentNode = ancestor.parentNode
|
|
240
|
+
const ref = ancestor.nextSibling
|
|
241
|
+
|
|
242
|
+
// Save selection offsets relative to CE for restoration after DOM mutations
|
|
243
|
+
const ce = ancestor.closest('[contenteditable="true"]') || parentNode
|
|
244
|
+
const savedSelStart = ce ? getTextOffset(ce, range.startContainer, range.startOffset) : -1
|
|
245
|
+
const savedSelEnd = ce ? getTextOffset(ce, range.endContainer, range.endOffset) : -1
|
|
246
|
+
|
|
247
|
+
// Save inner inline wrapper chain (between text node and ancestor, exclusive)
|
|
248
|
+
const innerWrappers = []
|
|
249
|
+
/** @type {Node | null} */
|
|
250
|
+
let n = range.startContainer
|
|
251
|
+
if (n.nodeType === Node.TEXT_NODE) n = n.parentElement
|
|
252
|
+
while (n && n !== ancestor) {
|
|
253
|
+
innerWrappers.push(/** @type {HTMLElement} */ (n).cloneNode(false))
|
|
254
|
+
n = /** @type {HTMLElement} */ (n).parentElement
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 1. Extract content AFTER selection
|
|
258
|
+
const afterRange = document.createRange()
|
|
259
|
+
afterRange.setStart(range.endContainer, range.endOffset)
|
|
260
|
+
afterRange.setEndAfter(ancestor.lastChild || ancestor)
|
|
261
|
+
const afterFrag = afterRange.extractContents()
|
|
262
|
+
|
|
263
|
+
// 2. Extract selected content and re-wrap with inner wrappers (preserving other formatting)
|
|
264
|
+
// wrapIfNeeded skips if extractContents already cloned the wrapper (partial coverage).
|
|
265
|
+
/** @type {Node} */
|
|
266
|
+
let wrappedSelected = range.extractContents()
|
|
267
|
+
for (let i = 0; i < innerWrappers.length; i++) {
|
|
268
|
+
const iw = innerWrappers[i]
|
|
269
|
+
if (!iw) continue
|
|
270
|
+
wrappedSelected = wrapIfNeeded(wrappedSelected, /** @type {HTMLElement} */ (iw))
|
|
271
|
+
}
|
|
272
|
+
parentNode?.insertBefore(wrappedSelected, ref)
|
|
273
|
+
|
|
274
|
+
// 4. Re-wrap after content with inner wrappers + ancestor tag
|
|
275
|
+
// extractContents already clones partially-covered inner wrappers,
|
|
276
|
+
// so wrapIfNeeded prevents duplication.
|
|
277
|
+
if (afterFrag.textContent) {
|
|
278
|
+
/** @type {Node} */
|
|
279
|
+
let wrappedAfter = afterFrag
|
|
280
|
+
for (let i = 0; i < innerWrappers.length; i++) {
|
|
281
|
+
const iw = innerWrappers[i]
|
|
282
|
+
if (!iw) continue
|
|
283
|
+
wrappedAfter = wrapIfNeeded(wrappedAfter, /** @type {HTMLElement} */ (iw))
|
|
284
|
+
}
|
|
285
|
+
const afterTag = /** @type {HTMLElement} */ (ancestor.cloneNode(false))
|
|
286
|
+
afterTag.appendChild(wrappedAfter)
|
|
287
|
+
parentNode?.insertBefore(afterTag, ref)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 5. Remove ancestor if empty
|
|
291
|
+
if (!ancestor.textContent) ancestor.remove()
|
|
292
|
+
|
|
293
|
+
// 6. Clean up empty inline tags left by extraction
|
|
294
|
+
removeEmptyInlineTags(parentNode)
|
|
295
|
+
mergeAdjacentTags(parentNode, tagName)
|
|
296
|
+
|
|
297
|
+
// 7. Restore selection from saved offsets
|
|
298
|
+
if (ce && savedSelStart >= 0) {
|
|
299
|
+
const s = findNodeAtOffset(ce, savedSelStart)
|
|
300
|
+
const e = findNodeAtOffset(ce, savedSelEnd, 'end')
|
|
301
|
+
if (s && e) {
|
|
302
|
+
try {
|
|
303
|
+
sel.removeAllRanges()
|
|
304
|
+
const nr = document.createRange()
|
|
305
|
+
nr.setStart(s.node, s.offset)
|
|
306
|
+
nr.setEnd(e.node, e.offset)
|
|
307
|
+
sel.addRange(nr)
|
|
308
|
+
} catch { /* detached */ }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Case 2: Selection CONTAINS descendant tags — unwrap (full or partial)
|
|
316
|
+
if (container) {
|
|
317
|
+
const descendants = container.querySelectorAll(tagName)
|
|
318
|
+
const toUnwrap = []
|
|
319
|
+
for (const el of descendants) {
|
|
320
|
+
if (range.intersectsNode(el) && matches(/** @type {HTMLElement} */ (el))) {
|
|
321
|
+
toUnwrap.push(el)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (toUnwrap.length) {
|
|
325
|
+
// Save selection offsets relative to the CE so we can restore after DOM mutations
|
|
326
|
+
const ce = container.closest('[contenteditable="true"]') || container
|
|
327
|
+
const savedStart = getTextOffset(ce, range.startContainer, range.startOffset)
|
|
328
|
+
const savedEnd = getTextOffset(ce, range.endContainer, range.endOffset)
|
|
329
|
+
|
|
330
|
+
// Process in reverse DOM order so extractContents/insertBefore
|
|
331
|
+
// mutations don't invalidate range references for earlier elements.
|
|
332
|
+
for (let ti = toUnwrap.length - 1; ti >= 0; ti--) {
|
|
333
|
+
const el = /** @type {HTMLElement} */ (toUnwrap[ti])
|
|
334
|
+
// Create a clipped range: intersection of selection with this element
|
|
335
|
+
const clipped = document.createRange()
|
|
336
|
+
if (el.contains(range.startContainer)) {
|
|
337
|
+
clipped.setStart(range.startContainer, range.startOffset)
|
|
338
|
+
} else {
|
|
339
|
+
clipped.setStart(el, 0)
|
|
340
|
+
}
|
|
341
|
+
if (el.contains(range.endContainer)) {
|
|
342
|
+
clipped.setEnd(range.endContainer, range.endOffset)
|
|
343
|
+
} else {
|
|
344
|
+
clipped.setEnd(el, el.childNodes.length)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const clippedLen = clipped.toString().length
|
|
348
|
+
const fullLen = el.textContent?.length ?? 0
|
|
349
|
+
|
|
350
|
+
if (clippedLen >= fullLen) {
|
|
351
|
+
// Full unwrap
|
|
352
|
+
const p = el.parentNode
|
|
353
|
+
while (el.firstChild) p?.insertBefore(el.firstChild, el)
|
|
354
|
+
el.remove()
|
|
355
|
+
} else {
|
|
356
|
+
// Partial unwrap — split element, preserve inner wrappers
|
|
357
|
+
const parentNode = el.parentNode
|
|
358
|
+
const ref = el.nextSibling
|
|
359
|
+
|
|
360
|
+
// Save inner wrapper chain
|
|
361
|
+
const innerWrappers = []
|
|
362
|
+
/** @type {Node | null} */
|
|
363
|
+
let nn = clipped.startContainer
|
|
364
|
+
if (nn.nodeType === Node.TEXT_NODE) nn = nn.parentElement
|
|
365
|
+
while (nn && nn !== el) {
|
|
366
|
+
innerWrappers.push(/** @type {HTMLElement} */ (nn).cloneNode(false))
|
|
367
|
+
nn = /** @type {HTMLElement} */ (nn).parentElement
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const afterRange = document.createRange()
|
|
371
|
+
afterRange.setStart(clipped.endContainer, clipped.endOffset)
|
|
372
|
+
afterRange.setEndAfter(el.lastChild || el)
|
|
373
|
+
const afterFrag = afterRange.extractContents()
|
|
374
|
+
|
|
375
|
+
// Re-wrap selected with inner wrappers (skip if extractContents already cloned them)
|
|
376
|
+
/** @type {Node} */
|
|
377
|
+
let wrappedSel = clipped.extractContents()
|
|
378
|
+
for (let i = 0; i < innerWrappers.length; i++) {
|
|
379
|
+
const iw = innerWrappers[i]
|
|
380
|
+
if (!iw) continue
|
|
381
|
+
wrappedSel = wrapIfNeeded(wrappedSel, /** @type {HTMLElement} */ (iw))
|
|
382
|
+
}
|
|
383
|
+
parentNode?.insertBefore(wrappedSel, ref)
|
|
384
|
+
|
|
385
|
+
if (afterFrag.textContent) {
|
|
386
|
+
/** @type {Node} */
|
|
387
|
+
let wrappedAfter = afterFrag
|
|
388
|
+
for (let i = 0; i < innerWrappers.length; i++) {
|
|
389
|
+
const iw = innerWrappers[i]
|
|
390
|
+
if (!iw) continue
|
|
391
|
+
wrappedAfter = wrapIfNeeded(wrappedAfter, /** @type {HTMLElement} */ (iw))
|
|
392
|
+
}
|
|
393
|
+
const afterTag = /** @type {HTMLElement} */ (el.cloneNode(false))
|
|
394
|
+
afterTag.appendChild(wrappedAfter)
|
|
395
|
+
parentNode?.insertBefore(afterTag, ref)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!el.textContent) el.remove()
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Clean up empty inline tags, merge adjacent, and normalize
|
|
403
|
+
removeEmptyInlineTags(container)
|
|
404
|
+
mergeAdjacentTags(container, tagName)
|
|
405
|
+
|
|
406
|
+
// Restore selection from saved offsets
|
|
407
|
+
const s = findNodeAtOffset(ce, savedStart)
|
|
408
|
+
const e = findNodeAtOffset(ce, savedEnd, 'end')
|
|
409
|
+
if (s && e) {
|
|
410
|
+
try {
|
|
411
|
+
sel.removeAllRanges()
|
|
412
|
+
const nr = document.createRange()
|
|
413
|
+
nr.setStart(s.node, s.offset)
|
|
414
|
+
nr.setEnd(e.node, e.offset)
|
|
415
|
+
sel.addRange(nr)
|
|
416
|
+
} catch { /* detached */ }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Case 3: No existing tag found — wrap selection
|
|
424
|
+
const wrapper = document.createElement(tagName)
|
|
425
|
+
try {
|
|
426
|
+
range.surroundContents(wrapper)
|
|
427
|
+
} catch {
|
|
428
|
+
// surroundContents fails when range crosses element boundaries.
|
|
429
|
+
// Per-text-node wrapping as safe fallback.
|
|
430
|
+
// Use contenteditable for single-block, commonAncestorContainer for cross-block.
|
|
431
|
+
const singleCe = container?.closest('[contenteditable="true"]')
|
|
432
|
+
const walkRoot = singleCe || (range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
433
|
+
? range.commonAncestorContainer
|
|
434
|
+
: range.commonAncestorContainer.parentElement)
|
|
435
|
+
if (walkRoot) {
|
|
436
|
+
const walker = editableTextWalker(walkRoot)
|
|
437
|
+
/** @type {{ node: Text, startOffset: number, endOffset: number }[]} */
|
|
438
|
+
const targets = []
|
|
439
|
+
while (walker.nextNode()) {
|
|
440
|
+
const textNode = /** @type {Text} */ (walker.currentNode)
|
|
441
|
+
if (!range.intersectsNode(textNode)) continue
|
|
442
|
+
let so = 0, eo = textNode.length
|
|
443
|
+
if (textNode === range.startContainer) so = range.startOffset
|
|
444
|
+
if (textNode === range.endContainer) eo = range.endOffset
|
|
445
|
+
if (so >= eo) continue
|
|
446
|
+
targets.push({ node: textNode, startOffset: so, endOffset: eo })
|
|
447
|
+
}
|
|
448
|
+
for (let i = targets.length - 1; i >= 0; i--) {
|
|
449
|
+
const target = targets[i]
|
|
450
|
+
if (!target) continue
|
|
451
|
+
const { node, startOffset, endOffset } = target
|
|
452
|
+
let targetNode = node
|
|
453
|
+
if (endOffset < node.length) node.splitText(endOffset)
|
|
454
|
+
if (startOffset > 0) targetNode = /** @type {Text} */ (node.splitText(startOffset))
|
|
455
|
+
const w = document.createElement(tagName)
|
|
456
|
+
targetNode.parentNode?.insertBefore(w, targetNode)
|
|
457
|
+
w.appendChild(targetNode)
|
|
458
|
+
}
|
|
459
|
+
walkRoot.normalize()
|
|
460
|
+
mergeAdjacentTags(walkRoot, tagName)
|
|
461
|
+
}
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Merge with adjacent same-tag siblings before restoring selection.
|
|
466
|
+
// mergeAdjacentTags may absorb `wrapper` into a preceding sibling,
|
|
467
|
+
// so we save the CE-relative offsets and restore from them.
|
|
468
|
+
const mergeParent = wrapper.parentNode
|
|
469
|
+
if (mergeParent) {
|
|
470
|
+
const ce = wrapper.closest('[contenteditable="true"]')
|
|
471
|
+
if (ce) {
|
|
472
|
+
const wrapStart = getTextOffset(ce, wrapper.firstChild || wrapper, 0)
|
|
473
|
+
const wrapEnd = wrapStart + (wrapper.textContent?.length ?? 0)
|
|
474
|
+
mergeAdjacentTags(mergeParent, tagName)
|
|
475
|
+
// Restore selection from saved offsets
|
|
476
|
+
const s = findNodeAtOffset(ce, wrapStart)
|
|
477
|
+
const e = findNodeAtOffset(ce, wrapEnd, 'end')
|
|
478
|
+
if (s && e) {
|
|
479
|
+
try {
|
|
480
|
+
sel.removeAllRanges()
|
|
481
|
+
const nr = document.createRange()
|
|
482
|
+
nr.setStart(s.node, s.offset)
|
|
483
|
+
nr.setEnd(e.node, e.offset)
|
|
484
|
+
sel.addRange(nr)
|
|
485
|
+
} catch { /* detached */ }
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
// No CE — fallback: select wrapper contents (no merge)
|
|
489
|
+
try {
|
|
490
|
+
sel.removeAllRanges()
|
|
491
|
+
const nr = document.createRange()
|
|
492
|
+
nr.selectNodeContents(wrapper)
|
|
493
|
+
sel.addRange(nr)
|
|
494
|
+
} catch { /* detached */ }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Find the closest .oe-editor root from a DOM node.
|
|
501
|
+
* Avoids global querySelector — works with multiple editor instances.
|
|
502
|
+
* @param {Node | null | undefined} [node]
|
|
503
|
+
* @returns {HTMLElement | null}
|
|
504
|
+
*/
|
|
505
|
+
export function getEditorRoot(node) {
|
|
506
|
+
if (!node) return null
|
|
507
|
+
const el = node.nodeType === Node.ELEMENT_NODE ? /** @type {Element} */ (node) : node.parentElement
|
|
508
|
+
return /** @type {HTMLElement | null} */ (el?.closest('.oe-editor'))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Clear the stored cross-block range (call after DOM mutations that
|
|
513
|
+
* invalidate the range's node references).
|
|
514
|
+
* @param {import('../types').ICrossBlockSelection | null} [cbs]
|
|
515
|
+
* @param {Node} [contextNode] - any node inside the editor for scoping
|
|
516
|
+
* @returns {void}
|
|
517
|
+
*/
|
|
518
|
+
export function clearCrossBlockRange(cbs, contextNode) {
|
|
519
|
+
if (cbs) {
|
|
520
|
+
const editor = getEditorRoot(contextNode ?? cbs.range?.startContainer)
|
|
521
|
+
cbs.deactivate(editor ?? undefined)
|
|
522
|
+
} else {
|
|
523
|
+
const editor = getEditorRoot(contextNode)
|
|
524
|
+
if (editor) editor.classList.remove('oe-editor--cross-selecting')
|
|
525
|
+
CrossBlockSelection.hideHighlight()
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export { getTextOffset }
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Find the text node and offset at a given character position.
|
|
533
|
+
* @param {Node} container
|
|
534
|
+
* @param {number} charOffset
|
|
535
|
+
* @param {'start' | 'end'} [bias='start'] - when the offset lands exactly at a
|
|
536
|
+
* node boundary, 'start' prefers the beginning of the next node (for range
|
|
537
|
+
* start points), 'end' prefers the end of the current node (for range end
|
|
538
|
+
* points). This avoids creating ranges that inadvertently cross into adjacent
|
|
539
|
+
* inline wrappers.
|
|
540
|
+
* @returns {{ node: Node, offset: number } | null}
|
|
541
|
+
*/
|
|
542
|
+
export function findNodeAtOffset(container, charOffset, bias = 'start') {
|
|
543
|
+
const walker = editableTextWalker(container)
|
|
544
|
+
let remaining = charOffset
|
|
545
|
+
/** @type {Text | null} */
|
|
546
|
+
let lastNode = null
|
|
547
|
+
while (walker.nextNode()) {
|
|
548
|
+
lastNode = /** @type {Text} */ (walker.currentNode)
|
|
549
|
+
const len = lastNode.textContent?.length ?? 0
|
|
550
|
+
// Use < for intermediate nodes, <= only for the last node
|
|
551
|
+
if (remaining < len) return { node: lastNode, offset: remaining }
|
|
552
|
+
if (remaining === len) {
|
|
553
|
+
const saved = /** @type {Text} */ (walker.currentNode)
|
|
554
|
+
if (walker.nextNode()) {
|
|
555
|
+
const next = /** @type {Text} */ (walker.currentNode)
|
|
556
|
+
// 'end' bias: stay in current node to avoid crossing into the next wrapper
|
|
557
|
+
if (bias === 'end' && saved.parentElement !== next.parentElement) {
|
|
558
|
+
return { node: saved, offset: remaining }
|
|
559
|
+
}
|
|
560
|
+
// 'start' bias (default): prefer start of next node for clean range boundaries
|
|
561
|
+
return { node: next, offset: 0 }
|
|
562
|
+
}
|
|
563
|
+
// Last node — return end position
|
|
564
|
+
return { node: saved, offset: remaining }
|
|
565
|
+
}
|
|
566
|
+
remaining -= len
|
|
567
|
+
}
|
|
568
|
+
// Clamp to end of last text node
|
|
569
|
+
if (lastNode) return { node: lastNode, offset: lastNode.textContent?.length ?? 0 }
|
|
570
|
+
return null
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Return every editable field owned by one block in DOM order.
|
|
575
|
+
* @param {HTMLElement} block
|
|
576
|
+
* @returns {HTMLElement[]}
|
|
577
|
+
*/
|
|
578
|
+
function editableFields(block) {
|
|
579
|
+
const descendants = Array.from(
|
|
580
|
+
block.querySelectorAll('[contenteditable]'),
|
|
581
|
+
element => /** @type {HTMLElement} */ (element),
|
|
582
|
+
)
|
|
583
|
+
return block.matches('[contenteditable]') ? [block, ...descendants] : descendants
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Resolve the editable field that owns a range boundary. A boundary may be an
|
|
588
|
+
* element node positioned between children, so inspect that child before
|
|
589
|
+
* falling back to the first field for legacy or synthetic ranges.
|
|
590
|
+
* @param {HTMLElement} block
|
|
591
|
+
* @param {Node} container
|
|
592
|
+
* @param {number} offset
|
|
593
|
+
* @returns {{ element: HTMLElement, index: number } | null}
|
|
594
|
+
*/
|
|
595
|
+
function editableAtBoundary(block, container, offset) {
|
|
596
|
+
const fields = editableFields(block)
|
|
597
|
+
if (!fields.length) return null
|
|
598
|
+
|
|
599
|
+
const containerElement = container.nodeType === Node.ELEMENT_NODE
|
|
600
|
+
? /** @type {HTMLElement} */ (container)
|
|
601
|
+
: container.parentElement
|
|
602
|
+
let candidate = containerElement?.closest('[contenteditable]') ?? null
|
|
603
|
+
|
|
604
|
+
if (!candidate && container.nodeType === Node.ELEMENT_NODE) {
|
|
605
|
+
const children = container.childNodes
|
|
606
|
+
const child = children[Math.min(offset, children.length - 1)] ?? null
|
|
607
|
+
const childElement = child?.nodeType === Node.ELEMENT_NODE
|
|
608
|
+
? /** @type {HTMLElement} */ (child)
|
|
609
|
+
: child?.parentElement
|
|
610
|
+
candidate = childElement?.closest('[contenteditable]')
|
|
611
|
+
?? childElement?.querySelector?.('[contenteditable]')
|
|
612
|
+
?? null
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const editable = candidate instanceof HTMLElement ? candidate : null
|
|
616
|
+
const index = editable && block.contains(editable) ? fields.indexOf(editable) : -1
|
|
617
|
+
return index >= 0 ? { element: fields[index], index } : { element: fields[0], index: 0 }
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Save cross-block range as block IDs + character offsets (survives DOM mutations).
|
|
622
|
+
* @param {Range} range
|
|
623
|
+
* @returns {{ editorRoot: HTMLElement, startBlockId: string, endBlockId: string, startFieldIndex: number, endFieldIndex: number, startOffset: number, endOffset: number } | null}
|
|
624
|
+
*/
|
|
625
|
+
export function saveCrossBlockOffsets(range) {
|
|
626
|
+
const startBlock = closestBlock(range.startContainer)
|
|
627
|
+
const endBlock = closestBlock(range.endContainer)
|
|
628
|
+
if (!startBlock || !endBlock || startBlock === endBlock) return null
|
|
629
|
+
|
|
630
|
+
const editorRoot = getEditorRoot(startBlock)
|
|
631
|
+
if (!editorRoot || getEditorRoot(endBlock) !== editorRoot) return null
|
|
632
|
+
|
|
633
|
+
const startField = editableAtBoundary(startBlock, range.startContainer, range.startOffset)
|
|
634
|
+
const endField = editableAtBoundary(endBlock, range.endContainer, range.endOffset)
|
|
635
|
+
if (!startField || !endField) return null
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
editorRoot,
|
|
639
|
+
startBlockId: startBlock.dataset.blockId ?? '',
|
|
640
|
+
endBlockId: endBlock.dataset.blockId ?? '',
|
|
641
|
+
startFieldIndex: startField.index,
|
|
642
|
+
endFieldIndex: endField.index,
|
|
643
|
+
startOffset: getTextOffset(startField.element, range.startContainer, range.startOffset),
|
|
644
|
+
endOffset: getTextOffset(endField.element, range.endContainer, range.endOffset),
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Restore cross-block range from saved offsets. Updates CrossBlockSelection and CSS Highlight.
|
|
650
|
+
* @param {import('../types').ICrossBlockSelection | null} cbs
|
|
651
|
+
* @param {{ editorRoot: HTMLElement, startBlockId: string, endBlockId: string, startFieldIndex?: number, endFieldIndex?: number, startOffset: number, endOffset: number }} offsets
|
|
652
|
+
* @returns {Range | null}
|
|
653
|
+
*/
|
|
654
|
+
export function restoreCrossBlockRange(cbs, offsets) {
|
|
655
|
+
const { editorRoot } = offsets
|
|
656
|
+
if (!editorRoot.isConnected) return null
|
|
657
|
+
|
|
658
|
+
// Block IDs are unique only within one editor document. Comparing dataset
|
|
659
|
+
// values also avoids interpolating document-owned IDs into a CSS selector.
|
|
660
|
+
const blocks = editorRoot.querySelectorAll('[data-block-id]')
|
|
661
|
+
const startBlock = Array.from(blocks).find(block => (
|
|
662
|
+
/** @type {HTMLElement} */ (block).dataset.blockId === offsets.startBlockId
|
|
663
|
+
))
|
|
664
|
+
const endBlock = Array.from(blocks).find(block => (
|
|
665
|
+
/** @type {HTMLElement} */ (block).dataset.blockId === offsets.endBlockId
|
|
666
|
+
))
|
|
667
|
+
if (!startBlock || !endBlock) return null
|
|
668
|
+
|
|
669
|
+
const startCe = editableFields(/** @type {HTMLElement} */ (startBlock))[offsets.startFieldIndex ?? 0]
|
|
670
|
+
const endCe = editableFields(/** @type {HTMLElement} */ (endBlock))[offsets.endFieldIndex ?? 0]
|
|
671
|
+
if (!startCe || !endCe) return null
|
|
672
|
+
|
|
673
|
+
const start = findNodeAtOffset(startCe, offsets.startOffset)
|
|
674
|
+
const end = findNodeAtOffset(endCe, offsets.endOffset, 'end')
|
|
675
|
+
if (!start || !end) return null
|
|
676
|
+
|
|
677
|
+
const range = document.createRange()
|
|
678
|
+
try {
|
|
679
|
+
range.setStart(start.node, start.offset)
|
|
680
|
+
range.setEnd(end.node, end.offset)
|
|
681
|
+
} catch { return null }
|
|
682
|
+
|
|
683
|
+
// Update stored range and visual highlight
|
|
684
|
+
if (cbs) {
|
|
685
|
+
cbs.activate(range, editorRoot)
|
|
686
|
+
} else {
|
|
687
|
+
editorRoot.classList.add('oe-editor--cross-selecting')
|
|
688
|
+
CrossBlockSelection.showHighlight(range)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Place native caret at end of cross-block range
|
|
692
|
+
try {
|
|
693
|
+
const sel = window.getSelection()
|
|
694
|
+
const caret = document.createRange()
|
|
695
|
+
caret.setStart(range.endContainer, range.endOffset)
|
|
696
|
+
caret.collapse(true)
|
|
697
|
+
sel?.removeAllRanges()
|
|
698
|
+
sel?.addRange(caret)
|
|
699
|
+
} catch { /* detached */ }
|
|
700
|
+
|
|
701
|
+
return range
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Check if a selection spans multiple .oe-block elements.
|
|
706
|
+
* @param {import('../types').ICrossBlockSelection | null} [cbs]
|
|
707
|
+
* @returns {HTMLElement[] | null} Array of plugin-owned block roots, or null if single-block
|
|
708
|
+
*/
|
|
709
|
+
export function getSelectedBlockElements(cbs) {
|
|
710
|
+
const sel = window.getSelection()
|
|
711
|
+
if (!sel || sel.isCollapsed || !sel.rangeCount) return null
|
|
712
|
+
|
|
713
|
+
// Native selection clips to focused contenteditable.
|
|
714
|
+
// Use the cross-block range if available.
|
|
715
|
+
const nativeRange = sel.getRangeAt(0)
|
|
716
|
+
const range = cbs?.range || nativeRange
|
|
717
|
+
const startBlock = closestBlock(range.startContainer)
|
|
718
|
+
const endBlock = closestBlock(range.endContainer)
|
|
719
|
+
|
|
720
|
+
if (!startBlock || !endBlock || startBlock === endBlock) return null
|
|
721
|
+
|
|
722
|
+
// Collect all plugin-owned block roots between start and end.
|
|
723
|
+
const blocks = []
|
|
724
|
+
const container = startBlock.parentElement
|
|
725
|
+
if (!container) return null
|
|
726
|
+
|
|
727
|
+
let inside = false
|
|
728
|
+
for (const child of container.children) {
|
|
729
|
+
if (child === startBlock) inside = true
|
|
730
|
+
if (inside && child.classList.contains(BLOCK_CLASS)) {
|
|
731
|
+
const content = child.firstElementChild
|
|
732
|
+
if (content instanceof HTMLElement) blocks.push(content)
|
|
733
|
+
}
|
|
734
|
+
if (child === endBlock) break
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return blocks.length > 1 ? blocks : null
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Dispatch a synthetic input event on the nearest contenteditable to trigger
|
|
742
|
+
* undo/redo snapshot via EditorFacade.onInput → editor:changed.
|
|
743
|
+
* @param {Node} [contextNode] - any node inside the editor for scoping
|
|
744
|
+
* @returns {void}
|
|
745
|
+
*/
|
|
746
|
+
export function notifyEditorChanged(contextNode) {
|
|
747
|
+
const element = contextNode?.nodeType === Node.ELEMENT_NODE
|
|
748
|
+
? /** @type {Element} */ (contextNode)
|
|
749
|
+
: contextNode?.parentElement
|
|
750
|
+
const ce = element?.closest('[contenteditable="true"]')
|
|
751
|
+
if (ce) ce.dispatchEvent(new InputEvent('input', { bubbles: true }))
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Create a simple tag-based inline tool.
|
|
756
|
+
* @param {string} type
|
|
757
|
+
* @param {string} title
|
|
758
|
+
* @param {string} icon
|
|
759
|
+
* @param {string} tag
|
|
760
|
+
* @param {string} [shortcut]
|
|
761
|
+
* @param {import('../types').ICrossBlockSelection} [cbs]
|
|
762
|
+
* @returns {import('../types').InlineTool}
|
|
763
|
+
*/
|
|
764
|
+
export function createSimpleInlineTool(type, title, icon, tag, shortcut, cbs) {
|
|
765
|
+
return {
|
|
766
|
+
type,
|
|
767
|
+
title,
|
|
768
|
+
icon,
|
|
769
|
+
tag,
|
|
770
|
+
shortcut,
|
|
771
|
+
|
|
772
|
+
isActive(selection) {
|
|
773
|
+
const range = selection?.range
|
|
774
|
+
if (!range) return false
|
|
775
|
+
|
|
776
|
+
const ancestor = range.commonAncestorContainer
|
|
777
|
+
const container = ancestor.nodeType === Node.ELEMENT_NODE
|
|
778
|
+
? /** @type {HTMLElement} */ (ancestor)
|
|
779
|
+
: ancestor.parentElement
|
|
780
|
+
|
|
781
|
+
// Entire selection is inside the tag
|
|
782
|
+
if (closestInEditable(container, tag)) return true
|
|
783
|
+
|
|
784
|
+
// For collapsed caret — closest check above is sufficient
|
|
785
|
+
if (range.collapsed) return false
|
|
786
|
+
|
|
787
|
+
// For non-collapsed: check if ANY text node in range is inside the tag
|
|
788
|
+
if (container) {
|
|
789
|
+
const walker = editableTextWalker(container)
|
|
790
|
+
while (walker.nextNode()) {
|
|
791
|
+
const textNode = walker.currentNode
|
|
792
|
+
if (!range.intersectsNode(textNode)) continue
|
|
793
|
+
// Calculate actual selected length within this text node
|
|
794
|
+
let start = 0
|
|
795
|
+
let end = textNode.textContent?.length ?? 0
|
|
796
|
+
if (textNode === range.startContainer) start = range.startOffset
|
|
797
|
+
if (textNode === range.endContainer) end = range.endOffset
|
|
798
|
+
if (start >= end) continue // zero-length intersection (boundary node)
|
|
799
|
+
const selectedText = (textNode.textContent || '').slice(start, end)
|
|
800
|
+
if (!selectedText.trim()) continue
|
|
801
|
+
if (closestInEditable(textNode.parentElement, tag)) return true
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return false
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
toggle(selection) {
|
|
808
|
+
const range = selection.range
|
|
809
|
+
if (!range) return
|
|
810
|
+
const saved = saveSelectionOffsets(range)
|
|
811
|
+
|
|
812
|
+
toggleTag(tag, range)
|
|
813
|
+
|
|
814
|
+
restoreSelectionOffsets(cbs ?? null, saved)
|
|
815
|
+
},
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Find the contentEditable block element from the current selection.
|
|
821
|
+
* @returns {HTMLElement | null}
|
|
822
|
+
*/
|
|
823
|
+
export function getBlockElement() {
|
|
824
|
+
const sel = window.getSelection()
|
|
825
|
+
if (!sel || !sel.anchorNode) return null
|
|
826
|
+
let node = sel.anchorNode.nodeType === Node.ELEMENT_NODE
|
|
827
|
+
? /** @type {HTMLElement} */ (sel.anchorNode)
|
|
828
|
+
: sel.anchorNode.parentElement
|
|
829
|
+
while (node) {
|
|
830
|
+
if (node.getAttribute && node.getAttribute('contenteditable') === 'true') return node
|
|
831
|
+
node = node.parentElement
|
|
832
|
+
}
|
|
833
|
+
return null
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Find the plugin-owned root of the block containing the current selection.
|
|
838
|
+
* Unlike {@link getBlockElement}, this returns the block content root rather
|
|
839
|
+
* than one nested editable field, so block-level settings survive plugins
|
|
840
|
+
* with several text fields.
|
|
841
|
+
* @returns {HTMLElement | null}
|
|
842
|
+
*/
|
|
843
|
+
export function getBlockContentElement() {
|
|
844
|
+
const selection = window.getSelection()
|
|
845
|
+
const block = closestBlock(selection?.anchorNode)
|
|
846
|
+
return block?.firstElementChild instanceof HTMLElement ? block.firstElementChild : null
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Find the contentEditable ancestor of a range's start.
|
|
851
|
+
* Returns null for cross-block ranges (start and end in different contenteditables).
|
|
852
|
+
* @param {Range} range
|
|
853
|
+
* @returns {HTMLElement | null}
|
|
854
|
+
*/
|
|
855
|
+
export function getContentEditable(range) {
|
|
856
|
+
const startNode = range.startContainer
|
|
857
|
+
const startEl = startNode.nodeType === Node.ELEMENT_NODE
|
|
858
|
+
? /** @type {HTMLElement} */ (startNode)
|
|
859
|
+
: startNode.parentElement
|
|
860
|
+
const startCe = startEl?.closest('[contenteditable="true"]') ?? null
|
|
861
|
+
|
|
862
|
+
const endNode = range.endContainer
|
|
863
|
+
const endEl = endNode.nodeType === Node.ELEMENT_NODE
|
|
864
|
+
? /** @type {HTMLElement} */ (endNode)
|
|
865
|
+
: endNode.parentElement
|
|
866
|
+
const endCe = endEl?.closest('[contenteditable="true"]') ?? null
|
|
867
|
+
|
|
868
|
+
if (startCe !== endCe) return null
|
|
869
|
+
|
|
870
|
+
return /** @type {HTMLElement | null} */ (startCe)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Resolve the DOM root to walk for range operations.
|
|
875
|
+
* Uses contenteditable for single-block ranges, commonAncestorContainer for cross-block.
|
|
876
|
+
* @param {Range} range
|
|
877
|
+
* @returns {HTMLElement | null}
|
|
878
|
+
*/
|
|
879
|
+
export function getWalkRoot(range) {
|
|
880
|
+
return getContentEditable(range)
|
|
881
|
+
|| (range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
882
|
+
? /** @type {HTMLElement} */ (range.commonAncestorContainer)
|
|
883
|
+
: range.commonAncestorContainer.parentElement)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Collect text nodes within a range, clipped to range boundaries.
|
|
888
|
+
* @param {HTMLElement} walkRoot
|
|
889
|
+
* @param {Range} range
|
|
890
|
+
* @returns {{ node: Text, startOffset: number, endOffset: number }[]}
|
|
891
|
+
*/
|
|
892
|
+
export function collectTextTargets(walkRoot, range) {
|
|
893
|
+
const walker = editableTextWalker(walkRoot)
|
|
894
|
+
/** @type {{ node: Text, startOffset: number, endOffset: number }[]} */
|
|
895
|
+
const targets = []
|
|
896
|
+
while (walker.nextNode()) {
|
|
897
|
+
const textNode = /** @type {Text} */ (walker.currentNode)
|
|
898
|
+
if (!range.intersectsNode(textNode)) continue
|
|
899
|
+
let startOffset = 0
|
|
900
|
+
let endOffset = textNode.length
|
|
901
|
+
if (textNode === range.startContainer) startOffset = range.startOffset
|
|
902
|
+
if (textNode === range.endContainer) endOffset = range.endOffset
|
|
903
|
+
if (startOffset >= endOffset) continue
|
|
904
|
+
targets.push({ node: textNode, startOffset, endOffset })
|
|
905
|
+
}
|
|
906
|
+
return targets
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Normalize contenteditables after DOM manipulation.
|
|
911
|
+
* @param {HTMLElement | null} singleCe - single contenteditable (null for cross-block)
|
|
912
|
+
* @param {HTMLElement} walkRoot
|
|
913
|
+
* @returns {void}
|
|
914
|
+
*/
|
|
915
|
+
export function normalizeAfterEdit(singleCe, walkRoot) {
|
|
916
|
+
if (singleCe) {
|
|
917
|
+
singleCe.normalize()
|
|
918
|
+
} else {
|
|
919
|
+
const blocks = walkRoot.querySelectorAll('[contenteditable]')
|
|
920
|
+
for (const b of blocks) b.normalize()
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* @typedef {Object} SavedOffsets
|
|
926
|
+
* @property {{ editorRoot: HTMLElement, startBlockId: string, endBlockId: string, startFieldIndex: number, endFieldIndex: number, startOffset: number, endOffset: number } | null} crossOffsets - Serialized selection spanning multiple editable block fields.
|
|
927
|
+
* @property {{ ce: Node, start: number, end: number } | null} singleOffsets - Text offsets and owning editable node for a selection contained in one field.
|
|
928
|
+
*/
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Save selection offsets (cross-block or single-block) before DOM mutation.
|
|
932
|
+
* @param {Range} range
|
|
933
|
+
* @returns {SavedOffsets}
|
|
934
|
+
*/
|
|
935
|
+
export function saveSelectionOffsets(range) {
|
|
936
|
+
const crossOffsets = saveCrossBlockOffsets(range)
|
|
937
|
+
/** @type {{ ce: Node, start: number, end: number } | null} */
|
|
938
|
+
let singleOffsets = null
|
|
939
|
+
if (!crossOffsets) {
|
|
940
|
+
const startEl = range.startContainer.nodeType === Node.ELEMENT_NODE
|
|
941
|
+
? /** @type {HTMLElement} */ (range.startContainer)
|
|
942
|
+
: range.startContainer.parentElement
|
|
943
|
+
const ce = startEl?.closest('[contenteditable="true"]')
|
|
944
|
+
if (ce) {
|
|
945
|
+
singleOffsets = {
|
|
946
|
+
ce,
|
|
947
|
+
start: getTextOffset(ce, range.startContainer, range.startOffset),
|
|
948
|
+
end: getTextOffset(ce, range.endContainer, range.endOffset),
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return { crossOffsets, singleOffsets }
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Restore selection from saved offsets after DOM mutation.
|
|
957
|
+
* @param {import('../types').ICrossBlockSelection | null} cbs
|
|
958
|
+
* @param {SavedOffsets} saved
|
|
959
|
+
* @returns {void}
|
|
960
|
+
*/
|
|
961
|
+
export function restoreSelectionOffsets(cbs, saved) {
|
|
962
|
+
if (saved.crossOffsets) {
|
|
963
|
+
restoreCrossBlockRange(cbs, saved.crossOffsets)
|
|
964
|
+
} else if (saved.singleOffsets) {
|
|
965
|
+
const { ce, start, end } = saved.singleOffsets
|
|
966
|
+
const s = findNodeAtOffset(ce, start)
|
|
967
|
+
const e = findNodeAtOffset(ce, end, 'end')
|
|
968
|
+
if (s && e) {
|
|
969
|
+
try {
|
|
970
|
+
const sel = window.getSelection()
|
|
971
|
+
const r = document.createRange()
|
|
972
|
+
r.setStart(s.node, s.offset)
|
|
973
|
+
r.setEnd(e.node, e.offset)
|
|
974
|
+
sel?.removeAllRanges()
|
|
975
|
+
sel?.addRange(r)
|
|
976
|
+
} catch { /* detached */ }
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Create a standard back button for inline toolbar drill-down panels.
|
|
983
|
+
* @param {{ close(): void }} ctx
|
|
984
|
+
* @returns {HTMLElement}
|
|
985
|
+
*/
|
|
986
|
+
export function createBackButton(ctx) {
|
|
987
|
+
const backBtn = el('button', 'oe-inline-tool oe-inline-tool--back', { type: 'button' })
|
|
988
|
+
backBtn.innerHTML = ICON_BACK
|
|
989
|
+
backBtn.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation() })
|
|
990
|
+
backBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); ctx.close() })
|
|
991
|
+
return backBtn
|
|
992
|
+
}
|