suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
787 lines (699 loc) • 27.3 kB
JavaScript
import { dom, unicode, env, numbers } from '../../../helper';
const { _w } = env;
/**
* @description Selection, Range related class
*/
class Selection_ {
#kernel;
#$;
#store;
#shadowRoot;
#instanceCheck;
#context;
#frameContext;
#options;
#frameOptions;
#scrollMargin = 0;
#hasScrollParents = false;
/**
* @constructor
* @param {SunEditor.Kernel} kernel
*/
constructor(kernel) {
this.#kernel = kernel;
this.#$ = kernel.$;
this.#store = kernel.store;
this.#shadowRoot = this.#$.contextProvider.shadowRoot;
this.#instanceCheck = this.#$.instanceCheck;
this.#context = this.#$.context;
this.#frameContext = this.#$.frameContext;
this.#options = this.#$.options;
this.#frameOptions = this.#$.frameOptions;
// members
/** @type {Range} */
this.range = null;
/** @type {HTMLElement|Text} */
this.selectionNode = null;
/** @internal */
this.__iframeFocus = false;
}
/**
* @description Get window selection obejct
* @returns {Selection}
*/
get() {
let selection = null;
if (typeof this.#shadowRoot?.getSelection === 'function') {
selection = this.#shadowRoot.getSelection();
} else {
selection = this.#frameContext.get('_ww').getSelection();
}
if (!selection) return null;
if (!this.#store.get('_range') && !this.#frameContext.get('wysiwyg').contains(selection.focusNode)) {
selection.removeAllRanges();
selection.addRange(this.#createDefaultRange());
}
return selection;
}
/**
* @description Check if the range object is valid
* @param {*} range Range object
* @returns {range is Range}
*/
isRange(range) {
// return /Range/.test(Object.prototype.toString.call(range?.__proto__));
return this.#instanceCheck.isRange(range);
}
/**
* @description Get current editor's range object
* @returns {Range}
*/
getRange() {
const range = this.#store.get('_range') || this.#createDefaultRange();
const selection = this.get();
if (range.collapsed === selection.isCollapsed || !this.#frameContext.get('wysiwyg').contains(selection.focusNode)) {
if (this.#$.component.is(range.startContainer)) {
const compInfo = this.#$.component.get(range.startContainer);
const container = compInfo?.container;
if (!container) return range;
return this.setRange(container, 0, container, 1);
}
return range;
}
if (selection.rangeCount > 0) {
const selectionRange = selection.getRangeAt(0);
this.#store.set('_range', selectionRange);
return selectionRange;
} else {
const sc = selection.anchorNode,
ec = selection.focusNode,
so = selection.anchorOffset,
eo = selection.focusOffset;
const compareValue = dom.query.compareElements(sc, ec);
const rightDir = compareValue.ancestor && (compareValue.result === 0 ? so <= eo : compareValue.result > 1 ? true : false);
return this.setRange(rightDir ? sc : ec, rightDir ? so : eo, rightDir ? ec : sc, rightDir ? eo : so);
}
}
/**
* @description Set current editor's range object and return.
* @param {Node|Range} startCon Range object or The `startContainer` property of the selection object
* @param {number} [startOff] The `startOffset` property of the selection object.
* @param {Node} [endCon] The `endContainer` property of the selection object.
* @param {number} [endOff] The `endOffset` property of the selection object.
* @returns {Range}
* @example
* // Set range using container and offset
* const textNode = editor.selection.getNode();
* editor.selection.setRange(textNode, 0, textNode, 5);
*
* // Set range using Range object
* const range = document.createRange();
* range.selectNodeContents(someElement);
* editor.selection.setRange(range);
*
* // Collapse cursor to start of element
* editor.selection.setRange(element, 0, element, 0);
*/
setRange(startCon, startOff, endCon, endOff) {
/** @type {Node} */
let sc;
/** @type {number} */
let so;
/** @type {Node} */
let ec;
/** @type {number} */
let eo;
if (this.isRange(startCon)) {
const r = /** @type {Range} */ (startCon);
sc = r.startContainer;
so = r.startOffset;
ec = r.endContainer;
eo = r.endOffset;
} else {
sc = /** @type {Node} */ (startCon);
so = startOff;
ec = endCon;
eo = endOff;
}
if (!sc || !ec) return;
if ((dom.check.isBreak(sc) || sc.nodeType === 3) && so > sc.textContent.length) so = sc.textContent.length;
if ((dom.check.isBreak(ec) || ec.nodeType === 3) && eo > ec.textContent.length) eo = ec.textContent.length;
if (this.#$.format.isLine(sc)) {
sc = sc.childNodes[so > 0 ? sc.childNodes.length - 1 : 0] || sc;
so = so > 0 ? (sc.nodeType === 1 && !dom.check.isBreak(sc) ? 1 : sc.textContent ? sc.textContent.length : 0) : 0;
}
if (this.#$.format.isLine(ec)) {
ec = ec.childNodes[eo > 0 ? ec.childNodes.length - 1 : 0] || ec;
eo = eo > 0 ? (ec.nodeType === 1 && !dom.check.isBreak(ec) ? 1 : ec.textContent ? ec.textContent.length : 0) : 0;
}
const range = this.#frameContext.get('_wd').createRange();
try {
so = Math.min(so, sc.textContent?.length || 0);
eo = eo > 0 && (ec.textContent?.length || 0) === 0 && ec.nodeType === 1 ? 1 : Math.min(Math.max(eo, 0), ec.textContent?.length || 0);
range.setStart(sc, so);
range.setEnd(ec, eo);
this.#store.set('hasFocus', true);
} catch (error) {
console.warn('[SUNEDITOR.selection.focus.warn]', error.message);
this.#$.focusManager.nativeFocus();
return;
}
const selection = this.get();
if (selection.removeAllRanges) {
selection.removeAllRanges();
}
selection.addRange(range);
this.#store.set('_range', range);
this.#rangeInfo(range, this.get());
if (this.#frameOptions.get('iframe')) this.__focus();
return range;
}
/**
* @description Remove range object and button effect
*/
removeRange() {
this.#store.set('_range', null);
this.#store.set('_lastSelectionNode', null);
this.selectionNode = null;
if (this.#store.get('hasFocus')) this.get().removeAllRanges();
this.#kernel._eventOrchestrator.selectionState.reset();
}
/**
* @description Returns the range (container and offset) near the given target node.
* - If the target node has a next sibling, it returns the next sibling with an offset of 0.
* - If there is no next sibling but a previous sibling exists, it returns the previous sibling with an offset of 1.
* @param {Node} target Target node whose neighboring range is to be determined.
* @returns {{container: Node, offset: number}|null} An object containing the nearest container node and its offset.
* @example
* const nearRange = editor.$.selection.getNearRange(targetNode);
* if (nearRange) editor.$.selection.setRange(nearRange.container, nearRange.offset, nearRange.container, nearRange.offset);
*/
getNearRange(target) {
const next = target.nextSibling;
const prev = target.previousSibling;
if (next) {
return {
container: next,
offset: 0,
};
} else if (prev) {
return {
container: prev,
offset: 1,
};
}
return null;
}
/**
* @description If the `range` object is a non-editable area, add a line at the top of the editor and update the `range` object.
* @param {Range} range core.getRange()
* @param {?Node} [container] If there is `container` argument, it creates a line in front of the container.
* @returns {Range} a new `range` or argument `range`.
*/
getRangeAndAddLine(range, container) {
if (this.#isNone(range)) {
const parent = container?.parentElement || this.#frameContext.get('wysiwyg');
const op = dom.utils.createElement(this.#options.get('defaultLine'), null, '<br>');
parent.insertBefore(op, container && container !== parent ? (!(/** @type {HTMLElement} */ (container).previousElementSibling) ? container : /** @type {HTMLElement} */ (container).nextElementSibling) : parent.firstElementChild);
this.setRange(op.firstElementChild, 0, op.firstElementChild, 1);
range = this.#store.get('_range');
}
return range;
}
/**
* @description Get current select node
* @returns {HTMLElement|Text}
* @example
* const node = editor.$.selection.getNode();
* const line = editor.$.format.getLine(node);
*/
getNode() {
if (!this.#frameContext.get('wysiwyg').contains(this.selectionNode)) this.init();
if (!this.selectionNode) {
const selectionNode = /** @type {HTMLElement|Text} */ (dom.query.getEdgeChild(this.#frameContext.get('wysiwyg').firstChild, (current) => current.childNodes.length === 0 || current.nodeType === 3, false));
if (!selectionNode) {
this.init();
} else {
this.selectionNode = selectionNode;
return selectionNode;
}
}
return this.selectionNode;
}
/**
* @description Get the Rects object.
* @param {?(Range|Node)} target `Range` | `Node` | `null`
* @param {"start"|"end"} position It is based on the position of the rect object to be returned in case of range selection.
* @returns {{rects: import('./offset').RectsInfo, position: "start"|"end", scrollLeft: number, scrollTop: number}}
* @example
* // Get rects at start of selection
* const { rects, position, scrollLeft, scrollTop } = editor.selection.getRects(null, 'start');
* console.log(rects.left, rects.top, rects.right, rects.bottom);
*
* // Get rects for specific node
* const node = editor.selection.getNode();
* const rectsInfo = editor.selection.getRects(node, 'end');
*
* // Use rects for positioning UI elements
* const { rects } = editor.selection.getRects(null, 'start');
* tooltip.style.left = rects.left + 'px';
* tooltip.style.top = rects.top + 'px';
*/
getRects(target, position) {
const targetAbs = dom.check.isElement(/** @type {Node} */ (target)) ? _w.getComputedStyle(target).position === 'absolute' : false;
target = /** @type {Range} */ (!target || dom.check.isText(/** @type {Node} */ (target)) ? this.getRange() : target);
let isStartPosition = position === 'start';
let scrollLeft = _w.scrollX;
let scrollTop = _w.scrollY;
let rects = /** @type {*} */ (target).getClientRects();
rects = rects[isStartPosition ? 0 : rects.length - 1];
if (!rects) {
const node = this.getNode();
if (this.#$.format.isLine(node)) {
const zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
this.#$.html.insertNode(zeroWidth, { afterNode: null, skipCharCount: true });
this.setRange(zeroWidth, 1, zeroWidth, 1);
this.init();
rects = this.getRange().getClientRects();
rects = rects[isStartPosition ? 0 : rects.length - 1];
}
if (!rects) {
const nodeOffset = this.#$.offset.get(node);
rects = {
left: nodeOffset.left,
top: nodeOffset.top,
right: nodeOffset.left + /** @type {HTMLElement} */ (node).offsetWidth,
bottom: nodeOffset.top + /** @type {HTMLElement} */ (node).offsetHeight,
noText: true,
};
scrollLeft = 0;
scrollTop = 0;
}
isStartPosition = true;
}
const iframeRects = /^iframe$/i.test(this.#frameContext.get('wysiwygFrame').nodeName) ? this.#frameContext.get('wysiwygFrame').getClientRects()[0] : null;
if (!targetAbs && iframeRects) {
rects = {
left: rects.left + iframeRects.left,
top: rects.top + iframeRects.top,
right: rects.right + iframeRects.right - iframeRects.width,
bottom: rects.bottom + iframeRects.bottom - iframeRects.height,
};
}
return {
rects: rects,
position: isStartPosition ? 'start' : 'end',
scrollLeft: scrollLeft,
scrollTop: scrollTop,
};
}
/**
* @description Get the custom range object of the event.
* @param {DragEvent} e Event object
* @returns {{sc: Node, so: number, ec: Node, eo: number}} {sc: `startContainer`, so: `startOffset`, ec: `endContainer`, eo: `endOffset`}
*/
getDragEventLocationRange(e) {
const wd = this.#frameContext.get('_wd');
let r, sc, so, ec, eo;
if (wd.caretPositionFromPoint) {
r = wd.caretPositionFromPoint(e.clientX, e.clientY);
sc = r.offsetNode;
so = r.offset;
ec = r.offsetNode;
eo = r.offset;
} else if (wd.caretRangeFromPoint) {
r = wd.caretRangeFromPoint(e.clientX, e.clientY);
sc = r.startContainer;
so = r.startOffset;
ec = r.endContainer;
eo = r.endOffset;
}
return {
sc,
so,
ec,
eo,
};
}
/**
* @description Scroll to the corresponding selection or range position.
* @param {Selection|Range|Node} ref selection or range object
* @param {ScrollIntoViewOptions & {noFocus?: boolean}} [scrollOption] Scroll options. Extends `ScrollIntoViewOptions` (`behavior`, `block`, `inline`) with `noFocus` to prevent focus change.
* @example
* // Scroll to current selection smoothly
* editor.selection.scrollTo(editor.selection.get());
*
* // Scroll to specific node
* const targetNode = document.querySelector('.target-element');
* editor.selection.scrollTo(targetNode);
*
* // Scroll with custom options
* editor.selection.scrollTo(editor.selection.getRange(), {
* behavior: 'auto',
* block: 'center'
* });
*/
scrollTo(ref, scrollOption) {
const noFocus = scrollOption?.noFocus;
if (this.#instanceCheck.isSelection(ref)) {
ref = ref.getRangeAt(0);
} else if (this.#instanceCheck.isNode(ref)) {
if (noFocus) {
const range = this.#frameContext.get('_wd').createRange();
range.selectNodeContents(ref);
ref = range;
} else {
ref = this.setRange(ref, 1, ref, 1);
}
} else if (typeof ref?.startContainer === 'undefined') {
console.warn('[SUNEDITOR.html.scrollTo.warn] "selectionRange" must be Selection or Range or Node object.', ref);
}
const el = dom.query.getParentElement(ref?.startContainer, (current) => current.nodeType === 1);
if (!el) return;
scrollOption = { behavior: 'smooth', block: 'nearest', inline: 'nearest', ...scrollOption };
delete scrollOption.noFocus;
const ww = this.#frameContext.get('_ww');
const wwFrame = this.#frameContext.get('wysiwygFrame');
const isIframe = this.#frameOptions.get('iframe');
const isAutoHeight = !this.#store.get('isScrollable')(this.#frameContext);
const viewportHeight = this.#store.get('currentViewportHeight');
const scrollY = isAutoHeight ? _w.scrollY : isIframe ? ww.scrollY : wwFrame.scrollTop;
const isBottom = this.#store.mode.isBottom;
const realToolbarHeight = this.#context.get('toolbar_main').offsetHeight;
const toolbarHeight = this.#$.toolbar.isSticky ? realToolbarHeight : 0;
const positionToolbarHeight = this.#$.toolbar.isSticky ? toolbarHeight + this.#options.get('_toolbar_sticky') : toolbarHeight;
const statusbarHeight = this.#frameContext.get('statusbar')?.offsetHeight || 0;
if (this.#hasScrollParents) {
el?.scrollIntoView(scrollOption);
if (scrollOption?.behavior === 'auto' && scrollY !== _w.scrollY) {
if (positionToolbarHeight) {
if (isBottom) {
// bottom toolbar covers the bottom — scroll down more so the element clears the toolbar
if (scrollY < _w.scrollY) {
_w.scrollBy(0, positionToolbarHeight);
}
} else {
// top toolbar covers the top — scroll up more so the element clears the toolbar
if (scrollY > _w.scrollY) {
_w.scrollBy(0, -positionToolbarHeight);
}
}
} else if (isAutoHeight) {
_w.scrollBy(0, statusbarHeight);
}
}
return;
}
// --- When there is no upper scroll and it is an iframe ---
const PADDING = this.#scrollMargin;
// Reduce effective viewport height by toolbar+offset when bottom toolbar is sticky
const viewHeight = isAutoHeight ? viewportHeight - (isBottom ? positionToolbarHeight : 0) : wwFrame.offsetHeight;
// Use range rect for accurate height — el.offsetHeight includes nested children (e.g. nested lists)
const refRect = ref?.getBoundingClientRect?.();
const elH = (refRect?.height > 0 ? refRect.height : el.offsetHeight) || 0;
const behavior = scrollOption?.behavior;
const topToolbarH = isBottom ? 0 : positionToolbarHeight;
if (isAutoHeight) {
if (isIframe) {
const rect = this.getRects(ref, 'end').rects;
const topMargin = rect.top + elH - topToolbarH;
const bottomMargin = viewHeight - PADDING - (rect.top + elH);
if (topMargin >= 0 && bottomMargin >= 0) return;
const newScrollTop = scrollY - (topMargin < 0 ? -(topMargin - PADDING) : bottomMargin);
_w.scrollTo({
top: newScrollTop < scrollY ? newScrollTop - topToolbarH : newScrollTop,
behavior,
});
} else {
const rect = this.#$.offset.getGlobal(el);
const scrollMargin = viewHeight + scrollY - rect.top - elH;
if (scrollMargin - PADDING > 0 && viewHeight > scrollMargin + PADDING + topToolbarH) return;
const newScrollTop = scrollMargin <= PADDING ? scrollY - scrollMargin + PADDING + statusbarHeight : scrollY - scrollMargin + (viewHeight - elH - PADDING);
_w.scrollTo({
top: newScrollTop < scrollY ? newScrollTop - topToolbarH : newScrollTop,
behavior,
});
}
} else {
// local scroll — use range rect for accurate position (el.getBoundingClientRect includes nested children)
const hasRefRect = refRect?.height > 0;
const targetTop = hasRefRect ? refRect.top : el.getBoundingClientRect().top;
const innerTop = isIframe ? targetTop : targetTop - wwFrame.getBoundingClientRect().top;
const keepLocalScroll = innerTop - PADDING > 0 && innerTop + PADDING <= viewHeight;
const rectScroll = isBottom ? (innerTop - PADDING > 0 ? innerTop + PADDING - viewHeight + toolbarHeight : innerTop - elH) : innerTop - PADDING > 0 ? innerTop + PADDING - viewHeight : innerTop - (toolbarHeight + elH);
let newScrollTop = scrollY + rectScroll;
// frame scroll
const gy = _w.scrollY;
const globalRect = this.#$.offset.getGlobal();
const topToolbarFrame = isBottom ? 0 : realToolbarHeight;
const bottomToolbarFrame = isBottom ? realToolbarHeight : 0;
const topMargin = gy - globalRect.top + topToolbarFrame;
const bottomMargin = globalRect.top + globalRect.height - (gy + viewportHeight) + bottomToolbarFrame;
// set frame scroll
if (topMargin > 0) {
const newFrameY = (keepLocalScroll ? innerTop : innerTop + scrollY - newScrollTop) - elH - PADDING - topMargin;
if (newFrameY < 0) {
newScrollTop += topToolbarFrame;
_w.scrollTo({
top: gy + newFrameY,
behavior: 'smooth',
});
}
}
if (bottomMargin > 0) {
const newFrameY = (keepLocalScroll ? innerTop : innerTop + scrollY - newScrollTop) + elH + PADDING - (globalRect.height - bottomMargin);
if (newFrameY > 0) {
newScrollTop += isBottom ? bottomToolbarFrame : statusbarHeight;
_w.scrollTo({
top: gy + newFrameY,
behavior: 'smooth',
});
}
}
// set local scroll
if (!keepLocalScroll) {
(isIframe ? ww : wwFrame).scrollTo({
top: newScrollTop,
behavior,
});
}
}
}
/**
* @description Normalizes and resets the selection range to properly target text nodes instead of element nodes for accurate text editing.
* @returns {boolean} Returns `false` if there is no valid selection.
*/
resetRangeToTextNode() {
let rangeObj = this.getRange();
if (this.#isNone(rangeObj)) {
if (!dom.check.isWysiwygFrame(rangeObj.startContainer) || !dom.check.isWysiwygFrame(rangeObj.endContainer)) return false;
const ww = /** @type {HTMLElement} */ (rangeObj.commonAncestorContainer);
const first = ww.children[rangeObj.startOffset];
const end = ww.children[rangeObj.endOffset];
if (!(rangeObj = this.setRange(first, 0, end, first === end ? 0 : 1))) return false;
}
const range = rangeObj;
const collapsed = range.collapsed;
let startCon = range.startContainer;
let startOff = range.startOffset;
let endCon = range.endContainer;
let endOff = range.endOffset;
let tempCon, tempOffset, tempChild;
if (this.#$.format.isLine(startCon)) {
if (!startCon.childNodes[startOff]) {
startCon = startCon.lastChild || startCon;
startOff = startCon.textContent.length;
} else {
startCon = startCon.childNodes[startOff] || startCon;
startOff = 0;
}
while (startCon?.nodeType === 1 && startCon.firstChild) {
startCon = startCon.firstChild || startCon;
startOff = 0;
}
}
if (this.#$.format.isLine(endCon)) {
endCon = endCon.childNodes[endOff] || endCon.lastChild || endCon;
while (endCon?.nodeType === 1 && endCon.lastChild) {
endCon = endCon.lastChild;
}
if (collapsed) endOff = 0;
else if (endOff > 0) endOff = endCon.textContent.length;
}
// startContainer
tempCon = dom.check.isWysiwygFrame(startCon) ? this.#frameContext.get('wysiwyg').firstChild : startCon;
tempOffset = startOff;
if (dom.check.isBreak(tempCon) || (tempCon.nodeType === 1 && tempCon.childNodes.length > 0)) {
const onlyBreak = dom.check.isBreak(tempCon);
if (!onlyBreak) {
const tempConCache = tempCon;
while (tempCon && !dom.check.isBreak(tempCon) && tempCon.nodeType === 1) {
tempChild = tempCon.childNodes;
if (tempChild.length === 0) break;
tempCon =
tempChild[tempOffset > 0 ? tempOffset - 1 : tempOffset] || !/FIGURE/i.test(tempChild[0].nodeName) ? tempChild[0] : /** @type {HTMLElement} */ (tempCon).previousElementSibling || tempCon.previousSibling || startCon;
tempOffset = tempOffset > 0 ? tempCon.textContent.length : tempOffset;
}
let format = this.#$.format.getLine(tempCon, null);
if (format === this.#$.format.getBlock(format, null)) {
tempCon ||= tempConCache;
format = dom.utils.createElement(dom.query.getParentElement(tempCon, dom.check.isTableCell) ? 'DIV' : this.#options.get('defaultLine'));
tempCon.parentNode.insertBefore(format, tempCon);
if (tempCon !== tempConCache) format.appendChild(tempCon);
}
}
if (dom.check.isBreak(tempCon) || this.#$.component.is(tempCon)) {
const emptyText = dom.utils.createTextNode(unicode.zeroWidthSpace);
tempCon.parentNode.insertBefore(emptyText, tempCon);
tempCon = emptyText;
if (onlyBreak) {
if (startCon === endCon) {
endCon = tempCon;
endOff = 1;
}
}
}
}
// set startContainer
startCon = tempCon;
startOff = tempOffset;
// endContainer
tempCon = dom.check.isWysiwygFrame(endCon) ? this.#frameContext.get('wysiwyg').lastChild : endCon;
tempOffset = endOff;
if (dom.check.isBreak(tempCon) || (tempCon.nodeType === 1 && tempCon.childNodes.length > 0)) {
const onlyBreak = dom.check.isBreak(tempCon);
if (!onlyBreak) {
while (tempCon && !dom.check.isBreak(tempCon) && tempCon.nodeType === 1) {
tempChild = tempCon.childNodes;
if (tempChild.length === 0) break;
tempCon =
tempChild[tempOffset > 0 ? tempOffset - 1 : tempOffset] || !/FIGURE/i.test(tempChild[0].nodeName) ? tempChild[0] : /** @type {HTMLElement} */ (tempCon).previousElementSibling || tempCon.previousSibling || startCon;
tempOffset = tempOffset > 0 ? tempCon.textContent.length : tempOffset;
}
let format = this.#$.format.getLine(tempCon, null);
if (format === this.#$.format.getBlock(format, null)) {
format = dom.utils.createElement(dom.check.isTableCell(format) ? 'DIV' : this.#options.get('defaultLine'));
tempCon.parentNode.insertBefore(format, tempCon);
format.appendChild(tempCon);
}
}
if (dom.check.isBreak(tempCon)) {
const emptyText = dom.utils.createTextNode(unicode.zeroWidthSpace);
tempCon.parentNode.insertBefore(emptyText, tempCon);
tempCon = emptyText;
tempOffset = 1;
if (onlyBreak && !tempCon.previousSibling) {
dom.utils.removeItem(endCon);
}
}
}
// set endContainer
endCon = tempCon;
endOff = tempOffset;
// set Range
this.setRange(startCon, startOff, endCon, endOff);
return true;
}
/**
* @description Saving the range object and the currently selected node of editor
*/
init() {
const activeEl = this.#frameContext.get('_wd').activeElement;
if (dom.check.isInputElement(activeEl)) {
this.selectionNode = activeEl;
return activeEl;
}
const selection = this.get();
if (!selection) return null;
let range = null;
if (selection.rangeCount > 0) {
range = selection.getRangeAt(0);
} else {
range = this.#createDefaultRange();
}
this.#rangeInfo(range, selection);
}
/**
Ï * @description Set `range` and `selection` info.
* @param {Range} range range object.
* @param {Selection} selection selection object.
*/
#rangeInfo(range, selection) {
let selectionNode = null;
this.#store.set('_range', range);
if (range.collapsed) {
if (dom.check.isWysiwygFrame(range.commonAncestorContainer)) selectionNode = range.commonAncestorContainer.children[range.startOffset] || range.commonAncestorContainer;
else selectionNode = range.commonAncestorContainer;
} else {
selectionNode = selection.anchorNode;
}
this.selectionNode = /** @type {HTMLElement|Text} */ (selectionNode);
}
/**
* @description Returns `true` if there is no valid selection.
* @param {Range} range selection.getRange()
* @returns {boolean}
*/
#isNone(range) {
const comm = /** @type {HTMLElement} */ (range.commonAncestorContainer);
return (
(dom.check.isWysiwygFrame(range.startContainer) && dom.check.isWysiwygFrame(range.endContainer)) ||
/FIGURE/i.test(comm.nodeName) ||
(this.#$.pluginManager.fileInfo.regExp.test(comm.nodeName) && (!this.#$.pluginManager.fileInfo.tagAttrs[comm.nodeName] || this.#$.pluginManager.fileInfo.tagAttrs[comm.nodeName]?.every((v) => comm.hasAttribute(v)))) ||
this.#$.component.is(comm)
);
}
/**
* @description Return the range object of editor's first child node
* @returns {Range}
*/
#createDefaultRange() {
const wysiwyg = this.#frameContext.get('wysiwyg');
const range = this.#frameContext.get('_wd').createRange();
let firstFormat = wysiwyg.firstElementChild;
let focusEl = null;
if (!firstFormat) {
focusEl = dom.utils.createElement('BR');
firstFormat = dom.utils.createElement(this.#options.get('defaultLine'), null, focusEl);
wysiwyg.appendChild(firstFormat);
} else {
focusEl = firstFormat.firstChild;
if (!focusEl) {
focusEl = dom.utils.createElement('BR');
firstFormat.appendChild(focusEl);
}
}
range.setStart(focusEl, 0);
range.setEnd(focusEl, 0);
return range;
}
/**
* @internal
* @description Sets focus to the editor's wysiwyg contenteditable area and restores the last selection range within iframe context.
*/
__focus() {
try {
this.__iframeFocus = true;
const caption = dom.query.getParentElement(this.getNode(), 'figcaption');
if (caption) {
caption.focus();
} else {
this.#frameContext.get('wysiwyg').focus();
}
} finally {
// Defer flag reset — iframe.focus() triggers synchronous focus/blur events that check this flag
_w.setTimeout(() => (this.__iframeFocus = false), 0);
}
}
/**
* @internal
* @description Initialize the scroll information when the editor first loads
*/
__init() {
this.#hasScrollParents = this.#kernel._eventOrchestrator.scrollparents?.length > 0;
this.#scrollMargin = !this.#frameContext?.get('wysiwyg')
? 40
: (numbers.get(_w.getComputedStyle(this.#frameContext.get('wysiwyg')).scrollMargin, 0) || 40) + numbers.get(_w.getComputedStyle(this.#frameContext.get('wrapper')).paddingBottom, 0);
}
}
export default Selection_;