@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
456 lines (454 loc) • 15.3 kB
JavaScript
;
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