UNPKG

@pageboard/pagecut

Version:
348 lines (320 loc) 9.84 kB
import BlocksView from './blocks-view.js'; export default class Blocks extends BlocksView { mutate(node, data) { let nodes = [], block, id; if (typeof node != "string") { id = node.getAttribute('block-id'); } else { id = node; } if (id) { block = this.get(id); if (!block) throw new Error("mutate node but block not found: " + id); } if (!id) { block = { type: node.getAttribute('block-type'), data: data }; nodes = [node]; } else { block.data = { ...block.data, ...data }; nodes = this.domQuery(id, { all: true }); } const view = this.view; const tr = view.state.tr; for (const node of nodes) { view.utils.refreshTr(tr, node, block); } view.dispatch(tr); } setStandalone(block, val) { if (val == block.standalone) return; if (!val) { // force new id for this block and its descendants by id-plugin const copy = this.copy(block); copy.focused = block.focused; block = copy; } block.standalone = val; const nodes = this.domQuery(block.id, { all: true }); const tr = this.view.state.tr; nodes.forEach(node => { this.view.utils.refreshTr(tr, node, block); }); this.view.dispatch(tr); } create(type) { return this.view.element(type).create(); } fromAttrs(attrs) { const block = {}; for (const [name, att] of Object.entries(attrs)) { if (!name.startsWith("_")) { block[name] = att; } } if (attrs.data) block.data = JSON.parse(attrs.data); if (attrs.expr) block.expr = JSON.parse(attrs.expr); if (attrs.lock) block.lock = JSON.parse(attrs.lock); if (attrs.content) block.content = JSON.parse(attrs.content); if (attrs.standalone == "true") block.standalone = true; else delete block.standalone; return block; } toAttrs(block) { const attrs = {}; if (!block) return attrs; if (block.type == null) console.warn("Missing type", block); if (block.id != null) attrs.id = block.id; if (block.type != null) attrs.type = block.type; if (block.data) attrs.data = JSON.stringify(block.data); if (block.expr) attrs.expr = JSON.stringify(block.expr); if (block.lock) attrs.lock = JSON.stringify(block.lock); if (block.focused) attrs.focused = block.focused; if (block.standalone) attrs.standalone = "true"; const el = this.view.element(block.type); if (el.contents.attrs.length) { const obj = {}; for (const item of el.contents.attrs) { obj[item.id] = block.content[item.id]; } attrs.content = JSON.stringify(obj); } return attrs; } serializeTo(parent, el, ancestor, blocks, parentDef = {}) { if (!el || typeof el == "string") el = this.view.element(el.name ?? parent.type); /* 1. parent is the direct parent 2. ancestor is the parent to relate to 3. virtual block does not relate to ancestor 4. standalone is ancestor to all its descendants Result: a virtual block need to be descendant of a standalone block, or else serialization cannot tell to which ancestor it must relate to (especially for new blocks - existing blocks are already related) */ if (parent.virtual && !parent.standalone) { throw new Error("A virtual block must be standalone"); } if (ancestor?.blocks && parent.id && parent.id != ancestor.id) { ancestor.blocks[parent.id] = parent; } if (parent.standalone) { ancestor = parent; } if (parent == ancestor && !parent.blocks) { parent.blocks = {}; } let contents = parent.content; if (!contents) contents = parent.content = {}; el.contents.each({ content: contents }, (content, def) => { if (!content) return; if (typeof content == "string") { el.contents.set(parent, def.id, content); return; } if (parent.standalone && Array.isArray(content)) { // this is set by nodeView.update content = content[0]; } content = content.cloneNode(true); let node, div, id, type, block, parentNode, blockEl; const list = []; while ((node = content.querySelector('[block-type],[block-id]'))) { type = node.getAttribute('block-type'); parentNode = node.parentNode; blockEl = this.view.element(type); id = node.getAttribute('block-id'); if (id) { div = content.ownerDocument.createElement(node.nodeName); parentNode.replaceChild(div, node); block = this.store[id]; if (!block) { // parentNode.removeChild(node); // console.warn("block", type, "not found", id, "while serializing"); } else { let copy = blocks[id]; if (!copy) { copy = blocks[id] = this.copy(block); } block = copy; if (blockEl) { if (blockEl.unmount) { blockEl.unmount(block, node, this.view); } Blocks.reassignContent(block, blockEl, node); // not sure why this is necessary } else { // these are not mounted, ignore them } } } else { block = { type }; if (blockEl.parse) { block.data = blockEl.parse(node); } type = null; div = node; // inplace blocks are no longer supported div.removeAttribute('block-data'); div.removeAttribute('block-text'); div.removeAttribute('spellcheck'); div.removeAttribute('block-type'); div.removeAttribute('block-focused'); } if (def.virtual) { block.virtual = parent.id; } if (!id || !block || this.serializeTo(block, blockEl, ancestor, blocks, def)) { if (id && !block) { parent.blocks[id] = { id: id, ignore: true }; } if (block?.type == type) type = null; list.push({ node: div, id, type }); } else { parentNode.removeChild(div); if (id) delete ancestor.blocks[id]; } } for (const item of list) { if (item.id) item.node.setAttribute('block-id', item.id); if (item.type) { // can override block.type item.node.setAttribute('block-type', item.type); } } for (const hack of content.querySelectorAll('.ProseMirror-trailingBreak,.ProseMirror-separator')) hack.remove(); el.contents.set(parent, def.id, this.view.utils.serializeHTML(content, true)); }); if (Object.keys(contents).length == 0) delete parent.content; if (el.inline && !el.leaf) { if (!el.contents.get(parent)) return; // TODO find the meaning of this } return parent; } static reassignContent(block, elt, dom) { if (!block.content && !elt.leaf) { if (!block.standalone) { console.warn("block without content", block, dom); } return; } elt.contents.each(block, (content, def) => { if (!def.id || def.id == dom.getAttribute('block-content') || elt.inline) { elt.contents.set(block, def.id, dom); } else { const node = dom.querySelector(`[block-content="${def.id}"]`); if (node?.closest('[block-id]') == dom) { elt.contents.set(block, def.id, node); } else { console.error(`block.content[${def.id}] not found`, block, dom); } } }); for (const attr of elt.contents.attrs ?? []) { const name = attr.name ?? attr.id; if (dom.hasAttribute(name)) { if (!block.content) block.content = {}; block.content[attr.id] = dom.getAttribute(name); } } } to() { const view = this.view; const id = view.dom.getAttribute('block-id'); const contentName = view.dom.getAttribute("block-content"); const copies = {}; const copy = this.copy(this.store[id]); copies[id] = copy; const el = view.element(copy.type); if (contentName) el.contents.set(copy, contentName, view.utils.getDom()); return this.serializeTo(copy, view.dom.getAttribute('block-type'), null, copies); } clear(id) { if (id === undefined) { this.store = {}; } else if (id == null || id == false) { console.warn('id.clear expects undefined or something not null'); } else if (!this.store[id]) { console.warn('id.clear expects blocks to contain id', id); } else { delete this.store[id]; } } get(id) { if (id == null) return; if (typeof id != "string" && id.getAttribute) { id = id.getAttribute('block-id'); if (!id) throw new Error("Node without block-id attribute"); } return this.store[id]; } find(types) { return Object.values(this.store) .filter(block => types.includes(block.type)); } set(data) { if (!Array.isArray(data)) data = [data]; for (let i = 0, cur; i < data.length; i++) { cur = data[i]; if (cur.id == null) { cur.id = this.genId(); } this.store[cur.id] = cur; } return data; } genId() { const bytes = new Uint8Array(10); crypto.getRandomValues(bytes); return btoa(String.fromCharCode(...bytes)).slice(0, 14) .replace(/=+$/, '') .replaceAll(/[/+]/g, () => { return String.fromCharCode(Math.round(Math.random() * 25) + 97); }); } domQuery(id, opts) { if (!opts) opts = {}; const rootDom = this.view.dom; let sel; if (id) { sel = `[block-id="${id}"]`; } else { sel = ''; } if (opts.focused) { if (typeof opts.focused == "string") { sel += `[block-focused="${opts.focused}"]`; } else { sel += '[block-focused]'; } } else if (!id) { throw new Error("domQuery expects at least id or opts.focused to be set " + id); } const nodes = Array.from(rootDom.querySelectorAll(sel)); if (rootDom.getAttribute('block-id') == id) { // root is always focused, but another node having actual focus and representing // the current page could take precedence nodes.push(rootDom); } if (opts.all) return nodes; if (nodes.length == 0) return; const node = nodes[0]; if (opts.content) { if (node.getAttribute('block-content') == opts.content) { return node; } else { return node.querySelector(`[block-content="${opts.content}"]`); } } else { return node; } } domSelect(node) { const { view } = this; view.focus(); const { tr } = view.state; const sel = view.utils.selectDomTr(tr, node); view.dispatch(tr.setSelection(sel)); } }