@pageboard/pagecut
Version:
Extensible web content editor
187 lines (172 loc) • 5.19 kB
JavaScript
import str2dom from './str2dom.js';
function htmlToFrag(str, {doc, ns}) {
try {
return str2dom(str, { doc, ns, frag: true });
} catch(ex) {
console.error(ex);
}
}
export default class BlocksView {
initial = {};
constructor(view, opts = {}) {
this.view = view;
this.store = opts.store ?? {};
if (opts.genId) this.genId = opts.genId;
}
render(el, block, opts = {}) {
const { scope = {} } = opts;
if (!scope.$doc) scope.$doc = this.view.doc;
if (!scope.$elements) scope.$elements = this.view.elements;
if (!scope.$element) scope.$element = el;
block = { ...block };
block.data = BlocksView.fill(el, block.data);
const dom = el.render(block, scope);
if (dom && opts.merge !== false) this.merge(el, dom, block);
return dom;
}
mount(el, block) {
el.contents.normalize(block);
const copy = this.copy(block);
const doc = this.view.doc;
el.contents.each(block, (content, def) => {
if (!(content instanceof Node)) {
el.contents.set(copy, def.id, htmlToFrag(content, { doc, ns: el.ns }));
}
});
return copy;
}
static fill(schema, data) {
if (!schema.properties) return data;
// sometimes data can carry an old odd value
if (data === undefined || typeof data == "string") data = {};
else data = { ...data };
for (const [key, prop] of Object.entries(schema.properties)) {
if (prop.default !== undefined && data[key] === undefined) {
data[key] = prop.default;
}
if (prop.properties) data[key] = BlocksView.fill(prop, data[key]);
}
return data;
}
copy(block) {
const copy = { ...block };
copy.data = { ...block.data };
if (block.expr) copy.expr = { ...block.expr };
if (block.lock) copy.lock = block.lock.slice();
if (block.content) copy.content = { ...block.content };
delete copy.focused;
return copy;
}
merge(el, dom, block) {
if (dom.nodeType != Node.ELEMENT_NODE) return;
el.contents.each(block, (content, def) => {
if (!content) return;
let node;
if (!def.id || def.id == dom.getAttribute('block-content') || el.inline) {
node = dom;
} else {
node = dom.querySelector(`[block-content="${def.id}"]`);
}
if (!node) return;
if (node.nodeName == "TEMPLATE") node = node.content;
if (typeof content == "string") {
content = node.ownerDocument.createTextNode(content);
} else if (content.nodeType == Node.DOCUMENT_FRAGMENT_NODE) {
content = node.ownerDocument.importNode(content, true);
} else if (content.nodeType == Node.ELEMENT_NODE) {
console.warn("already merged", content == node);
return;
} else {
console.warn("cannot merge content", content);
return;
}
node.textContent = "";
node.appendChild(content);
});
}
from(block, blocks, opts) {
this.rootId = block.id;
if (!blocks) blocks = {};
return this.renderFrom(block, blocks, this.store, opts);
}
renderFrom(block, blocks = {}, store, opts = {}) {
const { view } = this;
const type = opts.element?.name || opts.type || block.type;
const el = view.element(type);
if (block.id) {
this.initial[block.id] = block;
}
if (!el) {
console.warn("Unknown block type", block.id, type);
return;
}
block = this.mount(el, block);
if (!block) return;
if (block.id) {
// overwrite can happen when (re)loading virtual blocks
const oldBlock = store[block.id];
if (!oldBlock || oldBlock.type == block.type) store[block.id] = block;
}
let fragment;
try {
fragment = view.render(block, opts);
} catch (ex) {
console.error(ex);
}
if (block.children) {
for (const child of block.children) {
if (!blocks[child.id]) {
blocks[child.id] = child;
} else {
console.warn("child already exists", child.id, child.type, "in", block.id, block.type);
}
}
delete block.children;
}
// if (block.blocks) {
// Object.assign(blocks, block.blocks);
// }
if (!fragment || !fragment.querySelectorAll) return;
const fragments = [fragment.nodeName == "BODY" ? fragment.parentNode : fragment];
for (const node of fragment.querySelectorAll('template')) {
fragments.push(node.content);
}
for (const frag of fragments) {
if (opts.strip) for (const node of frag.querySelectorAll('[block-data]')) {
node.removeAttribute('block-data');
}
for (const node of frag.querySelectorAll('[block-id]')) {
const id = node.getAttribute('block-id');
if (id === block.id) continue;
const type = node.getAttribute('block-type');
const parent = node.parentNode;
const child = blocks[id];
if (!child) {
if (store[id]) {
continue;
}
console.warn("missing block for", parent.nodeName, '>', node.nodeName, id);
parent.removeChild(node);
continue;
}
const frag = this.renderFrom(child, blocks, store, {
...opts,
type: type,
element: null
});
if (!frag) {
parent.removeChild(node);
continue;
}
if (frag.attributes) {
for (const att of node.attributes) {
if (opts.strip && att.name == "block-id") continue;
if (!frag.hasAttribute(att.name)) frag.setAttribute(att.name, att.value);
}
}
parent.replaceChild(frag, node);
}
}
return fragment;
}
}