UNPKG

@wordpress/editor

Version:
362 lines (361 loc) 10.9 kB
// packages/editor/src/components/collab-sidebar/utils.js import { _x } from "@wordpress/i18n"; import { create, RichTextData } from "@wordpress/rich-text"; import { getRectangleFromRange } from "@wordpress/dom"; function sanitizeNoteContent(str) { return str.trim(); } var THREAD_ALIGN_OFFSET = -16; var THREAD_GAP = 16; var OVERLAP_MARGIN = 20; var AVATAR_BORDER_COLORS = [ "#6F42C1", // Purple "#D94145", // Red "#FBBF24", // Orange "#FF35EE", // Magenta "#879F11", // Olive "#0F766E", // Teal "#00CFFF" // Cyan ]; function getAvatarBorderColor(userId) { return AVATAR_BORDER_COLORS[userId % AVATAR_BORDER_COLORS.length]; } function getNoteExcerpt(text, excerptLength = 10) { if (!text) { return ""; } const wordCountType = _x("words", "Word count type. Do not translate!"); const rawText = text.trim(); let trimmedExcerpt = ""; if (wordCountType === "words") { trimmedExcerpt = rawText.split(" ", excerptLength).join(" "); } else if (wordCountType === "characters_excluding_spaces") { const textWithSpaces = rawText.split("", excerptLength).join(""); const numberOfSpaces = textWithSpaces.length - textWithSpaces.replaceAll(" ", "").length; trimmedExcerpt = rawText.split("", excerptLength + numberOfSpaces).join(""); } else if (wordCountType === "characters_including_spaces") { trimmedExcerpt = rawText.split("", excerptLength).join(""); } const isTrimmed = trimmedExcerpt !== rawText; return isTrimmed ? trimmedExcerpt + "…" : trimmedExcerpt; } function getNoteIdsFromMetadata(metadata) { const noteId = metadata?.noteId; const raw = Array.isArray(noteId) ? noteId : [noteId]; const ids = /* @__PURE__ */ new Set(); for (const value of raw) { const id = Number(value); if (Number.isFinite(id) && id > 0) { ids.add(id); } } return [...ids]; } function addNoteIdToMetadata(metadata, noteId) { const ids = new Set(getNoteIdsFromMetadata(metadata)); const id = Number(noteId); if (ids.has(id)) { return metadata; } ids.add(id); return { ...metadata, noteId: [...ids] }; } var NOTE_FORMAT_TYPE = "core/note"; function findNoteRange(value, noteId) { if (noteId === void 0 || noteId === null) { return null; } let html = null; if (value instanceof RichTextData) { html = value.toHTMLString(); } else if (typeof value === "string") { html = value; } if (!html || html.indexOf("wp-note") === -1) { return null; } const target = String(noteId); const record = create({ html }); const formats = record.formats; let start = -1; for (let i = 0; i < formats.length; i++) { const stack = formats[i]; const hit = stack?.find( (f) => f.type === NOTE_FORMAT_TYPE && f.attributes && f.attributes["data-id"] === target ); if (hit) { if (start === -1) { start = i; } } else if (start !== -1) { return { start, end: i }; } } if (start !== -1) { return { start, end: formats.length }; } return null; } function findNoteInBlock(attributes, noteId) { if (!attributes) { return null; } for (const attributeKey of Object.keys(attributes)) { const range = findNoteRange(attributes[attributeKey], noteId); if (range) { return { attributeKey, start: range.start, end: range.end }; } } return null; } function getNoteMarkerSelector(noteId) { const escapedId = String(noteId).replace( /["\\\n\r\f]/g, (char) => char === '"' || char === "\\" ? `\\${char}` : `\\${char.codePointAt(0).toString(16)} ` ); return `mark.wp-note[data-id="${escapedId}"]`; } function getSelectionRect(blockEl) { const selection = blockEl.ownerDocument.defaultView?.getSelection(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { return null; } const range = selection.getRangeAt(0); if (range.collapsed) { return null; } if (!blockEl.contains(range.commonAncestorContainer)) { return null; } const rect = getRectangleFromRange(range); if (!rect || rect.width === 0 && rect.height === 0) { return null; } return rect; } var BLOCK_LEVEL_NOTE_START = -1; function getInlineMarkerStart(thread, attributes) { const found = findNoteInBlock(attributes, thread?.id); return found ? found.start : BLOCK_LEVEL_NOTE_START; } function applyNoteFormat(record, format, start, end) { const formats = record.formats.slice(); for (let i = start; i < end; i++) { const stack = formats[i] ? formats[i].slice() : []; stack.push(format); formats[i] = stack; } const spans = /* @__PURE__ */ new Map(); for (let i = 0; i < formats.length; i++) { const stack = formats[i]; if (!stack) { continue; } for (const fmt of stack) { if (fmt.type !== NOTE_FORMAT_TYPE) { continue; } const id = fmt.attributes?.["data-id"]; const span = spans.get(id); if (span) { span.end = i; } else { spans.set(id, { start: i, end: i }); } } } const sizeOf = (id) => { const span = spans.get(id); return span ? span.end - span.start : 0; }; for (let i = 0; i < formats.length; i++) { const stack = formats[i]; if (!stack || stack.length < 2) { continue; } const notes = stack.filter((fmt) => fmt.type === NOTE_FORMAT_TYPE); if (notes.length === 0) { continue; } if (notes.length > 1) { notes.sort( (a, b) => sizeOf(b.attributes?.["data-id"]) - sizeOf(a.attributes?.["data-id"]) ); } const others = stack.filter((fmt) => fmt.type !== NOTE_FORMAT_TYPE); formats[i] = [...notes, ...others]; } return { ...record, formats }; } function removeNoteFormat(value, noteId) { if (!(value instanceof RichTextData)) { return null; } const target = String(noteId); const record = create({ html: value.toHTMLString() }); let changed = false; const formats = record.formats.map((stack) => { if (!stack) { return stack; } const filtered = stack.filter( (format) => !(format.type === NOTE_FORMAT_TYPE && format.attributes?.["data-id"] === target) ); if (filtered.length === stack.length) { return stack; } changed = true; return filtered.length ? filtered : void 0; }); return changed ? RichTextData.fromHTMLString( new RichTextData({ ...record, formats }).toHTMLString() ) : null; } function pickPrimaryNote(threads) { return threads.find((thread) => thread.status === "hold") ?? threads[0] ?? null; } function removeNoteIdFromMetadata(metadata, noteId) { const ids = new Set(getNoteIdsFromMetadata(metadata)); ids.delete(Number(noteId)); return { ...metadata, noteId: ids.size > 0 ? [...ids] : void 0 }; } function calculateNotePositions({ threads, selectedNoteId, blockRects, heights, scrollTop = 0 }) { const offsets = {}; const orderedThreads = [...threads].sort( (a, b) => (blockRects[a.id]?.top ?? Number.MAX_VALUE) - (blockRects[b.id]?.top ?? Number.MAX_VALUE) ); const anchorIndex = Math.max( 0, orderedThreads.findIndex((thread) => thread.id === selectedNoteId) ); const anchorThread = orderedThreads[anchorIndex]; if (!anchorThread || !blockRects[anchorThread.id]) { return { positions: {} }; } const anchorRect = blockRects[anchorThread.id]; const anchorTop = anchorRect.top || 0; const anchorHeight = heights[anchorThread.id] || 0; offsets[anchorThread.id] = THREAD_ALIGN_OFFSET; let prevAdjustedTop = anchorTop + THREAD_ALIGN_OFFSET; let prevHeight = anchorHeight; for (let i = anchorIndex + 1; i < orderedThreads.length; i++) { const thread = orderedThreads[i]; const threadRect = blockRects[thread.id]; if (!threadRect) { continue; } const threadTop = threadRect.top || 0; const threadHeight = heights[thread.id] || 0; let offset = THREAD_ALIGN_OFFSET; const prevBottom = prevAdjustedTop + prevHeight; if (threadTop < prevBottom + THREAD_GAP) { offset = prevBottom - threadTop + OVERLAP_MARGIN; } offsets[thread.id] = offset; prevAdjustedTop = threadTop + offset; prevHeight = threadHeight; } let belowAdjustedTop = anchorTop + THREAD_ALIGN_OFFSET; for (let i = anchorIndex - 1; i >= 0; i--) { const thread = orderedThreads[i]; const threadRect = blockRects[thread.id]; if (!threadRect) { continue; } const threadTop = threadRect.top || 0; const threadHeight = heights[thread.id] || 0; let offset = THREAD_ALIGN_OFFSET; const threadBottom = threadTop + threadHeight; if (threadBottom > belowAdjustedTop) { offset = belowAdjustedTop - threadTop - threadHeight - OVERLAP_MARGIN; } offsets[thread.id] = offset; belowAdjustedTop = threadTop + offset; } const positions = {}; for (const thread of orderedThreads) { const blockRect = blockRects[thread.id]; if (blockRect && offsets[thread.id] !== void 0) { positions[thread.id] = blockRect.top + scrollTop + offsets[thread.id]; } } return { positions }; } function findNoteThread(noteId, container, additionalSelector) { if (!container) { return Promise.resolve(null); } const threadSelector = noteId && noteId !== "new" ? `[role=treeitem][id="note-thread-${noteId}"]` : "[role=treeitem]:not([id])"; const selector = additionalSelector ? `${threadSelector} ${additionalSelector}` : threadSelector; return new Promise((resolve) => { if (container.querySelector(selector)) { return resolve(container.querySelector(selector)); } let timer = null; const observer = new window.MutationObserver(() => { if (container.querySelector(selector)) { clearTimeout(timer); observer.disconnect(); resolve(container.querySelector(selector)); } }); observer.observe(container, { childList: true, subtree: true }); timer = setTimeout(() => { observer.disconnect(); resolve(null); }, 3e3); }); } function focusNoteThread(noteId, container, additionalSelector) { return findNoteThread(noteId, container, additionalSelector).then( (element) => { if (!element) { return; } element.focus(); element.scrollIntoView({ block: "nearest" }); } ); } function scrollNoteThreadIntoView(noteId, container) { return findNoteThread(noteId, container).then((element) => { element?.scrollIntoView({ block: "nearest" }); }); } export { BLOCK_LEVEL_NOTE_START, addNoteIdToMetadata, applyNoteFormat, calculateNotePositions, findNoteInBlock, findNoteRange, focusNoteThread, getAvatarBorderColor, getInlineMarkerStart, getNoteExcerpt, getNoteIdsFromMetadata, getNoteMarkerSelector, getSelectionRect, pickPrimaryNote, removeNoteFormat, removeNoteIdFromMetadata, sanitizeNoteContent, scrollNoteThreadIntoView }; //# sourceMappingURL=utils.mjs.map