UNPKG

@udecode/plate-heading

Version:

Headings plugin for Plate

553 lines (531 loc) 17.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/react/index.ts var react_exports = {}; __export(react_exports, { HeadingPlugin: () => HeadingPlugin, TocPlugin: () => TocPlugin, checkIn: () => checkIn, heightToTop: () => heightToTop, useContentController: () => useContentController, useContentObserver: () => useContentObserver, useTocController: () => useTocController, useTocElement: () => useTocElement, useTocElementState: () => useTocElementState, useTocObserver: () => useTocObserver, useTocSideBar: () => useTocSideBar, useTocSideBarState: () => useTocSideBarState }); module.exports = __toCommonJS(react_exports); // src/react/HeadingPlugin.tsx var import_react = require("@udecode/plate-common/react"); // src/lib/BaseHeadingPlugin.ts var import_plate_common = require("@udecode/plate-common"); // src/lib/constants.ts var HEADING_KEYS = { h1: "h1", h2: "h2", h3: "h3", h4: "h4", h5: "h5", h6: "h6" }; var HEADING_LEVELS = [ HEADING_KEYS.h1, HEADING_KEYS.h2, HEADING_KEYS.h3, HEADING_KEYS.h4, HEADING_KEYS.h5, HEADING_KEYS.h6 ]; // src/lib/BaseHeadingPlugin.ts var BaseHeadingPlugin = (0, import_plate_common.createTSlatePlugin)({ key: "heading", options: { levels: [1, 2, 3, 4, 5, 6] } }).extend(({ plugin }) => { const { options: { levels } } = plugin; const plugins = []; const headingLevels = Array.isArray(levels) ? levels : Array.from({ length: levels || 6 }, (_, i) => i + 1); headingLevels.forEach((level) => { const plugin2 = (0, import_plate_common.createSlatePlugin)({ key: HEADING_LEVELS[level - 1], node: { isElement: true }, parsers: { html: { deserializer: { rules: [ { validNodeName: `H${level}` } ] } } } }); plugins.push(plugin2); }); return { plugins }; }); // src/react/HeadingPlugin.tsx var HeadingPlugin = (0, import_react.toPlatePlugin)(BaseHeadingPlugin, ({ plugin }) => ({ plugins: plugin.plugins.map( (p) => p.extend(({ editor, type }) => { const level = p.key.at(-1); if (level > 3) return {}; return { shortcuts: { ["toggleHeading" + level]: { keys: [ [import_react.Key.Mod, import_react.Key.Alt, level], [import_react.Key.Mod, import_react.Key.Shift, level] ], preventDefault: true, handler: () => { editor.tf.toggle.block({ type }); } } } }; }) ) })); // src/react/TocPlugin.tsx var import_react2 = require("@udecode/plate-common/react"); // src/lib/BaseTocPlugin.ts var import_plate_common2 = require("@udecode/plate-common"); var BaseTocPlugin = (0, import_plate_common2.createTSlatePlugin)({ key: "toc", node: { isElement: true, isVoid: true }, options: { isScroll: true, topOffset: 80 } }); // src/lib/utils/isHeading.ts var isHeading = (node) => { return node.type && HEADING_LEVELS.includes(node.type); }; // src/react/TocPlugin.tsx var TocPlugin = (0, import_react2.toPlatePlugin)(BaseTocPlugin); // src/react/hooks/useContentController.ts var import_react5 = __toESM(require("react")); var import_react6 = require("@udecode/plate-common/react"); // src/react/utils/checkIn.ts function checkIn(e) { const event = window.event; const x = Number(event.clientX); const y = Number(event.clientY); const ele = e.target; const div_x = Number(ele.getBoundingClientRect().left); const div_x_width = Number( ele.getBoundingClientRect().left + ele.clientWidth ); const div_y = Number(ele.getBoundingClientRect().top); const div_y_height = Number( ele.getBoundingClientRect().top + ele.clientHeight ); if (x > div_x && x < div_x_width && y > div_y && y < div_y_height) { return true; } return false; } // src/react/utils/heightToTop.ts var heightToTop = (ele, editorContentRef) => { const root = editorContentRef ? editorContentRef.current : document.body; if (!root || !ele) return 0; const containerRect = root.getBoundingClientRect(); const elementRect = ele.getBoundingClientRect(); const scrollY = root.scrollTop; const absoluteElementTop = elementRect.top + scrollY - containerRect.top; return absoluteElementTop; }; // src/react/hooks/useContentObserver.ts var import_react3 = __toESM(require("react")); var import_plate_common4 = require("@udecode/plate-common"); var import_react4 = require("@udecode/plate-common/react"); // src/internal/getHeadingList.ts var import_plate_common3 = require("@udecode/plate-common"); var headingDepth = { [HEADING_KEYS.h1]: 1, [HEADING_KEYS.h2]: 2, [HEADING_KEYS.h3]: 3, [HEADING_KEYS.h4]: 4, [HEADING_KEYS.h5]: 5, [HEADING_KEYS.h6]: 6 }; var getHeadingList = (editor) => { const options = editor.getOptions(BaseTocPlugin); if (options.queryHeading) { return options.queryHeading(editor); } const headingList = []; const values = (0, import_plate_common3.getNodeEntries)(editor, { at: [], match: (n) => isHeading(n) }); if (!values) return []; Array.from(values, ([node, path]) => { const { type } = node; const title = (0, import_plate_common3.getNodeString)(node); const depth = headingDepth[type]; const id = node.id; title && headingList.push({ id, depth, path, title, type }); }); return headingList; }; // src/react/hooks/useContentObserver.ts var useContentObserver = ({ editorContentRef, isObserve, isScroll, rootMargin, status }) => { const headingElementsRef = import_react3.default.useRef({}); const root = isScroll ? editorContentRef.current : void 0; const editor = (0, import_react4.useEditorRef)(); const headingList = (0, import_react4.useEditorSelector)(getHeadingList, []); const [activeId, setActiveId] = import_react3.default.useState(""); import_react3.default.useEffect(() => { const callback = (headings) => { if (!isObserve) return; headingElementsRef.current = headings.reduce((map, headingElement) => { map[headingElement.target.id] = headingElement; return map; }, headingElementsRef.current); const visibleHeadings = []; Object.keys(headingElementsRef.current).forEach((key) => { const headingElement = headingElementsRef.current[key]; if (headingElement.isIntersecting) visibleHeadings.push(key); }); const lastKey = Object.keys(headingElementsRef.current).pop(); visibleHeadings.length > 0 && setActiveId(visibleHeadings[0] || lastKey); headingElementsRef.current = {}; }; const observer = new IntersectionObserver(callback, { root, rootMargin }); headingList.forEach((item) => { const { path } = item; const node = (0, import_plate_common4.getNode)(editor, path); if (!node) return; const element = (0, import_react4.toDOMNode)(editor, node); return element && observer.observe(element); }); return () => { observer.disconnect(); }; }, [headingList, isObserve, editor, root, rootMargin, status]); return { activeId }; }; // src/react/hooks/useContentController.ts var useContentController = ({ containerRef, isObserve, rootMargin, topOffset }) => { var _a, _b; const editor = (0, import_react6.useEditorRef)(); const [editorContentRef, setEditorContentRef] = import_react5.default.useState(containerRef); const isScrollRef = import_react5.default.useRef(false); const isScroll = (((_a = editorContentRef.current) == null ? void 0 : _a.scrollHeight) || 0) > (((_b = editorContentRef.current) == null ? void 0 : _b.clientHeight) || 0); isScrollRef.current = isScroll; const scrollContainer = import_react5.default.useMemo(() => { if (typeof window !== "object") return; return isScroll ? editorContentRef.current : window; }, [isScroll]); const [status, setStatus] = import_react5.default.useState(0); const { activeId } = useContentObserver({ editorContentRef, isObserve, isScroll, rootMargin, status }); const [activeContentId, setActiveContentId] = import_react5.default.useState(activeId); const onContentScroll = ({ id, behavior = "instant", el }) => { var _a2, _b2, _c; setActiveContentId(id); if (isScrollRef.current) { (_a2 = editorContentRef.current) == null ? void 0 : _a2.scrollTo({ behavior, top: heightToTop(el, editorContentRef) - topOffset }); } else { const top = heightToTop(el) - topOffset; window.scrollTo({ behavior, top }); } (_c = (_b2 = editor.getApi({ key: "blockSelection" }).blockSelection) == null ? void 0 : _b2.addSelectedRow) == null ? void 0 : _c.call(_b2, id); }; import_react5.default.useEffect(() => { setEditorContentRef(containerRef); }, [containerRef]); import_react5.default.useEffect(() => { setActiveContentId(activeId); }, [activeId]); import_react5.default.useEffect(() => { if (!scrollContainer) return; const scroll = () => { if (isObserve) { setStatus(Date.now()); } }; scrollContainer.addEventListener("scroll", scroll); return () => { scrollContainer.removeEventListener("scroll", scroll); }; }, [isObserve, scrollContainer]); return { activeContentId, onContentScroll }; }; // src/react/hooks/useTocController.ts var import_react8 = __toESM(require("react")); // src/react/hooks/useTocObserver.ts var import_react7 = __toESM(require("react")); var useTocObserver = ({ activeId, isObserve, tocRef }) => { const root = tocRef.current; const [visible, setVisible] = import_react7.default.useState(true); const [offset, setOffset] = import_react7.default.useState(0); const updateOffset = import_react7.default.useCallback( (entries) => { if (!isObserve) return; const [entry] = entries; const { boundingClientRect, intersectionRatio, rootBounds } = entry; if (!rootBounds) return; const halfHeight = ((root == null ? void 0 : root.getBoundingClientRect().height) || 0) / 2; const isAbove = boundingClientRect.top < rootBounds.top; const isBelow = boundingClientRect.bottom > rootBounds.bottom; const isVisible = intersectionRatio === 1; setVisible(isVisible); if (!isVisible) { const offset2 = isAbove ? boundingClientRect.top - rootBounds.top - halfHeight : isBelow ? boundingClientRect.bottom - rootBounds.bottom + halfHeight : 0; setOffset(offset2); } }, [isObserve, root] ); import_react7.default.useEffect(() => { const observer = new IntersectionObserver(updateOffset, { root }); const element = root == null ? void 0 : root.querySelectorAll("#toc_item_active")[0]; if (element) observer.observe(element); return () => { observer.disconnect(); }; }, [root, activeId, updateOffset]); return { offset, visible }; }; // src/react/hooks/useTocController.ts var useTocController = ({ activeId, isObserve, tocRef }) => { const [activeTocId, setActiveTocId] = import_react8.default.useState(""); const { offset, visible } = useTocObserver({ activeId: activeTocId, isObserve, tocRef }); import_react8.default.useEffect(() => { var _a; if (!visible) { const tocItemWrapper = (_a = tocRef.current) == null ? void 0 : _a.querySelector("#toc_wrap"); const top = (tocItemWrapper == null ? void 0 : tocItemWrapper.scrollTop) + offset; tocItemWrapper == null ? void 0 : tocItemWrapper.scrollTo({ behavior: "instant", top }); } }, [visible, offset, tocRef]); import_react8.default.useEffect(() => { setActiveTocId(activeId); }, [activeId]); }; // src/react/hooks/useTocElement.ts var import_react9 = __toESM(require("react")); var import_plate_common5 = require("@udecode/plate-common"); var import_react10 = require("@udecode/plate-common/react"); var useTocElementState = () => { const { editor, getOptions } = (0, import_react10.useEditorPlugin)(TocPlugin); const { isScroll, topOffset } = getOptions(); const headingList = (0, import_react10.useEditorSelector)(getHeadingList, []); const containerRef = (0, import_react10.useScrollRef)(); const onContentScroll = import_react9.default.useCallback( (el, id, behavior = "instant") => { var _a; if (!containerRef.current) return; if (isScroll) { (_a = containerRef.current) == null ? void 0 : _a.scrollTo({ behavior, top: heightToTop(el, containerRef) - topOffset }); } else { const top = heightToTop(el) - topOffset; window.scrollTo({ behavior, top }); } setTimeout(() => { var _a2, _b; (_b = (_a2 = editor.getApi({ key: "blockSelection" }).blockSelection) == null ? void 0 : _a2.addSelectedRow) == null ? void 0 : _b.call(_a2, id); }, 0); }, // eslint-disable-next-line react-hooks/exhaustive-deps [isScroll, topOffset] ); return { editor, headingList, onContentScroll }; }; var useTocElement = ({ editor, onContentScroll }) => { return { props: { onClick: (e, item, behavior) => { e.preventDefault(); const { id, path } = item; const node = (0, import_plate_common5.getNode)(editor, path); if (!node) return; const el = (0, import_react10.toDOMNode)(editor, node); if (!el) return; onContentScroll(el, id, behavior); } } }; }; // src/react/hooks/useTocSideBar.ts var import_react11 = __toESM(require("react")); var import_plate_common6 = require("@udecode/plate-common"); var import_react12 = require("@udecode/plate-common/react"); var useTocSideBarState = ({ open = true, rootMargin = "0px 0px 0px 0px", topOffset = 0 }) => { const { editor } = (0, import_react12.useEditorPlugin)(TocPlugin); const headingList = (0, import_react12.useEditorSelector)(getHeadingList, []); const containerRef = (0, import_react12.useScrollRef)(); const tocRef = import_react11.default.useRef(null); const [mouseInToc, setMouseInToc] = import_react11.default.useState(false); const [isObserve, setIsObserve] = import_react11.default.useState(open); const { activeContentId, onContentScroll } = useContentController({ containerRef, isObserve, rootMargin, topOffset }); useTocController({ activeId: activeContentId, isObserve, tocRef }); return { activeContentId, editor, headingList, mouseInToc, open, setIsObserve, setMouseInToc, tocRef, onContentScroll }; }; var useTocSideBar = ({ editor, mouseInToc, open, setIsObserve, setMouseInToc, tocRef, onContentScroll }) => { import_react11.default.useEffect(() => { if (mouseInToc) { setIsObserve(false); } else { setIsObserve(true); } }, [mouseInToc]); const onContentClick = import_react11.default.useCallback( (e, item, behavior) => { e.preventDefault(); const { id, path } = item; const node = (0, import_plate_common6.getNode)(editor, path); if (!node) return; const el = (0, import_react12.toDOMNode)(editor, node); if (!el) return; onContentScroll({ id, behavior, el }); }, [editor, onContentScroll] ); return { navProps: { ref: tocRef, onMouseEnter: () => { !mouseInToc && open && setMouseInToc(true); }, onMouseLeave: (e) => { if (open) { const isIn = checkIn(e); isIn !== mouseInToc && setMouseInToc(isIn); } } }, onContentClick }; }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { HeadingPlugin, TocPlugin, checkIn, heightToTop, useContentController, useContentObserver, useTocController, useTocElement, useTocElementState, useTocObserver, useTocSideBar, useTocSideBarState }); //# sourceMappingURL=index.js.map