@threadlabs/looma 0.23.20 → 0.23.22
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/components/shared/overlay.js +1 -1
- package/components/ui-combobox/ui-combobox.html +7 -8
- package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +2 -2
- package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +1 -1
- package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +3 -3
- package/components/ui-editor-table-overlay/ui-editor-table-overlay.html +2 -2
- package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +4 -4
- package/components/ui-image/ui-image.html +177 -0
- package/components/ui-image/ui-image.js +107 -0
- package/components/ui-listbox/ui-listbox.html +1 -1
- package/components/ui-menu/ui-menu.js +1 -1
- package/components/ui-popover/ui-popover.js +4 -1
- package/components/ui-separator/ui-separator.html +18 -0
- package/components/ui-toast-region/ui-toast-region.html +1 -1
- package/dist/index.js +24 -23
- package/editor/chunks/extensions.js +633 -20
- package/editor/extensions/block-selection.d.ts +6 -0
- package/editor/extensions/expand.d.ts +31 -0
- package/editor/extensions/image.d.ts +12 -0
- package/editor/extensions/index.d.ts +4 -1
- package/editor/extensions/index.js +2 -2
- package/editor/extensions/preset.d.ts +8 -0
- package/editor/extensions/slash-command.d.ts +31 -3
- package/editor/extensions/table-of-contents.d.ts +58 -0
- package/editor/index.js +2 -2
- package/package.json +2 -2
- package/styles/ui-action-bar.css +1 -1
- package/styles/ui-affordance-scope.css +1 -1
- package/styles/ui-avatar-group.css +1 -1
- package/styles/ui-avatar.css +1 -1
- package/styles/ui-badge.css +1 -1
- package/styles/ui-breadcrumb-item.css +1 -1
- package/styles/ui-breadcrumbs.css +1 -1
- package/styles/ui-button.css +1 -1
- package/styles/ui-callout.css +1 -1
- package/styles/ui-card.css +1 -1
- package/styles/ui-checkbox.css +1 -1
- package/styles/ui-cluster.css +1 -1
- package/styles/ui-combobox.css +1 -1
- package/styles/ui-container.css +1 -1
- package/styles/ui-context-menu.css +1 -1
- package/styles/ui-cover.css +1 -1
- package/styles/ui-description-item.css +1 -1
- package/styles/ui-description-list.css +1 -1
- package/styles/ui-dialog.css +1 -1
- package/styles/ui-disclosure.css +1 -1
- package/styles/ui-editable.css +1 -1
- package/styles/ui-editor-insert-table-grid.css +1 -1
- package/styles/ui-editor-mention-menu-item.css +1 -1
- package/styles/ui-editor-mention-menu.css +1 -1
- package/styles/ui-editor-slash-menu-group.css +1 -1
- package/styles/ui-editor-slash-menu-item.css +1 -1
- package/styles/ui-editor-slash-menu.css +1 -1
- package/styles/ui-editor-table-context-menu.css +1 -1
- package/styles/ui-editor-table-overlay.css +1 -1
- package/styles/ui-editor-table-toolbar.css +1 -1
- package/styles/ui-editor-toolbar.css +1 -1
- package/styles/ui-floating-action-button.css +1 -1
- package/styles/ui-form-field.css +1 -1
- package/styles/ui-grid.css +1 -1
- package/styles/ui-icon-button.css +1 -1
- package/styles/ui-icon.css +1 -1
- package/styles/ui-image.css +153 -0
- package/styles/ui-input-group.css +1 -1
- package/styles/ui-input.css +1 -1
- package/styles/ui-list-item.css +1 -1
- package/styles/ui-list.css +1 -1
- package/styles/ui-listbox.css +1 -1
- package/styles/ui-menu-group.css +1 -1
- package/styles/ui-menu-item.css +1 -1
- package/styles/ui-menu.css +1 -1
- package/styles/ui-meter.css +1 -1
- package/styles/ui-nav-item.css +1 -1
- package/styles/ui-page-header.css +1 -1
- package/styles/ui-popover.css +1 -1
- package/styles/ui-radio-group.css +1 -1
- package/styles/ui-radio.css +1 -1
- package/styles/ui-reel.css +1 -1
- package/styles/ui-scroll-area.css +1 -1
- package/styles/ui-search-result-row.css +1 -1
- package/styles/ui-search-shell.css +1 -1
- package/styles/ui-section.css +1 -1
- package/styles/ui-select.css +1 -1
- package/styles/ui-separator.css +17 -1
- package/styles/ui-sidebar.css +1 -1
- package/styles/ui-spinner.css +1 -1
- package/styles/ui-stack.css +1 -1
- package/styles/ui-status-message.css +1 -1
- package/styles/ui-switch.css +1 -1
- package/styles/ui-switcher.css +1 -1
- package/styles/ui-table.css +1 -1
- package/styles/ui-tabs.css +1 -1
- package/styles/ui-text.css +1 -1
- package/styles/ui-textarea.css +1 -1
- package/styles/ui-toast-region.css +1 -1
- package/styles/ui-toast.css +1 -1
- package/styles/ui-tooltip.css +1 -1
- package/styles/ui-top-bar.css +1 -1
- package/styles/ui-tree-item.css +1 -1
- package/styles/ui-tree.css +1 -1
- package/vanilla/UiActionBar.d.ts +1 -1
- package/vanilla/UiActionBar.js +1 -1
- package/vanilla/UiAffordanceScope.d.ts +1 -1
- package/vanilla/UiAffordanceScope.js +1 -1
- package/vanilla/UiAvatar.d.ts +1 -1
- package/vanilla/UiAvatar.js +1 -1
- package/vanilla/UiAvatarGroup.d.ts +1 -1
- package/vanilla/UiAvatarGroup.js +1 -1
- package/vanilla/UiBadge.d.ts +1 -1
- package/vanilla/UiBadge.js +1 -1
- package/vanilla/UiBreadcrumbItem.d.ts +1 -1
- package/vanilla/UiBreadcrumbItem.js +1 -1
- package/vanilla/UiBreadcrumbs.d.ts +1 -1
- package/vanilla/UiBreadcrumbs.js +1 -1
- package/vanilla/UiButton.d.ts +1 -1
- package/vanilla/UiButton.js +1 -1
- package/vanilla/UiCallout.d.ts +1 -1
- package/vanilla/UiCallout.js +1 -1
- package/vanilla/UiCard.d.ts +1 -1
- package/vanilla/UiCard.js +1 -1
- package/vanilla/UiCheckbox.d.ts +1 -1
- package/vanilla/UiCheckbox.js +1 -1
- package/vanilla/UiCluster.d.ts +1 -1
- package/vanilla/UiCluster.js +1 -1
- package/vanilla/UiCombobox.d.ts +1 -1
- package/vanilla/UiCombobox.js +181 -167
- package/vanilla/UiContainer.d.ts +1 -1
- package/vanilla/UiContainer.js +1 -1
- package/vanilla/UiContextMenu.d.ts +1 -1
- package/vanilla/UiContextMenu.js +1 -1
- package/vanilla/UiCover.d.ts +1 -1
- package/vanilla/UiCover.js +1 -1
- package/vanilla/UiDescriptionItem.d.ts +1 -1
- package/vanilla/UiDescriptionItem.js +1 -1
- package/vanilla/UiDescriptionList.d.ts +1 -1
- package/vanilla/UiDescriptionList.js +1 -1
- package/vanilla/UiDialog.d.ts +1 -1
- package/vanilla/UiDialog.js +1 -1
- package/vanilla/UiDisclosure.d.ts +1 -1
- package/vanilla/UiDisclosure.js +1 -1
- package/vanilla/UiEditable.d.ts +1 -1
- package/vanilla/UiEditable.js +1 -1
- package/vanilla/UiEditorInsertTableGrid.d.ts +1 -1
- package/vanilla/UiEditorInsertTableGrid.js +1 -1
- package/vanilla/UiEditorMentionMenu.d.ts +1 -1
- package/vanilla/UiEditorMentionMenu.js +19 -13
- package/vanilla/UiEditorMentionMenuItem.d.ts +1 -1
- package/vanilla/UiEditorMentionMenuItem.js +1 -1
- package/vanilla/UiEditorSlashMenu.d.ts +1 -1
- package/vanilla/UiEditorSlashMenu.js +17 -13
- package/vanilla/UiEditorSlashMenuGroup.d.ts +1 -1
- package/vanilla/UiEditorSlashMenuGroup.js +1 -1
- package/vanilla/UiEditorSlashMenuItem.d.ts +1 -1
- package/vanilla/UiEditorSlashMenuItem.js +1 -1
- package/vanilla/UiEditorTableContextMenu.d.ts +1 -1
- package/vanilla/UiEditorTableContextMenu.js +26 -20
- package/vanilla/UiEditorTableOverlay.d.ts +1 -1
- package/vanilla/UiEditorTableOverlay.js +69 -65
- package/vanilla/UiEditorTableToolbar.d.ts +1 -1
- package/vanilla/UiEditorTableToolbar.js +72 -64
- package/vanilla/UiEditorToolbar.d.ts +1 -1
- package/vanilla/UiEditorToolbar.js +1 -1
- package/vanilla/UiFloatingActionButton.d.ts +1 -1
- package/vanilla/UiFloatingActionButton.js +1 -1
- package/vanilla/UiFormField.d.ts +1 -1
- package/vanilla/UiFormField.js +1 -1
- package/vanilla/UiGrid.d.ts +1 -1
- package/vanilla/UiGrid.js +1 -1
- package/vanilla/UiIcon.d.ts +1 -1
- package/vanilla/UiIcon.js +1 -1
- package/vanilla/UiIconButton.d.ts +1 -1
- package/vanilla/UiIconButton.js +1 -1
- package/vanilla/UiImage.d.ts +21 -0
- package/vanilla/UiImage.js +84 -0
- package/vanilla/UiInput.d.ts +1 -1
- package/vanilla/UiInput.js +1 -1
- package/vanilla/UiInputGroup.d.ts +1 -1
- package/vanilla/UiInputGroup.js +1 -1
- package/vanilla/UiList.d.ts +1 -1
- package/vanilla/UiList.js +1 -1
- package/vanilla/UiListItem.d.ts +1 -1
- package/vanilla/UiListItem.js +1 -1
- package/vanilla/UiListbox.d.ts +1 -1
- package/vanilla/UiListbox.js +33 -31
- package/vanilla/UiMenu.d.ts +1 -1
- package/vanilla/UiMenu.js +1 -1
- package/vanilla/UiMenuGroup.d.ts +1 -1
- package/vanilla/UiMenuGroup.js +1 -1
- package/vanilla/UiMenuItem.d.ts +1 -1
- package/vanilla/UiMenuItem.js +1 -1
- package/vanilla/UiMeter.d.ts +1 -1
- package/vanilla/UiMeter.js +1 -1
- package/vanilla/UiNavItem.d.ts +1 -1
- package/vanilla/UiNavItem.js +1 -1
- package/vanilla/UiPageHeader.d.ts +1 -1
- package/vanilla/UiPageHeader.js +1 -1
- package/vanilla/UiPopover.d.ts +1 -1
- package/vanilla/UiPopover.js +1 -1
- package/vanilla/UiRadio.d.ts +1 -1
- package/vanilla/UiRadio.js +1 -1
- package/vanilla/UiRadioGroup.d.ts +1 -1
- package/vanilla/UiRadioGroup.js +1 -1
- package/vanilla/UiReel.d.ts +1 -1
- package/vanilla/UiReel.js +1 -1
- package/vanilla/UiScrollArea.d.ts +1 -1
- package/vanilla/UiScrollArea.js +1 -1
- package/vanilla/UiSearchResultRow.d.ts +1 -1
- package/vanilla/UiSearchResultRow.js +1 -1
- package/vanilla/UiSearchShell.d.ts +1 -1
- package/vanilla/UiSearchShell.js +1 -1
- package/vanilla/UiSection.d.ts +1 -1
- package/vanilla/UiSection.js +1 -1
- package/vanilla/UiSelect.d.ts +1 -1
- package/vanilla/UiSelect.js +1 -1
- package/vanilla/UiSeparator.d.ts +3 -1
- package/vanilla/UiSeparator.js +11 -3
- package/vanilla/UiSidebar.d.ts +1 -1
- package/vanilla/UiSidebar.js +1 -1
- package/vanilla/UiSpinner.d.ts +1 -1
- package/vanilla/UiSpinner.js +1 -1
- package/vanilla/UiStack.d.ts +1 -1
- package/vanilla/UiStack.js +1 -1
- package/vanilla/UiStatusMessage.d.ts +1 -1
- package/vanilla/UiStatusMessage.js +1 -1
- package/vanilla/UiSwitch.d.ts +1 -1
- package/vanilla/UiSwitch.js +1 -1
- package/vanilla/UiSwitcher.d.ts +1 -1
- package/vanilla/UiSwitcher.js +1 -1
- package/vanilla/UiTable.d.ts +1 -1
- package/vanilla/UiTable.js +1 -1
- package/vanilla/UiTabs.d.ts +1 -1
- package/vanilla/UiTabs.js +1 -1
- package/vanilla/UiText.d.ts +1 -1
- package/vanilla/UiText.js +1 -1
- package/vanilla/UiTextarea.d.ts +1 -1
- package/vanilla/UiTextarea.js +1 -1
- package/vanilla/UiToast.d.ts +1 -1
- package/vanilla/UiToast.js +1 -1
- package/vanilla/UiToastRegion.d.ts +1 -1
- package/vanilla/UiToastRegion.js +13 -11
- package/vanilla/UiTooltip.d.ts +1 -1
- package/vanilla/UiTooltip.js +1 -1
- package/vanilla/UiTopBar.d.ts +1 -1
- package/vanilla/UiTopBar.js +1 -1
- package/vanilla/UiTree.d.ts +1 -1
- package/vanilla/UiTree.js +1 -1
- package/vanilla/UiTreeItem.d.ts +1 -1
- package/vanilla/UiTreeItem.js +1 -1
- package/vanilla/index.js +1 -0
- package/vue/UiActionBar.vue +1 -1
- package/vue/UiAffordanceScope.vue +1 -1
- package/vue/UiAvatar.vue +1 -1
- package/vue/UiAvatarGroup.vue +2 -2
- package/vue/UiBadge.vue +1 -1
- package/vue/UiBreadcrumbItem.vue +1 -1
- package/vue/UiBreadcrumbs.vue +1 -1
- package/vue/UiButton.vue +1 -1
- package/vue/UiCallout.vue +1 -1
- package/vue/UiCard.vue +1 -1
- package/vue/UiCheckbox.vue +1 -1
- package/vue/UiCluster.vue +1 -1
- package/vue/UiCombobox.vue +1 -1
- package/vue/UiContainer.vue +1 -1
- package/vue/UiContextMenu.vue +1 -1
- package/vue/UiCover.vue +1 -1
- package/vue/UiDescriptionItem.vue +1 -1
- package/vue/UiDescriptionList.vue +1 -1
- package/vue/UiDialog.vue +1 -1
- package/vue/UiDisclosure.vue +1 -1
- package/vue/UiEditable.vue +1 -1
- package/vue/UiEditorInsertTableGrid.vue +1 -1
- package/vue/UiEditorMentionMenu.vue +1 -1
- package/vue/UiEditorMentionMenuItem.vue +1 -1
- package/vue/UiEditorSlashMenu.vue +1 -1
- package/vue/UiEditorSlashMenuGroup.vue +1 -1
- package/vue/UiEditorSlashMenuItem.vue +1 -1
- package/vue/UiEditorTableContextMenu.vue +1 -1
- package/vue/UiEditorTableOverlay.vue +1 -1
- package/vue/UiEditorTableToolbar.vue +1 -1
- package/vue/UiEditorToolbar.vue +1 -1
- package/vue/UiFloatingActionButton.vue +1 -1
- package/vue/UiFormField.vue +1 -1
- package/vue/UiGrid.vue +1 -1
- package/vue/UiIcon.vue +1 -1
- package/vue/UiIconButton.vue +1 -1
- package/vue/UiImage.d.ts +100 -0
- package/vue/UiImage.js +2 -0
- package/vue/UiImage.vue +463 -0
- package/vue/UiInput.vue +1 -1
- package/vue/UiInputGroup.vue +1 -1
- package/vue/UiList.vue +1 -1
- package/vue/UiListItem.vue +1 -1
- package/vue/UiListbox.vue +1 -1
- package/vue/UiMenu.vue +1 -1
- package/vue/UiMenuGroup.vue +1 -1
- package/vue/UiMenuItem.vue +1 -1
- package/vue/UiMeter.vue +1 -1
- package/vue/UiNavItem.vue +1 -1
- package/vue/UiPageHeader.vue +1 -1
- package/vue/UiPopover.vue +1 -1
- package/vue/UiRadio.vue +1 -1
- package/vue/UiRadioGroup.vue +1 -1
- package/vue/UiReel.vue +1 -1
- package/vue/UiScrollArea.vue +1 -1
- package/vue/UiSearchResultRow.vue +1 -1
- package/vue/UiSearchShell.vue +1 -1
- package/vue/UiSection.vue +1 -1
- package/vue/UiSelect.vue +1 -1
- package/vue/UiSeparator.d.ts +18 -0
- package/vue/UiSeparator.vue +67 -2
- package/vue/UiSidebar.d.ts +1 -1
- package/vue/UiSidebar.vue +1 -1
- package/vue/UiSpinner.vue +1 -1
- package/vue/UiStack.vue +1 -1
- package/vue/UiStatusMessage.vue +1 -1
- package/vue/UiSwitch.vue +1 -1
- package/vue/UiSwitcher.vue +1 -1
- package/vue/UiTable.vue +1 -1
- package/vue/UiTabs.vue +1 -1
- package/vue/UiText.vue +1 -1
- package/vue/UiTextarea.vue +1 -1
- package/vue/UiToast.vue +1 -1
- package/vue/UiToastRegion.vue +1 -1
- package/vue/UiTooltip.vue +1 -1
- package/vue/UiTopBar.vue +1 -1
- package/vue/UiTree.vue +1 -1
- package/vue/UiTreeItem.vue +1 -1
- package/vue/chunks/UiActionBar.js +1 -1
- package/vue/chunks/UiAffordanceScope.js +1 -1
- package/vue/chunks/UiAvatar.js +1 -1
- package/vue/chunks/UiAvatarGroup.js +2 -2
- package/vue/chunks/UiBadge.js +1 -1
- package/vue/chunks/UiBreadcrumbItem.js +1 -1
- package/vue/chunks/UiBreadcrumbs.js +1 -1
- package/vue/chunks/UiButton.js +1 -1
- package/vue/chunks/UiCallout.js +1 -1
- package/vue/chunks/UiCard.js +1 -1
- package/vue/chunks/UiCheckbox.js +1 -1
- package/vue/chunks/UiCluster.js +1 -1
- package/vue/chunks/UiCombobox.js +1 -1
- package/vue/chunks/UiContainer.js +1 -1
- package/vue/chunks/UiContextMenu.js +1 -1
- package/vue/chunks/UiCover.js +1 -1
- package/vue/chunks/UiDescriptionItem.js +1 -1
- package/vue/chunks/UiDescriptionList.js +1 -1
- package/vue/chunks/UiDialog.js +1 -1
- package/vue/chunks/UiDisclosure.js +1 -1
- package/vue/chunks/UiEditable.js +1 -1
- package/vue/chunks/UiEditorInsertTableGrid.js +1 -1
- package/vue/chunks/UiEditorMentionMenu.js +1 -1
- package/vue/chunks/UiEditorMentionMenuItem.js +1 -1
- package/vue/chunks/UiEditorSlashMenu.js +1 -1
- package/vue/chunks/UiEditorSlashMenuGroup.js +1 -1
- package/vue/chunks/UiEditorSlashMenuItem.js +1 -1
- package/vue/chunks/UiEditorTableContextMenu.js +1 -1
- package/vue/chunks/UiEditorTableOverlay.js +1 -1
- package/vue/chunks/UiEditorTableToolbar.js +1 -1
- package/vue/chunks/UiEditorToolbar.js +1 -1
- package/vue/chunks/UiFloatingActionButton.js +1 -1
- package/vue/chunks/UiFormField.js +1 -1
- package/vue/chunks/UiGrid.js +1 -1
- package/vue/chunks/UiIcon.js +1 -1
- package/vue/chunks/UiIconButton.js +1 -1
- package/vue/chunks/UiImage.js +313 -0
- package/vue/chunks/UiInput.js +1 -1
- package/vue/chunks/UiInputGroup.js +1 -1
- package/vue/chunks/UiList.js +1 -1
- package/vue/chunks/UiListItem.js +1 -1
- package/vue/chunks/UiListbox.js +1 -1
- package/vue/chunks/UiMenu.js +1 -1
- package/vue/chunks/UiMenuGroup.js +1 -1
- package/vue/chunks/UiMenuItem.js +1 -1
- package/vue/chunks/UiMeter.js +1 -1
- package/vue/chunks/UiNavItem.js +1 -1
- package/vue/chunks/UiPageHeader.js +1 -1
- package/vue/chunks/UiPopover.js +1 -1
- package/vue/chunks/UiRadio.js +1 -1
- package/vue/chunks/UiRadioGroup.js +1 -1
- package/vue/chunks/UiReel.js +1 -1
- package/vue/chunks/UiScrollArea.js +1 -1
- package/vue/chunks/UiSearchResultRow.js +1 -1
- package/vue/chunks/UiSearchShell.js +1 -1
- package/vue/chunks/UiSection.js +1 -1
- package/vue/chunks/UiSelect.js +1 -1
- package/vue/chunks/UiSeparator.js +79 -22
- package/vue/chunks/UiSidebar.js +1 -1
- package/vue/chunks/UiSpinner.js +1 -1
- package/vue/chunks/UiStack.js +1 -1
- package/vue/chunks/UiStatusMessage.js +1 -1
- package/vue/chunks/UiSwitch.js +1 -1
- package/vue/chunks/UiSwitcher.js +1 -1
- package/vue/chunks/UiTable.js +1 -1
- package/vue/chunks/UiTabs.js +1 -1
- package/vue/chunks/UiText.js +1 -1
- package/vue/chunks/UiTextarea.js +1 -1
- package/vue/chunks/UiToast.js +1 -1
- package/vue/chunks/UiToastRegion.js +1 -1
- package/vue/chunks/UiTooltip.js +1 -1
- package/vue/chunks/UiTopBar.js +1 -1
- package/vue/chunks/UiTree.js +1 -1
- package/vue/chunks/UiTreeItem.js +1 -1
- package/vue/components.css +1469 -1309
- package/vue/editor/LoomaEditor.d.ts +19 -2
- package/vue/editor/divider.d.ts +6 -0
- package/vue/editor/expand.d.ts +7 -0
- package/vue/editor/image-controls.d.ts +19 -0
- package/vue/editor/image-delivery.d.ts +2 -2
- package/vue/editor/image.d.ts +7 -0
- package/vue/editor/index.d.ts +1 -1
- package/vue/editor/index.js +1188 -52
- package/vue/editor/table-of-contents.d.ts +7 -0
- package/vue/host.ts +1 -1
- package/vue/index.d.ts +1 -0
- package/vue/index.js +2 -1
package/vue/editor/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch } from "vue";
|
|
2
|
-
import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, resolveTableCellAt, shouldShowTextFormattingToolbar, siteRelativeHref } from "@threadlabs/looma/editor";
|
|
3
|
-
import { Button, Card, Checkbox, Cluster, Combobox, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, FormField, IconButton, Input, InputGroup, Menu, MenuItem, Popover, ScrollArea, SearchResultRow, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
|
|
1
|
+
import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, useId, watch } from "vue";
|
|
2
|
+
import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, normalizeTableOfContentsConfig, resolveTableCellAt, shouldShowTextFormattingToolbar, siteRelativeHref } from "@threadlabs/looma/editor";
|
|
3
|
+
import { Button, Card, Checkbox, Cluster, Combobox, Disclosure, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, EditorToolbar as EditorToolbar$1, FormField, Icon, IconButton, Image, Input, InputGroup, List, Menu, MenuItem, Popover, ScrollArea, SearchResultRow, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
|
|
4
4
|
import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
|
|
5
5
|
import { closeHistory } from "@tiptap/pm/history";
|
|
6
6
|
import { createLowlight } from "lowlight";
|
|
7
|
-
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
8
|
-
import { Extension } from "@tiptap/core";
|
|
7
|
+
import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
8
|
+
import { Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule } from "@tiptap/core";
|
|
9
9
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
10
|
+
import { DOMParser, DOMSerializer, Fragment as Fragment$1 } from "@tiptap/pm/model";
|
|
10
11
|
export * from "@threadlabs/looma/editor";
|
|
11
12
|
//#region src/vue/editor/primitives.ts
|
|
12
13
|
/** Each Vue editor component's element tag, for tooling that enumerates them. */
|
|
@@ -28,13 +29,107 @@ function announceOverlayOpen(document, id) {
|
|
|
28
29
|
document.dispatchEvent(new CustomEvent("ui-overlay-open", { detail: { id } }));
|
|
29
30
|
}
|
|
30
31
|
//#endregion
|
|
32
|
+
//#region ../../node_modules/.pnpm/@tiptap+extension-image@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2_/node_modules/@tiptap/extension-image/dist/index.js
|
|
33
|
+
/**
|
|
34
|
+
* Matches an image to a  on input.
|
|
35
|
+
*/
|
|
36
|
+
var inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/;
|
|
37
|
+
/**
|
|
38
|
+
* This extension allows you to insert images.
|
|
39
|
+
* @see https://www.tiptap.dev/api/nodes/image
|
|
40
|
+
*/
|
|
41
|
+
var Image$1 = Node$1.create({
|
|
42
|
+
name: "image",
|
|
43
|
+
addOptions() {
|
|
44
|
+
return {
|
|
45
|
+
inline: false,
|
|
46
|
+
allowBase64: false,
|
|
47
|
+
HTMLAttributes: {}
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
inline() {
|
|
51
|
+
return this.options.inline;
|
|
52
|
+
},
|
|
53
|
+
group() {
|
|
54
|
+
return this.options.inline ? "inline" : "block";
|
|
55
|
+
},
|
|
56
|
+
draggable: true,
|
|
57
|
+
addAttributes() {
|
|
58
|
+
return {
|
|
59
|
+
src: { default: null },
|
|
60
|
+
alt: { default: null },
|
|
61
|
+
title: { default: null }
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
parseHTML() {
|
|
65
|
+
return [{ tag: this.options.allowBase64 ? "img[src]" : "img[src]:not([src^=\"data:\"])" }];
|
|
66
|
+
},
|
|
67
|
+
renderHTML({ HTMLAttributes }) {
|
|
68
|
+
return ["img", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
|
69
|
+
},
|
|
70
|
+
addCommands() {
|
|
71
|
+
return { setImage: (options) => ({ commands }) => {
|
|
72
|
+
return commands.insertContent({
|
|
73
|
+
type: this.name,
|
|
74
|
+
attrs: options
|
|
75
|
+
});
|
|
76
|
+
} };
|
|
77
|
+
},
|
|
78
|
+
addInputRules() {
|
|
79
|
+
return [nodeInputRule({
|
|
80
|
+
find: inputRegex,
|
|
81
|
+
type: this.type,
|
|
82
|
+
getAttributes: (match) => {
|
|
83
|
+
const [, , alt, src, title] = match;
|
|
84
|
+
return {
|
|
85
|
+
src,
|
|
86
|
+
alt,
|
|
87
|
+
title
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
})];
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/editor/extensions/image.ts
|
|
95
|
+
/** Accepts only positive, integral CSS-pixel dimensions. */
|
|
96
|
+
function normalizeImageDimension(value) {
|
|
97
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
98
|
+
}
|
|
99
|
+
/** Unknown legacy values retain ordinary block placement. */
|
|
100
|
+
function normalizeImagePlacement(value) {
|
|
101
|
+
return value === "center" || value === "wrap-left" || value === "wrap-right" ? value : "block";
|
|
102
|
+
}
|
|
103
|
+
Image$1.extend({ addAttributes() {
|
|
104
|
+
return {
|
|
105
|
+
...this.parent?.(),
|
|
106
|
+
width: {
|
|
107
|
+
default: null,
|
|
108
|
+
parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("width"))) ?? null,
|
|
109
|
+
renderHTML: (attrs) => normalizeImageDimension(attrs.width) ? { width: attrs.width } : {}
|
|
110
|
+
},
|
|
111
|
+
height: {
|
|
112
|
+
default: null,
|
|
113
|
+
parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("height"))) ?? null,
|
|
114
|
+
renderHTML: (attrs) => normalizeImageDimension(attrs.height) ? { height: attrs.height } : {}
|
|
115
|
+
},
|
|
116
|
+
responsive: {
|
|
117
|
+
default: null,
|
|
118
|
+
parseHTML: (element) => element.hasAttribute("data-looma-responsive") || null,
|
|
119
|
+
renderHTML: (attrs) => attrs.responsive === true ? { "data-looma-responsive": "" } : {}
|
|
120
|
+
},
|
|
121
|
+
placement: {
|
|
122
|
+
default: "block",
|
|
123
|
+
parseHTML: (element) => normalizeImagePlacement(element.getAttribute("data-placement")),
|
|
124
|
+
renderHTML: (attrs) => normalizeImagePlacement(attrs.placement) === "block" ? {} : { "data-placement": normalizeImagePlacement(attrs.placement) }
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
} });
|
|
128
|
+
//#endregion
|
|
31
129
|
//#region src/vue/editor/image-delivery.ts
|
|
32
130
|
function optionalString(value) {
|
|
33
131
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
34
132
|
}
|
|
35
|
-
function normalizeImageDimension(value) {
|
|
36
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
37
|
-
}
|
|
38
133
|
function imageDescriptorFromAttrs(attrs) {
|
|
39
134
|
const src = optionalString(attrs.src);
|
|
40
135
|
if (!src) return null;
|
|
@@ -49,13 +144,15 @@ function imageDescriptorFromAttrs(attrs) {
|
|
|
49
144
|
...attrs.responsive === true ? { responsive: true } : {}
|
|
50
145
|
};
|
|
51
146
|
}
|
|
52
|
-
function transientAttributes(resolver, image) {
|
|
147
|
+
function transientAttributes(resolver, image, editable) {
|
|
53
148
|
const attributes = {
|
|
54
|
-
|
|
149
|
+
...editable ? {} : {
|
|
150
|
+
"aria-label": image.alt ? `Open image: ${image.alt}` : "Open image",
|
|
151
|
+
role: "button"
|
|
152
|
+
},
|
|
55
153
|
"data-looma-image": "",
|
|
56
|
-
role: "button",
|
|
57
154
|
src: image.src,
|
|
58
|
-
tabindex: "0"
|
|
155
|
+
tabindex: editable ? "-1" : "0"
|
|
59
156
|
};
|
|
60
157
|
if (!image.responsive || !resolver) return attributes;
|
|
61
158
|
let rendition;
|
|
@@ -85,7 +182,7 @@ function transientAttributes(resolver, image) {
|
|
|
85
182
|
if (src && src !== image.src || srcset) attributes["data-looma-rendition"] = "true";
|
|
86
183
|
return attributes;
|
|
87
184
|
}
|
|
88
|
-
function createDecorations(doc, resolver, failedRenditions) {
|
|
185
|
+
function createDecorations(doc, resolver, failedRenditions, editable) {
|
|
89
186
|
const decorations = [];
|
|
90
187
|
const currentSources = /* @__PURE__ */ new Set();
|
|
91
188
|
doc.descendants((node, position) => {
|
|
@@ -93,7 +190,7 @@ function createDecorations(doc, resolver, failedRenditions) {
|
|
|
93
190
|
const image = imageDescriptorFromAttrs(node.attrs);
|
|
94
191
|
if (!image) return;
|
|
95
192
|
currentSources.add(image.src);
|
|
96
|
-
decorations.push(Decoration.node(position, position + node.nodeSize, transientAttributes(failedRenditions.has(image.src) ? void 0 : resolver, image)));
|
|
193
|
+
decorations.push(Decoration.node(position, position + node.nodeSize, {}, { loomaImageAttributes: transientAttributes(failedRenditions.has(image.src) ? void 0 : resolver, image, editable) }));
|
|
97
194
|
});
|
|
98
195
|
for (const src of failedRenditions) if (!currentSources.has(src)) failedRenditions.delete(src);
|
|
99
196
|
return DecorationSet.create(doc, decorations);
|
|
@@ -127,36 +224,14 @@ function createLoomaImageDeliveryController(options) {
|
|
|
127
224
|
return {
|
|
128
225
|
extension: Extension.create({
|
|
129
226
|
name: "loomaImageDelivery",
|
|
130
|
-
addGlobalAttributes() {
|
|
131
|
-
return [{
|
|
132
|
-
types: ["image"],
|
|
133
|
-
attributes: {
|
|
134
|
-
width: {
|
|
135
|
-
default: null,
|
|
136
|
-
parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("width"))) ?? null,
|
|
137
|
-
renderHTML: (attributes) => normalizeImageDimension(attributes.width) ? { width: normalizeImageDimension(attributes.width) } : {}
|
|
138
|
-
},
|
|
139
|
-
height: {
|
|
140
|
-
default: null,
|
|
141
|
-
parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("height"))) ?? null,
|
|
142
|
-
renderHTML: (attributes) => normalizeImageDimension(attributes.height) ? { height: normalizeImageDimension(attributes.height) } : {}
|
|
143
|
-
},
|
|
144
|
-
responsive: {
|
|
145
|
-
default: null,
|
|
146
|
-
parseHTML: (element) => element.hasAttribute("data-looma-responsive") || null,
|
|
147
|
-
renderHTML: (attributes) => attributes.responsive === true ? { "data-looma-responsive": "" } : {}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}];
|
|
151
|
-
},
|
|
152
227
|
addProseMirrorPlugins() {
|
|
153
228
|
return [new Plugin({
|
|
154
229
|
key: pluginKey,
|
|
155
230
|
state: {
|
|
156
|
-
init: (_, state) => createDecorations(state.doc, options.resolveAttributes(), failedRenditions),
|
|
231
|
+
init: (_, state) => createDecorations(state.doc, options.resolveAttributes(), failedRenditions, options.isEditable()),
|
|
157
232
|
apply: (transaction, previous) => {
|
|
158
233
|
if (!transaction.getMeta(pluginKey) && !transactionChangesImages(transaction)) return previous.map(transaction.mapping, transaction.doc);
|
|
159
|
-
return createDecorations(transaction.doc, options.resolveAttributes(), failedRenditions);
|
|
234
|
+
return createDecorations(transaction.doc, options.resolveAttributes(), failedRenditions, options.isEditable());
|
|
160
235
|
}
|
|
161
236
|
},
|
|
162
237
|
props: { decorations: (state) => pluginKey.getState(state) }
|
|
@@ -407,6 +482,1010 @@ var autoCodeLanguageNames = [
|
|
|
407
482
|
"yaml"
|
|
408
483
|
];
|
|
409
484
|
//#endregion
|
|
485
|
+
//#region src/editor/extensions/expand.ts
|
|
486
|
+
/**
|
|
487
|
+
* Reveals collapsed ancestors before a host navigates to a document position.
|
|
488
|
+
* @contract Navigation changes presentation only, including in read-only mode.
|
|
489
|
+
*/
|
|
490
|
+
async function revealLoomaExpandAt(editor, position) {
|
|
491
|
+
const { node } = editor.view.domAtPos(position);
|
|
492
|
+
const target = node instanceof Element ? node : node.parentElement;
|
|
493
|
+
const pending = [];
|
|
494
|
+
target?.dispatchEvent(new CustomEvent("looma-editor-reveal", {
|
|
495
|
+
bubbles: true,
|
|
496
|
+
detail: { pending }
|
|
497
|
+
}));
|
|
498
|
+
await Promise.all(pending);
|
|
499
|
+
}
|
|
500
|
+
Node$1.create({
|
|
501
|
+
name: "loomaExpand",
|
|
502
|
+
group: "block",
|
|
503
|
+
content: "block+",
|
|
504
|
+
defining: true,
|
|
505
|
+
isolating: true,
|
|
506
|
+
addOptions: () => ({ nodeView: null }),
|
|
507
|
+
addAttributes() {
|
|
508
|
+
return {
|
|
509
|
+
summary: {
|
|
510
|
+
default: "Details",
|
|
511
|
+
parseHTML: (element) => element.querySelector(":scope > summary")?.textContent ?? "Details",
|
|
512
|
+
renderHTML: () => ({})
|
|
513
|
+
},
|
|
514
|
+
open: {
|
|
515
|
+
default: false,
|
|
516
|
+
parseHTML: (element) => element.hasAttribute("open"),
|
|
517
|
+
renderHTML: () => ({})
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
},
|
|
521
|
+
parseHTML() {
|
|
522
|
+
return [{
|
|
523
|
+
tag: "details",
|
|
524
|
+
getContent: (element, schema) => {
|
|
525
|
+
if (!(element instanceof Element)) return Fragment$1.from(schema.nodes.paragraph.create());
|
|
526
|
+
const body = element.ownerDocument.createElement("div");
|
|
527
|
+
const authoredBody = element.querySelector(":scope > [data-looma-expand-body]");
|
|
528
|
+
for (const child of (authoredBody ?? element).childNodes) {
|
|
529
|
+
if (child instanceof Element && child.localName === "summary") continue;
|
|
530
|
+
body.append(child.cloneNode(true));
|
|
531
|
+
}
|
|
532
|
+
const content = DOMParser.fromSchema(schema).parse(body).content;
|
|
533
|
+
return content.size ? content : Fragment$1.from(schema.nodes.paragraph.create());
|
|
534
|
+
}
|
|
535
|
+
}];
|
|
536
|
+
},
|
|
537
|
+
renderHTML({ node }) {
|
|
538
|
+
return [
|
|
539
|
+
"details",
|
|
540
|
+
{
|
|
541
|
+
"data-looma-expand": "",
|
|
542
|
+
...node.attrs.open ? { open: "" } : {}
|
|
543
|
+
},
|
|
544
|
+
[
|
|
545
|
+
"summary",
|
|
546
|
+
{},
|
|
547
|
+
String(node.attrs.summary)
|
|
548
|
+
],
|
|
549
|
+
[
|
|
550
|
+
"div",
|
|
551
|
+
{ "data-looma-expand-body": "" },
|
|
552
|
+
0
|
|
553
|
+
]
|
|
554
|
+
];
|
|
555
|
+
},
|
|
556
|
+
renderText({ node }) {
|
|
557
|
+
return `${node.attrs.summary}\n${getText(node, { textSerializers: getTextSerializersFromSchema(node.type.schema) })}`;
|
|
558
|
+
},
|
|
559
|
+
addCommands() {
|
|
560
|
+
return {
|
|
561
|
+
insertLoomaExpand: (attributes = {}) => ({ chain, state, dispatch }) => {
|
|
562
|
+
const from = state.selection.from;
|
|
563
|
+
const content = {
|
|
564
|
+
type: this.name,
|
|
565
|
+
attrs: {
|
|
566
|
+
summary: attributes.summary ?? "Details",
|
|
567
|
+
open: attributes.open === true
|
|
568
|
+
},
|
|
569
|
+
content: [{ type: "paragraph" }]
|
|
570
|
+
};
|
|
571
|
+
if (!dispatch) return chain().insertContent(content).run();
|
|
572
|
+
return chain().insertContent(content).command(({ tr, commands }) => {
|
|
573
|
+
let body = -1;
|
|
574
|
+
tr.doc.descendants((node, position) => {
|
|
575
|
+
if (node.type.name === this.name && position >= from - 1 && body < 0) body = position + 2;
|
|
576
|
+
});
|
|
577
|
+
return body >= 0 && commands.setTextSelection(body);
|
|
578
|
+
}).run();
|
|
579
|
+
},
|
|
580
|
+
exitLoomaExpand: () => ({ state, chain }) => {
|
|
581
|
+
const { $from } = state.selection;
|
|
582
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
583
|
+
if ($from.node(depth).type.name !== this.name) continue;
|
|
584
|
+
const after = $from.after(depth);
|
|
585
|
+
return chain().insertContentAt(after, { type: "paragraph" }).setTextSelection(after + 1).run();
|
|
586
|
+
}
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
},
|
|
591
|
+
addExtensions() {
|
|
592
|
+
return [Extension.create({
|
|
593
|
+
name: "loomaExpandExit",
|
|
594
|
+
priority: 110,
|
|
595
|
+
addKeyboardShortcuts() {
|
|
596
|
+
return { "Mod-Enter": () => this.editor.commands.exitLoomaExpand() };
|
|
597
|
+
}
|
|
598
|
+
})];
|
|
599
|
+
},
|
|
600
|
+
addNodeView() {
|
|
601
|
+
if (this.options.nodeView) return this.options.nodeView;
|
|
602
|
+
return ({ editor, node }) => {
|
|
603
|
+
const dom = document.createElement("details");
|
|
604
|
+
dom.dataset.loomaExpand = "";
|
|
605
|
+
const summary = document.createElement("summary");
|
|
606
|
+
summary.contentEditable = "false";
|
|
607
|
+
const contentDOM = document.createElement("div");
|
|
608
|
+
contentDOM.dataset.loomaExpandBody = "";
|
|
609
|
+
dom.append(summary, contentDOM);
|
|
610
|
+
let current = node;
|
|
611
|
+
let editable = editor.isEditable;
|
|
612
|
+
dom.open = editable || Boolean(node.attrs.open);
|
|
613
|
+
summary.textContent = node.attrs.summary;
|
|
614
|
+
const reveal = () => {
|
|
615
|
+
dom.open = true;
|
|
616
|
+
};
|
|
617
|
+
const refresh = () => {
|
|
618
|
+
if (editable !== editor.isEditable) {
|
|
619
|
+
editable = editor.isEditable;
|
|
620
|
+
dom.open = editable || Boolean(current.attrs.open);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
dom.addEventListener("looma-editor-reveal", reveal);
|
|
624
|
+
editor.on("update", refresh);
|
|
625
|
+
return {
|
|
626
|
+
dom,
|
|
627
|
+
contentDOM,
|
|
628
|
+
update(next) {
|
|
629
|
+
if (next.type !== current.type) return false;
|
|
630
|
+
if (next.attrs.open !== current.attrs.open) dom.open = editor.isEditable || Boolean(next.attrs.open);
|
|
631
|
+
current = next;
|
|
632
|
+
summary.textContent = next.attrs.summary;
|
|
633
|
+
refresh();
|
|
634
|
+
return true;
|
|
635
|
+
},
|
|
636
|
+
stopEvent: (event) => summary.contains(event.target),
|
|
637
|
+
ignoreMutation: (mutation) => mutation.type !== "selection" && !contentDOM.contains(mutation.target),
|
|
638
|
+
destroy() {
|
|
639
|
+
dom.removeEventListener("looma-editor-reveal", reveal);
|
|
640
|
+
editor.off("update", refresh);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region src/editor/extensions/table-of-contents.ts
|
|
648
|
+
/** Normalize loaded configuration without persisting unsupported presentation values. */
|
|
649
|
+
function normalizeTableOfContentsConfig$1(attributes) {
|
|
650
|
+
return {
|
|
651
|
+
format: attributes.format === "bulleted" || attributes.format === "numbered" ? attributes.format : "plain",
|
|
652
|
+
depth: attributes.depth === 1 || attributes.depth === 2 ? attributes.depth : 3
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Derives nonempty headings throughout the document, independent of TOC placement.
|
|
657
|
+
* @contract Depth is a maximum heading level. Relative nesting follows existing
|
|
658
|
+
* ancestors rather than inserting artificial entries for skipped levels.
|
|
659
|
+
*/
|
|
660
|
+
function getTableOfContentsEntries$1(doc, depth = 3) {
|
|
661
|
+
const entries = [];
|
|
662
|
+
const ancestors = [];
|
|
663
|
+
doc.descendants((node, position) => {
|
|
664
|
+
if (node.type.name !== "heading" || node.attrs.level > depth || !node.textContent.trim()) return;
|
|
665
|
+
const level = Number(node.attrs.level);
|
|
666
|
+
while (ancestors.length && ancestors[ancestors.length - 1] >= level) ancestors.pop();
|
|
667
|
+
entries.push({
|
|
668
|
+
id: String(node.attrs.id ?? ""),
|
|
669
|
+
text: node.textContent.trim(),
|
|
670
|
+
level,
|
|
671
|
+
depth: ancestors.length,
|
|
672
|
+
position
|
|
673
|
+
});
|
|
674
|
+
ancestors.push(level);
|
|
675
|
+
});
|
|
676
|
+
return entries;
|
|
677
|
+
}
|
|
678
|
+
/** Shared nesting for semantic HTML and the adapter's component composition. */
|
|
679
|
+
function tableOfContentsTree(entries) {
|
|
680
|
+
const roots = [];
|
|
681
|
+
const parents = [];
|
|
682
|
+
for (const entry of entries) {
|
|
683
|
+
const branch = {
|
|
684
|
+
entry,
|
|
685
|
+
children: []
|
|
686
|
+
};
|
|
687
|
+
parents.length = entry.depth;
|
|
688
|
+
(parents[entry.depth - 1]?.children ?? roots).push(branch);
|
|
689
|
+
parents.push(branch);
|
|
690
|
+
}
|
|
691
|
+
return roots;
|
|
692
|
+
}
|
|
693
|
+
function anchorTransaction(state) {
|
|
694
|
+
let hasToc = false;
|
|
695
|
+
const headings = [];
|
|
696
|
+
state.doc.descendants((node, position) => {
|
|
697
|
+
if (node.type.name === "loomaTableOfContents") hasToc = true;
|
|
698
|
+
if (node.type.name === "heading") headings.push({
|
|
699
|
+
node,
|
|
700
|
+
position
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
if (!hasToc) return null;
|
|
704
|
+
const reserved = new Set(headings.map(({ node }) => String(node.attrs.id ?? "")).filter(Boolean));
|
|
705
|
+
const used = /* @__PURE__ */ new Set();
|
|
706
|
+
const transaction = state.tr;
|
|
707
|
+
let sequence = 1;
|
|
708
|
+
for (const { node, position } of headings) {
|
|
709
|
+
let id = typeof node.attrs.id === "string" ? node.attrs.id : "";
|
|
710
|
+
if (!id || /\s/.test(id) || used.has(id)) {
|
|
711
|
+
do
|
|
712
|
+
id = `looma-heading-${sequence++}`;
|
|
713
|
+
while (reserved.has(id) || used.has(id));
|
|
714
|
+
transaction.setNodeMarkup(position, void 0, {
|
|
715
|
+
...node.attrs,
|
|
716
|
+
id
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
used.add(id);
|
|
720
|
+
}
|
|
721
|
+
return transaction.docChanged ? transaction : null;
|
|
722
|
+
}
|
|
723
|
+
var LoomaHeadingAnchors = Extension.create({
|
|
724
|
+
name: "loomaHeadingAnchors",
|
|
725
|
+
addGlobalAttributes() {
|
|
726
|
+
return [{
|
|
727
|
+
types: ["heading"],
|
|
728
|
+
attributes: { id: { default: null } }
|
|
729
|
+
}];
|
|
730
|
+
},
|
|
731
|
+
onCreate() {
|
|
732
|
+
const transaction = anchorTransaction(this.editor.state);
|
|
733
|
+
if (transaction) this.editor.view.dispatch(transaction.setMeta("addToHistory", false));
|
|
734
|
+
},
|
|
735
|
+
addProseMirrorPlugins() {
|
|
736
|
+
return [new Plugin({ appendTransaction: (transactions, _oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state) : null })];
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
function contentsHtml(doc, config) {
|
|
740
|
+
const branches = tableOfContentsTree(getTableOfContentsEntries$1(doc, config.depth));
|
|
741
|
+
const list = (items) => [
|
|
742
|
+
config.format === "plain" ? "div" : config.format === "numbered" ? "ol" : "ul",
|
|
743
|
+
{ role: "list" },
|
|
744
|
+
...items.map(({ entry, children }) => [
|
|
745
|
+
config.format === "plain" ? "div" : "li",
|
|
746
|
+
{ role: "listitem" },
|
|
747
|
+
[
|
|
748
|
+
"a",
|
|
749
|
+
{ href: `#${entry.id}` },
|
|
750
|
+
entry.text
|
|
751
|
+
],
|
|
752
|
+
...children.length ? [list(children)] : []
|
|
753
|
+
])
|
|
754
|
+
];
|
|
755
|
+
return [
|
|
756
|
+
"nav",
|
|
757
|
+
{
|
|
758
|
+
"data-looma-toc": "",
|
|
759
|
+
"data-format": config.format,
|
|
760
|
+
"data-depth": config.depth,
|
|
761
|
+
"aria-label": "Table of contents"
|
|
762
|
+
},
|
|
763
|
+
[
|
|
764
|
+
"strong",
|
|
765
|
+
{},
|
|
766
|
+
"Table of contents"
|
|
767
|
+
],
|
|
768
|
+
branches.length ? list(branches) : [
|
|
769
|
+
"p",
|
|
770
|
+
{},
|
|
771
|
+
"Add headings to build your table of contents."
|
|
772
|
+
]
|
|
773
|
+
];
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Scrolls within this editor so multiple documents cannot cross-navigate by ID.
|
|
777
|
+
* @contract Reader navigation does not edit content or invoke link-editing UI.
|
|
778
|
+
*/
|
|
779
|
+
async function navigateTableOfContents(editor, id) {
|
|
780
|
+
const target = [...editor.view.dom.querySelectorAll("h1[id], h2[id], h3[id]")].find((heading) => heading.id === id);
|
|
781
|
+
if (!target) return;
|
|
782
|
+
const entry = getTableOfContentsEntries$1(editor.state.doc).find((heading) => heading.id === id);
|
|
783
|
+
if (entry) await revealLoomaExpandAt(editor, entry.position + 1);
|
|
784
|
+
if (!target.isConnected || editor.isDestroyed) return;
|
|
785
|
+
target.scrollIntoView({
|
|
786
|
+
block: "start",
|
|
787
|
+
behavior: "auto"
|
|
788
|
+
});
|
|
789
|
+
target.tabIndex = -1;
|
|
790
|
+
target.focus({ preventScroll: true });
|
|
791
|
+
}
|
|
792
|
+
Node$1.create({
|
|
793
|
+
name: "loomaTableOfContents",
|
|
794
|
+
group: "block",
|
|
795
|
+
atom: true,
|
|
796
|
+
selectable: true,
|
|
797
|
+
isolating: true,
|
|
798
|
+
addOptions: () => ({ nodeView: null }),
|
|
799
|
+
addExtensions: () => [LoomaHeadingAnchors],
|
|
800
|
+
addAttributes() {
|
|
801
|
+
return {
|
|
802
|
+
format: {
|
|
803
|
+
default: "plain",
|
|
804
|
+
parseHTML: (element) => normalizeTableOfContentsConfig$1({ format: element.getAttribute("data-format") }).format,
|
|
805
|
+
renderHTML: () => ({})
|
|
806
|
+
},
|
|
807
|
+
depth: {
|
|
808
|
+
default: 3,
|
|
809
|
+
parseHTML: (element) => normalizeTableOfContentsConfig$1({ depth: Number(element.getAttribute("data-depth")) }).depth,
|
|
810
|
+
renderHTML: () => ({})
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
},
|
|
814
|
+
parseHTML: () => [{ tag: "nav[data-looma-toc]" }],
|
|
815
|
+
renderHTML({ node }) {
|
|
816
|
+
return contentsHtml(this.editor?.view?.state.doc ?? node, normalizeTableOfContentsConfig$1(node.attrs));
|
|
817
|
+
},
|
|
818
|
+
addCommands() {
|
|
819
|
+
return { insertLoomaTableOfContents: (config = {}) => ({ commands }) => commands.insertContent({
|
|
820
|
+
type: this.name,
|
|
821
|
+
attrs: normalizeTableOfContentsConfig$1(config)
|
|
822
|
+
}) };
|
|
823
|
+
},
|
|
824
|
+
addNodeView() {
|
|
825
|
+
if (this.options.nodeView) return this.options.nodeView;
|
|
826
|
+
return ({ editor, node }) => {
|
|
827
|
+
const dom = document.createElement("div");
|
|
828
|
+
dom.contentEditable = "false";
|
|
829
|
+
let current = node;
|
|
830
|
+
const render = () => {
|
|
831
|
+
const { dom: content } = DOMSerializer.renderSpec(document, contentsHtml(editor.state.doc, normalizeTableOfContentsConfig$1(current.attrs)));
|
|
832
|
+
dom.replaceChildren(content);
|
|
833
|
+
};
|
|
834
|
+
dom.addEventListener("click", (event) => {
|
|
835
|
+
const anchor = event.target.closest("a");
|
|
836
|
+
if (!anchor) return;
|
|
837
|
+
event.preventDefault();
|
|
838
|
+
navigateTableOfContents(editor, anchor.getAttribute("href").slice(1));
|
|
839
|
+
});
|
|
840
|
+
editor.on("transaction", render);
|
|
841
|
+
render();
|
|
842
|
+
return {
|
|
843
|
+
dom,
|
|
844
|
+
update(next) {
|
|
845
|
+
if (next.type !== current.type) return false;
|
|
846
|
+
current = next;
|
|
847
|
+
render();
|
|
848
|
+
return true;
|
|
849
|
+
},
|
|
850
|
+
stopEvent: () => true,
|
|
851
|
+
ignoreMutation: () => true,
|
|
852
|
+
destroy: () => {
|
|
853
|
+
editor.off("transaction", render);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/vue/editor/table-of-contents.ts
|
|
861
|
+
var tocSequence = 0;
|
|
862
|
+
/**
|
|
863
|
+
* Composes TOC presentation and settings from the same controls as link editing.
|
|
864
|
+
* @lifecycle Each node owns a Vue app and document subscriptions, all released
|
|
865
|
+
* by its node-view destroy callback. Only configuration enters editor state.
|
|
866
|
+
*/
|
|
867
|
+
var tableOfContentsNodeView = ({ editor, node, getPos }) => {
|
|
868
|
+
const dom = document.createElement("div");
|
|
869
|
+
dom.contentEditable = "false";
|
|
870
|
+
const current = shallowRef(node);
|
|
871
|
+
const doc = shallowRef(editor.state.doc);
|
|
872
|
+
const editable = ref(editor.isEditable);
|
|
873
|
+
const refresh = () => {
|
|
874
|
+
doc.value = editor.state.doc;
|
|
875
|
+
editable.value = editor.isEditable;
|
|
876
|
+
};
|
|
877
|
+
const app = createApp({ setup() {
|
|
878
|
+
const id = `looma-toc-${++tocSequence}`;
|
|
879
|
+
const open = ref(false);
|
|
880
|
+
const draft = ref(normalizeTableOfContentsConfig(current.value.attrs));
|
|
881
|
+
const config = computed(() => normalizeTableOfContentsConfig(current.value.attrs));
|
|
882
|
+
const branches = computed(() => tableOfContentsTree(getTableOfContentsEntries(doc.value, config.value.depth)));
|
|
883
|
+
const begin = () => {
|
|
884
|
+
draft.value = { ...config.value };
|
|
885
|
+
open.value = true;
|
|
886
|
+
};
|
|
887
|
+
const cancel = () => {
|
|
888
|
+
open.value = false;
|
|
889
|
+
};
|
|
890
|
+
const save = () => {
|
|
891
|
+
const position = getPos();
|
|
892
|
+
if (typeof position !== "number" || !editor.isEditable) return;
|
|
893
|
+
if (editor.state.doc.nodeAt(position)?.type.name !== "loomaTableOfContents") return;
|
|
894
|
+
editor.view.dispatch(editor.state.tr.setNodeMarkup(position, void 0, normalizeTableOfContentsConfig(draft.value)));
|
|
895
|
+
open.value = false;
|
|
896
|
+
editor.commands.focus();
|
|
897
|
+
};
|
|
898
|
+
const list = (items) => h(List, {
|
|
899
|
+
as: config.value.format === "numbered" ? "ol" : "ul",
|
|
900
|
+
markers: config.value.format === "bulleted" ? "disc" : "none",
|
|
901
|
+
density: "compact"
|
|
902
|
+
}, () => items.map(({ entry, children }) => h("li", { key: entry.id }, [h(Button, {
|
|
903
|
+
as: "a",
|
|
904
|
+
href: `#${entry.id}`,
|
|
905
|
+
variant: "link",
|
|
906
|
+
tone: "neutral",
|
|
907
|
+
size: "sm",
|
|
908
|
+
onClick: (event) => {
|
|
909
|
+
event.preventDefault();
|
|
910
|
+
navigateTableOfContents(editor, entry.id);
|
|
911
|
+
}
|
|
912
|
+
}, () => h(Text, {
|
|
913
|
+
as: "span",
|
|
914
|
+
wrap: "anywhere"
|
|
915
|
+
}, () => entry.text)), children.length ? list(children) : null])));
|
|
916
|
+
const formatLabels = {
|
|
917
|
+
plain: "Plain",
|
|
918
|
+
bulleted: "Bulleted",
|
|
919
|
+
numbered: "Numbered"
|
|
920
|
+
};
|
|
921
|
+
const dropdown = (field) => {
|
|
922
|
+
const label = field === "format" ? "Formatting" : "Heading depth";
|
|
923
|
+
const trigger = `${id}-${field}`;
|
|
924
|
+
const options = field === "format" ? Object.entries(formatLabels).map(([value, text]) => ({
|
|
925
|
+
value,
|
|
926
|
+
text
|
|
927
|
+
})) : [
|
|
928
|
+
1,
|
|
929
|
+
2,
|
|
930
|
+
3
|
|
931
|
+
].map((value) => ({
|
|
932
|
+
value: String(value),
|
|
933
|
+
text: `Through heading ${value}`
|
|
934
|
+
}));
|
|
935
|
+
return h(FormField, {}, {
|
|
936
|
+
label: () => h("label", { for: trigger }, label),
|
|
937
|
+
default: () => [h(Button, {
|
|
938
|
+
id: trigger,
|
|
939
|
+
variant: "outline",
|
|
940
|
+
tone: "neutral",
|
|
941
|
+
size: "sm",
|
|
942
|
+
stretch: true,
|
|
943
|
+
align: "start",
|
|
944
|
+
"aria-label": label
|
|
945
|
+
}, () => h(Cluster, {
|
|
946
|
+
justify: "between",
|
|
947
|
+
gap: "s"
|
|
948
|
+
}, () => [field === "format" ? formatLabels[draft.value.format] : `Through heading ${draft.value.depth}`, h(Icon, {
|
|
949
|
+
name: "chevron-down",
|
|
950
|
+
size: "sm"
|
|
951
|
+
})])), h(Menu, {
|
|
952
|
+
for: trigger,
|
|
953
|
+
size: "sm",
|
|
954
|
+
"aria-label": label,
|
|
955
|
+
onSelect: (event) => {
|
|
956
|
+
draft.value = normalizeTableOfContentsConfig({
|
|
957
|
+
...draft.value,
|
|
958
|
+
[field]: field === "depth" ? Number(event.detail.value) : event.detail.value
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}, () => options.map((option) => h(MenuItem, {
|
|
962
|
+
value: option.value,
|
|
963
|
+
type: "radio",
|
|
964
|
+
checked: String(draft.value[field]) === option.value
|
|
965
|
+
}, () => option.text)))]
|
|
966
|
+
});
|
|
967
|
+
};
|
|
968
|
+
return () => h("nav", {
|
|
969
|
+
"data-looma-toc": "",
|
|
970
|
+
"data-format": config.value.format,
|
|
971
|
+
"data-depth": config.value.depth,
|
|
972
|
+
"aria-label": "Table of contents"
|
|
973
|
+
}, [h(Stack, { gap: "s" }, () => [
|
|
974
|
+
h(Cluster, {
|
|
975
|
+
justify: "between",
|
|
976
|
+
gap: "s"
|
|
977
|
+
}, () => [h(Text, {
|
|
978
|
+
as: "strong",
|
|
979
|
+
size: "sm",
|
|
980
|
+
weight: "semibold"
|
|
981
|
+
}, () => "Table of contents"), editable.value ? h(IconButton, {
|
|
982
|
+
id,
|
|
983
|
+
label: "Table of contents settings",
|
|
984
|
+
variant: "ghost",
|
|
985
|
+
size: "sm"
|
|
986
|
+
}, () => h(Icon, {
|
|
987
|
+
name: "settings",
|
|
988
|
+
size: "sm"
|
|
989
|
+
})) : null]),
|
|
990
|
+
branches.value.length ? list(branches.value) : h(Text, {
|
|
991
|
+
size: "sm",
|
|
992
|
+
tone: "secondary"
|
|
993
|
+
}, () => "Add headings to build your table of contents."),
|
|
994
|
+
editable.value ? h(Popover, {
|
|
995
|
+
for: id,
|
|
996
|
+
open: open.value,
|
|
997
|
+
size: "sm",
|
|
998
|
+
"aria-label": "Table of contents settings",
|
|
999
|
+
onOpen: begin,
|
|
1000
|
+
onClose: cancel
|
|
1001
|
+
}, () => h(Stack, { gap: "s" }, () => [
|
|
1002
|
+
h(Text, {
|
|
1003
|
+
as: "strong",
|
|
1004
|
+
size: "sm",
|
|
1005
|
+
weight: "semibold"
|
|
1006
|
+
}, () => "Table of contents"),
|
|
1007
|
+
dropdown("format"),
|
|
1008
|
+
dropdown("depth"),
|
|
1009
|
+
h(Cluster, {
|
|
1010
|
+
justify: "end",
|
|
1011
|
+
gap: "xs"
|
|
1012
|
+
}, () => [h(Button, {
|
|
1013
|
+
variant: "ghost",
|
|
1014
|
+
tone: "neutral",
|
|
1015
|
+
size: "sm",
|
|
1016
|
+
onClick: cancel
|
|
1017
|
+
}, () => "Cancel"), h(Button, {
|
|
1018
|
+
variant: "solid",
|
|
1019
|
+
size: "sm",
|
|
1020
|
+
onClick: save
|
|
1021
|
+
}, () => "Save")])
|
|
1022
|
+
])) : null,
|
|
1023
|
+
editable.value ? h(Tooltip, { for: id }, () => "Table of contents settings") : null
|
|
1024
|
+
])]);
|
|
1025
|
+
} });
|
|
1026
|
+
editor.on("transaction", refresh);
|
|
1027
|
+
editor.on("update", refresh);
|
|
1028
|
+
app.mount(dom);
|
|
1029
|
+
return {
|
|
1030
|
+
dom,
|
|
1031
|
+
update(next) {
|
|
1032
|
+
if (next.type !== current.value.type) return false;
|
|
1033
|
+
current.value = next;
|
|
1034
|
+
refresh();
|
|
1035
|
+
return true;
|
|
1036
|
+
},
|
|
1037
|
+
stopEvent: () => true,
|
|
1038
|
+
ignoreMutation: () => true,
|
|
1039
|
+
destroy() {
|
|
1040
|
+
editor.off("transaction", refresh);
|
|
1041
|
+
editor.off("update", refresh);
|
|
1042
|
+
app.unmount();
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
};
|
|
1046
|
+
//#endregion
|
|
1047
|
+
//#region src/vue/editor/expand.ts
|
|
1048
|
+
var expandSequence = 0;
|
|
1049
|
+
/**
|
|
1050
|
+
* Composes a section's transient disclosure and saved settings from shared controls.
|
|
1051
|
+
* @lifecycle The content DOM remains owned by ProseMirror; Vue owns surrounding
|
|
1052
|
+
* controls. Unmount removes the app, listeners, and editor subscription.
|
|
1053
|
+
*/
|
|
1054
|
+
var expandNodeView = ({ editor, node, getPos }) => {
|
|
1055
|
+
const dom = document.createElement("div");
|
|
1056
|
+
dom.dataset.loomaExpand = "";
|
|
1057
|
+
const current = shallowRef(node);
|
|
1058
|
+
const editable = ref(editor.isEditable);
|
|
1059
|
+
const expanded = ref(editor.isEditable || Boolean(node.attrs.open));
|
|
1060
|
+
let contentDOM;
|
|
1061
|
+
const refresh = () => {
|
|
1062
|
+
if (editable.value !== editor.isEditable) expanded.value = editor.isEditable || Boolean(current.value.attrs.open);
|
|
1063
|
+
editable.value = editor.isEditable;
|
|
1064
|
+
};
|
|
1065
|
+
const reveal = (event) => {
|
|
1066
|
+
expanded.value = true;
|
|
1067
|
+
event.detail.pending.push(nextTick().then(async () => {
|
|
1068
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
1069
|
+
const panel = dom.querySelector("[data-component='ui-disclosure'] .panel");
|
|
1070
|
+
await Promise.all((panel?.getAnimations() ?? []).map((animation) => animation.finished.catch(() => {})));
|
|
1071
|
+
}));
|
|
1072
|
+
};
|
|
1073
|
+
const app = createApp({ setup() {
|
|
1074
|
+
const id = `looma-expand-${++expandSequence}`;
|
|
1075
|
+
const settings = ref(false);
|
|
1076
|
+
const title = ref(String(current.value.attrs.summary));
|
|
1077
|
+
const defaultOpen = ref(Boolean(current.value.attrs.open));
|
|
1078
|
+
const begin = () => {
|
|
1079
|
+
title.value = current.value.attrs.summary;
|
|
1080
|
+
defaultOpen.value = Boolean(current.value.attrs.open);
|
|
1081
|
+
settings.value = true;
|
|
1082
|
+
};
|
|
1083
|
+
const save = () => {
|
|
1084
|
+
const position = getPos();
|
|
1085
|
+
if (typeof position !== "number" || !editor.isEditable || editor.state.doc.nodeAt(position)?.type.name !== "loomaExpand") return;
|
|
1086
|
+
editor.view.dispatch(editor.state.tr.setNodeMarkup(position, void 0, {
|
|
1087
|
+
summary: title.value.trim() || "Details",
|
|
1088
|
+
open: defaultOpen.value
|
|
1089
|
+
}));
|
|
1090
|
+
settings.value = false;
|
|
1091
|
+
editor.commands.focus();
|
|
1092
|
+
};
|
|
1093
|
+
return () => h(Stack, { gap: "xs" }, () => [
|
|
1094
|
+
editable.value ? h(Cluster, {
|
|
1095
|
+
justify: "end",
|
|
1096
|
+
gap: "xs"
|
|
1097
|
+
}, () => h(IconButton, {
|
|
1098
|
+
id,
|
|
1099
|
+
label: "Section settings",
|
|
1100
|
+
variant: "ghost",
|
|
1101
|
+
size: "sm"
|
|
1102
|
+
}, () => h(Icon, {
|
|
1103
|
+
name: "settings",
|
|
1104
|
+
size: "sm"
|
|
1105
|
+
}))) : null,
|
|
1106
|
+
editable.value ? h(Tooltip, { for: id }, () => "Section settings") : null,
|
|
1107
|
+
h(Disclosure, {
|
|
1108
|
+
summary: current.value.attrs.summary,
|
|
1109
|
+
open: expanded.value,
|
|
1110
|
+
density: "compact",
|
|
1111
|
+
onOpen: () => {
|
|
1112
|
+
expanded.value = true;
|
|
1113
|
+
},
|
|
1114
|
+
onClose: () => {
|
|
1115
|
+
expanded.value = false;
|
|
1116
|
+
}
|
|
1117
|
+
}, () => h("div", {
|
|
1118
|
+
"data-looma-expand-body": "",
|
|
1119
|
+
ref: (element) => {
|
|
1120
|
+
if (element) contentDOM = element;
|
|
1121
|
+
}
|
|
1122
|
+
})),
|
|
1123
|
+
editable.value ? h(Popover, {
|
|
1124
|
+
for: id,
|
|
1125
|
+
open: settings.value,
|
|
1126
|
+
size: "sm",
|
|
1127
|
+
"aria-label": "Section settings",
|
|
1128
|
+
onOpen: begin,
|
|
1129
|
+
onClose: () => {
|
|
1130
|
+
settings.value = false;
|
|
1131
|
+
}
|
|
1132
|
+
}, () => h(Stack, { gap: "s" }, () => [
|
|
1133
|
+
h(Text, {
|
|
1134
|
+
as: "strong",
|
|
1135
|
+
size: "sm",
|
|
1136
|
+
weight: "semibold"
|
|
1137
|
+
}, () => "Section settings"),
|
|
1138
|
+
h(FormField, {}, {
|
|
1139
|
+
label: () => h("label", { for: `${id}-summary` }, "Summary"),
|
|
1140
|
+
default: () => h(Input, {
|
|
1141
|
+
id: `${id}-summary`,
|
|
1142
|
+
value: title.value,
|
|
1143
|
+
size: "sm",
|
|
1144
|
+
onInput: (event) => {
|
|
1145
|
+
title.value = event.target.value;
|
|
1146
|
+
}
|
|
1147
|
+
})
|
|
1148
|
+
}),
|
|
1149
|
+
h(Checkbox, {
|
|
1150
|
+
checked: defaultOpen.value,
|
|
1151
|
+
onChange: (event) => {
|
|
1152
|
+
defaultOpen.value = event.detail.checked;
|
|
1153
|
+
}
|
|
1154
|
+
}, () => "Initially expanded"),
|
|
1155
|
+
h(Cluster, {
|
|
1156
|
+
justify: "end",
|
|
1157
|
+
gap: "xs"
|
|
1158
|
+
}, () => [h(Button, {
|
|
1159
|
+
variant: "ghost",
|
|
1160
|
+
tone: "neutral",
|
|
1161
|
+
size: "sm",
|
|
1162
|
+
onClick: () => {
|
|
1163
|
+
settings.value = false;
|
|
1164
|
+
}
|
|
1165
|
+
}, () => "Cancel"), h(Button, {
|
|
1166
|
+
variant: "solid",
|
|
1167
|
+
size: "sm",
|
|
1168
|
+
onClick: save
|
|
1169
|
+
}, () => "Save")])
|
|
1170
|
+
])) : null
|
|
1171
|
+
]);
|
|
1172
|
+
} });
|
|
1173
|
+
dom.addEventListener("looma-editor-reveal", reveal);
|
|
1174
|
+
editor.on("update", refresh);
|
|
1175
|
+
app.mount(dom);
|
|
1176
|
+
return {
|
|
1177
|
+
dom,
|
|
1178
|
+
contentDOM,
|
|
1179
|
+
update(next) {
|
|
1180
|
+
if (next.type !== current.value.type) return false;
|
|
1181
|
+
if (next.attrs.open !== current.value.attrs.open) expanded.value = editor.isEditable || Boolean(next.attrs.open);
|
|
1182
|
+
current.value = next;
|
|
1183
|
+
refresh();
|
|
1184
|
+
return true;
|
|
1185
|
+
},
|
|
1186
|
+
stopEvent: (event) => !contentDOM.contains(event.target),
|
|
1187
|
+
ignoreMutation: (mutation) => mutation.type !== "selection" && !contentDOM.contains(mutation.target),
|
|
1188
|
+
destroy() {
|
|
1189
|
+
dom.removeEventListener("looma-editor-reveal", reveal);
|
|
1190
|
+
editor.off("update", refresh);
|
|
1191
|
+
app.unmount();
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
};
|
|
1195
|
+
//#endregion
|
|
1196
|
+
//#region src/vue/editor/divider.ts
|
|
1197
|
+
/** A pointer-sized document divider using the shared separator's selected state.
|
|
1198
|
+
* @lifecycle The editor owns the node and deletion; destroying the view releases
|
|
1199
|
+
* its Vue app and editability subscription.
|
|
1200
|
+
*/
|
|
1201
|
+
var dividerNodeView = ({ editor, getPos }) => {
|
|
1202
|
+
const mount = document.createElement("div");
|
|
1203
|
+
const selected = ref(false);
|
|
1204
|
+
const editable = ref(editor.isEditable);
|
|
1205
|
+
const refresh = () => {
|
|
1206
|
+
editable.value = editor.isEditable;
|
|
1207
|
+
};
|
|
1208
|
+
const app = createApp({ render: () => h(Separator, {
|
|
1209
|
+
selectable: editable.value,
|
|
1210
|
+
selected: editable.value && selected.value,
|
|
1211
|
+
contenteditable: "false",
|
|
1212
|
+
onClick: () => {
|
|
1213
|
+
const position = getPos();
|
|
1214
|
+
if (!editor.isEditable || typeof position !== "number") return;
|
|
1215
|
+
editor.view.dispatch(editor.state.tr.setSelection(NodeSelection.create(editor.state.doc, position)));
|
|
1216
|
+
editor.view.focus();
|
|
1217
|
+
}
|
|
1218
|
+
}) });
|
|
1219
|
+
app.mount(mount);
|
|
1220
|
+
const dom = mount.firstElementChild;
|
|
1221
|
+
editor.on("update", refresh);
|
|
1222
|
+
return {
|
|
1223
|
+
dom,
|
|
1224
|
+
selectNode() {
|
|
1225
|
+
selected.value = true;
|
|
1226
|
+
},
|
|
1227
|
+
deselectNode() {
|
|
1228
|
+
selected.value = false;
|
|
1229
|
+
},
|
|
1230
|
+
stopEvent: () => false,
|
|
1231
|
+
ignoreMutation: (mutation) => mutation.type !== "selection",
|
|
1232
|
+
destroy() {
|
|
1233
|
+
editor.off("update", refresh);
|
|
1234
|
+
app.unmount();
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
};
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region src/vue/editor/image-controls.ts
|
|
1240
|
+
var placements = [
|
|
1241
|
+
{
|
|
1242
|
+
value: "block",
|
|
1243
|
+
label: "Image in line",
|
|
1244
|
+
icon: "panel-top"
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
value: "center",
|
|
1248
|
+
label: "Center image",
|
|
1249
|
+
icon: "align-center"
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
value: "wrap-left",
|
|
1253
|
+
label: "Wrap text to the right",
|
|
1254
|
+
icon: "panel-left"
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
value: "wrap-right",
|
|
1258
|
+
label: "Wrap text to the left",
|
|
1259
|
+
icon: "panel-right"
|
|
1260
|
+
}
|
|
1261
|
+
];
|
|
1262
|
+
/** One contextual editing bar; sizing belongs to the image's corner handles.
|
|
1263
|
+
* @contract Placement applies immediately. Alt text is a secondary draft with
|
|
1264
|
+
* Save/Cancel; editing another node cancels it rather than changing that node.
|
|
1265
|
+
*/
|
|
1266
|
+
var ImageControls = defineComponent({
|
|
1267
|
+
props: { editor: {
|
|
1268
|
+
type: Object,
|
|
1269
|
+
required: true
|
|
1270
|
+
} },
|
|
1271
|
+
setup(props) {
|
|
1272
|
+
const id = `looma-image-${useId()}`;
|
|
1273
|
+
const open = ref(false);
|
|
1274
|
+
const alt = ref("");
|
|
1275
|
+
const placement = ref("block");
|
|
1276
|
+
let target = null;
|
|
1277
|
+
const selected = () => props.editor.state.selection instanceof NodeSelection && props.editor.state.selection.node.type.name === "image" ? props.editor.state.selection : null;
|
|
1278
|
+
const refresh = () => {
|
|
1279
|
+
const selection = selected();
|
|
1280
|
+
placement.value = normalizeImagePlacement(selection?.node.attrs.placement);
|
|
1281
|
+
if (open.value && selection?.node !== target) open.value = false;
|
|
1282
|
+
};
|
|
1283
|
+
props.editor.on("transaction", refresh);
|
|
1284
|
+
onBeforeUnmount(() => props.editor.off("transaction", refresh));
|
|
1285
|
+
const begin = () => {
|
|
1286
|
+
const selection = selected();
|
|
1287
|
+
if (!selection) return;
|
|
1288
|
+
target = selection.node;
|
|
1289
|
+
alt.value = selection.node.attrs.alt ?? "";
|
|
1290
|
+
open.value = true;
|
|
1291
|
+
};
|
|
1292
|
+
const update = (attrs) => {
|
|
1293
|
+
const selection = selected();
|
|
1294
|
+
if (!selection || !props.editor.isEditable) return;
|
|
1295
|
+
props.editor.view.dispatch(props.editor.state.tr.setNodeMarkup(selection.from, void 0, {
|
|
1296
|
+
...selection.node.attrs,
|
|
1297
|
+
...attrs
|
|
1298
|
+
}));
|
|
1299
|
+
props.editor.view.focus();
|
|
1300
|
+
};
|
|
1301
|
+
return () => h(Text, {
|
|
1302
|
+
as: "div",
|
|
1303
|
+
font: "sans"
|
|
1304
|
+
}, () => [
|
|
1305
|
+
h(EditorToolbar$1, {
|
|
1306
|
+
floating: true,
|
|
1307
|
+
"aria-label": "Image actions"
|
|
1308
|
+
}, () => [
|
|
1309
|
+
...placements.map((option) => h(IconButton, {
|
|
1310
|
+
id: `${id}-${option.value}`,
|
|
1311
|
+
label: option.label,
|
|
1312
|
+
size: "sm",
|
|
1313
|
+
variant: placement.value === option.value ? "outline" : "ghost",
|
|
1314
|
+
"aria-pressed": placement.value === option.value,
|
|
1315
|
+
onClick: () => update({
|
|
1316
|
+
placement: option.value,
|
|
1317
|
+
...option.value.startsWith("wrap-") ? {
|
|
1318
|
+
width: null,
|
|
1319
|
+
height: null
|
|
1320
|
+
} : {}
|
|
1321
|
+
})
|
|
1322
|
+
}, () => h(Icon, {
|
|
1323
|
+
name: option.icon,
|
|
1324
|
+
size: "sm"
|
|
1325
|
+
}))),
|
|
1326
|
+
h(IconButton, {
|
|
1327
|
+
id,
|
|
1328
|
+
label: "Image description",
|
|
1329
|
+
size: "sm",
|
|
1330
|
+
variant: "ghost"
|
|
1331
|
+
}, () => h(Icon, {
|
|
1332
|
+
name: "file-text",
|
|
1333
|
+
size: "sm"
|
|
1334
|
+
})),
|
|
1335
|
+
h(IconButton, {
|
|
1336
|
+
id: `${id}-delete`,
|
|
1337
|
+
label: "Delete image",
|
|
1338
|
+
size: "sm",
|
|
1339
|
+
variant: "ghost",
|
|
1340
|
+
onClick: () => {
|
|
1341
|
+
if (selected() && props.editor.isEditable) props.editor.chain().focus().deleteSelection().run();
|
|
1342
|
+
}
|
|
1343
|
+
}, () => h(Icon, {
|
|
1344
|
+
name: "trash",
|
|
1345
|
+
size: "sm"
|
|
1346
|
+
}))
|
|
1347
|
+
]),
|
|
1348
|
+
...placements.map((option) => h(Tooltip, { for: `${id}-${option.value}` }, () => option.label)),
|
|
1349
|
+
h(Tooltip, { for: id }, () => "Image description"),
|
|
1350
|
+
h(Tooltip, { for: `${id}-delete` }, () => "Delete image"),
|
|
1351
|
+
h(Popover, {
|
|
1352
|
+
for: id,
|
|
1353
|
+
open: open.value,
|
|
1354
|
+
size: "sm",
|
|
1355
|
+
"aria-label": "Image description",
|
|
1356
|
+
onOpen: begin,
|
|
1357
|
+
onClose: () => {
|
|
1358
|
+
open.value = false;
|
|
1359
|
+
}
|
|
1360
|
+
}, () => h("form", {
|
|
1361
|
+
"aria-label": "Image description",
|
|
1362
|
+
onSubmit: (event) => {
|
|
1363
|
+
event.preventDefault();
|
|
1364
|
+
if (selected()?.node !== target) return;
|
|
1365
|
+
open.value = false;
|
|
1366
|
+
update({ alt: alt.value });
|
|
1367
|
+
}
|
|
1368
|
+
}, [h(Stack, { gap: "s" }, () => [
|
|
1369
|
+
h(FormField, {}, {
|
|
1370
|
+
label: () => h("label", { for: `${id}-alt` }, "Alt text"),
|
|
1371
|
+
default: () => h(Input, {
|
|
1372
|
+
id: `${id}-alt`,
|
|
1373
|
+
size: "sm",
|
|
1374
|
+
value: alt.value,
|
|
1375
|
+
onInput: (event) => {
|
|
1376
|
+
alt.value = event.target.value;
|
|
1377
|
+
}
|
|
1378
|
+
})
|
|
1379
|
+
}),
|
|
1380
|
+
h(Text, {
|
|
1381
|
+
size: "xs",
|
|
1382
|
+
tone: "secondary"
|
|
1383
|
+
}, () => "Describe the image for people using a screen reader."),
|
|
1384
|
+
h(Cluster, {
|
|
1385
|
+
justify: "end",
|
|
1386
|
+
gap: "xs"
|
|
1387
|
+
}, () => [h(Button, {
|
|
1388
|
+
type: "button",
|
|
1389
|
+
variant: "ghost",
|
|
1390
|
+
tone: "neutral",
|
|
1391
|
+
size: "sm",
|
|
1392
|
+
onClick: () => {
|
|
1393
|
+
open.value = false;
|
|
1394
|
+
}
|
|
1395
|
+
}, () => "Cancel"), h(Button, {
|
|
1396
|
+
type: "submit",
|
|
1397
|
+
variant: "solid",
|
|
1398
|
+
size: "sm"
|
|
1399
|
+
}, () => "Save")])
|
|
1400
|
+
])]))
|
|
1401
|
+
]);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
//#endregion
|
|
1405
|
+
//#region src/vue/editor/image.ts
|
|
1406
|
+
/** Projects the shared image's resize intent into a single document transaction.
|
|
1407
|
+
* @ownership Delivery decorations belong to the inner image, never the figure;
|
|
1408
|
+
* persisted attributes remain independent of host renditions and drag previews.
|
|
1409
|
+
* @lifecycle Each atom owns a Vue app and an editability subscription.
|
|
1410
|
+
*/
|
|
1411
|
+
var imageNodeView = ({ editor, node, getPos, decorations }) => {
|
|
1412
|
+
const mount = document.createElement("div");
|
|
1413
|
+
const current = shallowRef(node);
|
|
1414
|
+
const delivery = shallowRef(decorations);
|
|
1415
|
+
const selected = ref(false);
|
|
1416
|
+
const editable = ref(editor.isEditable);
|
|
1417
|
+
const refresh = () => {
|
|
1418
|
+
editable.value = editor.isEditable;
|
|
1419
|
+
};
|
|
1420
|
+
const app = createApp({ render: () => {
|
|
1421
|
+
const attrs = current.value.attrs;
|
|
1422
|
+
const transient = Object.assign({}, ...delivery.value.map((decoration) => decoration.spec.loomaImageAttributes ?? {}));
|
|
1423
|
+
return h(Image, {
|
|
1424
|
+
src: attrs.src,
|
|
1425
|
+
alt: attrs.alt ?? "",
|
|
1426
|
+
width: attrs.width,
|
|
1427
|
+
height: attrs.height,
|
|
1428
|
+
placement: normalizeImagePlacement(attrs.placement),
|
|
1429
|
+
selected: editable.value && selected.value,
|
|
1430
|
+
resizable: editable.value,
|
|
1431
|
+
contenteditable: "false",
|
|
1432
|
+
"data-looma-image-node": "",
|
|
1433
|
+
onClick: (event) => {
|
|
1434
|
+
if (event.target.closest("button")) return;
|
|
1435
|
+
const position = getPos();
|
|
1436
|
+
if (!editor.isEditable || typeof position !== "number") return;
|
|
1437
|
+
editor.view.dispatch(editor.state.tr.setSelection(NodeSelection.create(editor.state.doc, position)));
|
|
1438
|
+
editor.view.focus();
|
|
1439
|
+
},
|
|
1440
|
+
onResize: (event) => {
|
|
1441
|
+
if (event.detail.phase !== "commit" || !editor.isEditable) return;
|
|
1442
|
+
const position = getPos();
|
|
1443
|
+
if (typeof position !== "number" || editor.state.doc.nodeAt(position) !== current.value) return;
|
|
1444
|
+
const { width, height } = event.detail;
|
|
1445
|
+
if (width === current.value.attrs.width && height === current.value.attrs.height) return;
|
|
1446
|
+
editor.view.dispatch(closeHistory(editor.state.tr).setNodeMarkup(position, void 0, {
|
|
1447
|
+
...current.value.attrs,
|
|
1448
|
+
width,
|
|
1449
|
+
height
|
|
1450
|
+
}));
|
|
1451
|
+
editor.view.dispatch(closeHistory(editor.state.tr));
|
|
1452
|
+
}
|
|
1453
|
+
}, { media: () => h("img", {
|
|
1454
|
+
src: attrs.src,
|
|
1455
|
+
alt: attrs.alt ?? "",
|
|
1456
|
+
title: attrs.title,
|
|
1457
|
+
width: attrs.width,
|
|
1458
|
+
height: attrs.height,
|
|
1459
|
+
...transient
|
|
1460
|
+
}) });
|
|
1461
|
+
} });
|
|
1462
|
+
app.mount(mount);
|
|
1463
|
+
const dom = mount.firstElementChild;
|
|
1464
|
+
editor.on("update", refresh);
|
|
1465
|
+
return {
|
|
1466
|
+
dom,
|
|
1467
|
+
update(next, nextDecorations) {
|
|
1468
|
+
if (next.type !== current.value.type) return false;
|
|
1469
|
+
current.value = next;
|
|
1470
|
+
delivery.value = nextDecorations;
|
|
1471
|
+
refresh();
|
|
1472
|
+
return true;
|
|
1473
|
+
},
|
|
1474
|
+
selectNode() {
|
|
1475
|
+
selected.value = true;
|
|
1476
|
+
},
|
|
1477
|
+
deselectNode() {
|
|
1478
|
+
selected.value = false;
|
|
1479
|
+
},
|
|
1480
|
+
stopEvent: (event) => event.target instanceof Element && Boolean(event.target.closest("[data-corner]")),
|
|
1481
|
+
ignoreMutation: (mutation) => mutation.type !== "selection",
|
|
1482
|
+
destroy() {
|
|
1483
|
+
editor.off("update", refresh);
|
|
1484
|
+
app.unmount();
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
};
|
|
1488
|
+
//#endregion
|
|
410
1489
|
//#region src/vue/editor/LoomaEditor.ts
|
|
411
1490
|
/**
|
|
412
1491
|
* Shows a key binding in ProseMirror notation ("Mod-Shift-8") the way the reader's platform writes
|
|
@@ -479,12 +1558,25 @@ function managedMenuAnchorRect(rect) {
|
|
|
479
1558
|
height: rect.height
|
|
480
1559
|
};
|
|
481
1560
|
}
|
|
482
|
-
function
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1561
|
+
function slashMenuRows(items) {
|
|
1562
|
+
const groups = [];
|
|
1563
|
+
for (const [index, item] of items.entries()) {
|
|
1564
|
+
if (!groups.length || groups.at(-1).label !== item.group) groups.push({
|
|
1565
|
+
label: item.group,
|
|
1566
|
+
rows: []
|
|
1567
|
+
});
|
|
1568
|
+
groups.at(-1).rows.push(h(EditorSlashMenuItem, {
|
|
1569
|
+
key: item.id ?? `${item.title}-${index}`,
|
|
1570
|
+
value: item.id ?? item.title,
|
|
1571
|
+
description: item.description,
|
|
1572
|
+
icon: item.icon,
|
|
1573
|
+
keywords: [...item.keywords, item.id ?? ""].join(" ")
|
|
1574
|
+
}, () => item.title));
|
|
1575
|
+
}
|
|
1576
|
+
return groups.flatMap(({ label, rows }, index) => label ? [h(EditorSlashMenuGroup, {
|
|
1577
|
+
key: `${label}-${index}`,
|
|
1578
|
+
label
|
|
1579
|
+
}, () => rows)] : rows);
|
|
488
1580
|
}
|
|
489
1581
|
function selectedTableElement(editor) {
|
|
490
1582
|
const { node } = editor.view.domAtPos(editor.state.selection.from);
|
|
@@ -547,6 +1639,11 @@ var LoomaEditor = defineComponent({
|
|
|
547
1639
|
type: Array,
|
|
548
1640
|
default: () => []
|
|
549
1641
|
},
|
|
1642
|
+
/** One inventory for the existing slash extension; an empty array intentionally supplies no commands. */
|
|
1643
|
+
slashCommands: {
|
|
1644
|
+
type: [Array, Function],
|
|
1645
|
+
default: void 0
|
|
1646
|
+
},
|
|
550
1647
|
/** Override the lazy built-in catalog with application-owned code grammars. An empty object disables highlighting. */
|
|
551
1648
|
codeLanguages: {
|
|
552
1649
|
type: Object,
|
|
@@ -774,7 +1871,12 @@ var LoomaEditor = defineComponent({
|
|
|
774
1871
|
chipOpen.value = false;
|
|
775
1872
|
if (focusChip) root.value?.querySelector(`#${chipAnchorId}`)?.focus();
|
|
776
1873
|
};
|
|
1874
|
+
const resolveSlashCommands = () => {
|
|
1875
|
+
const defaults = getDefaultSlashCommands(props.uploadImage ? () => fileInput.value?.click() : void 0, openChipEditor, () => openLinkEditor());
|
|
1876
|
+
return typeof props.slashCommands === "function" ? props.slashCommands(defaults) : props.slashCommands ?? defaults;
|
|
1877
|
+
};
|
|
777
1878
|
const slashExtension = createLoomaSlashCommandExtension({
|
|
1879
|
+
commands: resolveSlashCommands(),
|
|
778
1880
|
onOpenImagePicker: () => fileInput.value?.click(),
|
|
779
1881
|
onOpenChipEditor: openChipEditor,
|
|
780
1882
|
onOpenLinkEditor: () => openLinkEditor(),
|
|
@@ -794,7 +1896,10 @@ var LoomaEditor = defineComponent({
|
|
|
794
1896
|
Object.assign(mention, state);
|
|
795
1897
|
}
|
|
796
1898
|
});
|
|
797
|
-
const imageDelivery = createLoomaImageDeliveryController({
|
|
1899
|
+
const imageDelivery = createLoomaImageDeliveryController({
|
|
1900
|
+
resolveAttributes: () => props.resolveImageAttributes,
|
|
1901
|
+
isEditable: () => props.editable
|
|
1902
|
+
});
|
|
798
1903
|
let lastFocusedSelection = null;
|
|
799
1904
|
let linkPressedSelection = null;
|
|
800
1905
|
const rememberSelection = (instance) => {
|
|
@@ -862,7 +1967,11 @@ var LoomaEditor = defineComponent({
|
|
|
862
1967
|
mention: mentionExtension ?? false,
|
|
863
1968
|
disableHighlight: props.disableHighlight,
|
|
864
1969
|
codeLanguages: props.codeLanguages ?? void 0,
|
|
865
|
-
codeLowlight: codeHighlightLowlight
|
|
1970
|
+
codeLowlight: codeHighlightLowlight,
|
|
1971
|
+
tableOfContentsNodeView,
|
|
1972
|
+
expandNodeView,
|
|
1973
|
+
dividerNodeView,
|
|
1974
|
+
imageNodeView
|
|
866
1975
|
}),
|
|
867
1976
|
imageDelivery.extension,
|
|
868
1977
|
slashExtension,
|
|
@@ -1326,8 +2435,13 @@ var LoomaEditor = defineComponent({
|
|
|
1326
2435
|
instance.view.dispatch(transaction);
|
|
1327
2436
|
instance.commands.focus();
|
|
1328
2437
|
};
|
|
2438
|
+
watch([() => props.slashCommands, () => props.uploadImage], () => {
|
|
2439
|
+
slashExtension.options.commands = resolveSlashCommands();
|
|
2440
|
+
editor.value?.commands.dismissLoomaSlashMenu();
|
|
2441
|
+
});
|
|
1329
2442
|
watch(() => props.editable, (editable) => {
|
|
1330
2443
|
editor.value?.setEditable(editable);
|
|
2444
|
+
if (editor.value) imageDelivery.reset(editor.value);
|
|
1331
2445
|
root.value?.querySelectorAll("[data-looma-chip]").forEach((chip) => {
|
|
1332
2446
|
chip.setAttribute("aria-disabled", String(!editable));
|
|
1333
2447
|
});
|
|
@@ -1583,7 +2697,8 @@ var LoomaEditor = defineComponent({
|
|
|
1583
2697
|
const instance = editor.value;
|
|
1584
2698
|
if (!instance) return null;
|
|
1585
2699
|
try {
|
|
1586
|
-
const
|
|
2700
|
+
const frame = element.closest("[data-looma-image-node]");
|
|
2701
|
+
const position = frame?.parentNode ? instance.view.posAtDOM(frame.parentNode, Array.from(frame.parentNode.childNodes).indexOf(frame)) : instance.view.posAtDOM(element, 0);
|
|
1587
2702
|
const node = instance.state.doc.nodeAt(position);
|
|
1588
2703
|
return node?.type.name === "image" ? imageDescriptorFromAttrs(node.attrs) : null;
|
|
1589
2704
|
} catch {
|
|
@@ -1630,7 +2745,7 @@ var LoomaEditor = defineComponent({
|
|
|
1630
2745
|
return;
|
|
1631
2746
|
}
|
|
1632
2747
|
const element = imageElementForEvent(event);
|
|
1633
|
-
if (!element) return;
|
|
2748
|
+
if (!element || props.editable) return;
|
|
1634
2749
|
event.preventDefault();
|
|
1635
2750
|
activateImage(element, "keyboard");
|
|
1636
2751
|
};
|
|
@@ -2045,7 +3160,22 @@ var LoomaEditor = defineComponent({
|
|
|
2045
3160
|
role: "toolbar",
|
|
2046
3161
|
"aria-label": "Editor tools"
|
|
2047
3162
|
}, [renderToolbar(instance, false)]) : null,
|
|
2048
|
-
instance ? h(
|
|
3163
|
+
instance && props.editable ? h(BubbleMenu, {
|
|
3164
|
+
editor: instance,
|
|
3165
|
+
pluginKey: "looma-image-actions",
|
|
3166
|
+
shouldShow: ({ editor: menuEditor }) => menuEditor.isEditable && menuEditor.state.selection instanceof NodeSelection && menuEditor.state.selection.node.type.name === "image",
|
|
3167
|
+
tippyOptions: {
|
|
3168
|
+
appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
|
|
3169
|
+
placement: "top",
|
|
3170
|
+
duration: 100,
|
|
3171
|
+
maxWidth: window.innerWidth - 24,
|
|
3172
|
+
getReferenceClientRect: () => {
|
|
3173
|
+
const dom = instance.view.nodeDOM(instance.state.selection.from);
|
|
3174
|
+
return (dom instanceof Element ? dom.querySelector("img") ?? dom : instance.view.dom).getBoundingClientRect();
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
}, { default: () => h(ImageControls, { editor: instance }) }) : null,
|
|
3178
|
+
instance ? h(Stack, { gap: "none" }, () => h(EditorContent, { editor: instance })) : null,
|
|
2049
3179
|
codeUi.open ? h("div", {
|
|
2050
3180
|
ref: codeLanguageShell,
|
|
2051
3181
|
class: "looma-editor__code-language",
|
|
@@ -2193,18 +3323,24 @@ var LoomaEditor = defineComponent({
|
|
|
2193
3323
|
tablePickerOpen.value = false;
|
|
2194
3324
|
}
|
|
2195
3325
|
})]),
|
|
2196
|
-
slash.active
|
|
3326
|
+
slash.active ? h(EditorSlashMenu, {
|
|
2197
3327
|
open: true,
|
|
2198
3328
|
query: slash.query,
|
|
2199
|
-
items: managedSlashMenuItems(slash.items),
|
|
2200
3329
|
selectedIndex: slash.selectedIndex,
|
|
2201
3330
|
anchorRect: managedMenuAnchorRect(slash.rect),
|
|
2202
3331
|
onHighlight: (event) => {
|
|
2203
|
-
slash.
|
|
3332
|
+
slash.highlight?.(event.detail.index);
|
|
2204
3333
|
},
|
|
2205
3334
|
onSelect: (event) => {
|
|
2206
3335
|
slash.select?.(event.detail.index);
|
|
2207
3336
|
}
|
|
3337
|
+
}, {
|
|
3338
|
+
default: () => slashMenuRows(slash.items),
|
|
3339
|
+
empty: () => h(Text, {
|
|
3340
|
+
role: "status",
|
|
3341
|
+
size: "sm",
|
|
3342
|
+
tone: "muted"
|
|
3343
|
+
}, () => "No commands found.")
|
|
2208
3344
|
}) : null,
|
|
2209
3345
|
mention.active && (mention.loading || mention.items.length > 0) ? h(EditorMentionMenu, {
|
|
2210
3346
|
id: mentionMenuId,
|