UNPKG

rhino-editor

Version:

A custom element wrapped rich text editor

943 lines (930 loc) 28.5 kB
import { AttachmentRemoveEvent } from "./chunk-3PWTYHXV.js"; import { findAttribute } from "./chunk-DM4WISIL.js"; import { Node as Node2, mergeAttributes } from "./chunk-TVRRLXKH.js"; import { DOMSerializer, NodeSelection, Plugin, PluginKey, ReplaceAroundStep, ReplaceStep, Selection, TextSelection } from "./chunk-ZEDCHN2I.js"; import { AttachmentManager, toDefaultCaption } from "./chunk-KUC52EW2.js"; import { LOADING_STATES, nn } from "./chunk-CMZKZQUC.js"; import { captionPlaceholder, fileUploadErrorMessage } from "./chunk-5B6NQHMA.js"; import { D, Q, R, ke } from "./chunk-7BUHDUOD.js"; // src/internal/selection-to-insertion-end.ts function selectionToInsertionEnd(tr, startLen, bias) { const last = tr.steps.length - 1; if (last < startLen) { return; } const step = tr.steps[last]; if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep)) { return; } const map = tr.mapping.maps[last]; let end = 0; map.forEach((_from, _to, _newFrom, newTo) => { if (end === 0) { end = newTo; } }); tr.setSelection(Selection.near(tr.doc.resolve(end), bias)); } // node_modules/.pnpm/prosemirror-utils@1.2.2_prosemirror-model@1.23.0_prosemirror-state@1.4.3/node_modules/prosemirror-utils/dist/index.js var equalNodeType = (nodeType, node) => { return Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1 || node.type === nodeType; }; var findParentNodeClosestToPos = ($pos, predicate) => { for (let i2 = $pos.depth; i2 > 0; i2--) { const node = $pos.node(i2); if (predicate(node)) { return { pos: i2 > 0 ? $pos.before(i2) : 0, start: $pos.start(i2), depth: i2, node }; } } }; var findParentNodeOfTypeClosestToPos = ($pos, nodeType) => { return findParentNodeClosestToPos( $pos, (node) => equalNodeType(nodeType, node) ); }; var flatten = (node, descend = true) => { if (!node) { throw new Error('Invalid "node" parameter'); } const result = []; node.descendants((child, pos) => { result.push({ node: child, pos }); if (!descend) { return false; } }); return result; }; var findChildren = (node, predicate, descend = true) => { if (!node) { throw new Error('Invalid "node" parameter'); } else if (!predicate) { throw new Error('Invalid "predicate" parameter'); } return flatten(node, descend).filter((child) => predicate(child.node)); }; var findChildrenByType = (node, nodeType, descend = true) => { return findChildren(node, (child) => child.type === nodeType, descend); }; // node_modules/.pnpm/lit-html@3.2.0/node_modules/lit-html/directive.js var t = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 }; var e = (t2) => (...e2) => ({ _$litDirective$: t2, values: e2 }); var i = class { constructor(t2) { } get _$AU() { return this._$AM._$AU; } _$AT(t2, e2, i2) { this.t = t2, this._$AM = e2, this.i = i2; } _$AS(t2, e2) { return this.update(t2, e2); } update(t2, e2) { return this.render(...e2); } }; // node_modules/.pnpm/lit-html@3.2.0/node_modules/lit-html/directives/unsafe-html.js var le = class extends i { constructor(i2) { if (super(i2), this.it = D, i2.type !== t.CHILD) throw Error(this.constructor.directiveName + "() can only be used in child bindings"); } render(t2) { if (t2 === D || null == t2) return this._t = void 0, this.it = t2; if (t2 === R) return t2; if ("string" != typeof t2) throw Error(this.constructor.directiveName + "() called with a non-string value"); if (t2 === this.it) return this._t; this.it = t2; const i2 = [t2]; return i2.raw = i2, this._t = { _$litType$: this.constructor.resultType, strings: i2, values: [] }; } }; le.directiveName = "unsafeHTML", le.resultType = 1; var ae = e(le); // node_modules/.pnpm/lit-html@3.2.0/node_modules/lit-html/directives/if-defined.js var to = (t2) => t2 ?? D; // src/exports/extensions/attachment.ts var figureTypes = [ "previewable-attachment-figure", "attachment-figure" ]; function parseContentTypeFromElement(element) { return findAttribute(element, "content-type") || JSON.parse(element.getAttribute("data-trix-attachment") || "{}").contentType || "application/octet-stream"; } var canParseAttachment = (node, shouldPreview) => { if (node instanceof HTMLElement) { const contentType = parseContentTypeFromElement(node); if (contentType === "application/octet-stream") { return false; } const actionTextAttachment = node.closest("action-text-attachment"); if (actionTextAttachment) { const previewable2 = actionTextAttachment.getAttribute("previewable") === "true"; if (!actionTextAttachment.getAttribute("sgid")) { return false; } if (previewable2 === shouldPreview) { return true; } return false; } const previewable = canPreview( findAttribute(node, "previewable"), findAttribute(node, "contentType") ); if (previewable === shouldPreview) { return true; } } return false; }; function handleCaptions(node, tr, newState, pos) { let modified = false; if (figureTypes.includes(node.type.name) === false) return modified; let scratch = document.createElement("div"); scratch.appendChild( DOMSerializer.fromSchema(newState.schema).serializeNode(node) ); const figcaption = scratch.querySelector("figcaption"); if (figcaption == null) return modified; const caption = figcaption.innerHTML; if (node.attrs.caption !== caption) { tr.setNodeMarkup(pos, void 0, { ...node.attrs, caption }); modified = true; } return modified; } function canPreview(previewable, contentType) { return Boolean( previewable || AttachmentManager.isPreviewable(contentType || "") ); } function toExtension(fileName) { if (!fileName) return ""; return "attachment--" + fileName.match(/\.(\w+)$/)?.[1].toLowerCase(); } function toType(content, previewable) { if (previewable) { return "attachment--preview"; } if (content) { return "attachment--content"; } return "attachment--file"; } var Attachment = Node2.create({ name: "attachment-figure", group: "block attachmentFigure", content: "inline*", selectable: true, draggable: true, isolating: true, defining: true, addProseMirrorPlugins() { return [ new Plugin({ key: new PluginKey("rhino-attachment-fixer"), appendTransaction(_transactions, _oldState, newState) { const tr = newState.tr; let modified = false; newState.doc.descendants((node, pos, _parent) => { const mutations = [handleCaptions(node, tr, newState, pos)]; const shouldModify = mutations.some((bool) => bool === true); if (shouldModify) { modified = true; } }); if (modified) return tr; return void 0; } }), new Plugin({ key: new PluginKey("rhino-prevent-unintended-figcaption-behavior"), props: { handlePaste: (view, event) => { const name = view.state.selection.$anchor.parent.type.name; const { clipboardData } = event; if (!clipboardData) return false; if (figureTypes.includes(name)) { event.preventDefault(); const tr = view.state.tr; const text = clipboardData.getData("text/plain"); tr.insertText(text); view.dispatch(tr); return true; } return false; }, handleKeyDown: (view, event) => { if (["Backspace", "Enter"].includes(event.key)) { const name = view.state.selection.$head.parent.type.name; const content = view.state.selection.$head.parent.textContent; if (view.state.selection.to !== view.state.selection.from) { return false; } if (figureTypes.includes(name) && content === "") { event.preventDefault(); return true; } } return false; } } }), new Plugin({ key: new PluginKey("rhino-attachment-remove-event"), view() { const afterSgidsAndAttachmentIds = /* @__PURE__ */ new Map(); return { update(view, prevState) { const nodeTypes = [ view.state.schema.nodes["previewable-attachment-figure"], view.state.schema.nodes["attachment-figure"] ]; nodeTypes.forEach((nodeType) => { const attachmentNodesBefore = findChildrenByType( prevState.doc, nodeType ); findChildrenByType(view.state.doc, nodeType).forEach((node) => { const nodeAttrs = node.node.attrs; const sgid = nodeAttrs.sgid; const attachmentId = nodeAttrs.attachmentId; if (sgid) { afterSgidsAndAttachmentIds.set(sgid, node); } if (attachmentId) { afterSgidsAndAttachmentIds.set(attachmentId, node); } }); attachmentNodesBefore.forEach((node) => { const nodeAttrs = node.node.attrs; const { sgid, attachmentId } = nodeAttrs; if (sgid && afterSgidsAndAttachmentIds.has(sgid)) return; if (attachmentId && afterSgidsAndAttachmentIds.has(attachmentId)) return; const attachmentManager = new AttachmentManager( nodeAttrs, view ); view.dom.dispatchEvent( new AttachmentRemoveEvent(attachmentManager) ); }); afterSgidsAndAttachmentIds.clear(); }); } }; } }) ]; }, addOptions() { return { HTMLAttributes: { class: "attachment" }, fileUploadErrorMessage, captionPlaceholder, previewable: false, altTextEditor: false, shouldStopEvent: (event) => { const composedPath = event.composedPath(); const isInAttachmentEditor = composedPath.find( (el) => el?.tagName?.toLowerCase() === "rhino-attachment-editor" ); if (isInAttachmentEditor) { return true; } return false; } }; }, parseHTML() { return [ // When it's <figure data-trix-attachment> its coming from `to_trix_html` { tag: "figure[data-trix-attachment]", getAttrs: (node) => { const isValid = canParseAttachment(node, this.options.previewable); if (!isValid) { return false; } return null; } }, // When it's .attachment, its coming from <action-text-attachment><figure></figure></action-text-attachment> its the raw HTML. { tag: "action-text-attachment > figure.attachment", contentElement: "figcaption", getAttrs: (node) => { const isValid = canParseAttachment(node, this.options.previewable); if (!isValid) { return false; } return null; } }, { tag: "action-text-attachment", getAttrs: (node) => { const isValid = canParseAttachment(node, this.options.previewable); if (!isValid) { return false; } return null; } } ]; }, renderHTML({ node }) { const { // Figure content, contentType, sgid, fileName, fileSize, caption, url, previewable, // Image src, width, height, alt } = node.attrs; const attachmentAttrs = { caption, contentType, content, filename: fileName, filesize: fileSize, height, width, sgid, url, src, alt }; const figure = [ "figure", mergeAttributes(this.options.HTMLAttributes, { class: this.options.HTMLAttributes.class + " " + toType(content, canPreview(previewable, contentType)) + " " + toExtension(fileName), "data-trix-content-type": contentType, "data-trix-attachment": JSON.stringify(attachmentAttrs), "data-trix-attributes": JSON.stringify({ caption, ...canPreview(previewable, contentType) ? { presentation: "gallery" } : {} }) }) ]; const figcaption = [ "figcaption", mergeAttributes( {}, { class: "attachment__caption attachment__caption--edited" } ), 0 ]; const image = [ "img", mergeAttributes( {}, { src: url || src, contenteditable: false, width, height, alt } ) ]; if (!content && canPreview(previewable, contentType)) { return [...figure, image, figcaption]; } return [...figure, figcaption]; }, addAttributes() { return { attachmentId: { default: null }, altTextDialogOpen: { default: false }, caption: { default: "", parseHTML: (element) => { return element.querySelector("figcaption")?.innerHTML || findAttribute(element, "caption"); } }, progress: { default: 0, parseHTML: (element) => { return findAttribute(element, "sgid") || findAttribute(element, "content") || element.closest("action-text-attachment")?.innerHTML ? 100 : 0; } }, loadingState: { default: LOADING_STATES.notStarted, parseHTML: (element) => findAttribute(element, "sgid") ? LOADING_STATES.success : LOADING_STATES.notStarted }, alt: { default: "", parseHTML: (element) => { return findAttribute(element, "alt") || element.querySelector("img")?.getAttribute("alt") || ""; } }, sgid: { default: "", parseHTML: (element) => findAttribute(element, "sgid") }, src: { default: "", parseHTML: (element) => findAttribute(element, "src") }, height: { default: "", parseHTML: (element) => findAttribute(element, "height") }, width: { default: "", parseHTML: (element) => { return findAttribute(element, "width"); } }, contentType: { default: "", parseHTML: (element) => { return parseContentTypeFromElement(element); } }, fileName: { default: "", parseHTML: (element) => findAttribute(element, "filename") }, fileSize: { default: "", parseHTML: (element) => findAttribute(element, "filesize") }, content: { default: "", parseHTML: (element) => { const attachment = element.closest("action-text-attachment"); let content = ""; if (attachment) { const domParser = new DOMParser(); const parsedDom = domParser.parseFromString( attachment.innerHTML, "text/html" ); const firstChild = parsedDom.body.firstElementChild; if (firstChild) { if (firstChild.tagName.toLowerCase() !== "figure" || !firstChild.classList.contains("attachment")) { content = attachment.innerHTML; } } } return content || findAttribute(element, "content"); } }, url: { default: "", parseHTML: (element) => { return findAttribute(element, "url"); } }, previewable: { default: false, parseHTML: (element) => { let { previewable } = JSON.parse( element.getAttribute("data-trix-attachment") || "{}" ); if (previewable == null) { previewable = findAttribute(element, "previewable"); } return previewable; } } }; }, addNodeView() { return ({ node, getPos, editor }) => { const { content, contentType, sgid, fileName, progress, fileSize, url, src, width, height, caption, previewable, loadingState, alt, altTextDialogOpen } = node.attrs; const trixAttachment = JSON.stringify({ contentType, content, filename: fileName, filesize: fileSize, alt, height, width, sgid, url, caption }); const isPreviewable = canPreview(previewable, contentType); const trixAttributes = JSON.stringify({ ...isPreviewable ? { presentation: "gallery" } : {}, caption }); const figureClasses = ` ${this.options.HTMLAttributes.class} ${toType(content, canPreview(previewable, contentType))} ${toExtension(fileName)} `; function handleFigureClick(e2) { const target = e2.currentTarget; const figcaption = target.querySelector("figcaption"); const attachmentEditor = target.querySelector( "rhino-attachment-editor" ); const composedPath = e2.composedPath(); if (figcaption && composedPath.includes(figcaption)) { return; } if (attachmentEditor && composedPath.includes(attachmentEditor)) { return; } if (typeof getPos === "function") { const { view } = editor; const { tr } = view.state; const captionNode = view.state.doc.nodeAt(getPos() + 1); captionNode?.nodeSize; tr.setSelection( TextSelection.create( view.state.doc, getPos() + 1 + (captionNode?.nodeSize || 0) ) ); view.dispatch(tr); } } let imgSrc = void 0; if (isPreviewable && (url || src)) { imgSrc = url || src; } let mouseIsDown = false; let mouseTimeout = null; const handleMouseDown = (_e) => { mouseTimeout = setTimeout(() => { mouseIsDown = true; }, 20); }; const handleMouseUp = (_e) => { mouseIsDown = false; if (mouseTimeout) { clearTimeout(mouseTimeout); } }; const handleMouseMove = (_e) => { if (mouseIsDown && typeof getPos === "function") { const { view } = editor; view.dispatch( view.state.tr.setSelection( NodeSelection.create(view.state.doc, getPos()) ) ); } }; function setNodeAttributes(attrs) { if (typeof getPos === "function") { const { view } = editor; const { tr } = view.state; const pos = getPos(); tr.setNodeMarkup(pos, null, { ...node.attrs, ...attrs }); view.dispatch(tr); } } function removeFigure() { if (typeof getPos === "function") { const { view } = editor; const { tr } = view.state; const pos = getPos(); tr.delete(pos, pos + 1); view.dispatch(tr); } const closestAttachment = this.closest(".attachment"); if (closestAttachment) { closestAttachment.remove(); } } const template = ke` <figure class=${figureClasses} attachment-type=${this.name} sgid=${to(sgid ? sgid : void 0)} data-trix-content-type=${contentType} data-trix-attachment=${trixAttachment} data-trix-attributes=${trixAttributes} @click=${handleFigureClick} @mousedown=${handleMouseDown} @mouseup=${handleMouseUp} @mousemove=${handleMouseMove} > <rhino-attachment-editor file-name=${fileName || ""} file-size=${String(fileSize || 0)} loading-state=${loadingState || LOADING_STATES.notStarted} img-src=${to(imgSrc)} progress=${String( progress ? progress : sgid || content || !fileSize ? 100 : progress )} contenteditable="false" ?previewable=${isPreviewable} ?show-metadata=${isPreviewable} .fileUploadErrorMessage=${this.options.fileUploadErrorMessage} .removeFigure=${removeFigure} .setNodeAttributes=${setNodeAttributes} .altTextDialogOpen=${altTextDialogOpen} .altTextEditor=${Boolean(this.options.altTextEditor)} alt-text=${alt} > </rhino-attachment-editor> ${nn( content, /* This is really not great. This is how Trix does it, but it feels very unsafe. https://github.com/basecamp/trix/blob/fda14c5ae88a0821cf8999a53dcb3572b4172cf0/src/trix/views/attachment_view.js#L36 */ () => ke`${ae(content)}`, () => ke`` )} ${nn( isPreviewable && !content, () => ke` <img class=${loadingState === LOADING_STATES.error ? "rhino-upload-error" : ""} width=${String(width)} height=${String(height)} src=${to(imgSrc)} alt=${alt} contenteditable="false" />`, () => ke`` )} <figcaption style="${Boolean(content) ? "display: none;" : ""}" class=${`attachment__caption ${caption ? "attachment__caption--edited" : "is-empty"}`} data-placeholder=${this.options.captionPlaceholder} data-default-caption=${toDefaultCaption({ fileName, fileSize })} ></figcaption> </figure> `; const scratch = document.createElement("div"); Q(template, scratch); const dom = scratch.firstElementChild; const contentDOM = dom?.querySelector("figcaption"); let srcRevoked = false; const shouldStopEvent = this.options.shouldStopEvent; return { dom, contentDOM, stopEvent(event) { return shouldStopEvent(event); }, update(node2) { if (node2.type.name !== "attachment") return false; if (!srcRevoked && node2.attrs.url) { srcRevoked = true; try { URL.revokeObjectURL(node2.attrs.src); } catch (_e) { } } return false; } }; }; }, addCommands() { return { setAttachmentAtCoords: (options, coordinates) => ({ view, state, tr, dispatch }) => { let posAtCoords = view.posAtCoords(coordinates); const currentSelection = state.doc.resolve(posAtCoords?.pos || 0); return handleAttachment(options, currentSelection, { state, tr, dispatch }); }, setAttachment: (options) => ({ state, tr, dispatch }) => { const currentSelection = state.doc.resolve(state.selection.anchor); return handleAttachment(options, currentSelection, { state, tr, dispatch }); } }; } }); var PreviewableAttachment = Attachment.extend({ name: "previewable-attachment-figure", group: "block previewableAttachmentFigure", addOptions() { return { ...Attachment.options, previewable: true }; }, // We purposely override this to nothing. Because all of the extensions registered by Attachment // are global, they run twice. We don't want that. for example, this causes `rhino-attachment-remove` // to fire twice. No bueno. addProseMirrorPlugins() { return []; } }); function handleAttachment(options, currentSelection, { state, tr, dispatch }) { const { schema } = state; const minSize = 0; const maxSize = tr.doc.content.size; function clamp(val, min = minSize, max = maxSize) { if (val < min) return min; if (val > max) return max; return val; } const hasGalleriesDisabled = schema.nodes["attachment-gallery"] == null; const currentNode = state.doc.resolve(currentSelection.pos); const paragraphTopNode = findParentNodeOfTypeClosestToPos( currentNode, schema.nodes["paragraph"] ); let currentGallery = findParentNodeOfTypeClosestToPos( state.doc.resolve(currentSelection.pos), schema.nodes["attachment-gallery"] ); let priorGalleryPos = null; if (paragraphTopNode) { const paragraphIsEmpty = currentSelection.parent.textContent === ""; const prevNode = state.doc.resolve(clamp(paragraphTopNode.pos - 1)); if (paragraphIsEmpty && prevNode.parent.type.name === "attachment-gallery") { priorGalleryPos = clamp(paragraphTopNode.pos - 1); } } const isInGallery = currentGallery || priorGalleryPos; const attachments = Array.isArray(options) ? options : [].concat(options); let allNodesPreviewable = true; let attachmentNodes = []; let previewableNodes = []; attachments.forEach((attachment) => { const nodeType = attachment.isPreviewable ? "previewable-attachment-figure" : "attachment-figure"; const figure = schema.nodes[nodeType].create( attachment, attachment.caption ? [schema.text(attachment.caption)] : [] ); if (hasGalleriesDisabled) { attachmentNodes.push(figure); return; } if (!attachment.isPreviewable) { allNodesPreviewable = false; if (previewableNodes.length >= 1) { attachmentNodes = attachmentNodes.concat( schema.nodes["attachment-gallery"].create({}, previewableNodes) ); previewableNodes = []; } attachmentNodes.push(figure); return; } previewableNodes.push(figure); }); let end = 0; if (currentGallery) { end = currentGallery.start + currentGallery.node.nodeSize - 2; } else if (priorGalleryPos != null) { end = priorGalleryPos; } end = clamp(end); if (hasGalleriesDisabled) { attachmentNodes = attachmentNodes.flatMap((node) => [node]); tr.insert(end, attachmentNodes.concat([schema.nodes.paragraph.create()])); if (dispatch) dispatch(tr); return true; } if (isInGallery) { if (allNodesPreviewable) { tr.insert(end, previewableNodes); } else { if (!hasGalleriesDisabled && previewableNodes.length >= 1) { attachmentNodes = attachmentNodes.concat( schema.nodes["attachment-gallery"].create({}, previewableNodes) ); } tr.insert(end + 1, attachmentNodes); } } else { const currSelection = state.selection; if (!hasGalleriesDisabled && previewableNodes.length >= 1) { attachmentNodes = attachmentNodes.concat( schema.nodes["attachment-gallery"].create({}, previewableNodes) ); } let from = currSelection.from; const prevNode = state.doc.resolve(from - 1); const parentNode = prevNode.parent; if (parentNode && parentNode.type.name === "doc") { from -= 1; } else { const closestParagraph = findParentNodeOfTypeClosestToPos( prevNode, schema.nodes["paragraph"] ); if (closestParagraph && closestParagraph.node.textContent === "") { from -= 1; } } tr.replaceWith(from, currSelection.to, [ ...attachmentNodes, schema.nodes.paragraph.create() ]); selectionToInsertionEnd(tr, tr.steps.length - 1, -1); } if (dispatch) dispatch(tr); return true; } export { t, e, i, findParentNodeOfTypeClosestToPos, to, figureTypes, Attachment, PreviewableAttachment }; /*! Bundled license information: lit-html/directive.js: (** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause *) lit-html/directives/unsafe-html.js: (** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause *) lit-html/directives/if-defined.js: (** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause *) */ //# sourceMappingURL=chunk-4IWQPKLS.js.map