} pathValue Zero-based path value.
* @returns {HTMLLIElement|null} Matching list item.
*/
getItemByPath(tracker, pathValue) {
const indexes = normalizePathIndexes(pathValue);
if (!tracker?.listElement || !Array.isArray(indexes) || !indexes.length) {
return null;
}
let currentList = tracker.listElement;
let currentItem = null;
for (let index = 0; index < indexes.length; index++) {
const itemIndex = indexes[index];
const siblingItems = getDirectListItems(currentList);
currentItem = siblingItems[itemIndex] || null;
if (!currentItem) {
return null;
}
if (index === indexes.length - 1) {
return currentItem;
}
currentList = getDirectChildList(currentItem);
if (!currentList) {
return null;
}
}
return currentItem;
},
/**
* Resolve one list element back to its public list path.
*
* The root list is addressed as an empty string. Nested child lists are
* addressed by the path of the parent list item that owns them.
*
* @param {Object|null} tracker Active list tracker.
* @param {HTMLElement|null} listElement Candidate list element.
* @returns {string} Root-relative list path.
*/
getPathForList(tracker, listElement) {
if (!tracker) tracker = this.active;
if (!tracker?.listElement || !listElement || listElement.nodeType !== Node.ELEMENT_NODE) {
return '';
}
if (listElement === tracker.listElement) {
return '';
}
const parentItem = (
listElement.parentElement &&
listElement.parentElement.tagName === 'LI'
)
? listElement.parentElement
: null;
if (!parentItem) {
return '';
}
const indexes = this.getPathIndexesForItem(tracker, parentItem);
return Array.isArray(indexes) ? indexes.join('_') : '';
},
/**
* Return one tracked list item by runtime UUID.
*
* @param {Object|null} tracker Active list tracker.
* @param {string} runtimeUuid Session-scoped item UUID.
* @returns {HTMLLIElement|null} Matching list item.
*/
getItemByRuntimeUuid(tracker, runtimeUuid) {
if (!tracker) tracker = this.active;
const element = findElementByRuntimeUuid(tracker, runtimeUuid, 'item');
return element && element.tagName === 'LI' ? element : null;
},
/**
* Return one tracked list by runtime UUID.
*
* @param {Object|null} tracker Active list tracker.
* @param {string} runtimeUuid Session-scoped list UUID.
* @returns {HTMLElement|null} Matching list element.
*/
getListByRuntimeUuid(tracker, runtimeUuid) {
if (!tracker) tracker = this.active;
const element = findElementByRuntimeUuid(tracker, runtimeUuid, 'list');
return isListElementForRuntimeLookup(element) ? element : null;
},
/**
* Return the runtime UUID for one tracked list item.
*
* @param {Object|null} tracker Active list tracker.
* @param {HTMLLIElement} listItem Live list item.
* @returns {string} Session-scoped item UUID.
*/
getRuntimeUuidForItem(tracker, listItem) {
if (!tracker) tracker = this.active;
if (!tracker?.listElement || !listItem || listItem.tagName !== 'LI') {
return '';
}
return normalizeRuntimeUuid(
listItem.getAttribute(LIST_ITEM_RUNTIME_UUID_ATTR)
|| tracker.runtimeDomMap?.get(listItem)
);
},
/**
* Return the runtime UUID for one tracked list.
*
* @param {Object|null} tracker Active list tracker.
* @param {HTMLElement} listElement Live list element.
* @returns {string} Session-scoped list UUID.
*/
getRuntimeUuidForList(tracker, listElement) {
if (!tracker) tracker = this.active;
if (!tracker?.listElement || !isListElementForRuntimeLookup(listElement)) {
return '';
}
return normalizeRuntimeUuid(
listElement.getAttribute(LIST_RUNTIME_UUID_ATTR)
|| tracker.runtimeDomMap?.get(listElement)
);
},
/**
* Parse current DOM list structure to WordPress block format
* Uses UUID to preserve original attrs, updates only content and ordered
*
* @param {HTMLElement} listElement Live UL/OL element.
* @param {Object} tracker Active list tracker.
* @param {boolean} allowUuidOwnership True only for the outermost
* core/list. Nested lists must
* serialize without persisted
* plugin UUID attrs.
*/
parseListToBlock(listElement, tracker, allowUuidOwnership = true) {
const isOrdered = listElement.tagName === 'OL';
const uuid = listElement.getAttribute(LIST_ID_ATTR);
let attrs = { ordered: isOrdered };
if (uuid && tracker && tracker.uuidMap.has(uuid)) {
const original = tracker.uuidMap.get(uuid);
const originalAttrs = (Array.isArray(original.attrs) || !original.attrs)
? {}
: cloneListAttrs(original.attrs, allowUuidOwnership);
attrs = { ...originalAttrs, ordered: isOrdered };
}
const listItems = Array.from(listElement.children).filter(el => el.tagName === 'LI');
const innerBlocks = listItems.map(li => this.parseListItemToBlock(li, tracker));
return createBlock('core/list', attrs, innerBlocks);
},
/**
* Parse a list item to WordPress block format
* Preserves original attrs if UUID exists
*/
parseListItemToBlock(liElement, tracker) {
const uuid = liElement.getAttribute(LIST_ITEM_ID_ATTR);
let attrs = {};
if (uuid && tracker && tracker.uuidMap.has(uuid)) {
const original = tracker.uuidMap.get(uuid);
attrs = JSON.parse(JSON.stringify(original.attrs || {}));
}
// Recurse into nested list using the live element before cloning
const innerBlocks = [];
const nestedListEl = liElement.querySelector(':scope > ul, :scope > ol');
if (nestedListEl) {
innerBlocks.push(this.parseListToBlock(nestedListEl, tracker, false));
}
const clone = liElement.cloneNode(true);
const directSurface = clone.querySelector(`:scope > [${LIST_ITEM_TEXT_ATTR}="1"]`);
let content = '';
if (directSurface) {
const cleanedSurface = this.cleanElement(directSurface);
content = cleanedSurface.innerHTML.trim();
} else {
const nestedInClone = clone.querySelector(':scope > ul, :scope > ol');
if (nestedInClone) nestedInClone.remove();
const cleaned = this.cleanElement(clone);
content = cleaned.innerHTML.trim();
}
return createBlock('core/list-item', { ...attrs, content }, innerBlocks);
},
/**
* Clean element using ElementPrep
* Removes editing artifacts while preserving UUIDs and content
*/
cleanElement(element) {
if (SFE.ElementPrep) {
return SFE.ElementPrep.clean(element, {
removeIdentity: true,
removeControls: true,
clone: false // Already working with a clone
});
}
console.error('ElementPrep not found');
return element;
},
/**
* Serialize the current list structure to WordPress block format
* Preserves all original attrs, updates only content and ordered
*/
serialize(tracker) {
if (!tracker) tracker = this.active;
if (!tracker) return null;
const block = this.parseListToBlock(tracker.listElement, tracker);
return serializeBlocks([block]);
},
/**
* Update the tracker's element reference
* Used when the list element is replaced (e.g., OL to UL conversion)
*/
updateElement(tracker, newElement) {
if (!tracker) tracker = this.active;
if (!tracker) return;
// Ensure new element keeps the tracker reference
newElement._mwpListTracker = tracker;
this.syncEditableTextSurfaces(newElement);
tracker.listElement = newElement;
// Update element references in uuidMap
tracker.uuidMap.forEach((value, key) => {
if (value.type === 'list') {
const newEl = newElement.querySelector(`[${LIST_ID_ATTR}="${key}"]`) ||
(newElement.getAttribute(LIST_ID_ATTR) === key ? newElement : null);
if (newEl) {
value.element = newEl;
}
} else if (value.type === 'item') {
const newEl = newElement.querySelector(`[${LIST_ITEM_ID_ATTR}="${key}"]`);
if (newEl) {
value.element = newEl;
}
}
});
tracker.runtimeUuidMap.forEach((value, key) => {
if (value.type === 'list') {
const newEl = this.getListByRuntimeUuid(tracker, key);
if (newEl) {
value.element = newEl;
tracker.runtimeDomMap.set(newEl, key);
}
} else if (value.type === 'item') {
const newEl = this.getItemByRuntimeUuid(tracker, key);
if (newEl) {
value.element = newEl;
tracker.runtimeDomMap.set(newEl, key);
}
}
});
},
/**
* Return one tree-aware structural snapshot for the active list.
*
* @param {Object|null} tracker Active list tracker.
* @returns {Object|null} Lightweight structure snapshot.
*/
getStructure(tracker) {
if (!tracker) tracker = this.active;
if (!tracker?.listElement) {
return null;
}
const buildListNode = (listElement, listPath = '', parentIndexes = []) => ({
listUuid: this.getRuntimeUuidForList(tracker, listElement),
listPath,
ordered: listElement.tagName === 'OL',
items: getDirectListItems(listElement).map((listItem, index) => {
const indexes = [ ...parentIndexes, index ];
const pathMeta = buildPathMeta(indexes);
const directSurface = ensureDirectItemTextSurface(listItem);
const nestedList = getDirectChildList(listItem);
return {
itemUuid: this.getRuntimeUuidForItem(tracker, listItem),
path: pathMeta.path,
pathLabel: pathMeta.pathLabel,
depth: pathMeta.depth,
contentHtml: directSurface ? directSurface.innerHTML.trim() : '',
childList: nestedList
? buildListNode(nestedList, pathMeta.path, indexes)
: null,
};
}),
});
return buildListNode(tracker.listElement, '', []);
},
/**
* Apply one primitive structural list operation against the live tracked
* DOM.
*
* Public API calls are translated into this lower-level operation set by
* the shared schema executor so the tracker only needs to understand the
* canonical primitive mutation layer.
*
* Supported primitive kinds:
* - `insert_list_item`
* - `remove_list_item`
* - `move_list_item`
* - `indent_list_item`
* - `outdent_list_item`
* - `update_list_item_text`
* - `toggle_list_type`
*
* @param {Object|null} tracker Active list tracker.
* @param {Object} rawOperation Structural operation payload.
* @returns {Object|null} Result summary when applied.
*/
applyOperation(tracker, rawOperation, options = {}) {
if (!tracker) tracker = this.active;
if (!tracker?.listElement || !rawOperation || typeof rawOperation !== 'object') {
return null;
}
const kind = typeof rawOperation.kind === 'string' ? rawOperation.kind.trim().toLowerCase() : '';
const path = rawOperation.path ?? rawOperation.itemPath ?? rawOperation.item_path ?? '';
const listItem = this.getItemByPath(tracker, path);
let didApply = false;
if (kind === 'remove_list_item' && listItem) {
didApply = applyRemoveListItemOperation(tracker, listItem);
} else if (kind === 'indent_list_item' && listItem) {
didApply = applyIndentListItemOperation(tracker, listItem);
} else if (kind === 'outdent_list_item' && listItem) {
didApply = applyOutdentListItemOperation(tracker, listItem);
} else if (kind === 'toggle_list_type') {
didApply = applyToggleListTypeOperation(tracker, rawOperation, options);
} else if (kind === 'update_list_item_text' && listItem) {
didApply = applyUpdateListItemTextOperation(listItem, rawOperation);
} else if (kind === 'insert_list_item') {
didApply = applyInsertListItemOperation(tracker, rawOperation, this);
} else if (kind === 'move_list_item' && listItem) {
didApply = applyMoveListItemOperation(tracker, listItem, rawOperation, this);
}
if (!didApply) {
return null;
}
this.syncEditableTextSurfaces(tracker.listElement);
this.buildFromDOM(tracker, tracker.listElement, tracker.originalBlock || {});
return {
kind,
structure: this.getStructure(tracker),
};
},
/**
* Ensure every list item in one tree has one direct text surface.
*
* @param {HTMLElement|null} rootElement Candidate list root or list item.
* @returns {HTMLElement|null} Normalized root element.
*/
syncEditableTextSurfaces(rootElement) {
if (!rootElement || rootElement.nodeType !== Node.ELEMENT_NODE) {
return null;
}
if (rootElement.tagName === 'LI') {
ensureDirectItemTextSurface(rootElement);
Array.from(rootElement.children || [])
.filter(child => isNestedListNode(child))
.forEach(childList => this.syncEditableTextSurfaces(childList));
return rootElement;
}
if (rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL') {
return rootElement;
}
Array.from(rootElement.children || [])
.filter(child => child.tagName === 'LI')
.forEach(li => this.syncEditableTextSurfaces(li));
return rootElement;
},
/**
* Ensure runtime UUID attributes are unique within one live list tree.
*
* Native contenteditable list splitting can clone `li` and nested list
* attributes verbatim before FrontEdit regains control. This pass reseeds only the
* session-scoped runtime UUID attributes so freshly created structures become
* distinct live API/editor targets immediately, while leaving `data-item-id`
* and `data-list-id` untouched for their separate responsibilities.
*
* @param {HTMLElement|null} rootElement Candidate list root.
* @returns {HTMLElement|null} Normalized root element.
*/
ensureUniqueRuntimeUuids(rootElement) {
if (
!rootElement ||
rootElement.nodeType !== Node.ELEMENT_NODE ||
(rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL')
) {
return rootElement || null;
}
const seenListRuntimeUuids = new Set();
const seenItemRuntimeUuids = new Set();
const collectLists = [ rootElement, ...Array.from(rootElement.querySelectorAll('ul, ol')) ];
const collectItems = Array.from(rootElement.querySelectorAll('li'));
collectLists.forEach(listElement => {
const runtimeUuid = normalizeRuntimeUuid(
listElement.getAttribute(LIST_RUNTIME_UUID_ATTR)
);
if (!runtimeUuid) {
return;
}
if (seenListRuntimeUuids.has(runtimeUuid)) {
listElement.setAttribute(LIST_RUNTIME_UUID_ATTR, this.generateTempUuid());
return;
}
seenListRuntimeUuids.add(runtimeUuid);
});
collectItems.forEach(listItem => {
const runtimeUuid = normalizeRuntimeUuid(
listItem.getAttribute(LIST_ITEM_RUNTIME_UUID_ATTR)
);
if (!runtimeUuid) {
return;
}
if (seenItemRuntimeUuids.has(runtimeUuid)) {
listItem.setAttribute(LIST_ITEM_RUNTIME_UUID_ATTR, this.generateTempUuid());
return;
}
seenItemRuntimeUuids.add(runtimeUuid);
});
return rootElement;
},
/**
* Ensure one live list tree has the required structural IDs and runtime UUIDs.
*
* On first editor activation, list elements may not yet have either identity
* attribute family. Seed both from one generated UUID per element so the
* initial history snapshot captures stable targeting data. Later native list
* splits may intentionally copy the structural IDs; in those cases this pass
* preserves the structural IDs and only reseeds duplicated runtime UUIDs.
*
* @param {HTMLElement|null} rootElement Candidate list root.
* @returns {HTMLElement|null} Normalized root element.
*/
ensureIdentityAttributes(rootElement) {
if (
!rootElement ||
rootElement.nodeType !== Node.ELEMENT_NODE ||
(rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL')
) {
return rootElement || null;
}
const syncIdentityPair = (element, structuralAttrName, runtimeAttrName) => {
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
return;
}
let structuralId = normalizeRuntimeUuid(
element.getAttribute(structuralAttrName)
);
let runtimeUuid = normalizeRuntimeUuid(
element.getAttribute(runtimeAttrName)
);
if (!structuralId && !runtimeUuid) {
runtimeUuid = this.generateTempUuid();
structuralId = runtimeUuid;
} else if (!runtimeUuid) {
runtimeUuid = structuralId;
} else if (!structuralId) {
structuralId = runtimeUuid;
}
element.setAttribute(structuralAttrName, structuralId);
element.setAttribute(runtimeAttrName, runtimeUuid);
};
syncIdentityPair(rootElement, LIST_ID_ATTR, LIST_RUNTIME_UUID_ATTR);
Array.from(rootElement.querySelectorAll('ul, ol')).forEach(listElement => {
syncIdentityPair(listElement, LIST_ID_ATTR, LIST_RUNTIME_UUID_ATTR);
});
Array.from(rootElement.querySelectorAll('li')).forEach(listItem => {
syncIdentityPair(listItem, LIST_ITEM_ID_ATTR, LIST_ITEM_RUNTIME_UUID_ATTR);
});
return this.ensureUniqueRuntimeUuids(rootElement);
},
getDirectItemTextSurface,
normalizePathIndexes,
buildPathMeta,
getRuntimeUuidAttributeName,
getListRuntimeUuidAttributeName() {
return LIST_RUNTIME_UUID_ATTR;
},
getListItemRuntimeUuidAttributeName() {
return LIST_ITEM_RUNTIME_UUID_ATTR;
},
getListIdAttributeName() {
return LIST_ID_ATTR;
},
getListItemIdAttributeName() {
return LIST_ITEM_ID_ATTR;
},
/**
* Destroy tracker and clean up
*/
destroy(tracker) {
if (!tracker) tracker = this.active;
if (!tracker) return;
tracker.uuidMap.clear();
tracker.runtimeUuidMap.clear();
if (tracker.listElement) {
delete tracker.listElement._mwpListTracker;
}
if (this.active === tracker) this.active = null;
}
};
/**
* Return whether one element is a live list node for runtime lookup helpers.
*
* @param {Element|null} element Candidate DOM element.
* @returns {boolean} True when the element is `UL` or `OL`.
*/
function isListElementForRuntimeLookup(element) {
return !!(
element &&
element.nodeType === Node.ELEMENT_NODE &&
(element.tagName === 'UL' || element.tagName === 'OL')
);
}
// Expose globally
SFE.ListBlockTracker = ListBlockTracker;
})();
Пријава ‹ Алумни Универзитета у Приштини — Вордпрес
Пријава