@aquera/nile-elements 1.9.6 → 1.9.7
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/README.md +3 -0
- package/dist/index-178e1e57.cjs.js +2 -0
- package/dist/index-178e1e57.cjs.js.map +1 -0
- package/dist/index-1faa963c.esm.js +1 -0
- package/dist/index-249de90a.esm.js +1 -0
- package/dist/index-353172ed.esm.js +1 -0
- package/dist/index-6f3fa4ab.esm.js +1 -0
- package/dist/index-89c520e9.cjs.js +5 -0
- package/dist/index-89c520e9.cjs.js.map +1 -0
- package/dist/index-920675c4.esm.js +1 -0
- package/dist/index-b387a9f4.cjs.js +2 -0
- package/dist/index-b387a9f4.cjs.js.map +1 -0
- package/dist/index-cc1385e0.cjs.js +2 -0
- package/dist/index-cc1385e0.cjs.js.map +1 -0
- package/dist/index-dedc38a6.esm.js +1 -0
- package/dist/index-df196058.cjs.js +2 -0
- package/dist/index-df196058.cjs.js.map +1 -0
- package/dist/index-f956f56d.cjs.js +7 -0
- package/dist/index-f956f56d.cjs.js.map +1 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +3595 -629
- package/dist/internal/glyph.cjs.js +2 -0
- package/dist/internal/glyph.cjs.js.map +1 -0
- package/dist/internal/glyph.esm.js +5 -0
- package/dist/nile-auto-complete/index.cjs.js +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.cjs.js +1 -1
- package/dist/nile-auto-complete/portal-manager.cjs.js +1 -1
- package/dist/nile-auto-complete/portal-utils.cjs.js +1 -1
- package/dist/nile-code-editor/extensionSetup.cjs.js +7 -7
- package/dist/nile-code-editor/extensionSetup.cjs.js.map +1 -1
- package/dist/nile-code-editor/extensionSetup.esm.js +1 -1
- package/dist/nile-code-editor/index.cjs.js +1 -1
- package/dist/nile-code-editor/index.esm.js +1 -1
- package/dist/nile-code-editor/nile-code-editor.cjs.js +2 -2
- package/dist/nile-code-editor/nile-code-editor.cjs.js.map +1 -1
- package/dist/nile-code-editor/nile-code-editor.esm.js +1 -1
- package/dist/nile-color-picker/index.cjs.js +1 -1
- package/dist/nile-color-picker/nile-color-picker.cjs.js +1 -1
- package/dist/nile-combobox/index.cjs.js +1 -1
- package/dist/nile-combobox/index.esm.js +1 -1
- package/dist/nile-combobox/nile-combobox.cjs.js +1 -1
- package/dist/nile-combobox/nile-combobox.esm.js +1 -1
- package/dist/nile-combobox/portal-manager.cjs.js +1 -1
- package/dist/nile-context-menu/index.cjs.js +1 -1
- package/dist/nile-context-menu/nile-context-menu.cjs.js +1 -1
- package/dist/nile-context-submenu/index.cjs.js +1 -1
- package/dist/nile-context-submenu/nile-context-submenu.cjs.js +1 -1
- package/dist/nile-detail/index.cjs.js +1 -1
- package/dist/nile-detail/index.esm.js +1 -1
- package/dist/nile-detail/nile-detail.cjs.js +1 -1
- package/dist/nile-detail/nile-detail.esm.js +1 -1
- package/dist/nile-dropdown/index.cjs.js +1 -1
- package/dist/nile-dropdown/nile-dropdown.cjs.js +1 -1
- package/dist/nile-dropdown/portal-manager.cjs.js +1 -1
- package/dist/nile-file-upload/index.cjs.js +1 -1
- package/dist/nile-file-upload/nile-file-upload.cjs.js +1 -1
- package/dist/nile-file-upload/nile-file-upload.template.cjs.js +1 -1
- package/dist/nile-floating-panel/index.cjs.js +1 -1
- package/dist/nile-floating-panel/nile-floating-panel.cjs.js +1 -1
- package/dist/nile-lite-tooltip/index.cjs.js +1 -1
- package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js +1 -1
- package/dist/nile-markdown-editor/index.cjs.js +1 -1
- package/dist/nile-markdown-editor/nile-markdown-editor.cjs.js +1 -1
- package/dist/nile-popover/index.cjs.js +1 -1
- package/dist/nile-popover/nile-popover.cjs.js +1 -1
- package/dist/nile-popover/portal-manager.cjs.js +1 -1
- package/dist/nile-popup/index.cjs.js +1 -1
- package/dist/nile-popup/nile-popup.cjs.js +1 -1
- package/dist/nile-select/index.cjs.js +1 -1
- package/dist/nile-select/nile-select.cjs.js +1 -1
- package/dist/nile-select/portal-manager.cjs.js +1 -1
- package/dist/nile-side-bar-action/index.cjs.js +1 -1
- package/dist/nile-side-bar-action/nile-side-bar-action.cjs.js +1 -1
- package/dist/nile-side-bar-action/portal-manager.cjs.js +1 -1
- package/dist/nile-tooltip/index.cjs.js +1 -1
- package/dist/nile-tooltip/nile-tooltip.cjs.js +1 -1
- package/dist/nile-virtual-select/index.cjs.js +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js +1 -1
- package/dist/nile-virtual-select/portal-manager.cjs.js +1 -1
- package/dist/nile-virtual-select/renderer.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/emoji.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/emoji.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/emoji.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/commands.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/commands.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/commands.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/create-state.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/export.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/export.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/export.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/index.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/index.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/index.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/inputrules.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/inputrules.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/inputrules.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/keymap.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/markdown-parse.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/markdown-parse.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/markdown-parse.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/markdown.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/markdown.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/markdown.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/office-paste.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/office-paste.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/office-paste.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/embeds.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/find-replace.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/find-replace.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/find-replace.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/image-view.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/image-view.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/image-view.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/image.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/image.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/image.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/link.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/link.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/link.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/math-view.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/max-length.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/max-length.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/max-length.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/paste.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/paste.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/paste.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/single-line.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/single-line.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/single-line.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/tables.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/task-list.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/task-list.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/task-list.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/sanitize.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/schema.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/schema.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/schema.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/selection-state.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/serializer.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/serializer.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/serializer.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/index.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/index.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/index.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.esm.js +780 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.esm.js +97 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.esm.js +244 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.esm.js +137 -0
- package/dist/nile-wysiwyg-editor/types.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/types.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/types.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/ui/bookmark-panel.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/bookmark-panel.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/bookmark-panel.esm.js +97 -0
- package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/bubble-menu.esm.js +67 -0
- package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/code-block-menu.esm.js +65 -0
- package/dist/nile-wysiwyg-editor/ui/embed-menu.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/embed-menu.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/embed-menu.esm.js +64 -0
- package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/embed-panel.esm.js +68 -0
- package/dist/nile-wysiwyg-editor/ui/emoji-picker.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/emoji-picker.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/emoji-picker.esm.js +96 -0
- package/dist/nile-wysiwyg-editor/ui/find-replace.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/find-replace.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/find-replace.esm.js +112 -0
- package/dist/nile-wysiwyg-editor/ui/image-menu.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/image-menu.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/image-menu.esm.js +64 -0
- package/dist/nile-wysiwyg-editor/ui/image-panel.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/image-panel.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/image-panel.esm.js +171 -0
- package/dist/nile-wysiwyg-editor/ui/insert-menu.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/insert-menu.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/insert-menu.esm.js +90 -0
- package/dist/nile-wysiwyg-editor/ui/link-editor.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/link-editor.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/link-editor.esm.js +114 -0
- package/dist/nile-wysiwyg-editor/ui/markdown-panel.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/markdown-panel.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/markdown-panel.esm.js +80 -0
- package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/math-panel.esm.js +92 -0
- package/dist/nile-wysiwyg-editor/ui/shortcut-help.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/shortcut-help.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/shortcut-help.esm.js +147 -0
- package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/source-view.esm.js +79 -0
- package/dist/nile-wysiwyg-editor/ui/special-chars.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/special-chars.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/special-chars.esm.js +96 -0
- package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/suggest-list.esm.js +67 -0
- package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/table-grid-picker.esm.js +100 -0
- package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/table-menu.esm.js +58 -0
- package/dist/src/index.d.ts +24 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/internal/glyph.d.ts +6 -0
- package/dist/src/internal/glyph.js +13 -0
- package/dist/src/internal/glyph.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/emoji.d.ts +5 -0
- package/dist/src/nile-wysiwyg-editor/emoji.js +138 -0
- package/dist/src/nile-wysiwyg-editor/emoji.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/commands.d.ts +69 -0
- package/dist/src/nile-wysiwyg-editor/engine/commands.js +863 -0
- package/dist/src/nile-wysiwyg-editor/engine/commands.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/create-state.d.ts +35 -0
- package/dist/src/nile-wysiwyg-editor/engine/create-state.js +67 -0
- package/dist/src/nile-wysiwyg-editor/engine/create-state.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/export.d.ts +38 -0
- package/dist/src/nile-wysiwyg-editor/engine/export.js +208 -0
- package/dist/src/nile-wysiwyg-editor/engine/export.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/export.test.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/export.test.js +96 -0
- package/dist/src/nile-wysiwyg-editor/engine/export.test.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/index.d.ts +37 -0
- package/dist/src/nile-wysiwyg-editor/engine/index.js +29 -0
- package/dist/src/nile-wysiwyg-editor/engine/index.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/inputrules.d.ts +12 -0
- package/dist/src/nile-wysiwyg-editor/engine/inputrules.js +144 -0
- package/dist/src/nile-wysiwyg-editor/engine/inputrules.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/keymap.d.ts +15 -0
- package/dist/src/nile-wysiwyg-editor/engine/keymap.js +94 -0
- package/dist/src/nile-wysiwyg-editor/engine/keymap.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.d.ts +22 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.js +287 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.test.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.test.js +106 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.test.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown.d.ts +10 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown.js +162 -0
- package/dist/src/nile-wysiwyg-editor/engine/markdown.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/office-paste.d.ts +13 -0
- package/dist/src/nile-wysiwyg-editor/engine/office-paste.js +182 -0
- package/dist/src/nile-wysiwyg-editor/engine/office-paste.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/code-highlight.d.ts +10 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/code-highlight.js +165 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/code-highlight.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.d.ts +17 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js +69 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.d.ts +17 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js +96 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/find-replace.d.ts +26 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/find-replace.js +123 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/find-replace.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/format-painter.d.ts +28 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/format-painter.js +72 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/format-painter.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/image-view.d.ts +41 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/image-view.js +176 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/image-view.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/image.d.ts +15 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/image.js +64 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/image.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/link.d.ts +11 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/link.js +42 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/link.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.d.ts +18 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js +46 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/max-length.d.ts +7 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/max-length.js +26 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/max-length.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.d.ts +31 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.js +55 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/placeholder.d.ts +7 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/placeholder.js +25 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/placeholder.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/single-line.d.ts +10 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/single-line.js +48 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/single-line.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/suggestions.d.ts +29 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/suggestions.js +87 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/suggestions.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.d.ts +8 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js +11 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/task-list.d.ts +16 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/task-list.js +49 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/task-list.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/pm.test.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/pm.test.js +883 -0
- package/dist/src/nile-wysiwyg-editor/engine/pm.test.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/productivity.test.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/productivity.test.js +129 -0
- package/dist/src/nile-wysiwyg-editor/engine/productivity.test.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.d.ts +13 -0
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.js +155 -0
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/schema.d.ts +20 -0
- package/dist/src/nile-wysiwyg-editor/engine/schema.js +823 -0
- package/dist/src/nile-wysiwyg-editor/engine/schema.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/selection-state.d.ts +70 -0
- package/dist/src/nile-wysiwyg-editor/engine/selection-state.js +138 -0
- package/dist/src/nile-wysiwyg-editor/engine/selection-state.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/serializer.d.ts +18 -0
- package/dist/src/nile-wysiwyg-editor/engine/serializer.js +63 -0
- package/dist/src/nile-wysiwyg-editor/engine/serializer.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/index.d.ts +3 -0
- package/dist/src/nile-wysiwyg-editor/index.js +3 -0
- package/dist/src/nile-wysiwyg-editor/index.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js +782 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.d.ts +364 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js +2038 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js +636 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js +2 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.d.ts +6 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js +251 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.d.ts +41 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js +700 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/types.d.ts +49 -0
- package/dist/src/nile-wysiwyg-editor/types.js +8 -0
- package/dist/src/nile-wysiwyg-editor/types.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/bookmark-panel.d.ts +20 -0
- package/dist/src/nile-wysiwyg-editor/ui/bookmark-panel.js +159 -0
- package/dist/src/nile-wysiwyg-editor/ui/bookmark-panel.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.d.ts +21 -0
- package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js +125 -0
- package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/code-block-menu.d.ts +18 -0
- package/dist/src/nile-wysiwyg-editor/ui/code-block-menu.js +107 -0
- package/dist/src/nile-wysiwyg-editor/ui/code-block-menu.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/embed-menu.d.ts +19 -0
- package/dist/src/nile-wysiwyg-editor/ui/embed-menu.js +123 -0
- package/dist/src/nile-wysiwyg-editor/ui/embed-menu.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/embed-panel.d.ts +27 -0
- package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js +146 -0
- package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/emoji-picker.d.ts +21 -0
- package/dist/src/nile-wysiwyg-editor/ui/emoji-picker.js +154 -0
- package/dist/src/nile-wysiwyg-editor/ui/emoji-picker.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/find-replace.d.ts +26 -0
- package/dist/src/nile-wysiwyg-editor/ui/find-replace.js +178 -0
- package/dist/src/nile-wysiwyg-editor/ui/find-replace.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/image-menu.d.ts +18 -0
- package/dist/src/nile-wysiwyg-editor/ui/image-menu.js +129 -0
- package/dist/src/nile-wysiwyg-editor/ui/image-menu.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/image-panel.d.ts +56 -0
- package/dist/src/nile-wysiwyg-editor/ui/image-panel.js +349 -0
- package/dist/src/nile-wysiwyg-editor/ui/image-panel.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/insert-menu.d.ts +25 -0
- package/dist/src/nile-wysiwyg-editor/ui/insert-menu.js +142 -0
- package/dist/src/nile-wysiwyg-editor/ui/insert-menu.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/link-editor.d.ts +30 -0
- package/dist/src/nile-wysiwyg-editor/ui/link-editor.js +189 -0
- package/dist/src/nile-wysiwyg-editor/ui/link-editor.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/markdown-panel.d.ts +24 -0
- package/dist/src/nile-wysiwyg-editor/ui/markdown-panel.js +144 -0
- package/dist/src/nile-wysiwyg-editor/ui/markdown-panel.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/math-panel.d.ts +31 -0
- package/dist/src/nile-wysiwyg-editor/ui/math-panel.js +172 -0
- package/dist/src/nile-wysiwyg-editor/ui/math-panel.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/shortcut-help.d.ts +30 -0
- package/dist/src/nile-wysiwyg-editor/ui/shortcut-help.js +296 -0
- package/dist/src/nile-wysiwyg-editor/ui/shortcut-help.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/source-view.d.ts +19 -0
- package/dist/src/nile-wysiwyg-editor/ui/source-view.js +120 -0
- package/dist/src/nile-wysiwyg-editor/ui/source-view.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/special-chars.d.ts +20 -0
- package/dist/src/nile-wysiwyg-editor/ui/special-chars.js +199 -0
- package/dist/src/nile-wysiwyg-editor/ui/special-chars.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/suggest-list.d.ts +31 -0
- package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js +133 -0
- package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.d.ts +23 -0
- package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js +177 -0
- package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/table-menu.d.ts +16 -0
- package/dist/src/nile-wysiwyg-editor/ui/table-menu.js +101 -0
- package/dist/src/nile-wysiwyg-editor/ui/table-menu.js.map +1 -0
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -2
- package/src/index.ts +57 -1
- package/src/internal/glyph.ts +13 -0
- package/src/nile-wysiwyg-editor/emoji.ts +137 -0
- package/src/nile-wysiwyg-editor/engine/commands.ts +1035 -0
- package/src/nile-wysiwyg-editor/engine/create-state.ts +117 -0
- package/src/nile-wysiwyg-editor/engine/export.test.ts +113 -0
- package/src/nile-wysiwyg-editor/engine/export.ts +242 -0
- package/src/nile-wysiwyg-editor/engine/index.ts +69 -0
- package/src/nile-wysiwyg-editor/engine/inputrules.ts +169 -0
- package/src/nile-wysiwyg-editor/engine/keymap.ts +146 -0
- package/src/nile-wysiwyg-editor/engine/markdown-parse.test.ts +148 -0
- package/src/nile-wysiwyg-editor/engine/markdown-parse.ts +337 -0
- package/src/nile-wysiwyg-editor/engine/markdown.ts +165 -0
- package/src/nile-wysiwyg-editor/engine/office-paste.ts +199 -0
- package/src/nile-wysiwyg-editor/engine/plugins/code-highlight.ts +173 -0
- package/src/nile-wysiwyg-editor/engine/plugins/embed-view.ts +85 -0
- package/src/nile-wysiwyg-editor/engine/plugins/embeds.ts +99 -0
- package/src/nile-wysiwyg-editor/engine/plugins/find-replace.ts +155 -0
- package/src/nile-wysiwyg-editor/engine/plugins/format-painter.ts +80 -0
- package/src/nile-wysiwyg-editor/engine/plugins/image-view.ts +220 -0
- package/src/nile-wysiwyg-editor/engine/plugins/image.ts +80 -0
- package/src/nile-wysiwyg-editor/engine/plugins/link.ts +42 -0
- package/src/nile-wysiwyg-editor/engine/plugins/math-view.ts +56 -0
- package/src/nile-wysiwyg-editor/engine/plugins/max-length.ts +24 -0
- package/src/nile-wysiwyg-editor/engine/plugins/paste.ts +74 -0
- package/src/nile-wysiwyg-editor/engine/plugins/placeholder.ts +24 -0
- package/src/nile-wysiwyg-editor/engine/plugins/single-line.ts +47 -0
- package/src/nile-wysiwyg-editor/engine/plugins/suggestions.ts +110 -0
- package/src/nile-wysiwyg-editor/engine/plugins/tables.ts +12 -0
- package/src/nile-wysiwyg-editor/engine/plugins/task-list.ts +65 -0
- package/src/nile-wysiwyg-editor/engine/pm.test.ts +1088 -0
- package/src/nile-wysiwyg-editor/engine/productivity.test.ts +163 -0
- package/src/nile-wysiwyg-editor/engine/sanitize.ts +156 -0
- package/src/nile-wysiwyg-editor/engine/schema.ts +859 -0
- package/src/nile-wysiwyg-editor/engine/selection-state.ts +210 -0
- package/src/nile-wysiwyg-editor/engine/serializer.ts +78 -0
- package/src/nile-wysiwyg-editor/index.ts +3 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.ts +782 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.ts +750 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.ts +2155 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.ts +1 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.ts +251 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.ts +772 -0
- package/src/nile-wysiwyg-editor/types.ts +83 -0
- package/src/nile-wysiwyg-editor/ui/bookmark-panel.ts +155 -0
- package/src/nile-wysiwyg-editor/ui/bubble-menu.ts +135 -0
- package/src/nile-wysiwyg-editor/ui/code-block-menu.ts +105 -0
- package/src/nile-wysiwyg-editor/ui/embed-menu.ts +133 -0
- package/src/nile-wysiwyg-editor/ui/embed-panel.ts +143 -0
- package/src/nile-wysiwyg-editor/ui/emoji-picker.ts +152 -0
- package/src/nile-wysiwyg-editor/ui/find-replace.ts +174 -0
- package/src/nile-wysiwyg-editor/ui/image-menu.ts +140 -0
- package/src/nile-wysiwyg-editor/ui/image-panel.ts +337 -0
- package/src/nile-wysiwyg-editor/ui/insert-menu.ts +142 -0
- package/src/nile-wysiwyg-editor/ui/link-editor.ts +180 -0
- package/src/nile-wysiwyg-editor/ui/markdown-panel.ts +143 -0
- package/src/nile-wysiwyg-editor/ui/math-panel.ts +169 -0
- package/src/nile-wysiwyg-editor/ui/shortcut-help.ts +311 -0
- package/src/nile-wysiwyg-editor/ui/source-view.ts +119 -0
- package/src/nile-wysiwyg-editor/ui/special-chars.ts +209 -0
- package/src/nile-wysiwyg-editor/ui/suggest-list.ts +138 -0
- package/src/nile-wysiwyg-editor/ui/table-grid-picker.ts +173 -0
- package/src/nile-wysiwyg-editor/ui/table-menu.ts +109 -0
- package/vscode-html-custom-data.json +450 -0
- package/web-dev-server.config.mjs +6 -2
- package/dist/index-5fef1a0d.esm.js +0 -1
- package/dist/index-8f38fae3.cjs.js +0 -2
- package/dist/index-8f38fae3.cjs.js.map +0 -1
|
@@ -0,0 +1,2155 @@
|
|
|
1
|
+
import { html, TemplateResult, CSSResultArray, PropertyValues, nothing } from 'lit';
|
|
2
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
3
|
+
import { EditorView } from 'prosemirror-view';
|
|
4
|
+
import { EditorState, Transaction, TextSelection } from 'prosemirror-state';
|
|
5
|
+
import { Node as PMNode } from 'prosemirror-model';
|
|
6
|
+
|
|
7
|
+
import NileElement from '../internal/nile-element';
|
|
8
|
+
import { styles } from './nile-wysiwyg-editor.css';
|
|
9
|
+
import {
|
|
10
|
+
defaultSchema,
|
|
11
|
+
createEditorState,
|
|
12
|
+
computeToolbarState,
|
|
13
|
+
htmlToDoc,
|
|
14
|
+
htmlToSlice,
|
|
15
|
+
docToHtml,
|
|
16
|
+
docToText,
|
|
17
|
+
docToMarkdown,
|
|
18
|
+
markdownToHtml,
|
|
19
|
+
docToJson,
|
|
20
|
+
jsonToDoc,
|
|
21
|
+
isDocEmpty,
|
|
22
|
+
sanitizeHtml,
|
|
23
|
+
getCommand,
|
|
24
|
+
getMarkRange,
|
|
25
|
+
flattenToSingleParagraph,
|
|
26
|
+
imageUploadKey,
|
|
27
|
+
findUploadPos,
|
|
28
|
+
countWords,
|
|
29
|
+
TaskItemView,
|
|
30
|
+
ImageView,
|
|
31
|
+
FigureView,
|
|
32
|
+
EmbedView,
|
|
33
|
+
MathView,
|
|
34
|
+
isOfficeHtml,
|
|
35
|
+
cleanOfficeHtml,
|
|
36
|
+
toInlineStyledHtml,
|
|
37
|
+
buildWordDocument,
|
|
38
|
+
buildPrintDocument,
|
|
39
|
+
isFormatPainterArmed,
|
|
40
|
+
armFormatPainter,
|
|
41
|
+
disarmFormatPainter,
|
|
42
|
+
applyFormatPainter,
|
|
43
|
+
getFindState,
|
|
44
|
+
searchTr,
|
|
45
|
+
findStepTr,
|
|
46
|
+
clearFindTr,
|
|
47
|
+
replaceActiveTr,
|
|
48
|
+
replaceAllTr,
|
|
49
|
+
} from './engine';
|
|
50
|
+
import type { ToolbarState } from './engine/selection-state';
|
|
51
|
+
import type { CommandAttrs, CommandName } from './engine/commands';
|
|
52
|
+
import { selectedImageInfo } from './engine/commands';
|
|
53
|
+
import type { MathRenderer } from './engine/plugins/math-view';
|
|
54
|
+
import type { SuggestionMatch } from './engine/plugins/suggestions';
|
|
55
|
+
import type {
|
|
56
|
+
WysiwygChangeDetail,
|
|
57
|
+
WysiwygCommandDetail,
|
|
58
|
+
MentionsConfig,
|
|
59
|
+
SnippetConfig,
|
|
60
|
+
MergeFieldConfig,
|
|
61
|
+
TemplateConfig,
|
|
62
|
+
} from './types';
|
|
63
|
+
import { DEFAULT_TOOLBAR } from './types';
|
|
64
|
+
import { EMOJI } from './emoji';
|
|
65
|
+
|
|
66
|
+
import './nile-wysiwyg-toolbar/nile-wysiwyg-toolbar';
|
|
67
|
+
import './ui/link-editor';
|
|
68
|
+
import './ui/bubble-menu';
|
|
69
|
+
import './ui/image-panel';
|
|
70
|
+
import './ui/table-menu';
|
|
71
|
+
import './ui/suggest-list';
|
|
72
|
+
import './ui/find-replace';
|
|
73
|
+
import './ui/source-view';
|
|
74
|
+
import './ui/embed-panel';
|
|
75
|
+
import './ui/bookmark-panel';
|
|
76
|
+
import './ui/code-block-menu';
|
|
77
|
+
import './ui/shortcut-help';
|
|
78
|
+
import './ui/image-menu';
|
|
79
|
+
import './ui/embed-menu';
|
|
80
|
+
import './ui/special-chars';
|
|
81
|
+
import './ui/emoji-picker';
|
|
82
|
+
import './ui/insert-menu';
|
|
83
|
+
import './ui/math-panel';
|
|
84
|
+
import './ui/markdown-panel';
|
|
85
|
+
import type { NileWysiwygLinkEditor } from './ui/link-editor';
|
|
86
|
+
import type { NileWysiwygBubbleMenu } from './ui/bubble-menu';
|
|
87
|
+
import type { NileWysiwygImagePanel } from './ui/image-panel';
|
|
88
|
+
import type { NileWysiwygTableMenu } from './ui/table-menu';
|
|
89
|
+
import type { NileWysiwygSuggestList, SuggestItem } from './ui/suggest-list';
|
|
90
|
+
import type { NileWysiwygFindBar } from './ui/find-replace';
|
|
91
|
+
import type { NileWysiwygSourceView } from './ui/source-view';
|
|
92
|
+
import type { NileWysiwygEmbedPanel } from './ui/embed-panel';
|
|
93
|
+
import type { NileWysiwygBookmarkPanel } from './ui/bookmark-panel';
|
|
94
|
+
import type { NileWysiwygCodeMenu } from './ui/code-block-menu';
|
|
95
|
+
import type { NileWysiwygShortcutHelp } from './ui/shortcut-help';
|
|
96
|
+
import type { NileWysiwygImageMenu } from './ui/image-menu';
|
|
97
|
+
import type { NileWysiwygEmbedMenu } from './ui/embed-menu';
|
|
98
|
+
import type { NileWysiwygSpecialChars } from './ui/special-chars';
|
|
99
|
+
import type { NileWysiwygEmojiPicker } from './ui/emoji-picker';
|
|
100
|
+
import type { NileWysiwygInsertMenu } from './ui/insert-menu';
|
|
101
|
+
import type { NileWysiwygMathPanel } from './ui/math-panel';
|
|
102
|
+
import type { NileWysiwygMarkdownPanel } from './ui/markdown-panel';
|
|
103
|
+
|
|
104
|
+
interface SlashItem extends SuggestItem {
|
|
105
|
+
command: CommandName | 'openImage' | 'openEmbed' | 'openBookmark';
|
|
106
|
+
attrs?: CommandAttrs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const SLASH_ITEMS: SlashItem[] = [
|
|
110
|
+
{ key: 'heading1', label: 'Heading 1', hint: '#', command: 'heading', attrs: { level: 1 } },
|
|
111
|
+
{ key: 'heading2', label: 'Heading 2', hint: '##', command: 'heading', attrs: { level: 2 } },
|
|
112
|
+
{ key: 'heading3', label: 'Heading 3', hint: '###', command: 'heading', attrs: { level: 3 } },
|
|
113
|
+
{ key: 'bulletList', label: 'Bulleted list', hint: '-', command: 'bulletList' },
|
|
114
|
+
{ key: 'orderedList', label: 'Numbered list', hint: '1.', command: 'orderedList' },
|
|
115
|
+
{ key: 'taskList', label: 'Checklist', hint: '[ ]', command: 'taskList' },
|
|
116
|
+
{ key: 'blockquote', label: 'Blockquote', hint: '>', command: 'blockquote' },
|
|
117
|
+
{ key: 'codeBlock', label: 'Code block', hint: '```', command: 'codeBlock' },
|
|
118
|
+
{ key: 'table', label: 'Table', hint: '3×3', command: 'insertTable', attrs: { rows: 3, cols: 3 } },
|
|
119
|
+
{ key: 'image', label: 'Image', command: 'openImage' },
|
|
120
|
+
{ key: 'embed', label: 'Video embed', hint: 'YouTube…', command: 'openEmbed' },
|
|
121
|
+
{ key: 'bookmark', label: 'Bookmark (anchor)', command: 'openBookmark' },
|
|
122
|
+
{ key: 'horizontalRule', label: 'Divider', hint: '---', command: 'horizontalRule' },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
let uploadCounter = 0;
|
|
126
|
+
|
|
127
|
+
function readFileAsDataURL(file: File): Promise<string> {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const reader = new FileReader();
|
|
130
|
+
reader.onload = () => resolve(reader.result as string);
|
|
131
|
+
reader.onerror = () => reject(reader.error);
|
|
132
|
+
reader.readAsDataURL(file);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const CHANGE_DEBOUNCE_MS = 250;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* WYSIWYG rich text editor powered by ProseMirror.
|
|
140
|
+
*
|
|
141
|
+
* Requires evergreen browsers: selection inside shadow DOM relies on
|
|
142
|
+
* `Selection.getComposedRanges()` (Safari 17+, recent Firefox) or Chrome's
|
|
143
|
+
* `shadowRoot.getSelection()`.
|
|
144
|
+
*
|
|
145
|
+
* @tag nile-wysiwyg-editor
|
|
146
|
+
*
|
|
147
|
+
* @event nile-change - Debounced content change. detail: { value, json, content }
|
|
148
|
+
* @event nile-input - Every document change. detail: { value, json, content }
|
|
149
|
+
* @event nile-focus - Editor gained focus.
|
|
150
|
+
* @event nile-blur - Editor lost focus.
|
|
151
|
+
* @event nile-selection-change - detail: ToolbarState snapshot.
|
|
152
|
+
* @event nile-image-upload-request - Cancelable. detail: { file, insert(src, alt) }.
|
|
153
|
+
* Call preventDefault() and use insert() to fully own the upload.
|
|
154
|
+
* @event nile-image-upload-error - detail: { file, error } when uploadHandler rejects.
|
|
155
|
+
* @event nile-mention-query - detail: { trigger, query, provide(items) } for async sources.
|
|
156
|
+
* @event nile-mention-select - detail: { trigger, key, label } when a mention is inserted.
|
|
157
|
+
* @event nile-word-count - detail: { words, characters } on every document change.
|
|
158
|
+
* @event nile-autosave - detail: { value, json, content } on the autosave interval.
|
|
159
|
+
*
|
|
160
|
+
* @csspart base - Outer container.
|
|
161
|
+
* @csspart toolbar - The toolbar element.
|
|
162
|
+
* @csspart surface - The scrollable editing surface.
|
|
163
|
+
*/
|
|
164
|
+
@customElement('nile-wysiwyg-editor')
|
|
165
|
+
export class NileWysiwygEditor extends NileElement {
|
|
166
|
+
static formAssociated = true;
|
|
167
|
+
|
|
168
|
+
/** Document content as HTML. Not reflected to the attribute. */
|
|
169
|
+
@property({ type: String }) value = '';
|
|
170
|
+
|
|
171
|
+
@property({ type: String }) placeholder = '';
|
|
172
|
+
|
|
173
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
174
|
+
|
|
175
|
+
@property({ type: Boolean, reflect: true }) readonly = false;
|
|
176
|
+
|
|
177
|
+
/** Toolbar config string; `"none"` hides the toolbar. */
|
|
178
|
+
@property({ type: String }) toolbar = DEFAULT_TOOLBAR;
|
|
179
|
+
|
|
180
|
+
/** Shows a balloon toolbar over text selections. */
|
|
181
|
+
@property({ type: Boolean, attribute: 'bubble-menu' }) bubbleMenu = false;
|
|
182
|
+
|
|
183
|
+
/** Sanitizes incoming HTML (value, paste, insertHTML). Leave on. */
|
|
184
|
+
@property({ type: Boolean }) sanitize = true;
|
|
185
|
+
|
|
186
|
+
/** Constrains content to a single paragraph. */
|
|
187
|
+
@property({ type: Boolean, attribute: 'singleline' }) singleLine = false;
|
|
188
|
+
|
|
189
|
+
/** Form field name. */
|
|
190
|
+
@property({ type: String }) name = '';
|
|
191
|
+
|
|
192
|
+
@property({ type: Boolean, reflect: true }) required = false;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Async image upload handler. When set, pasted/dropped/picked image files
|
|
196
|
+
* are uploaded through it (an "Uploading…" placeholder shows meanwhile)
|
|
197
|
+
* and the returned src is inserted.
|
|
198
|
+
*/
|
|
199
|
+
@property({ attribute: false }) uploadHandler?: (
|
|
200
|
+
file: File
|
|
201
|
+
) => Promise<{ src: string; alt?: string }>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Fallback when no uploadHandler is set and no nile-image-upload-request
|
|
205
|
+
* listener takes over: embed image files as base64 data URLs.
|
|
206
|
+
*/
|
|
207
|
+
@property({ attribute: false }) allowBase64Images = true;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Shows an "Upload from computer" button in the Insert Image dialog. Off by
|
|
211
|
+
* default (URL-only, like the RTE's default); files can always be added via
|
|
212
|
+
* drag & drop or paste regardless.
|
|
213
|
+
*/
|
|
214
|
+
@property({ type: Boolean, attribute: 'upload-from-computer' })
|
|
215
|
+
uploadFromComputer = false;
|
|
216
|
+
|
|
217
|
+
/** Max upload size (bytes) shown/enforced by the image dialog. Default 2 MB. */
|
|
218
|
+
@property({ type: Number, attribute: 'image-max-file-size' })
|
|
219
|
+
imageMaxFileSize = 2 * 1024 * 1024;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Mention config: trigger character → items, same shape and JSON attribute
|
|
223
|
+
* format as nile-rich-text-editor. For async sources, configure the
|
|
224
|
+
* trigger with an empty array and listen to nile-mention-query.
|
|
225
|
+
*/
|
|
226
|
+
@property({
|
|
227
|
+
attribute: 'mentions',
|
|
228
|
+
converter: {
|
|
229
|
+
fromAttribute: (value: string): MentionsConfig => {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(value);
|
|
232
|
+
const out: MentionsConfig = {};
|
|
233
|
+
for (const trigger of Object.keys(parsed)) {
|
|
234
|
+
const items = parsed[trigger];
|
|
235
|
+
if (Array.isArray(items)) {
|
|
236
|
+
out[trigger] = items
|
|
237
|
+
.filter(i => i && typeof i.key === 'string' && typeof i.label === 'string')
|
|
238
|
+
.map(i => ({ key: i.key, label: i.label }));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
} catch {
|
|
243
|
+
return {};
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
toAttribute: (value: MentionsConfig) => JSON.stringify(value),
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
mentions: MentionsConfig = {};
|
|
250
|
+
|
|
251
|
+
/** Disables the "/" command menu. */
|
|
252
|
+
@property({ type: Boolean, attribute: 'no-slash-commands' }) noSlashCommands = false;
|
|
253
|
+
|
|
254
|
+
/** Disables the ":" emoji picker. */
|
|
255
|
+
@property({ type: Boolean, attribute: 'no-emoji' }) noEmoji = false;
|
|
256
|
+
|
|
257
|
+
/** Maximum character count; further input is rejected. 0 = unlimited. */
|
|
258
|
+
@property({ type: Number, attribute: 'maxlength' }) maxLength = 0;
|
|
259
|
+
|
|
260
|
+
/** Always show the word/character counter footer (implied by maxlength). */
|
|
261
|
+
@property({ type: Boolean, attribute: 'show-counter' }) showCounter = false;
|
|
262
|
+
|
|
263
|
+
/** Keeps the toolbar pinned to the viewport while scrolling long content. */
|
|
264
|
+
@property({ type: Boolean, attribute: 'sticky-toolbar', reflect: true })
|
|
265
|
+
stickyToolbar = false;
|
|
266
|
+
|
|
267
|
+
/** Reusable snippets surfaced in the "/" menu (JSON attribute). */
|
|
268
|
+
@property({
|
|
269
|
+
attribute: 'snippets',
|
|
270
|
+
converter: {
|
|
271
|
+
fromAttribute: (value: string): SnippetConfig[] => {
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(value);
|
|
274
|
+
return Array.isArray(parsed)
|
|
275
|
+
? parsed.filter(
|
|
276
|
+
s =>
|
|
277
|
+
s &&
|
|
278
|
+
typeof s.key === 'string' &&
|
|
279
|
+
typeof s.label === 'string' &&
|
|
280
|
+
typeof s.html === 'string'
|
|
281
|
+
)
|
|
282
|
+
: [];
|
|
283
|
+
} catch {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
toAttribute: (value: SnippetConfig[]) => JSON.stringify(value),
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
snippets: SnippetConfig[] = [];
|
|
291
|
+
|
|
292
|
+
/** Merge fields offered in the merge-field picker (JSON attribute). */
|
|
293
|
+
@property({
|
|
294
|
+
attribute: 'merge-fields',
|
|
295
|
+
converter: {
|
|
296
|
+
fromAttribute: (value: string): MergeFieldConfig[] => {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(value);
|
|
299
|
+
return Array.isArray(parsed)
|
|
300
|
+
? parsed.filter(f => f && typeof f.id === 'string' && typeof f.label === 'string')
|
|
301
|
+
: [];
|
|
302
|
+
} catch {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
toAttribute: (value: MergeFieldConfig[]) => JSON.stringify(value),
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
mergeFields: MergeFieldConfig[] = [];
|
|
310
|
+
|
|
311
|
+
/** Content templates offered in the template picker (JSON attribute). */
|
|
312
|
+
@property({
|
|
313
|
+
attribute: 'templates',
|
|
314
|
+
converter: {
|
|
315
|
+
fromAttribute: (value: string): TemplateConfig[] => {
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(value);
|
|
318
|
+
return Array.isArray(parsed)
|
|
319
|
+
? parsed.filter(t => t && typeof t.label === 'string' && typeof t.html === 'string')
|
|
320
|
+
: [];
|
|
321
|
+
} catch {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
toAttribute: (value: TemplateConfig[]) => JSON.stringify(value),
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
templates: TemplateConfig[] = [];
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Renders a math node's LaTeX to HTML (KaTeX/MathJax/MathML). When unset,
|
|
332
|
+
* the raw LaTeX is shown. Same trust level as uploadHandler — output is
|
|
333
|
+
* inserted as HTML.
|
|
334
|
+
*/
|
|
335
|
+
@property({ attribute: false }) mathRenderer?: MathRenderer;
|
|
336
|
+
|
|
337
|
+
/** Emit nile-autosave every N ms while the document has changed. 0 = off. */
|
|
338
|
+
@property({ type: Number, attribute: 'autosave-interval' }) autosaveInterval = 0;
|
|
339
|
+
|
|
340
|
+
/** When on, every paste drops formatting and inserts plain text. */
|
|
341
|
+
@property({ type: Boolean, attribute: 'paste-as-plain-text', reflect: true })
|
|
342
|
+
pasteAsPlainText = false;
|
|
343
|
+
|
|
344
|
+
/** Disables Markdown detection on plain-text pastes. */
|
|
345
|
+
@property({ type: Boolean, attribute: 'no-markdown-paste' })
|
|
346
|
+
noMarkdownPaste = false;
|
|
347
|
+
|
|
348
|
+
/** Disables typography autocorrect (smart quotes, dashes, (c) → ©). */
|
|
349
|
+
@property({ type: Boolean, attribute: 'no-text-transform' })
|
|
350
|
+
noTextTransform = false;
|
|
351
|
+
|
|
352
|
+
/** Expands the editor to fill the viewport. Esc exits. */
|
|
353
|
+
@property({ type: Boolean, reflect: true }) fullscreen = false;
|
|
354
|
+
|
|
355
|
+
/** Outlines block boundaries (paragraphs, headings, lists…) while editing. */
|
|
356
|
+
@property({ type: Boolean, attribute: 'show-blocks', reflect: true })
|
|
357
|
+
showBlocks = false;
|
|
358
|
+
|
|
359
|
+
@state() private toolbarState?: ToolbarState;
|
|
360
|
+
|
|
361
|
+
@state() private hasFocus = false;
|
|
362
|
+
|
|
363
|
+
@state() private sourceMode = false;
|
|
364
|
+
|
|
365
|
+
@state() private counter = { words: 0, characters: 0 };
|
|
366
|
+
|
|
367
|
+
/** Which list the shared insert-menu is showing, if open. */
|
|
368
|
+
@state() private insertMenuMode: 'template' | 'merge' | null = null;
|
|
369
|
+
|
|
370
|
+
@query('.nile-wysiwyg') private baseEl!: HTMLElement;
|
|
371
|
+
|
|
372
|
+
@query('nile-wysiwyg-toolbar') private toolbarEl?: HTMLElement;
|
|
373
|
+
|
|
374
|
+
@query('.nile-wysiwyg__surface') private surfaceEl!: HTMLElement;
|
|
375
|
+
|
|
376
|
+
@query('nile-wysiwyg-link-editor') private linkEditorEl!: NileWysiwygLinkEditor;
|
|
377
|
+
|
|
378
|
+
@query('nile-wysiwyg-bubble-menu') private bubbleMenuEl!: NileWysiwygBubbleMenu;
|
|
379
|
+
|
|
380
|
+
@query('nile-wysiwyg-image-panel') private imagePanelEl!: NileWysiwygImagePanel;
|
|
381
|
+
|
|
382
|
+
@query('nile-wysiwyg-table-menu') private tableMenuEl!: NileWysiwygTableMenu;
|
|
383
|
+
|
|
384
|
+
@query('nile-wysiwyg-suggest-list') private suggestListEl!: NileWysiwygSuggestList;
|
|
385
|
+
|
|
386
|
+
@query('nile-wysiwyg-find-bar') private findBarEl!: NileWysiwygFindBar;
|
|
387
|
+
|
|
388
|
+
@query('nile-wysiwyg-source-view') private sourceViewEl?: NileWysiwygSourceView;
|
|
389
|
+
|
|
390
|
+
@query('nile-wysiwyg-embed-panel') private embedPanelEl!: NileWysiwygEmbedPanel;
|
|
391
|
+
|
|
392
|
+
@query('nile-wysiwyg-bookmark-panel') private bookmarkPanelEl!: NileWysiwygBookmarkPanel;
|
|
393
|
+
|
|
394
|
+
@query('nile-wysiwyg-code-menu') private codeMenuEl!: NileWysiwygCodeMenu;
|
|
395
|
+
|
|
396
|
+
@query('nile-wysiwyg-shortcut-help') private shortcutHelpEl!: NileWysiwygShortcutHelp;
|
|
397
|
+
|
|
398
|
+
@query('nile-wysiwyg-image-menu') private imageMenuEl!: NileWysiwygImageMenu;
|
|
399
|
+
|
|
400
|
+
@query('nile-wysiwyg-embed-menu') private embedMenuEl!: NileWysiwygEmbedMenu;
|
|
401
|
+
|
|
402
|
+
@query('nile-wysiwyg-special-chars') private specialCharsEl!: NileWysiwygSpecialChars;
|
|
403
|
+
|
|
404
|
+
@query('nile-wysiwyg-emoji-picker') private emojiPickerEl!: NileWysiwygEmojiPicker;
|
|
405
|
+
|
|
406
|
+
@query('nile-wysiwyg-insert-menu') private insertMenuEl!: NileWysiwygInsertMenu;
|
|
407
|
+
|
|
408
|
+
@query('nile-wysiwyg-math-panel') private mathPanelEl!: NileWysiwygMathPanel;
|
|
409
|
+
|
|
410
|
+
@query('nile-wysiwyg-markdown-panel') private markdownPanelEl!: NileWysiwygMarkdownPanel;
|
|
411
|
+
|
|
412
|
+
private view?: EditorView;
|
|
413
|
+
|
|
414
|
+
private mentionMatch: SuggestionMatch | null = null;
|
|
415
|
+
|
|
416
|
+
private slashMatch: SuggestionMatch | null = null;
|
|
417
|
+
|
|
418
|
+
private emojiMatch: SuggestionMatch | null = null;
|
|
419
|
+
|
|
420
|
+
private activeSuggestSource: 'mention' | 'slash' | 'emoji' | null = null;
|
|
421
|
+
|
|
422
|
+
private suggestDismissed: string | null = null;
|
|
423
|
+
|
|
424
|
+
private mentionQueryToken = '';
|
|
425
|
+
|
|
426
|
+
private autosaveTimer?: ReturnType<typeof setInterval>;
|
|
427
|
+
|
|
428
|
+
private lastAutosaveValue = '';
|
|
429
|
+
|
|
430
|
+
private internals?: ElementInternals;
|
|
431
|
+
|
|
432
|
+
private defaultValue = '';
|
|
433
|
+
|
|
434
|
+
private syncingValue = false;
|
|
435
|
+
|
|
436
|
+
private changeTimer?: ReturnType<typeof setTimeout>;
|
|
437
|
+
|
|
438
|
+
private pendingChange = false;
|
|
439
|
+
|
|
440
|
+
/** Body overflow value before fullscreen locked it; null = not locked. */
|
|
441
|
+
private previousBodyOverflow: string | null = null;
|
|
442
|
+
|
|
443
|
+
public static get styles(): CSSResultArray {
|
|
444
|
+
return [styles];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
constructor() {
|
|
448
|
+
super();
|
|
449
|
+
if (typeof this.attachInternals === 'function') {
|
|
450
|
+
this.internals = this.attachInternals();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
connectedCallback(): void {
|
|
455
|
+
super.connectedCallback();
|
|
456
|
+
// Remount after a disconnect/reconnect (e.g. host moved in the DOM).
|
|
457
|
+
if (this.hasUpdated && !this.view) {
|
|
458
|
+
this.updateComplete.then(() => this.mountView());
|
|
459
|
+
}
|
|
460
|
+
// Close floating panels when the user clicks anywhere outside them.
|
|
461
|
+
document.addEventListener('pointerdown', this.onOutsidePointerDown, true);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
disconnectedCallback(): void {
|
|
465
|
+
super.disconnectedCallback();
|
|
466
|
+
document.removeEventListener('pointerdown', this.onOutsidePointerDown, true);
|
|
467
|
+
if (this.changeTimer) clearTimeout(this.changeTimer);
|
|
468
|
+
if (this.autosaveTimer) clearInterval(this.autosaveTimer);
|
|
469
|
+
if (this.previousBodyOverflow !== null) {
|
|
470
|
+
document.body.style.overflow = this.previousBodyOverflow;
|
|
471
|
+
this.previousBodyOverflow = null;
|
|
472
|
+
}
|
|
473
|
+
this.view?.destroy();
|
|
474
|
+
this.view = undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
protected firstUpdated(): void {
|
|
478
|
+
this.defaultValue = this.value;
|
|
479
|
+
this.mountView();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
protected updated(changed: PropertyValues): void {
|
|
483
|
+
if (changed.has('value') && this.view && !this.syncingValue) {
|
|
484
|
+
const current = docToHtml(this.view.state.doc);
|
|
485
|
+
if (current !== this.value) this.setContent(this.value);
|
|
486
|
+
}
|
|
487
|
+
this.syncingValue = false;
|
|
488
|
+
|
|
489
|
+
if ((changed.has('disabled') || changed.has('readonly')) && this.view) {
|
|
490
|
+
this.view.setProps({});
|
|
491
|
+
}
|
|
492
|
+
if (changed.has('placeholder') && this.view) {
|
|
493
|
+
// Decorations are recomputed on any transaction.
|
|
494
|
+
this.view.dispatch(this.view.state.tr);
|
|
495
|
+
}
|
|
496
|
+
if (changed.has('required')) this.updateValidity();
|
|
497
|
+
if (changed.has('autosaveInterval')) this.restartAutosave();
|
|
498
|
+
if (
|
|
499
|
+
(changed.has('pasteAsPlainText') ||
|
|
500
|
+
changed.has('fullscreen') ||
|
|
501
|
+
changed.has('showBlocks')) &&
|
|
502
|
+
this.view
|
|
503
|
+
) {
|
|
504
|
+
this.toolbarState = this.snapshotToolbar(this.view.state);
|
|
505
|
+
}
|
|
506
|
+
// Input rules are baked into the state at build time; rebuild in place
|
|
507
|
+
// (same doc, fresh plugins) when the autocorrect toggle changes.
|
|
508
|
+
if (changed.has('noTextTransform') && this.view && !changed.has('value')) {
|
|
509
|
+
this.view.updateState(this.buildState(this.view.state.doc));
|
|
510
|
+
}
|
|
511
|
+
// Lock page scrolling while fullscreen covers the viewport.
|
|
512
|
+
if (changed.has('fullscreen')) {
|
|
513
|
+
if (this.fullscreen) {
|
|
514
|
+
if (this.previousBodyOverflow === null) {
|
|
515
|
+
this.previousBodyOverflow = document.body.style.overflow;
|
|
516
|
+
document.body.style.overflow = 'hidden';
|
|
517
|
+
}
|
|
518
|
+
} else if (this.previousBodyOverflow !== null) {
|
|
519
|
+
document.body.style.overflow = this.previousBodyOverflow;
|
|
520
|
+
this.previousBodyOverflow = null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private restartAutosave(): void {
|
|
526
|
+
if (this.autosaveTimer) clearInterval(this.autosaveTimer);
|
|
527
|
+
this.autosaveTimer = undefined;
|
|
528
|
+
if (this.autosaveInterval > 0) {
|
|
529
|
+
this.lastAutosaveValue = this.value;
|
|
530
|
+
this.autosaveTimer = setInterval(() => {
|
|
531
|
+
if (!this.view || this.value === this.lastAutosaveValue) return;
|
|
532
|
+
this.lastAutosaveValue = this.value;
|
|
533
|
+
this.emit('nile-autosave', this.buildChangeDetail(this.view.state.doc));
|
|
534
|
+
}, this.autosaveInterval);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ------------------------------------------------------------------ view
|
|
539
|
+
|
|
540
|
+
private buildState(doc?: PMNode): EditorState {
|
|
541
|
+
return createEditorState({
|
|
542
|
+
schema: defaultSchema,
|
|
543
|
+
doc,
|
|
544
|
+
getPlaceholder: () => this.placeholder,
|
|
545
|
+
singleLine: this.singleLine,
|
|
546
|
+
onLinkShortcut: () => this.openLinkEditor(),
|
|
547
|
+
onFindShortcut: () => this.openFindBar(),
|
|
548
|
+
onShortcutHelp: () => this.openShortcutHelp(),
|
|
549
|
+
onImageFiles: (files, pos) => {
|
|
550
|
+
for (const file of files) this.insertImageFile(file, pos);
|
|
551
|
+
},
|
|
552
|
+
mentions: {
|
|
553
|
+
getTriggers: () => Object.keys(this.mentions),
|
|
554
|
+
onChange: match => {
|
|
555
|
+
this.mentionMatch = match;
|
|
556
|
+
this.updateSuggestList();
|
|
557
|
+
},
|
|
558
|
+
onKeyDown: event => this.handleSuggestKey(event, 'mention'),
|
|
559
|
+
},
|
|
560
|
+
slashCommands: {
|
|
561
|
+
onChange: match => {
|
|
562
|
+
this.slashMatch = match;
|
|
563
|
+
this.updateSuggestList();
|
|
564
|
+
},
|
|
565
|
+
onKeyDown: event => this.handleSuggestKey(event, 'slash'),
|
|
566
|
+
},
|
|
567
|
+
emoji: {
|
|
568
|
+
onChange: match => {
|
|
569
|
+
this.emojiMatch = match;
|
|
570
|
+
this.updateSuggestList();
|
|
571
|
+
},
|
|
572
|
+
onKeyDown: event => this.handleSuggestKey(event, 'emoji'),
|
|
573
|
+
},
|
|
574
|
+
getMaxLength: () => this.maxLength,
|
|
575
|
+
getForcePlainText: () => this.pasteAsPlainText,
|
|
576
|
+
getMarkdownPaste: () => !this.noMarkdownPaste,
|
|
577
|
+
getSanitize: () => this.sanitize,
|
|
578
|
+
textTransform: !this.noTextTransform,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* ToolbarState snapshot augmented with host-only flags the pure
|
|
584
|
+
* computeToolbarState can't know about (e.g. the paste-as-plain-text toggle).
|
|
585
|
+
*/
|
|
586
|
+
private snapshotToolbar(state: EditorState): ToolbarState {
|
|
587
|
+
return {
|
|
588
|
+
...computeToolbarState(state),
|
|
589
|
+
pasteAsPlainText: this.pasteAsPlainText,
|
|
590
|
+
fullscreen: this.fullscreen,
|
|
591
|
+
showBlocks: this.showBlocks,
|
|
592
|
+
formatPainter: isFormatPainterArmed(state),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private mountView(): void {
|
|
597
|
+
if (this.view || !this.surfaceEl) return;
|
|
598
|
+
let doc = this.value
|
|
599
|
+
? htmlToDoc(defaultSchema, this.value, this.sanitize)
|
|
600
|
+
: undefined;
|
|
601
|
+
if (doc && this.singleLine) doc = flattenToSingleParagraph(doc);
|
|
602
|
+
|
|
603
|
+
this.view = new EditorView(this.surfaceEl, {
|
|
604
|
+
state: this.buildState(doc),
|
|
605
|
+
dispatchTransaction: tr => this.handleTransaction(tr),
|
|
606
|
+
editable: () => !this.disabled && !this.readonly,
|
|
607
|
+
transformPastedHTML: pasted => {
|
|
608
|
+
// Word/Google Docs clipboard HTML gets normalized (real lists, no
|
|
609
|
+
// mso noise) before the sanitizer clamps it to the schema.
|
|
610
|
+
const cleaned = isOfficeHtml(pasted) ? cleanOfficeHtml(pasted) : pasted;
|
|
611
|
+
return this.sanitize ? sanitizeHtml(cleaned) : cleaned;
|
|
612
|
+
},
|
|
613
|
+
handleDOMEvents: {
|
|
614
|
+
mousedown: () => {
|
|
615
|
+
// Clicking into the document dismisses the never-focused popovers
|
|
616
|
+
// (they take no focus, so blur can't close them).
|
|
617
|
+
this.closeSpecialChars(false);
|
|
618
|
+
this.closeInsertMenu(false);
|
|
619
|
+
return false;
|
|
620
|
+
},
|
|
621
|
+
focus: () => {
|
|
622
|
+
this.hasFocus = true;
|
|
623
|
+
this.updateOverlays();
|
|
624
|
+
this.emit('nile-focus');
|
|
625
|
+
return false;
|
|
626
|
+
},
|
|
627
|
+
blur: (_view, event) => {
|
|
628
|
+
// Focus moving into the editor's own chrome (language picker,
|
|
629
|
+
// panels) is not a blur — closing overlays here would dismiss
|
|
630
|
+
// the control the user just clicked.
|
|
631
|
+
const related = (event as FocusEvent).relatedTarget as Node | null;
|
|
632
|
+
if (related && (this.shadowRoot?.contains(related) || this.contains(related))) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
this.hasFocus = false;
|
|
636
|
+
this.flushChange();
|
|
637
|
+
this.updateOverlays();
|
|
638
|
+
this.closeSuggestList();
|
|
639
|
+
this.closeSpecialChars(false);
|
|
640
|
+
this.emit('nile-blur');
|
|
641
|
+
return false;
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
attributes: {
|
|
645
|
+
role: 'textbox',
|
|
646
|
+
'aria-multiline': this.singleLine ? 'false' : 'true',
|
|
647
|
+
},
|
|
648
|
+
nodeViews: {
|
|
649
|
+
task_item: (node, view, getPos) =>
|
|
650
|
+
new TaskItemView(node, view, getPos as () => number | undefined),
|
|
651
|
+
image: (node, view, getPos) =>
|
|
652
|
+
new ImageView(node, view, getPos as () => number | undefined),
|
|
653
|
+
figure: (node, view, getPos) =>
|
|
654
|
+
new FigureView(node, view, getPos as () => number | undefined),
|
|
655
|
+
embed: (node, view, getPos) =>
|
|
656
|
+
new EmbedView(node, view, getPos as () => number | undefined),
|
|
657
|
+
math: node => new MathView(node, this.mathRenderer),
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
this.toolbarState = this.snapshotToolbar(this.view.state);
|
|
662
|
+
this.counter = countWords(this.view.state.doc);
|
|
663
|
+
this.syncFormValue(this.view.state.doc);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private handleTransaction(tr: Transaction): void {
|
|
667
|
+
if (!this.view) return;
|
|
668
|
+
const newState = this.view.state.apply(tr);
|
|
669
|
+
this.view.updateState(newState);
|
|
670
|
+
|
|
671
|
+
// Format painter: once armed, the next non-empty selection the user makes
|
|
672
|
+
// receives the copied formatting, then the painter disarms.
|
|
673
|
+
if (tr.selectionSet && !newState.selection.empty && isFormatPainterArmed(newState)) {
|
|
674
|
+
const applyTr = applyFormatPainter(newState);
|
|
675
|
+
if (applyTr) {
|
|
676
|
+
this.view.dispatch(applyTr);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.toolbarState = this.snapshotToolbar(newState);
|
|
682
|
+
this.emit('nile-selection-change', this.toolbarState);
|
|
683
|
+
this.updateOverlays();
|
|
684
|
+
|
|
685
|
+
if (this.findBarEl?.open) {
|
|
686
|
+
const find = getFindState(newState);
|
|
687
|
+
this.findBarEl.matchCount = find.matches.length;
|
|
688
|
+
this.findBarEl.activeIndex = find.activeIndex;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (tr.docChanged) {
|
|
692
|
+
const detail = this.buildChangeDetail(newState.doc);
|
|
693
|
+
this.syncingValue = true;
|
|
694
|
+
this.value = detail.value;
|
|
695
|
+
this.syncFormValue(newState.doc);
|
|
696
|
+
this.emit('nile-input', detail);
|
|
697
|
+
this.counter = countWords(newState.doc);
|
|
698
|
+
this.emit('nile-word-count', this.counter);
|
|
699
|
+
this.scheduleChange();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private buildChangeDetail(doc: PMNode): WysiwygChangeDetail {
|
|
704
|
+
const value = isDocEmpty(doc) ? '' : docToHtml(doc);
|
|
705
|
+
return { value, json: docToJson(doc), content: value };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private scheduleChange(): void {
|
|
709
|
+
this.pendingChange = true;
|
|
710
|
+
if (this.changeTimer) clearTimeout(this.changeTimer);
|
|
711
|
+
this.changeTimer = setTimeout(() => this.flushChange(), CHANGE_DEBOUNCE_MS);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private flushChange(): void {
|
|
715
|
+
if (!this.pendingChange || !this.view) return;
|
|
716
|
+
this.pendingChange = false;
|
|
717
|
+
if (this.changeTimer) clearTimeout(this.changeTimer);
|
|
718
|
+
this.emit('nile-change', this.buildChangeDetail(this.view.state.doc));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ------------------------------------------------------------------ form
|
|
722
|
+
|
|
723
|
+
private syncFormValue(doc: PMNode): void {
|
|
724
|
+
if (!this.internals) return;
|
|
725
|
+
const value = isDocEmpty(doc) ? '' : docToHtml(doc);
|
|
726
|
+
this.internals.setFormValue(value === '' ? null : value);
|
|
727
|
+
this.updateValidity();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private updateValidity(): void {
|
|
731
|
+
if (!this.internals) return;
|
|
732
|
+
const empty = this.view ? isDocEmpty(this.view.state.doc) : this.value === '';
|
|
733
|
+
if (this.required && empty) {
|
|
734
|
+
this.internals.setValidity(
|
|
735
|
+
{ valueMissing: true },
|
|
736
|
+
'Please fill out this field.',
|
|
737
|
+
this.surfaceEl ?? undefined
|
|
738
|
+
);
|
|
739
|
+
} else {
|
|
740
|
+
this.internals.setValidity({});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
formResetCallback(): void {
|
|
745
|
+
this.setContent(this.defaultValue);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
formDisabledCallback(disabled: boolean): void {
|
|
749
|
+
this.disabled = disabled;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
public checkValidity(): boolean {
|
|
753
|
+
return this.internals ? this.internals.checkValidity() : true;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
public reportValidity(): boolean {
|
|
757
|
+
return this.internals ? this.internals.reportValidity() : true;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// -------------------------------------------------------------- overlays
|
|
761
|
+
|
|
762
|
+
/** Positions an overlay element near a doc position (default: selection). */
|
|
763
|
+
private positionOverlay(
|
|
764
|
+
el: HTMLElement,
|
|
765
|
+
placement: 'above' | 'below',
|
|
766
|
+
pos?: number
|
|
767
|
+
): void {
|
|
768
|
+
if (!this.view || !this.baseEl) return;
|
|
769
|
+
const from = pos ?? this.view.state.selection.from;
|
|
770
|
+
const coords = this.view.coordsAtPos(from);
|
|
771
|
+
const baseRect = this.baseEl.getBoundingClientRect();
|
|
772
|
+
const left = Math.max(
|
|
773
|
+
4,
|
|
774
|
+
Math.min(coords.left - baseRect.left, baseRect.width - el.offsetWidth - 4)
|
|
775
|
+
);
|
|
776
|
+
el.style.left = `${left}px`;
|
|
777
|
+
if (placement === 'above') {
|
|
778
|
+
el.style.top = `${Math.max(4, coords.top - baseRect.top - el.offsetHeight - 8)}px`;
|
|
779
|
+
} else {
|
|
780
|
+
el.style.top = `${coords.bottom - baseRect.top + 6}px`;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Positions a floating panel directly below a toolbar/anchor element. */
|
|
785
|
+
private positionOverlayBelowAnchor(el: HTMLElement, anchor: HTMLElement): void {
|
|
786
|
+
if (!this.baseEl) return;
|
|
787
|
+
const baseRect = this.baseEl.getBoundingClientRect();
|
|
788
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
789
|
+
const left = Math.max(
|
|
790
|
+
4,
|
|
791
|
+
Math.min(anchorRect.left - baseRect.left, baseRect.width - el.offsetWidth - 4)
|
|
792
|
+
);
|
|
793
|
+
// Clear any CSS-pinned `right` (e.g. the find bar) so `left` wins.
|
|
794
|
+
el.style.right = 'auto';
|
|
795
|
+
el.style.left = `${left}px`;
|
|
796
|
+
el.style.top = `${anchorRect.bottom - baseRect.top + 6}px`;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Finds a toolbar button by its aria-label, to anchor a panel below it. */
|
|
800
|
+
private toolbarButton(label: string): HTMLElement | null {
|
|
801
|
+
return (
|
|
802
|
+
(this.toolbarEl?.shadowRoot?.querySelector(
|
|
803
|
+
`button[aria-label="${label}"]`
|
|
804
|
+
) as HTMLElement | null) ?? null
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private updateOverlays(): void {
|
|
809
|
+
if (!this.bubbleMenuEl) return;
|
|
810
|
+
const panelOpen =
|
|
811
|
+
this.linkEditorEl?.open ||
|
|
812
|
+
this.imagePanelEl?.open ||
|
|
813
|
+
this.embedPanelEl?.open ||
|
|
814
|
+
this.bookmarkPanelEl?.open ||
|
|
815
|
+
this.emojiPickerEl?.open;
|
|
816
|
+
const show =
|
|
817
|
+
this.bubbleMenu &&
|
|
818
|
+
this.hasFocus &&
|
|
819
|
+
!!this.toolbarState &&
|
|
820
|
+
!this.toolbarState.selectionEmpty &&
|
|
821
|
+
!this.toolbarState.imageSelected &&
|
|
822
|
+
!this.toolbarState.embedSelected &&
|
|
823
|
+
!panelOpen;
|
|
824
|
+
this.bubbleMenuEl.open = show;
|
|
825
|
+
if (show) {
|
|
826
|
+
this.bubbleMenuEl.toolbarState = this.toolbarState;
|
|
827
|
+
this.bubbleMenuEl.updateComplete.then(() =>
|
|
828
|
+
this.positionOverlay(this.bubbleMenuEl, 'above')
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (this.tableMenuEl) {
|
|
833
|
+
const showTable =
|
|
834
|
+
this.hasFocus && !!this.toolbarState && this.toolbarState.inTable && !panelOpen;
|
|
835
|
+
this.tableMenuEl.open = showTable;
|
|
836
|
+
if (showTable) {
|
|
837
|
+
this.tableMenuEl.updateComplete.then(() =>
|
|
838
|
+
this.positionOverlay(this.tableMenuEl, 'below')
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (this.codeMenuEl) {
|
|
844
|
+
const showCode =
|
|
845
|
+
this.hasFocus && !!this.toolbarState && this.toolbarState.inCodeBlock && !panelOpen;
|
|
846
|
+
this.codeMenuEl.open = showCode;
|
|
847
|
+
if (showCode) {
|
|
848
|
+
this.codeMenuEl.language = this.toolbarState!.codeBlockLanguage ?? '';
|
|
849
|
+
this.codeMenuEl.updateComplete.then(() =>
|
|
850
|
+
this.positionOverlay(this.codeMenuEl, 'above')
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (this.imageMenuEl) {
|
|
856
|
+
const showImage =
|
|
857
|
+
this.hasFocus && !!this.toolbarState && this.toolbarState.imageSelected && !panelOpen;
|
|
858
|
+
this.imageMenuEl.open = showImage;
|
|
859
|
+
if (showImage) {
|
|
860
|
+
this.imageMenuEl.toolbarState = this.toolbarState;
|
|
861
|
+
this.imageMenuEl.updateComplete.then(() =>
|
|
862
|
+
this.positionOverlay(this.imageMenuEl, 'above')
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (this.embedMenuEl) {
|
|
868
|
+
const showEmbed =
|
|
869
|
+
this.hasFocus && !!this.toolbarState && this.toolbarState.embedSelected && !panelOpen;
|
|
870
|
+
this.embedMenuEl.open = showEmbed;
|
|
871
|
+
if (showEmbed) {
|
|
872
|
+
this.embedMenuEl.toolbarState = this.toolbarState;
|
|
873
|
+
this.embedMenuEl.updateComplete.then(() =>
|
|
874
|
+
this.positionOverlay(this.embedMenuEl, 'above')
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// -------------------------------------------------------- special chars
|
|
881
|
+
|
|
882
|
+
private openSpecialChars(): void {
|
|
883
|
+
if (!this.view || !this.specialCharsEl || this.disabled || this.readonly) return;
|
|
884
|
+
this.specialCharsEl.open = true;
|
|
885
|
+
this.specialCharsEl.updateComplete.then(() =>
|
|
886
|
+
this.positionOverlay(this.specialCharsEl, 'below')
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private closeSpecialChars(refocus = true): void {
|
|
891
|
+
if (this.specialCharsEl) this.specialCharsEl.open = false;
|
|
892
|
+
if (refocus) this.view?.focus();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private insertSpecialChar(char: string): void {
|
|
896
|
+
if (!this.view) return;
|
|
897
|
+
this.view.dispatch(this.view.state.tr.insertText(char).scrollIntoView());
|
|
898
|
+
this.view.focus();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ------------------------------------------------ templates & merge fields
|
|
902
|
+
|
|
903
|
+
private openInsertMenu(mode: 'template' | 'merge'): void {
|
|
904
|
+
if (!this.view || !this.insertMenuEl || this.disabled || this.readonly) return;
|
|
905
|
+
this.insertMenuMode = mode;
|
|
906
|
+
const menu = this.insertMenuEl;
|
|
907
|
+
if (mode === 'merge') {
|
|
908
|
+
menu.heading = 'Merge field';
|
|
909
|
+
menu.emptyText = 'No merge fields configured.';
|
|
910
|
+
menu.items = this.mergeFields.map(f => ({ label: f.label, description: `{{${f.id}}}` }));
|
|
911
|
+
} else {
|
|
912
|
+
menu.heading = 'Template';
|
|
913
|
+
menu.emptyText = 'No templates configured.';
|
|
914
|
+
menu.items = this.templates.map(t => ({ label: t.label, description: t.description }));
|
|
915
|
+
}
|
|
916
|
+
menu.open = true;
|
|
917
|
+
if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
|
|
918
|
+
menu.updateComplete.then(() => this.positionOverlay(menu, 'below'));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private closeInsertMenu(refocus = true): void {
|
|
922
|
+
if (this.insertMenuEl) this.insertMenuEl.open = false;
|
|
923
|
+
this.insertMenuMode = null;
|
|
924
|
+
if (refocus) this.view?.focus();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private handleInsertSelect(index: number): void {
|
|
928
|
+
const mode = this.insertMenuMode;
|
|
929
|
+
this.closeInsertMenu();
|
|
930
|
+
if (!this.view) return;
|
|
931
|
+
if (mode === 'merge') {
|
|
932
|
+
const field = this.mergeFields[index];
|
|
933
|
+
if (field) this.execCommand('insertMergeField', { id: field.id, label: field.label });
|
|
934
|
+
} else if (mode === 'template') {
|
|
935
|
+
const template = this.templates[index];
|
|
936
|
+
if (template) this.insertHTML(template.html);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Inserts a merge-field placeholder at the selection. */
|
|
941
|
+
public insertMergeField(id: string, label?: string): void {
|
|
942
|
+
this.execCommand('insertMergeField', { id, label: label ?? null });
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Changes the case of the selected text. */
|
|
946
|
+
public changeCase(mode: 'upper' | 'lower' | 'title' | 'sentence'): void {
|
|
947
|
+
this.execCommand('changeCase', { textCase: mode });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// ---------------------------------------------------------------- math
|
|
951
|
+
|
|
952
|
+
private openMathPanel(): void {
|
|
953
|
+
if (!this.view || !this.mathPanelEl || this.disabled || this.readonly) return;
|
|
954
|
+
this.mathPanelEl.reset();
|
|
955
|
+
this.mathPanelEl.open = true;
|
|
956
|
+
if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
|
|
957
|
+
if (this.tableMenuEl) this.tableMenuEl.open = false;
|
|
958
|
+
this.mathPanelEl.updateComplete.then(() => {
|
|
959
|
+
const anchor = this.toolbarButton('Insert math (LaTeX)');
|
|
960
|
+
if (anchor) this.positionOverlayBelowAnchor(this.mathPanelEl, anchor);
|
|
961
|
+
else this.positionOverlay(this.mathPanelEl, 'below');
|
|
962
|
+
this.mathPanelEl.focusInput();
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private closeMathPanel(refocus = true): void {
|
|
967
|
+
if (this.mathPanelEl) this.mathPanelEl.open = false;
|
|
968
|
+
if (refocus) this.view?.focus();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private openMarkdownPanel(): void {
|
|
972
|
+
if (!this.view || !this.markdownPanelEl || this.disabled || this.readonly) return;
|
|
973
|
+
this.markdownPanelEl.reset();
|
|
974
|
+
this.markdownPanelEl.open = true;
|
|
975
|
+
if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
|
|
976
|
+
if (this.tableMenuEl) this.tableMenuEl.open = false;
|
|
977
|
+
this.markdownPanelEl.updateComplete.then(() => {
|
|
978
|
+
const anchor = this.toolbarButton('Insert from Markdown');
|
|
979
|
+
if (anchor) this.positionOverlayBelowAnchor(this.markdownPanelEl, anchor);
|
|
980
|
+
else this.positionOverlay(this.markdownPanelEl, 'below');
|
|
981
|
+
this.markdownPanelEl.focusInput();
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private closeMarkdownPanel(refocus = true): void {
|
|
986
|
+
if (this.markdownPanelEl) this.markdownPanelEl.open = false;
|
|
987
|
+
if (refocus) this.view?.focus();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private applyMath(latex: string, display: 'inline' | 'block'): void {
|
|
991
|
+
this.closeMathPanel();
|
|
992
|
+
this.execCommand('insertMath', { latex, mathDisplay: display });
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Inserts a math formula (LaTeX) at the selection. */
|
|
996
|
+
public insertMath(latex: string, display: 'inline' | 'block' = 'inline'): void {
|
|
997
|
+
this.execCommand('insertMath', { latex, mathDisplay: display });
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ----------------------------------------------------------------- emoji
|
|
1001
|
+
|
|
1002
|
+
private openEmojiPicker(): void {
|
|
1003
|
+
if (!this.view || !this.emojiPickerEl || this.disabled || this.readonly) return;
|
|
1004
|
+
this.emojiPickerEl.open = true;
|
|
1005
|
+
if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
|
|
1006
|
+
if (this.tableMenuEl) this.tableMenuEl.open = false;
|
|
1007
|
+
this.emojiPickerEl.updateComplete.then(() => {
|
|
1008
|
+
this.positionOverlay(this.emojiPickerEl, 'below');
|
|
1009
|
+
this.emojiPickerEl.focusInput();
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
private closeEmojiPicker(refocus = true): void {
|
|
1014
|
+
if (this.emojiPickerEl) this.emojiPickerEl.open = false;
|
|
1015
|
+
if (refocus) this.view?.focus();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// ---------------------------------------------------------------- embeds
|
|
1019
|
+
|
|
1020
|
+
private openEmbedPanel(): void {
|
|
1021
|
+
if (!this.view || !this.embedPanelEl || this.disabled || this.readonly) return;
|
|
1022
|
+
this.embedPanelEl.reset();
|
|
1023
|
+
this.embedPanelEl.open = true;
|
|
1024
|
+
if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
|
|
1025
|
+
if (this.tableMenuEl) this.tableMenuEl.open = false;
|
|
1026
|
+
this.embedPanelEl.updateComplete.then(() => {
|
|
1027
|
+
// Anchor below the embed toolbar button when available.
|
|
1028
|
+
const anchor = this.toolbarButton('Embed video (YouTube, Vimeo, Loom)');
|
|
1029
|
+
if (anchor) this.positionOverlayBelowAnchor(this.embedPanelEl, anchor);
|
|
1030
|
+
else this.positionOverlay(this.embedPanelEl, 'below');
|
|
1031
|
+
this.embedPanelEl.focusInput();
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private closeEmbedPanel(refocus = true): void {
|
|
1036
|
+
if (this.embedPanelEl) this.embedPanelEl.open = false;
|
|
1037
|
+
if (refocus) this.view?.focus();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// -------------------------------------------------------------- bookmarks
|
|
1041
|
+
|
|
1042
|
+
private openBookmarkPanel(): void {
|
|
1043
|
+
if (!this.view || !this.bookmarkPanelEl || this.disabled || this.readonly) return;
|
|
1044
|
+
this.bookmarkPanelEl.open = true;
|
|
1045
|
+
if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
|
|
1046
|
+
if (this.tableMenuEl) this.tableMenuEl.open = false;
|
|
1047
|
+
this.bookmarkPanelEl.updateComplete.then(() => {
|
|
1048
|
+
this.positionOverlay(this.bookmarkPanelEl, 'below');
|
|
1049
|
+
this.bookmarkPanelEl.focusInput();
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private closeBookmarkPanel(refocus = true): void {
|
|
1054
|
+
if (this.bookmarkPanelEl) this.bookmarkPanelEl.open = false;
|
|
1055
|
+
if (refocus) this.view?.focus();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ---------------------------------------------------------------- images
|
|
1059
|
+
|
|
1060
|
+
private openImagePanel(): void {
|
|
1061
|
+
if (!this.view || !this.imagePanelEl || this.disabled || this.readonly) return;
|
|
1062
|
+
// Editing an already-selected image/figure pre-fills the fields (and shows
|
|
1063
|
+
// Remove); otherwise open a blank insert panel.
|
|
1064
|
+
const info = selectedImageInfo(this.view.state);
|
|
1065
|
+
if (info) {
|
|
1066
|
+
this.imagePanelEl.prefill({
|
|
1067
|
+
src: info.node.attrs.src,
|
|
1068
|
+
alt: info.node.attrs.alt ?? '',
|
|
1069
|
+
caption: info.isFigure ? info.node.textContent : '',
|
|
1070
|
+
});
|
|
1071
|
+
} else {
|
|
1072
|
+
this.imagePanelEl.reset();
|
|
1073
|
+
}
|
|
1074
|
+
this.imagePanelEl.open = true;
|
|
1075
|
+
if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
|
|
1076
|
+
if (this.tableMenuEl) this.tableMenuEl.open = false;
|
|
1077
|
+
this.imagePanelEl.updateComplete.then(() => {
|
|
1078
|
+
// Anchor below the image toolbar button when it's available; otherwise
|
|
1079
|
+
// fall back to positioning at the cursor.
|
|
1080
|
+
const anchor = this.toolbarButton('Insert image');
|
|
1081
|
+
if (anchor) this.positionOverlayBelowAnchor(this.imagePanelEl, anchor);
|
|
1082
|
+
else this.positionOverlay(this.imagePanelEl, 'below');
|
|
1083
|
+
this.imagePanelEl.focusInput();
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
private closeImagePanel(refocus = true): void {
|
|
1088
|
+
if (this.imagePanelEl) this.imagePanelEl.open = false;
|
|
1089
|
+
if (refocus) this.view?.focus();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private insertImageAt(
|
|
1093
|
+
src: string,
|
|
1094
|
+
alt: string | null,
|
|
1095
|
+
pos: number | null,
|
|
1096
|
+
caption: string | null = null
|
|
1097
|
+
): void {
|
|
1098
|
+
if (!this.view) return;
|
|
1099
|
+
if (pos !== null) {
|
|
1100
|
+
const $pos = this.view.state.doc.resolve(
|
|
1101
|
+
Math.min(pos, this.view.state.doc.content.size)
|
|
1102
|
+
);
|
|
1103
|
+
this.view.dispatch(this.view.state.tr.setSelection(TextSelection.near($pos)));
|
|
1104
|
+
}
|
|
1105
|
+
this.execCommand('insertImage', { src, alt, caption });
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Routes an image file through the upload pipeline: a cancelable
|
|
1110
|
+
* `nile-image-upload-request` first, then `uploadHandler` (with an
|
|
1111
|
+
* uploading placeholder), then the base64 fallback.
|
|
1112
|
+
*/
|
|
1113
|
+
public async insertImageFile(
|
|
1114
|
+
file: File,
|
|
1115
|
+
pos: number | null = null,
|
|
1116
|
+
meta?: { alt?: string | null; caption?: string | null }
|
|
1117
|
+
): Promise<void> {
|
|
1118
|
+
if (!this.view) return;
|
|
1119
|
+
const metaAlt = meta?.alt ?? null;
|
|
1120
|
+
const metaCaption = meta?.caption ?? null;
|
|
1121
|
+
|
|
1122
|
+
const request = this.emit(
|
|
1123
|
+
'nile-image-upload-request',
|
|
1124
|
+
{
|
|
1125
|
+
file,
|
|
1126
|
+
insert: (src: string, alt?: string) =>
|
|
1127
|
+
this.insertImageAt(src, alt ?? metaAlt, pos, metaCaption),
|
|
1128
|
+
},
|
|
1129
|
+
true,
|
|
1130
|
+
true,
|
|
1131
|
+
true
|
|
1132
|
+
);
|
|
1133
|
+
if (request.defaultPrevented) return;
|
|
1134
|
+
|
|
1135
|
+
if (this.uploadHandler) {
|
|
1136
|
+
uploadCounter += 1;
|
|
1137
|
+
const id = `nile-upload-${uploadCounter}`;
|
|
1138
|
+
const at = pos ?? this.view.state.selection.from;
|
|
1139
|
+
this.view.dispatch(this.view.state.tr.setMeta(imageUploadKey, { add: { id, pos: at } }));
|
|
1140
|
+
try {
|
|
1141
|
+
const { src, alt } = await this.uploadHandler(file);
|
|
1142
|
+
const mappedPos = this.view ? findUploadPos(this.view.state, id) : null;
|
|
1143
|
+
this.view?.dispatch(this.view.state.tr.setMeta(imageUploadKey, { remove: { id } }));
|
|
1144
|
+
this.insertImageAt(src, metaAlt ?? alt ?? null, mappedPos, metaCaption);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
this.view?.dispatch(this.view.state.tr.setMeta(imageUploadKey, { remove: { id } }));
|
|
1147
|
+
this.emit('nile-image-upload-error', { file, error });
|
|
1148
|
+
}
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (this.allowBase64Images) {
|
|
1153
|
+
const src = await readFileAsDataURL(file);
|
|
1154
|
+
this.insertImageAt(src, metaAlt, pos, metaCaption);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ---------------------------------------------------- mentions & slash
|
|
1159
|
+
|
|
1160
|
+
private dismissKeyFor(
|
|
1161
|
+
source: 'mention' | 'slash' | 'emoji',
|
|
1162
|
+
match: SuggestionMatch
|
|
1163
|
+
): string {
|
|
1164
|
+
return `${source}:${match.trigger}:${match.from}`;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
private handleSuggestKey(
|
|
1168
|
+
event: KeyboardEvent,
|
|
1169
|
+
source: 'mention' | 'slash' | 'emoji'
|
|
1170
|
+
): boolean {
|
|
1171
|
+
if (this.activeSuggestSource !== source || !this.suggestListEl?.open) return false;
|
|
1172
|
+
switch (event.key) {
|
|
1173
|
+
case 'ArrowDown':
|
|
1174
|
+
this.suggestListEl.next();
|
|
1175
|
+
return true;
|
|
1176
|
+
case 'ArrowUp':
|
|
1177
|
+
this.suggestListEl.prev();
|
|
1178
|
+
return true;
|
|
1179
|
+
case 'Enter':
|
|
1180
|
+
case 'Tab':
|
|
1181
|
+
return this.suggestListEl.selectCurrent();
|
|
1182
|
+
case 'Escape': {
|
|
1183
|
+
const match =
|
|
1184
|
+
source === 'mention'
|
|
1185
|
+
? this.mentionMatch
|
|
1186
|
+
: source === 'slash'
|
|
1187
|
+
? this.slashMatch
|
|
1188
|
+
: this.emojiMatch;
|
|
1189
|
+
if (match) this.suggestDismissed = this.dismissKeyFor(source, match);
|
|
1190
|
+
this.closeSuggestList();
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
default:
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
private closeSuggestList(): void {
|
|
1199
|
+
if (this.suggestListEl) this.suggestListEl.open = false;
|
|
1200
|
+
this.activeSuggestSource = null;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
private updateSuggestList(): void {
|
|
1204
|
+
if (!this.suggestListEl || !this.view) return;
|
|
1205
|
+
const mention = this.mentionMatch?.active ? this.mentionMatch : null;
|
|
1206
|
+
const slash =
|
|
1207
|
+
!this.noSlashCommands && this.slashMatch?.active ? this.slashMatch : null;
|
|
1208
|
+
const emoji = !this.noEmoji && this.emojiMatch?.active ? this.emojiMatch : null;
|
|
1209
|
+
const source: 'mention' | 'slash' | 'emoji' | null = mention
|
|
1210
|
+
? 'mention'
|
|
1211
|
+
: slash
|
|
1212
|
+
? 'slash'
|
|
1213
|
+
: emoji
|
|
1214
|
+
? 'emoji'
|
|
1215
|
+
: null;
|
|
1216
|
+
const match = mention ?? slash ?? emoji;
|
|
1217
|
+
|
|
1218
|
+
if (!source || !match || this.disabled || this.readonly) {
|
|
1219
|
+
this.closeSuggestList();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (this.suggestDismissed === this.dismissKeyFor(source, match)) return;
|
|
1223
|
+
this.suggestDismissed = null;
|
|
1224
|
+
|
|
1225
|
+
if (source === 'mention') {
|
|
1226
|
+
const all = this.mentions[match.trigger] ?? [];
|
|
1227
|
+
const query = match.query.toLowerCase();
|
|
1228
|
+
this.suggestListEl.items = all.filter(item =>
|
|
1229
|
+
item.label.toLowerCase().includes(query)
|
|
1230
|
+
);
|
|
1231
|
+
// Async override: listeners may provide items for this exact query.
|
|
1232
|
+
const token = `${match.trigger} ${match.query}`;
|
|
1233
|
+
this.mentionQueryToken = token;
|
|
1234
|
+
this.emit('nile-mention-query', {
|
|
1235
|
+
trigger: match.trigger,
|
|
1236
|
+
query: match.query,
|
|
1237
|
+
provide: (items: { key: string; label: string }[]) => {
|
|
1238
|
+
if (this.mentionQueryToken !== token || !Array.isArray(items)) return;
|
|
1239
|
+
this.suggestListEl.items = items
|
|
1240
|
+
.filter(i => i && typeof i.key === 'string' && typeof i.label === 'string')
|
|
1241
|
+
.map(i => ({ key: i.key, label: i.label }));
|
|
1242
|
+
},
|
|
1243
|
+
});
|
|
1244
|
+
} else if (source === 'slash') {
|
|
1245
|
+
const query = match.query.toLowerCase();
|
|
1246
|
+
const snippetItems: SuggestItem[] = this.snippets.map(s => ({
|
|
1247
|
+
key: `snippet:${s.key}`,
|
|
1248
|
+
label: s.label,
|
|
1249
|
+
hint: 'snippet',
|
|
1250
|
+
}));
|
|
1251
|
+
this.suggestListEl.items = [...SLASH_ITEMS, ...snippetItems].filter(item =>
|
|
1252
|
+
item.label.toLowerCase().includes(query)
|
|
1253
|
+
);
|
|
1254
|
+
} else {
|
|
1255
|
+
const query = match.query.toLowerCase();
|
|
1256
|
+
this.suggestListEl.items = EMOJI.filter(e => e.name.includes(query))
|
|
1257
|
+
.slice(0, 12)
|
|
1258
|
+
.map(e => ({ key: e.name, label: `${e.char} ${e.name}` }));
|
|
1259
|
+
if (this.suggestListEl.items.length === 0) {
|
|
1260
|
+
this.closeSuggestList();
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
this.activeSuggestSource = source;
|
|
1266
|
+
this.suggestListEl.resetActive();
|
|
1267
|
+
this.suggestListEl.open = true;
|
|
1268
|
+
this.suggestListEl.updateComplete.then(() =>
|
|
1269
|
+
this.positionOverlay(this.suggestListEl, 'below', match.from)
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
private handleSuggestSelect(item: SuggestItem): void {
|
|
1274
|
+
if (!this.view) return;
|
|
1275
|
+
if (this.activeSuggestSource === 'mention' && this.mentionMatch?.active) {
|
|
1276
|
+
const match = this.mentionMatch;
|
|
1277
|
+
const { state } = this.view;
|
|
1278
|
+
const mention = state.schema.nodes.mention.create({
|
|
1279
|
+
trigger: match.trigger,
|
|
1280
|
+
key: item.key,
|
|
1281
|
+
label: item.label,
|
|
1282
|
+
});
|
|
1283
|
+
this.view.dispatch(
|
|
1284
|
+
state.tr.replaceWith(match.from, match.to, [mention, state.schema.text(' ')])
|
|
1285
|
+
);
|
|
1286
|
+
this.view.focus();
|
|
1287
|
+
this.emit('nile-mention-select', {
|
|
1288
|
+
trigger: match.trigger,
|
|
1289
|
+
key: item.key,
|
|
1290
|
+
label: item.label,
|
|
1291
|
+
});
|
|
1292
|
+
} else if (this.activeSuggestSource === 'slash' && this.slashMatch?.active) {
|
|
1293
|
+
const match = this.slashMatch;
|
|
1294
|
+
this.view.dispatch(this.view.state.tr.delete(match.from, match.to));
|
|
1295
|
+
this.closeSuggestList();
|
|
1296
|
+
if (item.key.startsWith('snippet:')) {
|
|
1297
|
+
const snippet = this.snippets.find(s => `snippet:${s.key}` === item.key);
|
|
1298
|
+
if (snippet) this.insertHTML(snippet.html);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const slashItem = SLASH_ITEMS.find(s => s.key === item.key);
|
|
1302
|
+
if (!slashItem) return;
|
|
1303
|
+
if (slashItem.command === 'openImage') this.openImagePanel();
|
|
1304
|
+
else if (slashItem.command === 'openEmbed') this.openEmbedPanel();
|
|
1305
|
+
else if (slashItem.command === 'openBookmark') this.openBookmarkPanel();
|
|
1306
|
+
else this.execCommand(slashItem.command, slashItem.attrs);
|
|
1307
|
+
} else if (this.activeSuggestSource === 'emoji' && this.emojiMatch?.active) {
|
|
1308
|
+
const match = this.emojiMatch;
|
|
1309
|
+
const emoji = EMOJI.find(e => e.name === item.key);
|
|
1310
|
+
if (emoji) {
|
|
1311
|
+
this.view.dispatch(
|
|
1312
|
+
this.view.state.tr.insertText(emoji.char, match.from, match.to)
|
|
1313
|
+
);
|
|
1314
|
+
this.view.focus();
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
this.closeSuggestList();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// ---------------------------------------------------------- find & replace
|
|
1321
|
+
|
|
1322
|
+
/** Opens the find & replace bar (also bound to Mod-f inside the editor). */
|
|
1323
|
+
public openFindBar(): void {
|
|
1324
|
+
if (!this.findBarEl || this.sourceMode) return;
|
|
1325
|
+
this.findBarEl.open = true;
|
|
1326
|
+
const find = this.view ? getFindState(this.view.state) : null;
|
|
1327
|
+
this.findBarEl.matchCount = find?.matches.length ?? 0;
|
|
1328
|
+
this.findBarEl.activeIndex = find?.activeIndex ?? 0;
|
|
1329
|
+
this.findBarEl.updateComplete.then(() => {
|
|
1330
|
+
// Anchor below the Find & replace toolbar button when present; otherwise
|
|
1331
|
+
// fall back to the CSS-pinned top-right position.
|
|
1332
|
+
const anchor = this.toolbarButton('Find & replace (Ctrl+F)');
|
|
1333
|
+
if (anchor) this.positionOverlayBelowAnchor(this.findBarEl, anchor);
|
|
1334
|
+
this.findBarEl.focusInput();
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
public closeFindBar(): void {
|
|
1339
|
+
if (this.view) this.view.dispatch(clearFindTr(this.view.state));
|
|
1340
|
+
if (this.findBarEl) this.findBarEl.open = false;
|
|
1341
|
+
this.view?.focus();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
public find(query: string): number {
|
|
1345
|
+
if (!this.view) return 0;
|
|
1346
|
+
this.view.dispatch(searchTr(this.view.state, query));
|
|
1347
|
+
this.scrollToActiveMatch();
|
|
1348
|
+
return getFindState(this.view.state).matches.length;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
public findNext(): void {
|
|
1352
|
+
if (!this.view) return;
|
|
1353
|
+
this.view.dispatch(findStepTr(this.view.state, 'next'));
|
|
1354
|
+
this.scrollToActiveMatch();
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
public findPrev(): void {
|
|
1358
|
+
if (!this.view) return;
|
|
1359
|
+
this.view.dispatch(findStepTr(this.view.state, 'prev'));
|
|
1360
|
+
this.scrollToActiveMatch();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
public replace(replacement: string): void {
|
|
1364
|
+
if (!this.view) return;
|
|
1365
|
+
const tr = replaceActiveTr(this.view.state, replacement);
|
|
1366
|
+
if (tr) this.view.dispatch(tr);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
public replaceAll(replacement: string): void {
|
|
1370
|
+
if (!this.view) return;
|
|
1371
|
+
const tr = replaceAllTr(this.view.state, replacement);
|
|
1372
|
+
if (tr) this.view.dispatch(tr);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
private scrollToActiveMatch(): void {
|
|
1376
|
+
if (!this.view) return;
|
|
1377
|
+
const find = getFindState(this.view.state);
|
|
1378
|
+
const match = find.matches[find.activeIndex];
|
|
1379
|
+
if (!match) return;
|
|
1380
|
+
this.view.dispatch(
|
|
1381
|
+
this.view.state.tr
|
|
1382
|
+
.setSelection(TextSelection.create(this.view.state.doc, match.from, match.to))
|
|
1383
|
+
.scrollIntoView()
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// -------------------------------------------------------------- source view
|
|
1388
|
+
|
|
1389
|
+
/** Toggles raw-HTML source editing. Content re-sanitizes on the way back. */
|
|
1390
|
+
public toggleSourceView(): void {
|
|
1391
|
+
if (this.sourceMode) {
|
|
1392
|
+
this.applySourceView(this.sourceViewEl?.textarea?.value ?? null);
|
|
1393
|
+
} else {
|
|
1394
|
+
this.sourceMode = true;
|
|
1395
|
+
this.updateComplete.then(() => {
|
|
1396
|
+
if (this.sourceViewEl) {
|
|
1397
|
+
this.sourceViewEl.value = this.getHTML();
|
|
1398
|
+
this.sourceViewEl.focusInput();
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
private applySourceView(htmlContent: string | null): void {
|
|
1405
|
+
this.sourceMode = false;
|
|
1406
|
+
if (htmlContent !== null) this.setContent(htmlContent, true);
|
|
1407
|
+
this.updateComplete.then(() => this.view?.focus());
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** Word and character counts for the current document. */
|
|
1411
|
+
public getWordCount(): { words: number; characters: number } {
|
|
1412
|
+
return this.view ? countWords(this.view.state.doc) : { words: 0, characters: 0 };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Arms the format painter (copies the formatting at the selection) or, if
|
|
1417
|
+
* already armed, disarms it. Once armed, the next non-empty selection the
|
|
1418
|
+
* user makes receives the copied formatting.
|
|
1419
|
+
*/
|
|
1420
|
+
public toggleFormatPainter(): void {
|
|
1421
|
+
if (!this.view) return;
|
|
1422
|
+
const tr = isFormatPainterArmed(this.view.state)
|
|
1423
|
+
? disarmFormatPainter(this.view.state)
|
|
1424
|
+
: armFormatPainter(this.view.state);
|
|
1425
|
+
if (tr) this.view.dispatch(tr);
|
|
1426
|
+
this.view.focus();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// ---------------------------------------------------------- export & import
|
|
1430
|
+
|
|
1431
|
+
/** The live, styled content element used as the source for exports. */
|
|
1432
|
+
private get contentRoot(): HTMLElement | null {
|
|
1433
|
+
return (this.view?.dom as HTMLElement | undefined) ?? null;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/** Sanitizes `this.name` (or a fallback) into a safe download basename. */
|
|
1437
|
+
private exportFilename(extension: string): string {
|
|
1438
|
+
const base =
|
|
1439
|
+
(this.name || 'document').replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '') || 'document';
|
|
1440
|
+
return `${base}.${extension}`;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
private downloadBlob(content: string, mime: string, filename: string): void {
|
|
1444
|
+
const blob = new Blob([content], { type: mime });
|
|
1445
|
+
const url = URL.createObjectURL(blob);
|
|
1446
|
+
const anchor = document.createElement('a');
|
|
1447
|
+
anchor.href = url;
|
|
1448
|
+
anchor.download = filename;
|
|
1449
|
+
anchor.style.display = 'none';
|
|
1450
|
+
document.body.appendChild(anchor);
|
|
1451
|
+
anchor.click();
|
|
1452
|
+
anchor.remove();
|
|
1453
|
+
// Revoke after the click so the download has a chance to start.
|
|
1454
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* The document as HTML with every style folded into inline `style`
|
|
1459
|
+
* attributes — the form email clients (which strip `<style>` blocks and
|
|
1460
|
+
* classes) can render. Returns the string; does not download.
|
|
1461
|
+
*/
|
|
1462
|
+
public getInlineStyledHTML(): string {
|
|
1463
|
+
const root = this.contentRoot;
|
|
1464
|
+
if (!this.view || !root || isDocEmpty(this.view.state.doc)) return '';
|
|
1465
|
+
return toInlineStyledHtml(root);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/** Downloads the document as an inline-styled, email-ready `.html` file. */
|
|
1469
|
+
public exportInlineHtml(filename?: string): void {
|
|
1470
|
+
const body = this.getInlineStyledHTML();
|
|
1471
|
+
if (!body) return;
|
|
1472
|
+
const doc = `<!DOCTYPE html>\n<html><head><meta charset="utf-8"></head><body>${body}</body></html>`;
|
|
1473
|
+
this.downloadBlob(doc, 'text/html;charset=utf-8', filename ?? this.exportFilename('html'));
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/** Downloads the document as a Word-openable `.doc` file. */
|
|
1477
|
+
public exportToWord(filename?: string): void {
|
|
1478
|
+
const body = this.getInlineStyledHTML();
|
|
1479
|
+
if (!body) return;
|
|
1480
|
+
const doc = buildWordDocument(body, this.name || 'Document');
|
|
1481
|
+
this.downloadBlob(doc, 'application/msword', filename ?? this.exportFilename('doc'));
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Opens the browser print dialog with a self-contained, print-tuned copy of
|
|
1486
|
+
* the document ("Save as PDF"). Honours page breaks, embedded images and the
|
|
1487
|
+
* base font; optional running header/footer text.
|
|
1488
|
+
*/
|
|
1489
|
+
public exportToPdf(
|
|
1490
|
+
options: { header?: string; footer?: string; fontFamily?: string } = {}
|
|
1491
|
+
): void {
|
|
1492
|
+
const body = this.getInlineStyledHTML();
|
|
1493
|
+
if (!body) return;
|
|
1494
|
+
const source = buildPrintDocument(body, { title: this.name || 'Document', ...options });
|
|
1495
|
+
|
|
1496
|
+
const frame = document.createElement('iframe');
|
|
1497
|
+
frame.setAttribute('aria-hidden', 'true');
|
|
1498
|
+
Object.assign(frame.style, {
|
|
1499
|
+
position: 'fixed',
|
|
1500
|
+
right: '0',
|
|
1501
|
+
bottom: '0',
|
|
1502
|
+
width: '0',
|
|
1503
|
+
height: '0',
|
|
1504
|
+
border: '0',
|
|
1505
|
+
});
|
|
1506
|
+
document.body.appendChild(frame);
|
|
1507
|
+
|
|
1508
|
+
const cleanup = (): void => {
|
|
1509
|
+
// Delay removal so the browser's print job keeps its document alive.
|
|
1510
|
+
setTimeout(() => frame.remove(), 1000);
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
frame.onload = (): void => {
|
|
1514
|
+
const win = frame.contentWindow;
|
|
1515
|
+
if (!win) {
|
|
1516
|
+
cleanup();
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
win.focus();
|
|
1520
|
+
win.addEventListener('afterprint', cleanup, { once: true });
|
|
1521
|
+
win.print();
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
const fdoc = frame.contentDocument;
|
|
1525
|
+
if (!fdoc) {
|
|
1526
|
+
frame.remove();
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
fdoc.open();
|
|
1530
|
+
fdoc.write(source);
|
|
1531
|
+
fdoc.close();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* Imports Word / Google-Docs HTML: normalizes the proprietary markup to
|
|
1536
|
+
* clean semantic HTML, then replaces the document. Fidelity note: comments
|
|
1537
|
+
* and track-changes are not preserved — the editor has no model for them,
|
|
1538
|
+
* and binary `.docx`/`.doc` files are not parsed (export as "Web Page" /
|
|
1539
|
+
* HTML first, or paste directly, which is handled automatically).
|
|
1540
|
+
*/
|
|
1541
|
+
public importFromWord(html: string, emitChange = true): void {
|
|
1542
|
+
if (!html) return;
|
|
1543
|
+
const cleaned = isOfficeHtml(html) ? cleanOfficeHtml(html) : html;
|
|
1544
|
+
this.setContent(cleaned, emitChange);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/** Opens a file picker for an HTML export and imports it (toolbar action). */
|
|
1548
|
+
private importFromWordFile(): void {
|
|
1549
|
+
const input = document.createElement('input');
|
|
1550
|
+
input.type = 'file';
|
|
1551
|
+
input.accept = '.htm,.html,text/html';
|
|
1552
|
+
input.style.display = 'none';
|
|
1553
|
+
input.addEventListener('change', () => {
|
|
1554
|
+
const file = input.files?.[0];
|
|
1555
|
+
input.remove();
|
|
1556
|
+
if (!file) return;
|
|
1557
|
+
file.text().then(text => this.importFromWord(text));
|
|
1558
|
+
});
|
|
1559
|
+
document.body.appendChild(input);
|
|
1560
|
+
input.click();
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// -------------------------------------------------------- shortcut help
|
|
1564
|
+
|
|
1565
|
+
/** Opens the keyboard shortcut reference (also bound to Mod-/). */
|
|
1566
|
+
public openShortcutHelp(): void {
|
|
1567
|
+
if (!this.shortcutHelpEl) return;
|
|
1568
|
+
this.shortcutHelpEl.open = true;
|
|
1569
|
+
this.shortcutHelpEl.focusPanel();
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
public closeShortcutHelp(): void {
|
|
1573
|
+
if (this.shortcutHelpEl) this.shortcutHelpEl.open = false;
|
|
1574
|
+
this.view?.focus();
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// ------------------------------------------------------------ fullscreen
|
|
1578
|
+
|
|
1579
|
+
/** Toggles distraction-free fullscreen mode. Esc exits. */
|
|
1580
|
+
public toggleFullscreen(): void {
|
|
1581
|
+
this.fullscreen = !this.fullscreen;
|
|
1582
|
+
this.view?.focus();
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
private openLinkEditor(): void {
|
|
1586
|
+
if (!this.view || !this.linkEditorEl || this.disabled || this.readonly) return;
|
|
1587
|
+
const ts = this.toolbarState ?? computeToolbarState(this.view.state);
|
|
1588
|
+
this.linkEditorEl.href = ts.linkHref ?? '';
|
|
1589
|
+
this.linkEditorEl.text = '';
|
|
1590
|
+
this.linkEditorEl.allowText = ts.selectionEmpty && !ts.linkActive;
|
|
1591
|
+
this.linkEditorEl.canRemove = ts.linkActive;
|
|
1592
|
+
this.linkEditorEl.open = true;
|
|
1593
|
+
if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
|
|
1594
|
+
this.linkEditorEl.updateComplete.then(() => {
|
|
1595
|
+
this.positionOverlay(this.linkEditorEl, 'below');
|
|
1596
|
+
this.linkEditorEl.focusInput();
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
private closeLinkEditor(refocus = true): void {
|
|
1601
|
+
if (this.linkEditorEl) this.linkEditorEl.open = false;
|
|
1602
|
+
if (refocus) this.view?.focus();
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
private applyLink(href: string, text: string): void {
|
|
1606
|
+
if (!this.view) return;
|
|
1607
|
+
const { state } = this.view;
|
|
1608
|
+
const linkType = state.schema.marks.link;
|
|
1609
|
+
if (state.selection.empty && !getMarkRange(state.selection.$from, linkType)) {
|
|
1610
|
+
const label = text || href;
|
|
1611
|
+
const node = state.schema.text(label, [linkType.create({ href })]);
|
|
1612
|
+
this.view.dispatch(state.tr.replaceSelectionWith(node, false).scrollIntoView());
|
|
1613
|
+
} else {
|
|
1614
|
+
this.execCommand('link', { href });
|
|
1615
|
+
}
|
|
1616
|
+
this.closeLinkEditor();
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// ---------------------------------------------------------------- public
|
|
1620
|
+
|
|
1621
|
+
/** The underlying ProseMirror EditorView. Advanced escape hatch. */
|
|
1622
|
+
public get pmView(): EditorView | undefined {
|
|
1623
|
+
return this.view;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/** Document as ProseMirror JSON. */
|
|
1627
|
+
public get json(): Record<string, unknown> | null {
|
|
1628
|
+
return this.view ? docToJson(this.view.state.doc) : null;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
public set json(value: Record<string, unknown> | null) {
|
|
1632
|
+
if (value) this.setContent(value);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
public override focus(): void {
|
|
1636
|
+
this.view?.focus();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
public override blur(): void {
|
|
1640
|
+
(this.view?.dom as HTMLElement | undefined)?.blur();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
public getHTML(): string {
|
|
1644
|
+
return this.view && !isDocEmpty(this.view.state.doc)
|
|
1645
|
+
? docToHtml(this.view.state.doc)
|
|
1646
|
+
: '';
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
public getJSON(): Record<string, unknown> | null {
|
|
1650
|
+
return this.json;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
public getText(): string {
|
|
1654
|
+
return this.view ? docToText(this.view.state.doc) : '';
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Document as Markdown. Best-effort: structural nodes and Markdown-native
|
|
1659
|
+
* marks are converted; color/font marks are dropped to plain text. Use
|
|
1660
|
+
* getHTML() when a loss-free representation is required.
|
|
1661
|
+
*/
|
|
1662
|
+
public getMarkdown(): string {
|
|
1663
|
+
return this.view && !isDocEmpty(this.view.state.doc)
|
|
1664
|
+
? docToMarkdown(this.view.state.doc)
|
|
1665
|
+
: '';
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* Replaces the document with parsed Markdown. Covers the same subset as
|
|
1670
|
+
* getMarkdown(): headings, lists, code fences, tables, quotes and the
|
|
1671
|
+
* inline marks. The parsed HTML flows through the schema and sanitizer.
|
|
1672
|
+
*/
|
|
1673
|
+
public setMarkdown(markdown: string, emitChange = false): void {
|
|
1674
|
+
this.setContent(markdownToHtml(markdown), emitChange);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/** Parses and inserts Markdown at the current selection. */
|
|
1678
|
+
public insertMarkdown(markdown: string): void {
|
|
1679
|
+
this.insertHTML(markdownToHtml(markdown));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/** Replaces the document. Accepts an HTML string or ProseMirror JSON. */
|
|
1683
|
+
public setContent(content: string | Record<string, unknown>, emitChange = false): void {
|
|
1684
|
+
if (!this.view) {
|
|
1685
|
+
if (typeof content === 'string') this.value = content;
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
let doc =
|
|
1689
|
+
typeof content === 'string'
|
|
1690
|
+
? htmlToDoc(defaultSchema, content, this.sanitize)
|
|
1691
|
+
: jsonToDoc(defaultSchema, content);
|
|
1692
|
+
if (this.singleLine) doc = flattenToSingleParagraph(doc);
|
|
1693
|
+
this.view.updateState(this.buildState(doc));
|
|
1694
|
+
this.toolbarState = this.snapshotToolbar(this.view.state);
|
|
1695
|
+
this.syncingValue = true;
|
|
1696
|
+
this.value = isDocEmpty(doc) ? '' : docToHtml(doc);
|
|
1697
|
+
this.syncFormValue(doc);
|
|
1698
|
+
if (emitChange) {
|
|
1699
|
+
this.emit('nile-input', this.buildChangeDetail(doc));
|
|
1700
|
+
this.emit('nile-change', this.buildChangeDetail(doc));
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
public clear(): void {
|
|
1705
|
+
this.setContent('');
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/** Parses and inserts HTML at the current selection. */
|
|
1709
|
+
public insertHTML(htmlContent: string): void {
|
|
1710
|
+
if (!this.view) return;
|
|
1711
|
+
const slice = htmlToSlice(defaultSchema, htmlContent, this.sanitize);
|
|
1712
|
+
this.view.dispatch(this.view.state.tr.replaceSelection(slice).scrollIntoView());
|
|
1713
|
+
this.view.focus();
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** Runs a named editor command. Returns whether the command applied. */
|
|
1717
|
+
public execCommand(name: string, attrs?: CommandAttrs): boolean {
|
|
1718
|
+
if (!this.view) return false;
|
|
1719
|
+
const command = getCommand(this.view.state, name as CommandName, attrs);
|
|
1720
|
+
if (!command) return false;
|
|
1721
|
+
const applied = command(this.view.state, this.view.dispatch, this.view);
|
|
1722
|
+
this.view.focus();
|
|
1723
|
+
return applied;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/** True when the given mark/block command is active at the selection. */
|
|
1727
|
+
public isCommandActive(name: string): boolean {
|
|
1728
|
+
const ts = this.toolbarState;
|
|
1729
|
+
if (!ts) return false;
|
|
1730
|
+
switch (name) {
|
|
1731
|
+
case 'bold':
|
|
1732
|
+
return ts.bold;
|
|
1733
|
+
case 'italic':
|
|
1734
|
+
return ts.italic;
|
|
1735
|
+
case 'underline':
|
|
1736
|
+
return ts.underline;
|
|
1737
|
+
case 'strike':
|
|
1738
|
+
return ts.strike;
|
|
1739
|
+
case 'code':
|
|
1740
|
+
return ts.code;
|
|
1741
|
+
case 'link':
|
|
1742
|
+
return ts.linkActive;
|
|
1743
|
+
case 'bulletList':
|
|
1744
|
+
return ts.inBulletList;
|
|
1745
|
+
case 'orderedList':
|
|
1746
|
+
return ts.inOrderedList;
|
|
1747
|
+
case 'blockquote':
|
|
1748
|
+
return ts.inBlockquote;
|
|
1749
|
+
default:
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// ---------------------------------------------------------------- render
|
|
1755
|
+
|
|
1756
|
+
private handleCommand(event: CustomEvent<WysiwygCommandDetail>): void {
|
|
1757
|
+
event.stopPropagation();
|
|
1758
|
+
const { name, attrs } = event.detail;
|
|
1759
|
+
if (name === 'openLink') {
|
|
1760
|
+
this.openLinkEditor();
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (name === 'openImage') {
|
|
1764
|
+
this.openImagePanel();
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (name === 'openEmbed') {
|
|
1768
|
+
this.openEmbedPanel();
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (name === 'openBookmark') {
|
|
1772
|
+
this.openBookmarkPanel();
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (name === 'toggleShowBlocks') {
|
|
1776
|
+
this.showBlocks = !this.showBlocks;
|
|
1777
|
+
this.view?.focus();
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (name === 'toggleSource') {
|
|
1781
|
+
this.toggleSourceView();
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (name === 'togglePastePlainText') {
|
|
1785
|
+
this.pasteAsPlainText = !this.pasteAsPlainText;
|
|
1786
|
+
this.view?.focus();
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (name === 'toggleFullscreen') {
|
|
1790
|
+
this.toggleFullscreen();
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (name === 'openShortcutHelp') {
|
|
1794
|
+
this.openShortcutHelp();
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (name === 'openSpecialChars') {
|
|
1798
|
+
this.openSpecialChars();
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (name === 'openEmoji') {
|
|
1802
|
+
this.openEmojiPicker();
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (name === 'openFind') {
|
|
1806
|
+
this.openFindBar();
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (name === 'exportPdf') {
|
|
1810
|
+
this.exportToPdf();
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (name === 'exportWord') {
|
|
1814
|
+
this.exportToWord();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (name === 'exportInlineHtml') {
|
|
1818
|
+
this.exportInlineHtml();
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (name === 'importWord') {
|
|
1822
|
+
this.importFromWordFile();
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (name === 'toggleFormatPainter') {
|
|
1826
|
+
this.toggleFormatPainter();
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (name === 'openMergeField') {
|
|
1830
|
+
this.openInsertMenu('merge');
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (name === 'openTemplate') {
|
|
1834
|
+
this.openInsertMenu('template');
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if (name === 'openMarkdown') {
|
|
1838
|
+
this.openMarkdownPanel();
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
if (name === 'openMath') {
|
|
1842
|
+
this.openMathPanel();
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
this.execCommand(name, attrs);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
private handleBaseKeydown(event: KeyboardEvent): void {
|
|
1849
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
1850
|
+
// The special-chars panel never takes focus, so its Esc lands here.
|
|
1851
|
+
if (this.specialCharsEl?.open) {
|
|
1852
|
+
this.closeSpecialChars();
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
// Esc exits fullscreen — but only when nothing else claimed it (panels
|
|
1856
|
+
// stopPropagation, ProseMirror handlers preventDefault).
|
|
1857
|
+
if (!this.fullscreen) return;
|
|
1858
|
+
const panelOpen =
|
|
1859
|
+
this.linkEditorEl?.open ||
|
|
1860
|
+
this.imagePanelEl?.open ||
|
|
1861
|
+
this.embedPanelEl?.open ||
|
|
1862
|
+
this.bookmarkPanelEl?.open ||
|
|
1863
|
+
this.emojiPickerEl?.open ||
|
|
1864
|
+
this.findBarEl?.open ||
|
|
1865
|
+
this.shortcutHelpEl?.open ||
|
|
1866
|
+
this.suggestListEl?.open;
|
|
1867
|
+
if (!panelOpen) this.fullscreen = false;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
private handleChromeFocusOut(event: FocusEvent): void {
|
|
1871
|
+
// Focus left the widget entirely (e.g. clicked the page) while it was
|
|
1872
|
+
// parked on chrome like the language picker: close the overlays.
|
|
1873
|
+
const related = event.relatedTarget as Node | null;
|
|
1874
|
+
if (related && (this.shadowRoot?.contains(related) || this.contains(related))) return;
|
|
1875
|
+
if (this.view?.hasFocus()) return;
|
|
1876
|
+
this.hasFocus = false;
|
|
1877
|
+
this.updateOverlays();
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/** The floating insert panels that should dismiss on outside interaction. */
|
|
1881
|
+
private get floatingPanels(): Array<{ open: boolean } | undefined> {
|
|
1882
|
+
return [
|
|
1883
|
+
this.linkEditorEl,
|
|
1884
|
+
this.imagePanelEl,
|
|
1885
|
+
this.embedPanelEl,
|
|
1886
|
+
this.bookmarkPanelEl,
|
|
1887
|
+
this.specialCharsEl,
|
|
1888
|
+
this.emojiPickerEl,
|
|
1889
|
+
this.insertMenuEl,
|
|
1890
|
+
this.mathPanelEl,
|
|
1891
|
+
this.markdownPanelEl,
|
|
1892
|
+
];
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/** Closes every open insert panel. `refocus` returns focus to the editor. */
|
|
1896
|
+
private closeAllPanels(refocus = true): void {
|
|
1897
|
+
if (this.linkEditorEl?.open) this.closeLinkEditor(refocus);
|
|
1898
|
+
if (this.imagePanelEl?.open) this.closeImagePanel(refocus);
|
|
1899
|
+
if (this.embedPanelEl?.open) this.closeEmbedPanel(refocus);
|
|
1900
|
+
if (this.bookmarkPanelEl?.open) this.closeBookmarkPanel(refocus);
|
|
1901
|
+
if (this.specialCharsEl?.open) this.closeSpecialChars(refocus);
|
|
1902
|
+
if (this.emojiPickerEl?.open) this.closeEmojiPicker(refocus);
|
|
1903
|
+
if (this.insertMenuEl?.open) this.closeInsertMenu(refocus);
|
|
1904
|
+
if (this.mathPanelEl?.open) this.closeMathPanel(refocus);
|
|
1905
|
+
if (this.markdownPanelEl?.open) this.closeMarkdownPanel(refocus);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/**
|
|
1909
|
+
* Closes any open insert panel when the pointer goes down outside of it —
|
|
1910
|
+
* whether elsewhere in the editor (e.g. the content area) or off the widget.
|
|
1911
|
+
*/
|
|
1912
|
+
private onOutsidePointerDown = (event: Event): void => {
|
|
1913
|
+
const openPanels = this.floatingPanels.filter(
|
|
1914
|
+
(p): p is HTMLElement & { open: boolean } => !!p && (p as { open: boolean }).open
|
|
1915
|
+
);
|
|
1916
|
+
if (!openPanels.length) return;
|
|
1917
|
+
const path = event.composedPath();
|
|
1918
|
+
// Click landed inside an open panel — leave it be.
|
|
1919
|
+
if (openPanels.some(panel => path.includes(panel))) return;
|
|
1920
|
+
// Refocus the editor only when the click stayed within the widget.
|
|
1921
|
+
const insideWidget =
|
|
1922
|
+
path.includes(this) || (!!this.shadowRoot && path.includes(this.shadowRoot));
|
|
1923
|
+
this.closeAllPanels(insideWidget);
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
public render(): TemplateResult {
|
|
1927
|
+
const showToolbar = this.toolbar !== 'none' && !this.readonly && !this.sourceMode;
|
|
1928
|
+
return html`
|
|
1929
|
+
<div
|
|
1930
|
+
class="nile-wysiwyg ${this.showBlocks ? 'nile-wysiwyg--show-blocks' : ''}"
|
|
1931
|
+
part="base"
|
|
1932
|
+
@wysiwyg-command=${this.handleCommand}
|
|
1933
|
+
@focusout=${this.handleChromeFocusOut}
|
|
1934
|
+
@keydown=${this.handleBaseKeydown}
|
|
1935
|
+
>
|
|
1936
|
+
${showToolbar
|
|
1937
|
+
? html`
|
|
1938
|
+
<nile-wysiwyg-toolbar
|
|
1939
|
+
part="toolbar"
|
|
1940
|
+
exportparts="button, select, color"
|
|
1941
|
+
config=${this.toolbar}
|
|
1942
|
+
?disabled=${this.disabled}
|
|
1943
|
+
.toolbarState=${this.toolbarState}
|
|
1944
|
+
></nile-wysiwyg-toolbar>
|
|
1945
|
+
`
|
|
1946
|
+
: nothing}
|
|
1947
|
+
${this.sourceMode
|
|
1948
|
+
? html`
|
|
1949
|
+
<nile-wysiwyg-source-view
|
|
1950
|
+
@wysiwyg-source-apply=${(e: CustomEvent) => {
|
|
1951
|
+
e.stopPropagation();
|
|
1952
|
+
this.applySourceView(e.detail.html);
|
|
1953
|
+
}}
|
|
1954
|
+
@wysiwyg-source-cancel=${(e: Event) => {
|
|
1955
|
+
e.stopPropagation();
|
|
1956
|
+
this.applySourceView(null);
|
|
1957
|
+
}}
|
|
1958
|
+
></nile-wysiwyg-source-view>
|
|
1959
|
+
`
|
|
1960
|
+
: nothing}
|
|
1961
|
+
<div
|
|
1962
|
+
class="nile-wysiwyg__surface"
|
|
1963
|
+
part="surface"
|
|
1964
|
+
style=${this.sourceMode ? 'display: none' : ''}
|
|
1965
|
+
></div>
|
|
1966
|
+
${(this.maxLength > 0 || this.showCounter) && !this.sourceMode
|
|
1967
|
+
? html`
|
|
1968
|
+
<div
|
|
1969
|
+
class="nile-wysiwyg__counter ${this.maxLength > 0 &&
|
|
1970
|
+
this.counter.characters >= this.maxLength
|
|
1971
|
+
? 'over'
|
|
1972
|
+
: ''}"
|
|
1973
|
+
part="counter"
|
|
1974
|
+
>
|
|
1975
|
+
${this.maxLength > 0
|
|
1976
|
+
? `${this.counter.characters} / ${this.maxLength}`
|
|
1977
|
+
: `${this.counter.words} words · ${this.counter.characters} characters`}
|
|
1978
|
+
</div>
|
|
1979
|
+
`
|
|
1980
|
+
: nothing}
|
|
1981
|
+
<nile-wysiwyg-shortcut-help
|
|
1982
|
+
@wysiwyg-shortcut-help-close=${(e: Event) => {
|
|
1983
|
+
e.stopPropagation();
|
|
1984
|
+
this.closeShortcutHelp();
|
|
1985
|
+
}}
|
|
1986
|
+
></nile-wysiwyg-shortcut-help>
|
|
1987
|
+
<nile-wysiwyg-bubble-menu></nile-wysiwyg-bubble-menu>
|
|
1988
|
+
<nile-wysiwyg-table-menu></nile-wysiwyg-table-menu>
|
|
1989
|
+
<nile-wysiwyg-code-menu></nile-wysiwyg-code-menu>
|
|
1990
|
+
<nile-wysiwyg-image-menu></nile-wysiwyg-image-menu>
|
|
1991
|
+
<nile-wysiwyg-embed-menu></nile-wysiwyg-embed-menu>
|
|
1992
|
+
<nile-wysiwyg-special-chars
|
|
1993
|
+
@wysiwyg-char-select=${(e: CustomEvent) => {
|
|
1994
|
+
e.stopPropagation();
|
|
1995
|
+
this.insertSpecialChar(e.detail.char);
|
|
1996
|
+
}}
|
|
1997
|
+
@wysiwyg-char-cancel=${(e: Event) => {
|
|
1998
|
+
e.stopPropagation();
|
|
1999
|
+
this.closeSpecialChars();
|
|
2000
|
+
}}
|
|
2001
|
+
></nile-wysiwyg-special-chars>
|
|
2002
|
+
<nile-wysiwyg-emoji-picker
|
|
2003
|
+
@wysiwyg-emoji-select=${(e: CustomEvent) => {
|
|
2004
|
+
e.stopPropagation();
|
|
2005
|
+
this.closeEmojiPicker();
|
|
2006
|
+
this.insertSpecialChar(e.detail.char);
|
|
2007
|
+
}}
|
|
2008
|
+
@wysiwyg-emoji-cancel=${(e: Event) => {
|
|
2009
|
+
e.stopPropagation();
|
|
2010
|
+
this.closeEmojiPicker();
|
|
2011
|
+
}}
|
|
2012
|
+
></nile-wysiwyg-emoji-picker>
|
|
2013
|
+
<nile-wysiwyg-insert-menu
|
|
2014
|
+
@wysiwyg-insert-select=${(e: CustomEvent) => {
|
|
2015
|
+
e.stopPropagation();
|
|
2016
|
+
this.handleInsertSelect(e.detail.index);
|
|
2017
|
+
}}
|
|
2018
|
+
@wysiwyg-insert-cancel=${(e: Event) => {
|
|
2019
|
+
e.stopPropagation();
|
|
2020
|
+
this.closeInsertMenu();
|
|
2021
|
+
}}
|
|
2022
|
+
></nile-wysiwyg-insert-menu>
|
|
2023
|
+
<nile-wysiwyg-math-panel
|
|
2024
|
+
@wysiwyg-math-apply=${(e: CustomEvent) => {
|
|
2025
|
+
e.stopPropagation();
|
|
2026
|
+
this.applyMath(e.detail.latex, e.detail.display);
|
|
2027
|
+
}}
|
|
2028
|
+
@wysiwyg-math-cancel=${(e: Event) => {
|
|
2029
|
+
e.stopPropagation();
|
|
2030
|
+
this.closeMathPanel();
|
|
2031
|
+
}}
|
|
2032
|
+
></nile-wysiwyg-math-panel>
|
|
2033
|
+
<nile-wysiwyg-markdown-panel
|
|
2034
|
+
@wysiwyg-markdown-apply=${(e: CustomEvent) => {
|
|
2035
|
+
e.stopPropagation();
|
|
2036
|
+
this.closeMarkdownPanel();
|
|
2037
|
+
this.insertMarkdown(e.detail.markdown);
|
|
2038
|
+
}}
|
|
2039
|
+
@wysiwyg-markdown-cancel=${(e: Event) => {
|
|
2040
|
+
e.stopPropagation();
|
|
2041
|
+
this.closeMarkdownPanel();
|
|
2042
|
+
}}
|
|
2043
|
+
></nile-wysiwyg-markdown-panel>
|
|
2044
|
+
<nile-wysiwyg-embed-panel
|
|
2045
|
+
@wysiwyg-embed-apply=${(e: CustomEvent) => {
|
|
2046
|
+
e.stopPropagation();
|
|
2047
|
+
this.closeEmbedPanel();
|
|
2048
|
+
this.execCommand('insertEmbed', {
|
|
2049
|
+
src: e.detail.src,
|
|
2050
|
+
provider: e.detail.provider,
|
|
2051
|
+
});
|
|
2052
|
+
}}
|
|
2053
|
+
@wysiwyg-embed-cancel=${(e: Event) => {
|
|
2054
|
+
e.stopPropagation();
|
|
2055
|
+
this.closeEmbedPanel();
|
|
2056
|
+
}}
|
|
2057
|
+
></nile-wysiwyg-embed-panel>
|
|
2058
|
+
<nile-wysiwyg-bookmark-panel
|
|
2059
|
+
@wysiwyg-bookmark-apply=${(e: CustomEvent) => {
|
|
2060
|
+
e.stopPropagation();
|
|
2061
|
+
this.closeBookmarkPanel();
|
|
2062
|
+
this.execCommand('insertBookmark', { id: e.detail.id });
|
|
2063
|
+
}}
|
|
2064
|
+
@wysiwyg-bookmark-cancel=${(e: Event) => {
|
|
2065
|
+
e.stopPropagation();
|
|
2066
|
+
this.closeBookmarkPanel();
|
|
2067
|
+
}}
|
|
2068
|
+
></nile-wysiwyg-bookmark-panel>
|
|
2069
|
+
<nile-wysiwyg-suggest-list
|
|
2070
|
+
@wysiwyg-suggest-select=${(e: CustomEvent) => {
|
|
2071
|
+
e.stopPropagation();
|
|
2072
|
+
this.handleSuggestSelect(e.detail.item);
|
|
2073
|
+
}}
|
|
2074
|
+
></nile-wysiwyg-suggest-list>
|
|
2075
|
+
<nile-wysiwyg-find-bar
|
|
2076
|
+
@wysiwyg-find=${(e: CustomEvent) => {
|
|
2077
|
+
e.stopPropagation();
|
|
2078
|
+
this.find(e.detail.query);
|
|
2079
|
+
}}
|
|
2080
|
+
@wysiwyg-find-step=${(e: CustomEvent) => {
|
|
2081
|
+
e.stopPropagation();
|
|
2082
|
+
if (e.detail.direction === 'prev') this.findPrev();
|
|
2083
|
+
else this.findNext();
|
|
2084
|
+
}}
|
|
2085
|
+
@wysiwyg-replace=${(e: CustomEvent) => {
|
|
2086
|
+
e.stopPropagation();
|
|
2087
|
+
this.replace(e.detail.value);
|
|
2088
|
+
}}
|
|
2089
|
+
@wysiwyg-replace-all=${(e: CustomEvent) => {
|
|
2090
|
+
e.stopPropagation();
|
|
2091
|
+
this.replaceAll(e.detail.value);
|
|
2092
|
+
}}
|
|
2093
|
+
@wysiwyg-find-close=${(e: Event) => {
|
|
2094
|
+
e.stopPropagation();
|
|
2095
|
+
this.closeFindBar();
|
|
2096
|
+
}}
|
|
2097
|
+
></nile-wysiwyg-find-bar>
|
|
2098
|
+
<nile-wysiwyg-image-panel
|
|
2099
|
+
?upload-from-computer=${this.uploadFromComputer}
|
|
2100
|
+
.maxFileSize=${this.imageMaxFileSize}
|
|
2101
|
+
@wysiwyg-image-apply=${(e: CustomEvent) => {
|
|
2102
|
+
e.stopPropagation();
|
|
2103
|
+
const editing = this.imagePanelEl?.editing;
|
|
2104
|
+
this.closeImagePanel();
|
|
2105
|
+
this.execCommand(editing ? 'updateImage' : 'insertImage', {
|
|
2106
|
+
src: e.detail.src,
|
|
2107
|
+
alt: e.detail.alt || null,
|
|
2108
|
+
caption: e.detail.caption || null,
|
|
2109
|
+
});
|
|
2110
|
+
}}
|
|
2111
|
+
@wysiwyg-image-file=${(e: CustomEvent) => {
|
|
2112
|
+
e.stopPropagation();
|
|
2113
|
+
this.closeImagePanel();
|
|
2114
|
+
this.insertImageFile(e.detail.file, null, {
|
|
2115
|
+
alt: e.detail.alt || null,
|
|
2116
|
+
caption: e.detail.caption || null,
|
|
2117
|
+
});
|
|
2118
|
+
}}
|
|
2119
|
+
@wysiwyg-image-remove=${(e: Event) => {
|
|
2120
|
+
e.stopPropagation();
|
|
2121
|
+
this.closeImagePanel();
|
|
2122
|
+
this.execCommand('deleteImage');
|
|
2123
|
+
}}
|
|
2124
|
+
@wysiwyg-image-cancel=${(e: Event) => {
|
|
2125
|
+
e.stopPropagation();
|
|
2126
|
+
this.closeImagePanel();
|
|
2127
|
+
}}
|
|
2128
|
+
></nile-wysiwyg-image-panel>
|
|
2129
|
+
<nile-wysiwyg-link-editor
|
|
2130
|
+
@wysiwyg-link-apply=${(e: CustomEvent) => {
|
|
2131
|
+
e.stopPropagation();
|
|
2132
|
+
this.applyLink(e.detail.href, e.detail.text);
|
|
2133
|
+
}}
|
|
2134
|
+
@wysiwyg-link-remove=${(e: Event) => {
|
|
2135
|
+
e.stopPropagation();
|
|
2136
|
+
this.execCommand('unlink');
|
|
2137
|
+
this.closeLinkEditor();
|
|
2138
|
+
}}
|
|
2139
|
+
@wysiwyg-link-cancel=${(e: Event) => {
|
|
2140
|
+
e.stopPropagation();
|
|
2141
|
+
this.closeLinkEditor();
|
|
2142
|
+
}}
|
|
2143
|
+
></nile-wysiwyg-link-editor>
|
|
2144
|
+
</div>
|
|
2145
|
+
`;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
export default NileWysiwygEditor;
|
|
2150
|
+
|
|
2151
|
+
declare global {
|
|
2152
|
+
interface HTMLElementTagNameMap {
|
|
2153
|
+
'nile-wysiwyg-editor': NileWysiwygEditor;
|
|
2154
|
+
}
|
|
2155
|
+
}
|