UNPKG

rhino-editor

Version:

A custom element wrapped rich text editor

1,367 lines (1,343 loc) 45.9 kB
import { TipTapEditorBase } from "./chunk-E3HP7G66.js"; import { toolbarButtonStyles } from "./chunk-Y7ECDMDC.js"; import { findNodeViewAnchor } from "./chunk-4EN52UIW.js"; import { icons_exports } from "./chunk-KPIYU2DV.js"; import { isiOS, translations } from "./chunk-76S5BEJL.js"; // src/exports/elements/tip-tap-editor.ts import { ref, createRef } from "lit/directives/ref.js"; import RoleToolbar from "role-components/exports/components/toolbar/toolbar.js"; import RoleTooltip from "role-components/exports/components/tooltip/tooltip.js"; import { html } from "lit/html.js"; // src/internal/string-map.ts function stringMap(obj) { let string = ""; for (const [key, value] of Object.entries(obj)) { if (value) { string += `${key} `; } } return string; } // src/internal/is-exact-node-active.ts import { objectIncludes, getNodeType } from "@tiptap/core"; function isExactNodeActive(state, typeOrName, attributes = {}) { const { from, to, empty } = state.selection; const type = typeOrName ? getNodeType(typeOrName, state.schema) : null; const nodeRanges = []; state.doc.nodesBetween(from, to, (node, pos) => { if (node.isText) { return; } const relativeFrom = Math.max(from, pos); const relativeTo = Math.min(to, pos + node.nodeSize); nodeRanges.push({ node, from: relativeFrom, to: relativeTo }); }); const selectionRange = to - from; const matchedNodeRanges = nodeRanges.slice(-3).filter((nodeRange) => { if (!type) { return true; } return type.name === nodeRange.node.type.name; }).filter( (nodeRange) => objectIncludes(nodeRange.node.attrs, attributes, { strict: false }) ); if (empty) { return !!matchedNodeRanges.length; } const range = matchedNodeRanges.reduce( (sum, nodeRange) => sum + nodeRange.to - nodeRange.from, 0 ); return range >= selectionRange; } // src/exports/elements/tip-tap-editor.ts import RoleAnchoredRegion from "role-components/exports/components/anchored-region/anchored-region.js"; import { isNodeSelection, posToDOMRect } from "@tiptap/core"; function findElement(editor) { if (!editor) { return null; } const state = editor.state; const { selection } = state; const view = editor.view; if (view.composing) { return null; } const { ranges } = selection; const from = Math.min(...ranges.map((range) => range.$from.pos)); const to = Math.max(...ranges.map((range) => range.$to.pos)); let clientRect = null; if (isNodeSelection(state.selection)) { const node = findNodeViewAnchor?.({ editor, view, from }) || view.nodeDOM(from); if (node) { node.scrollIntoView({ block: "nearest" }); clientRect = () => { const rect = node.getBoundingClientRect(); return rect; }; } } else { const toNode = view.domAtPos(to).node; if (toNode instanceof HTMLElement) { toNode.scrollIntoView({ block: "nearest" }); } clientRect = () => { const rect = posToDOMRect(view, from, to); rect.x = rect.x - rect.width / 2; return rect; }; } return clientRect; } var TipTapEditor = class extends TipTapEditorBase { constructor() { super(); /** * Whether or not to enable the alt text editor. */ this.altTextEditor = false; /** * The heading level to use for the heading button */ this.defaultHeadingLevel = 1; /** * Translations for various aspects of the editor. */ this.translations = translations; /** * The <input> for inserting links */ this.linkInputRef = createRef(); /** * The dialog that contains the link input + link / unlink buttons */ this.linkDialogExpanded = false; this.__invalidLink__ = false; /** Closes the dialog for link previews */ this.handleKeyboardDialogToggle = (e) => { let { key, metaKey, ctrlKey } = e; if (key == null) return; key = key.toLowerCase(); if (key === "escape" && this.linkDialogExpanded) { this.closeLinkDialog(); return; } const shortcutModifier = isiOS ? metaKey : ctrlKey; if (key === "k" && shortcutModifier) { this.showLinkDialog(); } }; /** * @private */ this.__handleLinkDialogClick = (e) => { if (e.defaultPrevented) { return; } const linkDialogContainer = this.shadowRoot?.querySelector( ".link-dialog__container" ); if (!linkDialogContainer) { this.linkDialogExpanded = false; return; } const composedPath = e.composedPath(); const linkButton = this.shadowRoot?.querySelector("[name='link-button']"); if (composedPath.includes(linkDialogContainer)) { return; } if (linkButton && composedPath.includes(linkButton)) { return; } this.linkDialogExpanded = false; }; this.starterKitOptions = Object.assign(this.starterKitOptions, { rhinoPlaceholder: { placeholder: this.translations.placeholder }, rhinoAttachment: { fileUploadErrorMessage: this.translations.fileUploadErrorMessage, captionPlaceholder: this.translations.captionPlaceholder } }); this.addEventListener("keydown", this.handleKeyboardDialogToggle); } static get styles() { return TipTapEditorBase.styles.concat([toolbarButtonStyles]); } static get properties() { return Object.assign(TipTapEditorBase.properties, { linkDialogExpanded: { type: Boolean, reflect: true, attribute: "link-dialog-expanded" }, altTextEditor: { type: Boolean, reflect: true, attribute: "alt-text-editor" }, defaultHeadingLevel: { attribute: "default-heading-level", type: Number }, linkInputRef: { state: true }, translations: { state: true }, __invalidLink__: { state: true, type: Boolean } }); } /** * @override */ registerDependencies() { super.registerDependencies(); [RoleToolbar, RoleTooltip, RoleAnchoredRegion].forEach((el) => el.define()); } updated(changedProperties) { if (changedProperties.has("altTextEditor")) { if (this.starterKitOptions.rhinoAttachment !== false) { this.starterKitOptions = { ...this.starterKitOptions, rhinoAttachment: { ...this.starterKitOptions.rhinoAttachment, altTextEditor: this.altTextEditor } }; } } if (!this.hasInitialized) { return super.updated(changedProperties); } if (changedProperties.has("translations")) { const { rhinoAttachment, rhinoPlaceholder } = this.starterKitOptions; if (rhinoPlaceholder) { rhinoPlaceholder.placeholder = this.translations.placeholder; } if (rhinoAttachment) { rhinoAttachment.captionPlaceholder = this.translations.captionPlaceholder; rhinoAttachment.fileUploadErrorMessage = this.translations.fileUploadErrorMessage; } } return super.updated(changedProperties); } /** * @override */ async connectedCallback() { super.connectedCallback(); await this.updateComplete; this.starterKitOptions = Object.assign(this.starterKitOptions, { rhinoBubbleMenu: { ...this.starterKitOptions.rhinoBubbleMenu, element: this.shadowRoot?.querySelector("role-anchored-region") } }); document.addEventListener("click", this.__handleLinkDialogClick); } /** * @override */ async startEditor() { await super.startEditor(); if (this.editor) { this.editor.on("focus", this.closeLinkDialog); } } disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener("click", this.__handleLinkDialogClick); } get icons() { return icons_exports; } toggleLinkDialog() { if (this.linkDialogExpanded) { this.closeLinkDialog(); return; } this.showLinkDialog(); } closeLinkDialog() { this.linkDialogExpanded = false; this.editor?.commands.focus(); } showLinkDialog() { const inputElement = this.linkInputRef.value; if (inputElement != null) { inputElement.value = this.editor?.getAttributes("link").href || ""; inputElement.setSelectionRange(0, inputElement.value.length); } this.__invalidLink__ = false; this.linkDialogExpanded = true; setTimeout(() => { requestAnimationFrame(() => { if (inputElement != null) { inputElement.focus(); } }); }); } get linkDialog() { return this.shadowRoot?.querySelector("#link-dialog"); } attachFiles() { const input = this.fileInputEl; if (input == null) return; input.click(); } addLink() { const inputElement = this.linkInputRef.value; if (inputElement == null) return; const href = inputElement.value; try { new URL(href); inputElement.setCustomValidity(""); this.__invalidLink__ = false; } catch (error) { inputElement.setCustomValidity("Not a valid URL"); this.__invalidLink__ = true; inputElement.reportValidity(); return; } if (!this.editor) { return; } if (href) { this.closeLinkDialog(); inputElement.value = ""; if (this.editor.state.selection.empty && !this.editor.getAttributes("link").href) { const from = this.editor.state.selection.anchor; this.editor.commands.insertContent(href); const to = this.editor.state.selection.anchor; this.editor.commands.setTextSelection({ from, to }); } this.editor?.chain().extendMarkRange("link").setLink({ href }).run(); } } get fileInputEl() { return this.shadowRoot?.getElementById( "file-input" ); } async handleFileUpload() { const input = this.fileInputEl; if (input == null) return; if (input.files == null) return; const attachments = await this.handleFiles(input.files); if (attachments.length > 0) { this.editor?.chain().focus().setAttachment(attachments).run(); } input.value = ""; } get __tooltipExportParts() { return "base:toolbar__tooltip__base, arrow:toolbar__tooltip__arrow"; } renderBoldButton(prefix = "") { const boldEnabled = this.starterKitOptions.bold !== false || Boolean(this.editor?.commands.toggleBold); if (!boldEnabled) return html``; const isDisabled = this.editor == null || !this.editor.can().toggleBold(); const isActive = Boolean(this.editor?.isActive("bold")); let tooltip_slot_name = "bold-tooltip"; let tooltip_id = "bold"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--bold"; let icon_slot_name = "bold-icon"; if (prefix) { tooltip_slot_name = prefix + "__" + tooltip_slot_name; icon_slot_name = prefix + "__" + icon_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.bold} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" part=${stringMap({ toolbar__button: true, "toolbar__button--bold": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role="toolbar-item" data-role-tooltip=${tooltip_id} @click=${async (e) => { if (elementDisabled(e.currentTarget)) return; this.editor?.chain().focus().toggleBold().run(); }} > <slot name=${icon_slot_name}>${this.icons.bold}</slot> </button> `; } renderItalicButton(prefix = "") { const italicEnabled = this.starterKitOptions.italic !== false || Boolean(this.editor?.commands.toggleItalic); if (!italicEnabled) return html``; const isActive = Boolean(this.editor?.isActive("italic")); const isDisabled = this.editor == null || !this.editor.can().toggleItalic(); let tooltip_slot_name = "italics-tooltip"; let tooltip_id = "italics"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--italics"; let icon_slot_name = "italics-icon"; if (prefix) { tooltip_slot_name = prefix + "__" + tooltip_slot_name; icon_slot_name = prefix + "__" + icon_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.italics} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" tabindex="-1" type="button" part=${stringMap({ toolbar__button: true, "toolbar__button--italic": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role="toolbar-item" data-role-tooltip=${tooltip_id} @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().toggleItalic().run(); }} > <slot name=${icon_slot_name}> ${this.icons.italics} </slot> </button> `; } renderStrikeButton(prefix = "") { const strikeEnabled = this.starterKitOptions.rhinoStrike !== false || Boolean(this.editor?.commands.toggleStrike); if (!strikeEnabled) return html``; const isActive = Boolean(this.editor?.isActive("rhino-strike")); const isDisabled = this.editor == null || !this.editor.can().toggleStrike(); let tooltip_slot_name = "strike-tooltip"; let tooltip_id = "strike"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--strike"; let icon_slot_name = "strike-icon"; if (prefix) { tooltip_slot_name = prefix + "__" + tooltip_slot_name; icon_slot_name = prefix + "__" + icon_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.strike} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--strike": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().toggleStrike().run(); }} > <slot name=${icon_slot_name}>${this.icons.strike}</slot> </button> `; } renderLinkButton(prefix = "") { const linkEnabled = this.starterKitOptions.rhinoLink !== false || Boolean(this.editor?.commands.setLink); if (!linkEnabled) return html``; const isActive = Boolean( this.editor?.isActive("link") || this.linkDialogExpanded ); const isDisabled = this.editor == null || !this.editor.can().setLink({ href: "" }); let tooltip_slot_name = "link-tooltip"; let tooltip_id = "link"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--link"; let icon_slot_name = "link-icon"; if (prefix) { tooltip_slot_name = prefix + "__" + tooltip_slot_name; icon_slot_name = prefix + "__" + icon_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.link} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--link": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} aria-controls="link-dialog" data-role="toolbar-item" data-role-tooltip=${tooltip_id} @click=${(e) => { if (this.editor == null) return; if (elementDisabled(e.currentTarget)) return; e.preventDefault(); this.toggleLinkDialog(); }} > <slot name=${icon_slot_name}>${this.icons.link}</slot> </button> `; } renderHeadingButton(prefix = "") { const headingEnabled = this.starterKitOptions.heading !== false || Boolean(this.editor?.commands.toggleHeading); if (!headingEnabled) return html``; const defaultHeadingLevel = this.defaultHeadingLevel || 1; const isActive = Boolean(this.editor?.isActive("heading")); const isDisabled = this.editor == null || !this.editor.can().toggleHeading({ level: defaultHeadingLevel }); let tooltip_slot_name = "heading-tooltip"; let tooltip_id = "heading"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--heading"; let icon_slot_name = "heading-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.heading} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--heading": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role="toolbar-item" data-role-tooltip=${tooltip_id} @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().toggleHeading({ level: defaultHeadingLevel }).run(); }} > <slot name=${icon_slot_name}>${this.icons.heading}</slot> </button> `; } renderBlockquoteButton(prefix = "") { const blockQuoteEnabled = this.starterKitOptions.blockquote !== false || Boolean(this.editor?.commands.toggleBlockquote); if (!blockQuoteEnabled) return html``; const isActive = Boolean(this.editor?.isActive("blockquote")); const isDisabled = this.editor == null || !this.editor.can().toggleBlockquote(); let tooltip_slot_name = "blockquote-tooltip"; let tooltip_id = "blockquote"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--blockquote"; let icon_slot_name = "blockquote-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.blockQuote} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--blockquote": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().toggleBlockquote().run(); }} > <slot name=${icon_slot_name}>${this.icons.blockQuote}</slot> </button> `; } renderCodeButton(prefix = "") { const codeEnabled = this.starterKitOptions.code !== false || Boolean(this.editor?.commands.toggleCode); if (!codeEnabled) return html``; const isActive = Boolean(this.editor?.isActive("code")); const isDisabled = this.editor == null || !this.editor.can().toggleCode(); let tooltip_slot_name = "code-tooltip"; let tooltip_id = "code"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--code"; let icon_slot_name = "code-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.code} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--code": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().toggleCode().run(); }} > <slot name=${icon_slot_name}>${this.icons.code}</slot> </button> `; } renderCodeBlockButton(prefix = "") { const codeBlockEnabled = this.starterKitOptions.codeBlock !== false || Boolean(this.editor?.commands.toggleCodeBlock); if (!codeBlockEnabled) return html``; const isActive = Boolean(this.editor?.isActive("codeBlock")); const isDisabled = this.editor == null || !this.editor.can().toggleCodeBlock(); let tooltip_slot_name = "code-block-tooltip"; let tooltip_id = "code-block"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--code-block"; let icon_slot_name = "code-block-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.codeBlock} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--code-block": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().toggleCodeBlock().run(); }} > <slot name=${icon_slot_name}>${this.icons.codeBlock}</slot> </button> `; } renderBulletListButton(prefix = "") { const bulletListEnabled = this.starterKitOptions.bulletList !== false || Boolean(this.editor?.commands.toggleBulletList); if (!bulletListEnabled) return html``; const isDisabled = this.editor == null || !(this.editor.can().toggleOrderedList?.() || this.editor.can().toggleBulletList()); const isActive = Boolean( this.editor != null && isExactNodeActive(this.editor.state, "bulletList") ); let tooltip_slot_name = "bullet-list-tooltip"; let tooltip_id = "bullet-list"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--bullet-list"; let icon_slot_name = "bullet-list-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.bulletList} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--bullet-list": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().toggleBulletList().run(); }} > <slot name=${icon_slot_name}>${this.icons.bulletList}</slot> </button> `; } renderOrderedListButton(prefix = "") { const orderedListEnabled = this.starterKitOptions.orderedList !== false || Boolean(this.editor?.commands.toggleOrderedList); if (!orderedListEnabled) return html``; const isDisabled = this.editor == null || !(this.editor.can().toggleOrderedList() || this.editor.can().toggleBulletList?.()); const isActive = Boolean( this.editor != null && isExactNodeActive(this.editor.state, "orderedList") ); let tooltip_slot_name = "ordered-list-tooltip"; let tooltip_id = "ordered-list"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--ordered-list"; let icon_slot_name = "ordered-list-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.orderedList} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--ordered-list": true, "toolbar__button--active": isActive, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} aria-pressed=${isActive} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().toggleOrderedList().run(); }} > <slot name=${icon_slot_name}>${this.icons.orderedList}</slot> </button> `; } renderAttachmentButton(prefix = "") { const attachmentEnabled = this.starterKitOptions.rhinoAttachment !== false || Boolean(this.editor?.commands.setAttachment); if (!attachmentEnabled) return html``; const isDisabled = this.editor == null; let tooltip_slot_name = "attach-files-tooltip"; let tooltip_id = "attach-files"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--attach-files"; let icon_slot_name = "attach-files-icon"; let file_input_id = "file-input"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; file_input_id = prefix + "__" + file_input_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.attachFiles} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" tabindex="-1" type="button" part=${stringMap({ toolbar__button: true, "toolbar__button--attach-files": true, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${this.attachFiles} > <slot name=${icon_slot_name}>${this.icons.attachFiles}</slot> <!-- @TODO: Write documentation. Hookup onchange to the slotted elements? --> <slot name="attach-files-input"> <input id=${file_input_id} type="file" hidden multiple accept=${this.accept || "*"} @change=${this.handleFileUpload} /> </slot> </button> `; } renderUndoButton(prefix = "") { const undoEnabled = this.starterKitOptions.undoRedo !== false || Boolean(this.editor?.commands.undo); if (!undoEnabled) return html``; const isDisabled = this.editor == null || !this.editor.can().undo(); let tooltip_slot_name = "undo-tooltip"; let tooltip_id = "undo"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--undo"; let icon_slot_name = "undo-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.undo} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--undo": true, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().undo().run(); }} > <slot name=${icon_slot_name}>${this.icons.undo}</slot> </button> `; } renderDecreaseIndentation(prefix = "") { const decreaseIndentationEnabled = this.starterKitOptions.decreaseIndentation !== false; if (!decreaseIndentationEnabled) return html``; const isDisabled = this.editor == null || !this.editor.can().liftListItem("listItem"); let tooltip_slot_name = "decrease-indentation-tooltip"; let tooltip_id = "decrease-indentation"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--decrease-indentation"; let icon_slot_name = "decrease-indentation-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.decreaseIndentation} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--decrease-indentation": true, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().liftListItem("listItem").run(); }} > <slot name=${icon_slot_name}> ${this.icons.decreaseIndentation} </slot> </button> `; } renderIncreaseIndentation(prefix = "") { const increaseIndentationEnabled = this.starterKitOptions.increaseIndentation !== false; if (!increaseIndentationEnabled) return html``; const isDisabled = this.editor == null || !this.editor.can().sinkListItem("listItem"); let tooltip_slot_name = "increase-indentation-tooltip"; let tooltip_id = "increase-indentation"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--increase-indentation"; let icon_slot_name = "increase-indentation-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.increaseIndentation} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" type="button" tabindex="-1" part=${stringMap({ toolbar__button: true, "toolbar__button--increase-indentation": true, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().sinkListItem("listItem").run(); }} > <slot name=${icon_slot_name}> ${this.icons.increaseIndentation} </slot> </button> `; } renderRedoButton(prefix = "") { const redoEnabled = this.starterKitOptions.undoRedo !== false || Boolean(this.editor?.commands.redo); if (!redoEnabled) return html``; const isDisabled = this.editor == null || !this.editor.can().redo?.(); let tooltip_slot_name = "redo-indentation-tooltip"; let tooltip_id = "redo-indentation"; let tooltip_parts = "toolbar__tooltip toolbar__tooltip--redo-indentation"; let icon_slot_name = "redo-indentation-icon"; if (prefix) { icon_slot_name = prefix + "__" + icon_slot_name; tooltip_slot_name = prefix + "__" + tooltip_slot_name; tooltip_id = prefix + "__" + tooltip_id; } return html` <slot name=${tooltip_slot_name}> <role-tooltip id=${tooltip_id} part=${tooltip_parts} exportparts=${this.__tooltipExportParts} > ${this.translations.redo} </role-tooltip> </slot> <button class="toolbar__button rhino-toolbar-button" tabindex="-1" type="button" part=${stringMap({ toolbar__button: true, "toolbar__button--redo": true, "toolbar__button--disabled": isDisabled })} aria-disabled=${isDisabled} data-role-tooltip=${tooltip_id} data-role="toolbar-item" @click=${(e) => { if (elementDisabled(e.currentTarget)) { return; } this.editor?.chain().focus().redo().run(); }} > <slot name=${icon_slot_name}>${this.icons.redo}</slot> </button> `; } renderToolbarStart() { return html``; } renderToolbarEnd() { return html``; } renderToolbar() { if (this.readonly) return html``; return html` <slot name="toolbar"> <role-toolbar part="toolbar main__toolbar" role="toolbar" exportparts=" base:toolbar__base, base:main__toolbar__base " > <slot name="toolbar-start">${this.renderToolbarStart()}</slot> <!-- Bold --> <slot name="before-bold-button"></slot> <slot name="bold-button">${this.renderBoldButton()}</slot> <slot name="after-bold-button"></slot> <!-- Italic --> <slot name="before-italic-button"></slot> <slot name="italic-button">${this.renderItalicButton()}</slot> <slot name="after-italic-button"></slot> <!-- Strike --> <slot name="before-strike-button"></slot> <slot name="strike-button">${this.renderStrikeButton()}</slot> <slot name="after-strike-button"></slot> <!-- Code --> <slot name="before-code-button"></slot> <slot name="code-button">${this.renderCodeButton()}</slot> <slot name="after-code-button"></slot> <!-- Link --> <slot name="before-link-button"></slot> <slot name="link-button">${this.renderLinkButton()}</slot> <slot name="after-link-button"></slot> <!-- Heading --> <slot name="before-heading-button"></slot> <slot name="heading-button">${this.renderHeadingButton()}</slot> <slot name="after-heading-button"></slot> <!-- Blockquote --> <slot name="before-blockquote-button"></slot> <slot name="blockquote-button">${this.renderBlockquoteButton()}</slot> <slot name="after-blockquote-button"></slot> <!-- Code block --> <slot name="before-code-block-button"></slot> <slot name="code-block-button">${this.renderCodeBlockButton()}</slot> <slot name="after-code-block-button"></slot> <!-- Bullet List --> <slot name="before-bullet-list-button"></slot> <slot name="bullet-list-button" >${this.renderBulletListButton()}</slot > <slot name="after-bullet-list-button"></slot> <!-- Ordered list --> <slot name="before-ordered-list-button"></slot> <slot name="ordered-list-button"> ${this.renderOrderedListButton()} </slot> <slot name="after-ordered-list-button"></slot> <slot name="before-decrease-indentation-button"></slot> <slot name="decrease-indentation-button" >${this.renderDecreaseIndentation()}</slot > <slot name="after-decrease-indentation-button"></slot> <slot name="before-increase-indentation-button"></slot> <slot name="increase-indentation-button" >${this.renderIncreaseIndentation()}</slot > <slot name="after-increase-indentation-button"></slot> <!-- Attachments --> <slot name="before-attach-files-button"></slot> <slot name="attach-files-button" >${this.renderAttachmentButton()}</slot > <slot name="after-attach-files-button"></slot> <!-- Undo --> <slot name="before-undo-button"></slot> <!-- @ts-expect-error --> <slot name="undo-button"> ${this.renderUndoButton()} </slot> <slot name="after-undo-button"></slot> <!-- Redo --> <slot name="before-redo-button"></slot> <slot name="redo-button"> ${this.renderRedoButton()} </slot> <slot name="after-redo-button"></slot> <slot name="toolbar-end">${this.renderToolbarEnd()}</slot> </role-toolbar> </slot> ${this.renderBubbleMenuToolbar()} ${this.renderLinkDialogAnchoredRegion()} `; } renderLinkDialogAnchoredRegion() { const clientRect = this.linkDialogExpanded ? findElement(this.editor) : null; return html` <role-anchored-region part="link-bubble-menu__anchored-region" exportparts=" popover-base:link-bubble-menu__popover-base, hover-bridge:link-bubble-menu__hover-bridge, hover-bridge--visible:link-bubble-menu__hover-bridge--visible, popover:link-bubble-menu__popover, popover--active:link-bubble-menu__popover--active, popover--fixed:link-bubble-menu__popover--fixed, popover--has-arrow:link-bubble-menu__popover--has-arrow, arrow:link-bubble-menu__arrow " anchored-popover-type="manual" distance="4" .active=${this.linkDialogExpanded} .shiftBoundary=${this.querySelector(".ProseMirror") || this} .flipBoundary=${this.querySelector(".ProseMirror") || this} .anchor=${typeof clientRect === "function" ? { getBoundingClientRect: clientRect } : null} > <slot name="link-bubble-menu-dialog"> ${this.renderLinkDialog()} </slot> </role-anchored-region> `; } /** @TODO: Lets think of a more friendly way to render dialogs for users to extend. */ renderLinkDialog() { if (this.readonly) { return html``; } return html` <div id="link-dialog" part=${stringMap({ "link-dialog": true, "link-dialog--expanded": this.linkDialogExpanded })} > <div class="link-dialog__container" part="link-dialog__container"> <input id="link-dialog__input" class=${`link-dialog__input ${this.__invalidLink__ ? "link-validate" : ""}`} part=${`link-dialog__input ${this.__invalidLink__ ? "link-dialog__input--invalid" : ""}`} type="text" placeholder="Enter a URL..." aria-label="Enter a URL" required type="url" ${ref(this.linkInputRef)} @input=${() => { const inputElement = this.linkInputRef.value; if (inputElement == null) return; inputElement.setCustomValidity(""); this.__invalidLink__ = false; }} @blur=${() => { const inputElement = this.linkInputRef.value; if (inputElement == null) return; this.__invalidLink__ = false; }} @keydown=${(e) => { if (e.key?.toLowerCase() === "enter") { e.preventDefault(); this.addLink(); } }} /> <div class="link-dialog__buttons" part="link-dialog__buttons"> <button class="rhino-toolbar-button link-dialog__button" part="link-dialog__button link-dialog__button--link" @click=${this.addLink} > ${this.translations.linkDialogLink} </button> <button class="rhino-toolbar-button link-dialog__button" part="link-dialog__button link-dialog__button--unlink" @click=${() => { this.linkDialogExpanded = false; this.editor?.chain().focus().extendMarkRange("link").unsetLink().run(); }} > ${this.translations.linkDialogUnlink} </button> </div> </div> </div>`; } /** * Returns the bubble menu toolbar from the shadow root. */ get defaultBubbleMenuToolbar() { return this.shadowRoot?.querySelector( "[part~='bubble-menu__toolbar']" ); } renderBubbleMenuToolbar() { return html` <role-anchored-region part="bubble-menu__anchored-region" exportparts=" popover-base:bubble-menu__popover-base, hover-bridge:bubble-menu__hover-bridge, hover-bridge--visible:bubble-menu__hover-bridge--visible, popover:bubble-menu__popover, popover--active:bubble-menu__popover--active, popover--fixed:bubble-menu__popover--fixed, popover--has-arrow:bubble-menu__popover--has-arrow, arrow:bubble-menu__arrow " @rhino-bubble-menu-show=${(e) => { if (e.defaultPrevented) { return; } const anchoredRegion = e.currentTarget; anchoredRegion.anchor = { getBoundingClientRect: e.clientRect }; anchoredRegion.shiftBoundary = this.querySelector(".ProseMirror") || this; anchoredRegion.flipBoundary = this.querySelector(".ProseMirror") || this; anchoredRegion.active = true; }} @rhino-bubble-menu-hide=${(e) => { if (e.defaultPrevented) { return; } const anchoredRegion = e.currentTarget; anchoredRegion.anchor = null; anchoredRegion.active = false; }} anchored-popover-type="manual" distance="4" > <slot name="bubble-menu-toolbar"> <role-toolbar part="toolbar bubble-menu__toolbar" role="toolbar" exportparts=" base:toolbar__base, base:bubble-menu__toolbar__base " > <slot name="before-bubble-menu-toolbar-items"></slot> <slot name="bubble-menu-toolbar-items"> ${this.renderBoldButton("bubble-menu")} ${this.renderItalicButton("bubble-menu")} ${this.renderStrikeButton("bubble-menu")} ${this.renderCodeButton("bubble-menu")} ${this.renderLinkButton("bubble-menu")} </slot> <slot name="after-bubble-menu-toolbar-items"></slot> </role-toolbar> </slot> <slot name="additional-bubble-menu-toolbar"></slot> </role-anchored-region> `; } }; function elementDisabled(element) { if (element == null) return true; if (!("getAttribute" in element)) return true; return element.getAttribute("aria-disabled") === "true" || element.hasAttribute("disabled"); } export { TipTapEditor }; //# sourceMappingURL=chunk-CFYCPZAT.js.map