UNPKG

@wordpress/editor

Version:
456 lines (454 loc) 15.3 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/editor/src/components/collab-sidebar/hooks.js var hooks_exports = {}; __export(hooks_exports, { useEnableFloatingSidebar: () => useEnableFloatingSidebar, useFloatingBoard: () => useFloatingBoard, useNoteActions: () => useNoteActions, useNoteThreads: () => useNoteThreads }); module.exports = __toCommonJS(hooks_exports); var import_a11y = require("@wordpress/a11y"); var import_i18n = require("@wordpress/i18n"); var import_element = require("@wordpress/element"); var import_core_data = require("@wordpress/core-data"); var import_data = require("@wordpress/data"); var import_block_editor = require("@wordpress/block-editor"); var import_notices = require("@wordpress/notices"); var import_dom = require("@wordpress/dom"); var import_html_entities = require("@wordpress/html-entities"); var import_interface = require("@wordpress/interface"); var import_rich_text = require("@wordpress/rich-text"); var import_store = require("../../store/index.cjs"); var import_constants = require("./constants.cjs"); var import_lock_unlock = require("../../lock-unlock.cjs"); var import_board_store = require("./board-store.cjs"); var import_format = require("./format.cjs"); var import_utils = require("./utils.cjs"); var { cleanEmptyObject } = (0, import_lock_unlock.unlock)(import_block_editor.privateApis); function useNoteThreads(postId) { const queryArgs = { post: postId, type: "note", status: "all", per_page: -1 }; const { records: threads } = (0, import_core_data.useEntityRecords)( "root", "comment", queryArgs, { enabled: !!postId && typeof postId === "number" } ); const { getBlockAttributes } = (0, import_data.useSelect)(import_block_editor.store); const { clientIds } = (0, import_data.useSelect)((select) => { const { getClientIdsWithDescendants } = select(import_block_editor.store); return { clientIds: getClientIdsWithDescendants() }; }, []); const { notes, unresolvedNotes } = (0, import_element.useMemo)(() => { if (!threads || threads.length === 0) { return { notes: [], unresolvedNotes: [] }; } const blocksWithNotes = {}; const clientIdByNoteId = /* @__PURE__ */ new Map(); for (const clientId of clientIds) { const metadata = getBlockAttributes(clientId)?.metadata; const noteIds = (0, import_utils.getNoteIdsFromMetadata)(metadata); if (noteIds.length > 0) { blocksWithNotes[clientId] = noteIds; for (const noteId of noteIds) { clientIdByNoteId.set(noteId, clientId); } } } const threadsById = /* @__PURE__ */ new Map(); const rootThreads = []; for (const item of threads) { const thread = { ...item, reply: [], blockClientId: item.parent === 0 ? clientIdByNoteId.get(item.id) ?? null : null }; threadsById.set(item.id, thread); if (item.parent === 0) { rootThreads.push(thread); } } for (const item of threads) { if (item.parent !== 0) { threadsById.get(item.parent)?.reply.unshift(threadsById.get(item.id)); } } if (rootThreads.length === 0) { return { notes: [], unresolvedNotes: [] }; } const unresolved = []; const resolved = []; for (const [clientId, noteIds] of Object.entries( blocksWithNotes )) { const attributes = getBlockAttributes(clientId); const orderedThreads = noteIds.map((noteId) => { const thread = threadsById.get(noteId); if (!thread) { return null; } return { thread, start: (0, import_utils.getInlineMarkerStart)(thread, attributes) }; }).filter(Boolean).sort((a, b) => { if (a.start !== b.start) { return a.start - b.start; } return a.thread.id - b.thread.id; }); for (const { thread } of orderedThreads) { if (thread.status === "hold") { unresolved.push(thread); } else if (thread.status === "approved") { resolved.push(thread); } } } const orphans = rootThreads.filter( (thread) => !thread.blockClientId ); return { notes: [...unresolved, ...orphans, ...resolved], unresolvedNotes: unresolved }; }, [clientIds, threads, getBlockAttributes]); return { notes, unresolvedNotes }; } function readInlineSelection(getSelectionStart, getSelectionEnd) { const start = getSelectionStart(); const end = getSelectionEnd(); if (!start?.clientId || start.clientId !== end.clientId || !start.attributeKey || start.offset === void 0 || end.offset === void 0 || start.offset === end.offset) { return null; } const [startOffset, endOffset] = start.offset < end.offset ? [start.offset, end.offset] : [end.offset, start.offset]; return { clientId: start.clientId, attributeKey: start.attributeKey, start: startOffset, end: endOffset }; } function wrapInlineNote(value, id, start, end) { if (!(value instanceof import_rich_text.RichTextData)) { return null; } const record = (0, import_utils.applyNoteFormat)( (0, import_rich_text.create)({ html: value.toHTMLString() }), { type: import_format.NOTE_FORMAT_NAME, attributes: { "data-id": String(id) } }, start, end ); return import_rich_text.RichTextData.fromHTMLString( new import_rich_text.RichTextData(record).toHTMLString() ); } function clearInlineNoteMarker(noteId, getClientIdsWithDescendants, getBlockAttributes, updateBlockAttributes) { for (const clientId of getClientIdsWithDescendants()) { const attributes = getBlockAttributes(clientId); const found = (0, import_utils.findNoteInBlock)(attributes, noteId); if (!found) { continue; } const next = (0, import_utils.removeNoteFormat)( attributes[found.attributeKey], noteId ); if (next) { updateBlockAttributes(clientId, { [found.attributeKey]: next }); } return; } } function useNoteActions() { const { createNotice } = (0, import_data.useDispatch)(import_notices.store); const { saveEntityRecord, deleteEntityRecord } = (0, import_data.useDispatch)(import_core_data.store); const { getCurrentPostId } = (0, import_data.useSelect)(import_store.store); const { getBlockAttributes, getClientIdsWithDescendants, getSelectedBlockClientId, getSelectionStart, getSelectionEnd } = (0, import_data.useSelect)(import_block_editor.store); const { updateBlockAttributes } = (0, import_data.useDispatch)(import_block_editor.store); const onError = (error) => { const errorMessage = error.message && error.code !== "unknown_error" ? (0, import_html_entities.decodeEntities)(error.message) : (0, import_i18n.__)("An error occurred while performing an update."); createNotice("error", errorMessage, { type: "snackbar", isDismissible: true }); }; const onCreate = async ({ content, parent }) => { try { const inlineSelection = !parent ? readInlineSelection(getSelectionStart, getSelectionEnd) : null; const clientId = !parent ? inlineSelection?.clientId || getSelectedBlockClientId() : null; const savedRecord = await saveEntityRecord( "root", "comment", { post: getCurrentPostId(), content, status: "hold", type: "note", parent: parent || 0 }, { throwOnError: true } ); if (!parent && savedRecord?.id && clientId) { const attributes = getBlockAttributes(clientId); const metadata = attributes?.metadata; const updatedMetadata = (0, import_utils.addNoteIdToMetadata)( metadata, savedRecord.id ); const newAttributes = { metadata: cleanEmptyObject(updatedMetadata) }; if (inlineSelection) { const wrapped = wrapInlineNote( attributes?.[inlineSelection.attributeKey], savedRecord.id, inlineSelection.start, inlineSelection.end ); if (wrapped) { newAttributes[inlineSelection.attributeKey] = wrapped; } } updateBlockAttributes(clientId, newAttributes); } createNotice( "snackbar", parent ? (0, import_i18n.__)("Reply added.") : (0, import_i18n.__)("Note added."), { type: "snackbar", isDismissible: true } ); return savedRecord; } catch (error) { onError(error); } }; const onEdit = async ({ id, content, status }) => { try { if (status === "approved" || status === "hold") { await saveEntityRecord( "root", "comment", { id, status }, { throwOnError: true } ); const newNoteData = { post: getCurrentPostId(), content: content || "", // Empty content for resolve, content for reopen. type: "note", status, parent: id, meta: { _wp_note_status: status === "approved" ? "resolved" : "reopen" } }; const savedRecord2 = await saveEntityRecord( "root", "comment", newNoteData, { throwOnError: true } ); if (status === "approved") { clearInlineNoteMarker( id, getClientIdsWithDescendants, getBlockAttributes, updateBlockAttributes ); } (0, import_a11y.speak)( status === "approved" ? (0, import_i18n.__)("Note marked as resolved.") : (0, import_i18n.__)("Note reopened.") ); return savedRecord2; } const updateData = { id, content, status }; const savedRecord = await saveEntityRecord( "root", "comment", updateData, { throwOnError: true } ); createNotice("snackbar", (0, import_i18n.__)("Note updated."), { type: "snackbar", isDismissible: true }); return savedRecord; } catch (error) { onError(error); } }; const onDelete = async (note) => { try { const clientId = !note.parent ? note.blockClientId || getSelectedBlockClientId() : null; await deleteEntityRecord("root", "comment", note.id, void 0, { throwOnError: true }); if (clientId) { const attributes = getBlockAttributes(clientId); const newAttributes = { metadata: cleanEmptyObject( (0, import_utils.removeNoteIdFromMetadata)( attributes?.metadata, note.id ) ) }; const found = (0, import_utils.findNoteInBlock)(attributes, note.id); if (found) { const next = (0, import_utils.removeNoteFormat)( attributes[found.attributeKey], note.id ); if (next) { newAttributes[found.attributeKey] = next; } } updateBlockAttributes(clientId, newAttributes); } createNotice("snackbar", (0, import_i18n.__)("Note deleted."), { type: "snackbar", isDismissible: true }); return true; } catch (error) { onError(error); } }; return { onCreate, onEdit, onDelete }; } function useEnableFloatingSidebar(enabled = false) { const registry = (0, import_data.useRegistry)(); (0, import_element.useEffect)(() => { if (!enabled) { return; } const { getActiveComplementaryArea } = registry.select(import_interface.store); const { disableComplementaryArea, enableComplementaryArea } = registry.dispatch(import_interface.store); const unsubscribe = registry.subscribe(() => { if (getActiveComplementaryArea("core") === null) { enableComplementaryArea("core", import_constants.FLOATING_NOTES_SIDEBAR); } }); return () => { unsubscribe(); if (getActiveComplementaryArea("core") === import_constants.FLOATING_NOTES_SIDEBAR) { disableComplementaryArea("core", import_constants.FLOATING_NOTES_SIDEBAR); } }; }, [enabled, registry]); } function useFloatingBoard({ threads, selectedNoteId, isFloating, sidebarRef }) { const [notePositions, setNotePositions] = (0, import_element.useState)({}); const [store] = (0, import_element.useState)(import_board_store.createBoardStore); const heights = (0, import_element.useSyncExternalStore)(store.subscribe, store.getSnapshot); (0, import_element.useEffect)(() => { if (!isFloating || !sidebarRef?.current) { return; } const panel = sidebarRef.current; const blockEl = store.getFirstBlockElement(); const rootEl = blockEl?.closest(".is-root-container") ?? blockEl; const canvas = rootEl ? (0, import_dom.getScrollContainer)(rootEl) : null; const applyScroll = () => { panel.style.setProperty( "--canvas-scroll", `${-(canvas?.scrollTop ?? 0)}px` ); }; let rafId; const schedule = () => { window.cancelAnimationFrame(rafId); rafId = window.requestAnimationFrame(() => { const result = (0, import_utils.calculateNotePositions)({ threads, selectedNoteId, blockRects: store.getAnchorRects(), heights, scrollTop: canvas?.scrollTop ?? 0 }); setNotePositions(result.positions); applyScroll(); }); }; schedule(); const contentObserver = new window.ResizeObserver(schedule); if (rootEl) { contentObserver.observe(rootEl); } const view = canvas?.ownerDocument?.defaultView; const listenerOptions = { passive: true, capture: true }; view?.addEventListener("scroll", applyScroll, listenerOptions); return () => { window.cancelAnimationFrame(rafId); contentObserver.disconnect(); view?.removeEventListener("scroll", applyScroll, listenerOptions); }; }, [sidebarRef, heights, isFloating, selectedNoteId, store, threads]); return { notePositions, registerThread: store.registerThread, unregisterThread: store.unregisterThread }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { useEnableFloatingSidebar, useFloatingBoard, useNoteActions, useNoteThreads }); //# sourceMappingURL=hooks.cjs.map