a list (parent #d “${parentDTrim}”). Reload the page if this persists.`,
+ );
+ return;
+ }
+ const parentFs = this.findFieldsetByDTag(parentDTrim);
+ const anchor = parentFs ? lastDescendantFieldsetAmongNodes(parentFs, this.nodesTarget) : null;
+ if (anchor?.parentElement === this.nodesTarget) {
+ anchor.insertAdjacentElement('afterend', fs);
+ } else {
+ this.nodesTarget.appendChild(fs);
+ }
+ this.nodeBaseline.set(fs, snapshotFromElement(fs));
+ }
+
+ /**
+ * @param {HTMLElement} el
+ */
+ applySlugChange(el) {
+ const ownerHex = (this.ownerHexValue || '').toLowerCase().trim();
+ const parentD = (el.dataset.magParentD || '').trim();
+ const placed = (el.dataset.placedSlug || '').trim();
+ const next = readDTag(el).trim();
+ if (placed === next) {
+ return;
+ }
+ if (parentD === '' || ownerHex.length !== 64) {
+ return;
+ }
+ const parentFs = this.findFieldsetByDTag(parentD);
+ if (!parentFs) {
+ return;
+ }
+ if (placed !== '') {
+ this.remove30040ChildLine(parentFs, placed, ownerHex);
+ }
+ if (next !== '') {
+ if (!this.append30040ChildLine(parentD, next, ownerHex)) {
+ if (placed !== '') {
+ this.append30040ChildLine(parentD, placed, ownerHex);
+ }
+ this.setStatus(
+ `Could not add #d “${next}” to the parent’s a list (parent “${parentD}”).`,
+ );
+ return;
+ }
+ }
+ el.dataset.placedSlug = next;
+ }
+
+ /**
+ * @param {HTMLElement} el Fieldset (not root, not new-node).
+ */
+ commitExistingDTagRename(el) {
+ const ownerHex = (this.ownerHexValue || '').toLowerCase().trim();
+ if (ownerHex.length !== 64) {
+ return;
+ }
+ const dIn = el.querySelector('[data-magazine-hierarchy-editor-target="dTag"]');
+ const oldD = (el.dataset.magPlacedD || '').trim();
+ const next = readDTag(el).trim();
+ if (oldD === next) {
+ return;
+ }
+ if (next === '') {
+ this.setStatus('#d cannot be empty.');
+ if (dIn instanceof HTMLInputElement) {
+ dIn.value = oldD;
+ }
+ return;
+ }
+ if (!DTAG_PATTERN.test(next)) {
+ this.setStatus(`Invalid #d "${next}" (use letters, digits, underscores, and hyphens only).`);
+ if (dIn instanceof HTMLInputElement) {
+ dIn.value = oldD;
+ }
+ return;
+ }
+ const rootD = (this.rootDTagValue || '').trim();
+ if (next === rootD) {
+ this.setStatus('A category #d cannot equal the root magazine identifier.');
+ if (dIn instanceof HTMLInputElement) {
+ dIn.value = oldD;
+ }
+ return;
+ }
+ for (const other of queryEditorNodeFieldsets(this.nodesTarget)) {
+ if (other === el) {
+ continue;
+ }
+ if (readDTag(other) === next) {
+ this.setStatus(`Duplicate #d "${next}".`);
+ if (dIn instanceof HTMLInputElement) {
+ dIn.value = oldD;
+ }
+ return;
+ }
+ }
+ const parentD = (el.dataset.magParentD || '').trim();
+ if (parentD === '') {
+ return;
+ }
+ const parentFs = this.findFieldsetByDTag(parentD);
+ if (!parentFs) {
+ return;
+ }
+ const list = parentFs.querySelector('[data-mag-a-list]');
+ if (!list) {
+ return;
+ }
+ if (!rewrite30040ChildLineInList(list, ownerHex, oldD, next)) {
+ this.setStatus(`Could not find parent link for old #d "${oldD}"; reload the page.`);
+ if (dIn instanceof HTMLInputElement) {
+ dIn.value = oldD;
+ }
+ return;
+ }
+ el.dataset.magPlacedD = next;
+ this.setStatus('');
+ }
+
+ /**
+ * @param {string} d
+ * @returns {HTMLElement | null}
+ */
+ findFieldsetByDTag(d) {
+ const want = d.trim();
+ if (!this.hasNodesTarget) {
+ return null;
+ }
+ for (const el of queryEditorNodeFieldsets(this.nodesTarget)) {
+ if (readDTag(el).trim() === want) {
+ return el;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param {string} parentD Parent index #d (root or category).
+ * @param {string} childD Nested kind-30040 child #d.
+ * @param {string} ownerHex
+ */
+ /**
+ * @returns {boolean} false if the parent fieldset or a-list could not be updated
+ */
+ append30040ChildLine(parentD, childD, ownerHex) {
+ const parentFs = this.findFieldsetByDTag(parentD.trim());
+ if (!parentFs) {
+ return false;
+ }
+ const list = parentFs.querySelector('[data-mag-a-list]');
+ const addBtn = list?.querySelector('[data-mag-a-add]');
+ if (!list || !addBtn || !this.hasARowTemplateTarget) {
+ return false;
+ }
+ const oh = ownerHex.toLowerCase();
+ const cd = childD.trim();
+ const want = `${KIND_PUBLICATION_INDEX}:${oh}:${cd}`;
+ for (const line of collectALineValuesFromList(list)) {
+ if (lineMatches30040Child(line, oh, cd)) {
+ return true;
+ }
+ }
+ const frag = this.aRowTemplateTarget.content.cloneNode(true);
+ const row = frag.querySelector('[data-mag-a-row]');
+ const inp = row?.querySelector('[data-mag-a-line]');
+ if (!row || !(inp instanceof HTMLInputElement)) {
+ return false;
+ }
+ inp.value = want;
+ list.insertBefore(row, addBtn);
+ return true;
+ }
+
+ /**
+ * @param {HTMLElement} parentFs
+ * @param {string} childD
+ * @param {string} ownerHex
+ */
+ remove30040ChildLine(parentFs, childD, ownerHex) {
+ const list = parentFs.querySelector('[data-mag-a-list]');
+ if (!list) {
+ return;
+ }
+ const oh = ownerHex.toLowerCase();
+ const cd = childD.trim();
+ for (const row of list.querySelectorAll('[data-mag-a-row]')) {
+ const inp = row.querySelector('[data-mag-a-line]');
+ if (!(inp instanceof HTMLInputElement)) {
+ continue;
+ }
+ if (lineMatches30040Child(inp.value, oh, cd)) {
+ row.remove();
+
+ return;
+ }
+ }
+ }
+
+ /**
+ * @param {string} rootD
+ * @returns {string | null}
+ */
+ validateAllNodes(rootD) {
+ const seen = new Set();
+ if (!this.hasNodesTarget) {
+ return 'Editor is missing the nodes list.';
+ }
+ for (const el of queryEditorNodeFieldsets(this.nodesTarget)) {
+ const d = readDTag(el).trim();
+ if (d === '') {
+ return 'Every magazine index must have a non-empty #d identifier.';
+ }
+ if (isRootFieldset(el)) {
+ if (d !== rootD) {
+ return 'Root fieldset #d does not match the configured magazine root.';
+ }
+ } else {
+ if (!DTAG_PATTERN.test(d)) {
+ return `Invalid #d "${d}" (use letters, digits, underscores, and hyphens only).`;
+ }
+ if (d === rootD) {
+ return 'A category #d cannot equal the root magazine identifier.';
+ }
+ }
+ if (seen.has(d)) {
+ return `Duplicate #d identifier: ${d}.`;
+ }
+ seen.add(d);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param {HTMLElement} el
+ */
+ finalizeNewNodeFieldset(el) {
+ const d = readDTag(el).trim();
+ const depth = parseInt(el.dataset.magDepth || '1', 10);
+ el.querySelector('[data-new-node-d-row]')?.removeAttribute('data-new-node-d-row');
+
+ const legend = el.querySelector('legend.magazine-editor__legend');
+ if (legend) {
+ const kindLabel = depth > 1 ? 'Subcategory' : 'Category';
+ legend.replaceChildren();
+ legend.className = 'magazine-editor__legend magazine-editor__legend--row';
+ const main = document.createElement('span');
+ main.className = 'magazine-editor__legend-main';
+ main.append(`${kindLabel}`);
+ legend.appendChild(main);
+ const actions = document.createElement('span');
+ actions.className = 'magazine-editor__legend-actions';
+ const btnSub = document.createElement('button');
+ btnSub.type = 'button';
+ btnSub.className = 'btn btn-secondary btn-sm magazine-editor__add-sub';
+ btnSub.setAttribute('data-mag-editor-cmd', 'add-subcategory');
+ btnSub.textContent = 'Add subcategory';
+ actions.appendChild(btnSub);
+ const btnRm = document.createElement('button');
+ btnRm.type = 'button';
+ btnRm.className = 'btn btn-secondary btn-sm magazine-editor__remove-node';
+ btnRm.setAttribute('data-mag-editor-cmd', 'remove-category');
+ btnRm.setAttribute('aria-label', 'Remove category');
+ btnRm.textContent = 'Remove';
+ actions.appendChild(btnRm);
+ legend.appendChild(actions);
+ }
+
+ el.dataset.magPlacedD = d;
+ el.classList.remove('magazine-editor__node--new');
+ el.removeAttribute('data-is-new-node');
+ el.removeAttribute('data-placed-slug');
+ }
+
async _publish() {
this.setStatus('');
if (!this.hasNip07()) {
@@ -51,12 +658,28 @@ export default class extends Controller {
return;
}
- const nodes = this.nodeTargets;
+ if (!this.hasNodesTarget) {
+ this.setStatus('Editor is missing the nodes list.');
+ return;
+ }
+ const nodes = queryEditorNodeFieldsets(this.nodesTarget);
if (nodes.length === 0) {
this.setStatus('Nothing to publish.');
return;
}
+ for (const el of nodes) {
+ if (el.dataset.isNewNode === '1') {
+ this.applySlugChange(el);
+ }
+ }
+
+ const graphErr = this.validateAllNodes(rootD);
+ if (graphErr !== null) {
+ this.setStatus(graphErr);
+ return;
+ }
+
const dirty = nodes.filter((el) => this.isNodeDirty(el));
if (dirty.length === 0) {
this.setStatus('No changes to publish.');
@@ -92,7 +715,7 @@ export default class extends Controller {
const title = el.querySelector('[data-magazine-hierarchy-editor-target="title"]')?.value ?? '';
const summary = el.querySelector('[data-magazine-hierarchy-editor-target="summary"]')?.value ?? '';
const content = el.querySelector('[data-magazine-hierarchy-editor-target="content"]')?.value ?? '';
- const aText = el.querySelector('[data-magazine-hierarchy-editor-target="aLines"]')?.value ?? '';
+ const aText = readJoinedALinesFromNode(el);
const preservedRaw = el.querySelector('[data-magazine-hierarchy-editor-target="preservedJson"]')?.value ?? '[]';
let preserved;
@@ -156,18 +779,25 @@ export default class extends Controller {
}
const n = Number(data.published);
for (const el of ordered) {
+ if (el.dataset.isNewNode === '1') {
+ this.finalizeNewNodeFieldset(el);
+ }
this.nodeBaseline.set(el, snapshotFromElement(el));
}
this.setStatus(Number.isFinite(n) ? `Published and stored ${n} index event(s).` : 'Published.');
}
isNodeDirty(el) {
+ if (el.dataset.isNewNode === '1') {
+ return true;
+ }
const cur = snapshotFromElement(el);
const base = this.nodeBaseline.get(el);
if (!base) {
return true;
}
return (
+ cur.dTag !== base.dTag ||
cur.title !== base.title ||
cur.summary !== base.summary ||
cur.content !== base.content ||
@@ -187,10 +817,12 @@ export default class extends Controller {
async expandPublishSet(rootD, dirtyFieldsets, ownerHex) {
/** @type {Map