import DOMUtils from 'tinymce/core/api/dom/DOMUtils';
interface Bookmark {
startContainer: Node;
startOffset: number;
endContainer?: Node;
endOffset?: number;
}
/**
* Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with
* index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans
* added to them since they can be restored after a dom operation.
*
* So this:
||
* becomes: ||
*
* @param {DOMRange} rng DOM Range to get bookmark on.
* @return {Object} Bookmark object.
*/
const create = (dom: DOMUtils, rng: Range): Bookmark => {
const bookmark: Partial = {};
const setupEndPoint = (start?: boolean) => {
let container = rng[start ? 'startContainer' : 'endContainer'];
let offset = rng[start ? 'startOffset' : 'endOffset'];
if (container.nodeType === 1) {
const offsetNode = dom.create('span', { 'data-mce-type': 'bookmark' });
if (container.hasChildNodes()) {
offset = Math.min(offset, container.childNodes.length - 1);
if (start) {
container.insertBefore(offsetNode, container.childNodes[offset]);
} else {
dom.insertAfter(offsetNode, container.childNodes[offset]);
}
} else {
container.appendChild(offsetNode);
}
container = offsetNode;
offset = 0;
}
bookmark[start ? 'startContainer' : 'endContainer'] = container;
bookmark[start ? 'startOffset' : 'endOffset'] = offset;
};
setupEndPoint(true);
if (!rng.collapsed) {
setupEndPoint();
}
return bookmark as Bookmark;
};
/**
* Moves the selection to the current bookmark and removes any selection container wrappers.
*
* @param {Object} bookmark Bookmark object to move selection to.
*/
const resolve = (dom: DOMUtils, bookmark: Bookmark): Range => {
const restoreEndPoint = (start?: boolean) => {
const nodeIndex = (container: Node) => {
let node = container.parentNode?.firstChild;
let idx = 0;
while (node) {
if (node === container) {
return idx;
}
// Skip data-mce-type=bookmark nodes
if (node.nodeType !== 1 || (node as Element).getAttribute('data-mce-type') !== 'bookmark') {
idx++;
}
node = node.nextSibling;
}
return -1;
};
let container = bookmark[start ? 'startContainer' : 'endContainer'];
let offset = bookmark[start ? 'startOffset' : 'endOffset'];
if (!container) {
return;
}
if (container.nodeType === 1 && container.parentNode) {
const node = container;
offset = nodeIndex(container);
container = container.parentNode;
dom.remove(node);
}
bookmark[start ? 'startContainer' : 'endContainer'] = container;
bookmark[start ? 'startOffset' : 'endOffset'] = offset as number;
};
restoreEndPoint(true);
restoreEndPoint();
const rng = dom.createRng();
rng.setStart(bookmark.startContainer, bookmark.startOffset);
if (bookmark.endContainer) {
rng.setEnd(bookmark.endContainer, bookmark.endOffset as number);
}
return rng;
};
export {
create,
resolve
};