@ -9,23 +9,120 @@ const KIND_LONGFORM_DRAFT = 30024;
@@ -9,23 +9,120 @@ const KIND_LONGFORM_DRAFT = 30024;
* Only signs nodes that differ from the page load snapshot , plus any other index required for
* graph closure ( root + every nested kind - 30040 ` a ` reachable from those events ) .
* /
export default class extends Controller {
static targets = [ 'status' , 'node' ] ;
const DTAG _PATTERN = /^[a-zA-Z0-9_-]+$/ ;
export default class MagazineHierarchyEditorController extends Controller {
static targets = [ 'status' , 'node' , 'nodes' , 'newNodeTemplate' , 'aRowTemplate' ] ;
static values = {
publishUrl : String ,
csrf : String ,
ownerHex : String ,
rootDTag : String ,
defaultPreservedJson : { type : String , default : '[]' } ,
/** Site-owned NIP-56 `client` name; foreign `client` rows are dropped before signing. */
clientTag : { type : String , default : '' } ,
} ;
connect ( ) {
this . nodeBaseline = new WeakMap ( ) ;
this . captureBaselines ( ) ;
/ * *
* Clicks : ` document ` capture + ` this.element.contains(target) ` so we still run when bubble
* never reaches the panel ( e . g . ` stopPropagation ` from another listener ) or fieldset / legend
* hit - testing is odd .
* /
this . _onDocClickCapture = this . _onDocClickCapture . bind ( this ) ;
this . _onPanelFocusOut = this . _onPanelFocusOut . bind ( this ) ;
document . addEventListener ( 'click' , this . _onDocClickCapture , true ) ;
this . element . addEventListener ( 'focusout' , this . _onPanelFocusOut ) ;
}
disconnect ( ) {
document . removeEventListener ( 'click' , this . _onDocClickCapture , true ) ;
this . element . removeEventListener ( 'focusout' , this . _onPanelFocusOut ) ;
}
/ * *
* @ param { MouseEvent } ev
* /
_onDocClickCapture ( ev ) {
if ( ! ( ev . target instanceof Node ) ) {
return ;
}
if ( ! this . element . contains ( ev . target ) ) {
return ;
}
this . _onPanelClick ( ev ) ;
}
/ * *
* @ param { MouseEvent } ev
* /
_onPanelClick ( ev ) {
if ( ! ( ev . target instanceof Element ) ) {
return ;
}
const t = ev . target ;
if ( t . closest ( '[data-mag-editor-cmd="publish"]' ) ) {
void this . _publish ( ) ;
return ;
}
if ( t . closest ( '[data-mag-editor-cmd="add-top-category"]' ) ) {
this . addTopLevelCategory ( ) ;
return ;
}
const addSubCmd = t . closest ( '[data-mag-editor-cmd="add-subcategory"]' ) ;
if ( addSubCmd instanceof HTMLElement ) {
this . addSubcategory ( ev , addSubCmd ) ;
return ;
}
const rmCatCmd = t . closest ( '[data-mag-editor-cmd="remove-category"]' ) ;
if ( rmCatCmd instanceof HTMLElement ) {
this . removeCategory ( ev , rmCatCmd ) ;
return ;
}
const addArticle = t . closest ( '[data-mag-a-add]' ) ;
if ( addArticle instanceof HTMLElement ) {
this . addALine ( ev , addArticle ) ;
return ;
}
const rmArticle = t . closest ( '.magazine-editor__a-remove-icon' ) ;
if ( rmArticle instanceof HTMLElement ) {
this . removeALine ( ev , rmArticle ) ;
return ;
}
const sub = t . closest ( '.magazine-editor__add-sub' ) ;
if ( sub instanceof HTMLElement ) {
this . addSubcategory ( ev , sub ) ;
return ;
}
const rmCat = t . closest ( '.magazine-editor__remove-node' ) ;
if ( rmCat instanceof HTMLElement ) {
this . removeCategory ( ev , rmCat ) ;
return ;
}
}
/ * *
* @ param { FocusEvent } ev
* /
_onPanelFocusOut ( ev ) {
const inp = ev . target ;
if ( ! ( inp instanceof HTMLInputElement ) ) {
return ;
}
if ( ! inp . matches ( '[data-magazine-hierarchy-editor-target="dTag"]' ) ) {
return ;
}
this . commitDTag ( ev ) ;
}
captureBaselines ( ) {
for ( const el of this . nodeTargets ) {
if ( ! this . hasNodesTarget ) {
return ;
}
for ( const el of queryEditorNodeFieldsets ( this . nodesTarget ) ) {
this . nodeBaseline . set ( el , snapshotFromElement ( el ) ) ;
}
}
@ -34,6 +131,516 @@ export default class extends Controller {
@@ -34,6 +131,516 @@ export default class extends Controller {
void this . _publish ( ) ;
}
/ * *
* @ param { Event } event
* @ param { HTMLElement } [ triggerEl ]
* /
addALine ( event , triggerEl ) {
const trigger =
triggerEl instanceof HTMLElement
? triggerEl
: event . currentTarget instanceof HTMLElement && event . currentTarget . hasAttribute ( 'data-mag-a-add' )
? event . currentTarget
: event . target instanceof Element
? event . target . closest ( '[data-mag-a-add]' )
: null ;
if ( ! ( trigger instanceof HTMLElement ) ) {
return ;
}
const list = trigger . closest ( '[data-mag-a-list]' ) ;
if ( ! list || ! this . hasARowTemplateTarget ) {
return ;
}
const addBtn = list . querySelector ( '[data-mag-a-add]' ) ;
if ( ! addBtn ) {
return ;
}
const frag = this . aRowTemplateTarget . content . cloneNode ( true ) ;
const row = frag . querySelector ( '[data-mag-a-row]' ) ;
if ( ! row ) {
return ;
}
list . insertBefore ( row , addBtn ) ;
}
/ * *
* @ param { Event } event
* @ param { HTMLElement } [ triggerEl ]
* /
removeALine ( event , triggerEl ) {
const trigger =
triggerEl instanceof HTMLElement
? triggerEl
: event . currentTarget instanceof HTMLElement && event . currentTarget . classList . contains ( 'magazine-editor__a-remove-icon' )
? event . currentTarget
: event . target instanceof Element
? event . target . closest ( '.magazine-editor__a-remove-icon' )
: null ;
if ( ! ( trigger instanceof HTMLElement ) ) {
return ;
}
const row = trigger . closest ( '[data-mag-a-row]' ) ;
row ? . remove ( ) ;
}
addTopLevelCategory ( ) {
this . setStatus ( '' ) ;
const rootD = ( this . rootDTagValue || '' ) . trim ( ) ;
if ( rootD === '' ) {
this . setStatus ( 'Missing root index #d.' ) ;
return ;
}
this . insertNewNode ( rootD , 1 ) ;
}
/ * *
* @ param { Event } event
* @ param { HTMLElement } [ triggerEl ]
* /
addSubcategory ( event , triggerEl ) {
this . setStatus ( '' ) ;
const btn =
triggerEl instanceof HTMLElement
? triggerEl
: event . currentTarget instanceof HTMLElement && event . currentTarget . classList . contains ( 'magazine-editor__add-sub' )
? event . currentTarget
: event . target instanceof Element
? event . target . closest ( '.magazine-editor__add-sub' )
: null ;
if ( ! ( btn instanceof HTMLElement ) ) {
return ;
}
const parentEl = btn . closest ( '[data-magazine-hierarchy-editor-target="node"]' ) ;
if ( ! parentEl ) {
return ;
}
const rootD = ( this . rootDTagValue || '' ) . trim ( ) ;
const placed = ( parentEl . dataset . magPlacedD || '' ) . trim ( ) ;
const parentD = ( placed !== '' ? placed : readDTag ( parentEl ) ) . trim ( ) ;
if ( parentD === '' || parentD === rootD ) {
this . setStatus ( 'Use “Add top-level category” for indices linked from the root.' ) ;
return ;
}
const depth = parseInt ( parentEl . dataset . magDepth || '1' , 10 ) ;
this . insertNewNode ( parentD , depth + 1 ) ;
}
/ * *
* Blur on Index # d ( new or existing category / subcategory ) .
* @ param { Event } event
* /
commitDTag ( event ) {
const t =
event . target instanceof HTMLElement
? event . target
: event . currentTarget instanceof HTMLElement
? event . currentTarget
: null ;
if ( ! ( t instanceof HTMLElement ) ) {
return ;
}
const el = t . closest ( '[data-magazine-hierarchy-editor-target="node"]' ) ;
if ( ! el || isRootFieldset ( el ) ) {
return ;
}
if ( el . dataset . isNewNode === '1' ) {
this . applySlugChange ( el ) ;
} else {
this . commitExistingDTagRename ( el ) ;
}
}
/ * *
* @ param { Event } event
* @ param { HTMLElement } [ triggerEl ]
* /
removeCategory ( event , triggerEl ) {
this . setStatus ( '' ) ;
const btn =
triggerEl instanceof HTMLElement
? triggerEl
: event . currentTarget instanceof HTMLElement && event . currentTarget . classList . contains ( 'magazine-editor__remove-node' )
? event . currentTarget
: event . target instanceof Element
? event . target . closest ( '.magazine-editor__remove-node' )
: null ;
if ( ! ( btn instanceof HTMLElement ) ) {
return ;
}
const fs = btn . closest ( '[data-magazine-hierarchy-editor-target="node"]' ) ;
if ( ! fs || isRootFieldset ( fs ) ) {
return ;
}
const ownerHex = ( this . ownerHexValue || '' ) . toLowerCase ( ) . trim ( ) ;
if ( ownerHex . length !== 64 ) {
this . setStatus ( 'Missing owner pubkey.' ) ;
return ;
}
if ( fs . dataset . isNewNode === '1' ) {
const parentD = ( fs . dataset . magParentD || '' ) . trim ( ) ;
const placed = ( fs . dataset . placedSlug || '' ) . trim ( ) ;
if ( parentD !== '' && placed !== '' ) {
const parentFs = this . findFieldsetByDTag ( parentD ) ;
if ( parentFs ) {
this . remove30040ChildLine ( parentFs , placed , ownerHex ) ;
}
}
fs . remove ( ) ;
return ;
}
const sub = collectSubtreeFieldsets ( fs , this . nodesTarget ) ;
sub . sort ( ( a , b ) => parseInt ( b . dataset . magDepth || '0' , 10 ) - parseInt ( a . dataset . magDepth || '0' , 10 ) ) ;
for ( const n of sub ) {
const p = ( n . dataset . magParentD || '' ) . trim ( ) ;
const d = readDTag ( n ) ;
if ( p === '' || d === '' ) {
continue ;
}
const pFs = this . findFieldsetByDTag ( p ) ;
if ( pFs ) {
this . remove30040ChildLine ( pFs , d , ownerHex ) ;
}
}
for ( const n of sub ) {
n . remove ( ) ;
}
}
insertNewNode ( parentD , depth ) {
if ( ! this . hasNewNodeTemplateTarget || ! this . hasNodesTarget ) {
this . setStatus ( 'Editor template is incomplete; reload the page.' ) ;
return ;
}
const ownerHex = ( this . ownerHexValue || '' ) . toLowerCase ( ) . trim ( ) ;
if ( ownerHex . length !== 64 ) {
this . setStatus ( 'Missing owner pubkey.' ) ;
return ;
}
const parentDTrim = parentD . trim ( ) ;
if ( parentDTrim === '' ) {
return ;
}
const frag = this . newNodeTemplateTarget . content . cloneNode ( true ) ;
const fs = frag . querySelector ( 'fieldset[data-magazine-hierarchy-editor-target="node"]' ) ;
if ( ! fs ) {
this . setStatus ( 'Could not clone new node template.' ) ;
return ;
}
fs . dataset . magParentD = parentDTrim ;
fs . dataset . isNewNode = '1' ;
fs . dataset . magDepth = String ( depth ) ;
const slug = ` mag-new- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
fs . dataset . placedSlug = slug ;
fs . style . setProperty ( '--mag-node-depth' , String ( depth ) ) ;
fs . style . marginLeft = ` calc(max(0, var(--mag-node-depth) - 1) * 1.15rem) ` ;
if ( depth > 1 ) {
fs . classList . add ( 'magazine-editor__node--nested' ) ;
}
const lab = fs . querySelector ( '[data-new-node-legend-label]' ) ;
if ( lab ) {
lab . textContent = depth <= 1 ? 'New category' : 'New subcategory' ;
}
let preservedStr = '[]' ;
try {
preservedStr = JSON . stringify ( JSON . parse ( this . defaultPreservedJsonValue || '[]' ) ) ;
} catch {
preservedStr = '[]' ;
}
const preservedEl = fs . querySelector ( '[data-magazine-hierarchy-editor-target="preservedJson"]' ) ;
if ( preservedEl ) {
preservedEl . value = preservedStr ;
}
const dIn = fs . querySelector ( '[data-magazine-hierarchy-editor-target="dTag"]' ) ;
if ( dIn && 'value' in dIn ) {
dIn . value = slug ;
}
if ( ! this . append30040ChildLine ( parentDTrim , slug , ownerHex ) ) {
this . setStatus (
` Could not add the nested kind-30040 link to the parent’s <code>a</code> 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 <code>a</code> 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 {
@@ -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 {
@@ -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 {
@@ -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,12 +817,14 @@ export default class extends Controller {
@@ -187,12 +817,14 @@ export default class extends Controller {
async expandPublishSet ( rootD , dirtyFieldsets , ownerHex ) {
/** @type {Map<string, HTMLElement>} */
const dToEl = new Map ( ) ;
for ( const el of this . nodeTargets ) {
if ( this . hasNodesTarget ) {
for ( const el of queryEditorNodeFieldsets ( this . nodesTarget ) ) {
const d = readDTag ( el ) ;
if ( d ) {
dToEl . set ( d , el ) ;
}
}
}
const W = new Set ( ) ;
for ( const el of dirtyFieldsets ) {
@ -251,10 +883,17 @@ export default class extends Controller {
@@ -251,10 +883,17 @@ export default class extends Controller {
if ( norm [ 0 ] . toLowerCase ( ) === 'd' ) {
continue ;
}
if ( norm [ 0 ] . toLowerCase ( ) === 'client' ) {
continue ;
}
tags . push ( norm ) ;
}
tags . push ( [ 'title' , title ] ) ;
tags . push ( [ 'summary' , summary ] ) ;
const clientName = ( this . clientTagValue || '' ) . trim ( ) ;
if ( clientName !== '' ) {
tags . push ( [ 'client' , clientName ] ) ;
}
const lines = aText
. split ( '\n' )
@ -302,6 +941,85 @@ export default class extends Controller {
@@ -302,6 +941,85 @@ export default class extends Controller {
}
}
/ * *
* Index fieldsets that are direct children of the nodes list ( excludes ` <template> ` fragments ) .
* @ param { HTMLElement } nodesContainer
* @ returns { HTMLElement [ ] }
* /
function queryEditorNodeFieldsets ( nodesContainer ) {
return [ ... nodesContainer . querySelectorAll ( ':scope > [data-magazine-hierarchy-editor-target="node"]' ) ] ;
}
/ * *
* @ param { HTMLElement } parentFs
* @ param { HTMLElement } nodesContainer
* /
function lastDescendantFieldsetAmongNodes ( parentFs , nodesContainer ) {
const nodes = queryEditorNodeFieldsets ( nodesContainer ) ;
const idx = nodes . indexOf ( parentFs ) ;
if ( idx === - 1 ) {
return parentFs ;
}
const pd = parseInt ( parentFs . dataset . magDepth || '0' , 10 ) ;
let last = parentFs ;
for ( let i = idx + 1 ; i < nodes . length ; i ++ ) {
const nd = parseInt ( nodes [ i ] . dataset . magDepth || '0' , 10 ) ;
if ( nd <= pd ) {
break ;
}
last = nodes [ i ] ;
}
return last ;
}
/ * *
* ` rootFs ` first , then following fieldsets with strictly greater depth ( subtree in DOM order ) .
* @ param { HTMLElement } rootFs
* @ param { HTMLElement } nodesContainer
* @ returns { HTMLElement [ ] }
* /
function collectSubtreeFieldsets ( rootFs , nodesContainer ) {
const nodes = queryEditorNodeFieldsets ( nodesContainer ) ;
const idx = nodes . indexOf ( rootFs ) ;
if ( idx === - 1 ) {
return [ rootFs ] ;
}
const pd = parseInt ( rootFs . dataset . magDepth || '0' , 10 ) ;
const out = [ rootFs ] ;
for ( let i = idx + 1 ; i < nodes . length ; i ++ ) {
const nd = parseInt ( nodes [ i ] . dataset . magDepth || '0' , 10 ) ;
if ( nd <= pd ) {
break ;
}
out . push ( nodes [ i ] ) ;
}
return out ;
}
/ * *
* @ param { HTMLElement } list
* @ param { string } ownerHex
* @ param { string } oldD
* @ param { string } newD
* @ returns { boolean }
* /
function rewrite30040ChildLineInList ( list , ownerHex , oldD , newD ) {
const oh = ownerHex . toLowerCase ( ) ;
const o = oldD . trim ( ) ;
const n = newD . 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 , o ) ) {
inp . value = ` ${ KIND _PUBLICATION _INDEX } : ${ oh } : ${ n } ` ;
return true ;
}
}
return false ;
}
/ * *
* @ param { string [ ] [ ] } tags
* @ param { string } ownerHex
@ -339,7 +1057,8 @@ function ownedNested30040DsFromTags(tags, ownerHex) {
@@ -339,7 +1057,8 @@ function ownedNested30040DsFromTags(tags, ownerHex) {
* @ param { HTMLElement } el
* /
function readDTag ( el ) {
return ( el . querySelector ( '[data-magazine-hierarchy-editor-target="dTag"]' ) ? . value || '' ) . trim ( ) ;
const inp = el . querySelector ( 'input[data-magazine-hierarchy-editor-target="dTag"]' ) ;
return inp instanceof HTMLInputElement ? ( inp . value || '' ) . trim ( ) : '' ;
}
/ * *
@ -347,14 +1066,74 @@ function readDTag(el) {
@@ -347,14 +1066,74 @@ function readDTag(el) {
* /
function snapshotFromElement ( el ) {
return {
dTag : readDTag ( el ) ,
title : el . querySelector ( '[data-magazine-hierarchy-editor-target="title"]' ) ? . value ? ? '' ,
summary : el . querySelector ( '[data-magazine-hierarchy-editor-target="summary"]' ) ? . value ? ? '' ,
content : el . querySelector ( '[data-magazine-hierarchy-editor-target="content"]' ) ? . value ? ? '' ,
aText : el . querySelector ( '[data-magazine-hierarchy-editor-target="aLines"]' ) ? . value ? ? '' ,
aText : readJoinedALinesFromNode ( el ) ,
preservedRaw : el . querySelector ( '[data-magazine-hierarchy-editor-target="preservedJson"]' ) ? . value ? ? '[]' ,
} ;
}
/ * *
* @ param { HTMLElement } list
* @ returns { string [ ] }
* /
function collectALineValuesFromList ( list ) {
const out = [ ] ;
for ( const inp of list . querySelectorAll ( '[data-mag-a-line]' ) ) {
if ( inp instanceof HTMLInputElement ) {
const v = inp . value . trim ( ) ;
if ( v !== '' ) {
out . push ( v ) ;
}
}
}
return out ;
}
/ * *
* @ param { HTMLElement } el Fieldset for one index node .
* /
function readJoinedALinesFromNode ( el ) {
const list = el . querySelector ( '[data-mag-a-list]' ) ;
if ( ! list ) {
return '' ;
}
return collectALineValuesFromList ( list ) . join ( '\n' ) ;
}
/ * *
* @ param { HTMLElement } el
* /
function isRootFieldset ( el ) {
return el . dataset . magazineNodeIsRoot === '1' ;
}
/ * *
* @ param { string } line
* @ param { string } ownerHexLower
* @ param { string } childD
* /
function lineMatches30040Child ( line , ownerHexLower , childD ) {
const s = stripNostrScheme ( line ) ;
const parts = splitThree ( s ) ;
if ( ! parts ) {
return false ;
}
const kind = parseInt ( parts . kind , 10 ) ;
if ( kind !== KIND _PUBLICATION _INDEX ) {
return false ;
}
if ( parts . pubkey . toLowerCase ( ) !== ownerHexLower . toLowerCase ( ) ) {
return false ;
}
return parts . identifier . trim ( ) === childD ;
}
/ * *
* @ param { HTMLElement } el
* @ returns { { dTag : string , title : string , summary : string , content : string , aText : string , preserved : unknown [ ] } }
@ -364,7 +1143,7 @@ function readFieldsForBuild(el) {
@@ -364,7 +1143,7 @@ function readFieldsForBuild(el) {
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 ;
try {