UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

892 lines (806 loc) 33.6 kB
import { getParentElement } from '../../../helper/dom/domQuery'; import { isWysiwygFrame, isElement } from '../../../helper/dom/domCheck'; import { hasClass, addClass, removeClass, getClientSize } from '../../../helper/dom/domUtils'; import { numbers } from '../../../helper'; import { _w, _d } from '../../../helper/env'; /** * @typedef {Object} RectsInfo Bounding rectangle information of the selection range. * @property {number} rects.left - The left position of the selection. * @property {number} rects.right - The right position of the selection. * @property {number} rects.top - The top position of the selection. * @property {number} rects.bottom - The bottom position of the selection. * @property {boolean} [rects.noText] - Whether the selection contains text. * @property {number} [rects.width] - The width of the selection. * @property {number} [rects.height] - The height of the selection. */ /** * @typedef {Object} OffsetInfo * @property {number} top - The top position of the node relative to the entire document, including iframe offsets. * @property {number} left - The left position of the node relative to the entire document, including iframe offsets. */ /** * @typedef {Object} OffsetLocalInfo * @property {number} top - The top position of the node relative to the WYSIWYG editor. * @property {number} left - The left position of the node relative to the WYSIWYG editor. * @property {number} right - The right position of the node relative to the WYSIWYG editor. * @property {number} scrollX - The horizontal scroll offset inside the WYSIWYG editor. * @property {number} scrollY - The vertical scroll offset inside the WYSIWYG editor. * @property {number} scrollH - The vertical scroll height inside the WYSIWYG editor. */ /** * @typedef {Object} OffsetGlobalInfo * @property {number} top - The top position of the element relative to the entire document. * @property {number} left - The left position of the element relative to the entire document. * @property {number} fixedTop - The top position within the current viewport, without taking scrolling into account. * @property {number} fixedLeft - The left position within the current viewport, without taking scrolling into account. * @property {number} width - The total width of the element, including its content, padding, and border. * @property {number} height - The total height of the element, including its content, padding, and border. */ /** * @typedef {Object} OffsetGlobalScrollInfo * @property {number} top - Total top scroll distance * @property {number} left - Total left scroll distance * @property {number} width - Total width including scrollable area * @property {number} height - Total height including scrollable area * @property {number} x - Horizontal offset from the top reference element * @property {number} y - Vertical offset from the top reference element * @property {HTMLElement|Window|null} ohOffsetEl - Element or window used as the vertical scroll reference * @property {HTMLElement|Window|null} owOffsetEl - Element or window used as the horizontal scroll reference * @property {number} oh - Height of the vertical scrollable area (clientHeight) * @property {number} ow - Width of the horizontal scrollable area (clientWidth) * @property {boolean} heightEditorRefer - Indicates if the vertical scroll reference is the editor area * @property {boolean} widthEditorRefer - Indicates if the horizontal scroll reference is the editor area * @property {number} ts - Top position of the height offset element relative to the viewport * @property {number} ls - Left position of the width offset element relative to the viewport */ /** * @typedef {Object} OffsetWWScrollInfo * @property {number} top - The top scroll offset inside the WYSIWYG editor. * @property {number} left - The left scroll offset inside the WYSIWYG editor. * @property {number} width - The total width of the WYSIWYG editor's scrollable area. * @property {number} height - The total height of the WYSIWYG editor's scrollable area. * @property {number} bottom - The sum of `top` and `height`, representing the bottom-most scrollable position. */ /** * @description Offset class, get the position of the element */ class Offset { #$; #store; #shadowRoot; #carrierWrapper; #context; #frameContext; #options; #frameOptions; /** * @constructor * @param {SunEditor.Kernel} kernel */ constructor(kernel) { this.#$ = kernel.$; this.#store = kernel.store; this.#shadowRoot = this.#$.contextProvider.shadowRoot; this.#carrierWrapper = this.#$.contextProvider.carrierWrapper; this.#context = this.#$.context; this.#frameContext = this.#$.frameContext; this.#options = this.#$.options; this.#frameOptions = this.#$.frameOptions; } /** * @description Gets the position just outside the argument's internal editor (wysiwygFrame). * @param {Node} node Target node. * @returns {OffsetInfo} Position relative to the editor frame. */ get(node) { const wFrame = this.#frameContext.get('wysiwygFrame'); const iframe = /iframe/i.test(wFrame?.nodeName); const off = this.getLocal(node); return { left: off.left + (iframe ? wFrame.parentElement.offsetLeft : 0), top: off.top + (iframe ? wFrame.parentElement.offsetTop : 0), }; } /** * @description Gets the position inside the internal editor of the argument. * @param {Node} node Target node. * @returns {OffsetLocalInfo} Position relative to the WYSIWYG editor. */ getLocal(node) { const target = /** @type {HTMLElement} */ (node); let offsetLeft = 0; let offsetTop = 0; let l = 0; let t = 0; let r = 0; let offsetElement = target.nodeType === 3 ? target.parentElement : target; const targetWidth = target.offsetWidth; const wysiwyg = getParentElement(target, isWysiwygFrame.bind(this)); const self = offsetElement; while (offsetElement && !hasClass(offsetElement, 'se-wrapper') && offsetElement !== wysiwyg) { offsetLeft += offsetElement.offsetLeft - (self !== offsetElement ? offsetElement.scrollLeft : 0); offsetTop += offsetElement.offsetTop + (self !== offsetElement ? offsetElement.scrollTop : 0); offsetElement = /** @type {HTMLElement} */ (offsetElement.offsetParent); } const wwFrame = this.#frameContext.get('wysiwygFrame'); if (this.#frameContext.get('wysiwyg').contains(target)) { l = wwFrame.offsetLeft; t = wwFrame.offsetTop; r = wwFrame.parentElement.offsetWidth - (wwFrame.offsetLeft + wwFrame.offsetWidth); } const eventWysiwyg = this.#frameContext.get('eventWysiwyg'); offsetLeft += l - (wysiwyg ? wysiwyg.scrollLeft : 0); offsetTop += t - (wysiwyg ? wysiwyg.scrollTop : 0); return { left: offsetLeft, top: offsetTop, right: offsetElement?.offsetWidth ? offsetElement.offsetWidth - (offsetLeft - l + targetWidth) + r : 0, scrollX: eventWysiwyg.scrollLeft || eventWysiwyg.scrollX || 0, scrollY: eventWysiwyg.scrollTop || eventWysiwyg.scrollY || 0, scrollH: this.#frameContext.get('wysiwyg').scrollHeight || 0, }; } /** * @description Returns the position of the argument relative to the global document. * This is a refactored version using getBoundingClientRect for better performance and accuracy. * @param {?Node} [node] Target element. * @returns {OffsetGlobalInfo} Global position and scroll values. */ getGlobal(node) { const topArea = this.#frameContext.get('topArea'); const wFrame = this.#frameContext.get('wysiwygFrame'); node ||= topArea; if (!isElement(node)) { return { top: 0, left: 0, fixedTop: 0, fixedLeft: 0, width: 0, height: 0 }; } const element = /** @type {HTMLElement} */ (node); const rect = element.getBoundingClientRect(); let top = rect.top; let left = rect.left; const isIframe = /^iframe$/i.test(wFrame.nodeName); if (isIframe && wFrame.contentDocument.contains(element)) { const iframeRect = wFrame.getBoundingClientRect(); top += iframeRect.top; left += iframeRect.left; } const wy = _w.scrollY; const wx = _w.scrollX; return { top: top + wy, left: left + wx, fixedTop: top, fixedLeft: left, width: element.offsetWidth, height: element.offsetHeight, }; } /** * @deprecated * @description Gets the current editor-relative scroll offset. * @param {?Node} [node] Target element. * @returns {OffsetGlobalScrollInfo} Global scroll information. */ getGlobalScroll(node) { const topArea = this.#frameContext.get('topArea'); let isTop = false; let targetAbs = false; node ||= topArea; if (node === topArea) isTop = true; if (!isTop && isElement(node)) { targetAbs = _w.getComputedStyle(node).position === 'absolute'; } const element = /** @type {HTMLElement} */ (node); let t = 0, l = 0, h = 0, w = 0, x = 0, y = 0, oh = 0, ow = 0, ohOffsetEl = null, owOffsetEl = null, ohel = null, owel = null, el = element; while (el) { t += el.scrollTop; l += el.scrollLeft; h += el.scrollHeight; w += el.scrollWidth; if (el.scrollTop > 0) { y += el.offsetTop; } if (el.scrollHeight >= el.clientHeight) { oh = /^html$/i.test(el.nodeName) ? oh || el.clientHeight : el.clientHeight + (ohel ? -ohel.clientTop : 0); ohOffsetEl = ohel || ohOffsetEl || el; ohel = el; } if (el.scrollLeft > 0) { x += el.offsetLeft; } if (el.scrollWidth >= el.clientWidth) { ow = /^html$/i.test(el.nodeName) ? ow || el.clientWidth : el.clientWidth + (owel ? -owel.clientLeft : 0); owOffsetEl = owel || owOffsetEl || el; owel = el; } el = el.parentElement; } if (!targetAbs && !isTop && /^iframe$/i.test(this.#frameContext.get('wysiwygFrame').nodeName)) { el = this.#frameContext.get('wrapper'); ohOffsetEl = owOffsetEl = topArea; while (el) { t += el.scrollTop; l += el.scrollLeft; h += el.scrollHeight; w += el.scrollWidth; if (el.scrollTop > 0) { y += el.offsetTop; } if (el.scrollHeight >= el.clientHeight) { oh = /^html$/i.test(el.nodeName) ? oh || el.clientHeight : el.clientHeight + (ohel ? -ohel.clientTop : 0); ohel = el; } if (el.scrollLeft > 0) { x += el.offsetLeft; } if (el.scrollWidth >= el.clientWidth) { ow = /^html$/i.test(el.nodeName) ? ow || el.clientWidth : el.clientWidth + (owel ? -owel.clientLeft : 0); owel = el; } el = el.parentElement; } } el = /** @type {HTMLElement} */ (this.#shadowRoot?.host); if (el) ohOffsetEl = owOffsetEl = topArea; while (el) { t += el.scrollTop; l += el.scrollLeft; h += el.scrollHeight; w += el.scrollWidth; if (el.scrollTop > 0) { y += el.offsetTop; } if (el.scrollHeight >= el.clientHeight) { oh = /^html$/i.test(el.nodeName) ? oh || el.clientHeight : el.clientHeight + (ohel ? -ohel.clientTop : 0); ohel = el; } if (el.scrollLeft > 0) { x += el.offsetLeft; } if (el.scrollWidth >= el.clientWidth) { ow = /^html$/i.test(el.nodeName) ? ow || el.clientWidth : el.clientWidth + (owel ? -owel.clientLeft : 0); owel = el; } el = el.parentElement; } const heightEditorRefer = topArea.contains(ohOffsetEl); const widthEditorRefer = topArea.contains(owOffsetEl); ohOffsetEl = heightEditorRefer ? topArea : ohOffsetEl; owOffsetEl = widthEditorRefer ? topArea : owOffsetEl; const ts = !ohOffsetEl ? 0 : ohOffsetEl.getBoundingClientRect().top + (!ohOffsetEl.parentElement || /^html$/i.test(ohOffsetEl.parentElement.nodeName) ? _w.scrollY : 0); const ls = !owOffsetEl ? 0 : owOffsetEl.getBoundingClientRect().left + (!owOffsetEl.parentElement || /^html$/i.test(owOffsetEl.parentElement.nodeName) ? _w.scrollX : 0); oh = heightEditorRefer ? topArea.clientHeight : oh; ow = widthEditorRefer ? topArea.clientWidth : ow; const clientSize = getClientSize(this.#frameContext.get('_wd')); return { top: t, left: l, ts: ts, ls: ls, width: w, height: h, x: x, y: y, ohOffsetEl: targetAbs ? window : ohOffsetEl, owOffsetEl: targetAbs ? window : owOffsetEl, oh: targetAbs ? clientSize.h : oh, ow: targetAbs ? clientSize.w : ow, heightEditorRefer: heightEditorRefer, widthEditorRefer: widthEditorRefer, }; } /** * @description Get the scroll info of the WYSIWYG area. * @returns {OffsetWWScrollInfo} Scroll information within the editor. */ getWWScroll() { const eventWysiwyg = this.#frameContext.get('eventWysiwyg'); const top = eventWysiwyg.scrollTop || eventWysiwyg.scrollY || 0; const height = eventWysiwyg.scrollHeight || eventWysiwyg.document?.documentElement.scrollHeight || 0; return { top, left: eventWysiwyg.scrollLeft || eventWysiwyg.scrollX || 0, width: eventWysiwyg.scrollWidth || eventWysiwyg.document?.documentElement.scrollWidth || 0, height, bottom: top + height, }; } /** * @description Sets the relative position of an element * @param {HTMLElement} element Element to position * @param {HTMLElement} e_container Element's root container * @param {HTMLElement} target Target element to position against * @param {HTMLElement} t_container Target's root container * @param {Object} [opts] Options * @param {boolean} [opts.preferUp=false] Open upward by default (for bottom toolbar) */ setRelPosition(element, e_container, target, t_container, { preferUp } = {}) { const isFixedContainer = /^fixed$/i.test(_w.getComputedStyle(t_container).position); const tGlobal = this.getGlobal(target); // top if (isFixedContainer) { element.style.position = 'fixed'; if (preferUp) { element.style.top = `${tGlobal.fixedTop - element.offsetHeight}px`; } else { element.style.top = `${tGlobal.fixedTop + tGlobal.height}px`; } } else { element.style.position = ''; const isSameContainer = t_container.contains(element); const containerTop = isSameContainer ? this.getGlobal(e_container).top : 0; const elHeight = element.offsetHeight; const scrollTop = _w.scrollY; const bt = tGlobal.top; if (preferUp) { // Try to open above const menuHeight_top = containerTop - scrollTop + bt; if (menuHeight_top < elHeight) { // Not enough space above — try below const menuHeight_bottom = getClientSize(_d).h - (containerTop - scrollTop + bt + target.offsetHeight); if (menuHeight_bottom >= elHeight) { element.style.top = `${bt + target.offsetHeight}px`; } else if (menuHeight_bottom > menuHeight_top) { element.style.height = `${menuHeight_bottom}px`; element.style.top = `${bt + target.offsetHeight}px`; } else { element.style.height = `${menuHeight_top}px`; element.style.top = `${-1 * (menuHeight_top - bt + 3)}px`; } } else { element.style.top = `${bt - elHeight}px`; } } else { const menuHeight_bottom = getClientSize(_d).h - (containerTop - scrollTop + bt + target.offsetHeight); if (menuHeight_bottom < elHeight) { let menuTop = -1 * (elHeight - bt + 3); const insTop = containerTop - scrollTop + menuTop; const menuHeight_top = elHeight + (insTop < 0 ? insTop : 0); if (menuHeight_top > menuHeight_bottom) { element.style.height = `${menuHeight_top}px`; menuTop = -1 * (menuHeight_top - bt + 3); } else { element.style.height = `${menuHeight_bottom}px`; menuTop = bt + target.offsetHeight; } element.style.top = `${menuTop}px`; } else { element.style.top = `${bt + target.offsetHeight}px`; } } } // left const ew = element.offsetWidth; const tw = target.offsetWidth; const tl = tGlobal.left; const tcleft = this.getGlobal(t_container).left; if (this.#options.get('_rtl')) { const rtlW = ew > tw ? ew - tw : 0; const rtlL = rtlW > 0 ? 0 : tw - ew; element.style.left = `${tl - rtlW + rtlL}px`; if (tcleft > this.getGlobal(element).left) { element.style.left = tcleft + 'px'; } } else { const cw = t_container.offsetWidth + tcleft; const overLeft = cw <= ew ? 0 : cw - (tl + ew); if (overLeft < 0) { element.style.left = `${tl + overLeft}px`; } else { element.style.left = `${tl}px`; } } } /** * @description Sets the absolute position of an element * @param {HTMLElement} element Element to position * @param {HTMLElement} target Target element * @param {Object} params Position parameters * @param {boolean} [params.isWWTarget=false] Whether the target is within the editor's WYSIWYG area * @param {{left:number, right:number, top:number}} [params.addOffset={left:0, right:0, top:0}] Additional offset * @param {"bottom"|"top"} [params.position="bottom"] Position ('bottom'|'top') * @param {*} params.inst Instance object of caller * @param {HTMLElement} [params.sibling=null] The sibling controller element * @returns {{position: "top" | "bottom"} | undefined} Success -> {position: current position} * @example * const result = editor.$.offset.setAbsPosition(controller, targetElement, { * position: 'bottom', inst: this, addOffset: { left: 0, right: 0, top: 0 } * }); */ setAbsPosition(element, target, params) { const addOffset = { left: 0, right: 0, top: 0, ...params.addOffset, }; const position = params.position || 'bottom'; const inst = params.inst; const isLTR = !this.#options.get('_rtl'); if (!isLTR) { addOffset.left *= -1; } const isIframe = this.#frameOptions.get('iframe'); const isWWTarget = this.#frameContext.get('wrapper').contains(target) || params.isWWTarget || (isIframe ? this.#frameContext.get('wysiwyg').contains(target) : false); const isToolbarTarget = Boolean(getParentElement(target, '.se-toolbar')); const isElTarget = target.nodeType === 1; const isTextSelection = isWWTarget && !isElTarget; const isInlineTarget = isElTarget && /inline/.test(_w.getComputedStyle(target).display); const clientSize = getClientSize(_d); const wwScroll = isTextSelection ? this.getWWScroll() : this.#getWindowScroll(); const targetRect = !isWWTarget || (!isIframe && isElTarget) ? target.getBoundingClientRect() : this.#$.selection.getRects(target, 'start').rects; const targetOffset = this.getGlobal(target); const arrow = /** @type {HTMLElement} */ (hasClass(element.firstElementChild, 'se-arrow') ? element.firstElementChild : null); // top ---------------------------------------------------------------------------------------------------- const siblingH = params.sibling?.offsetHeight || 0; const ah = arrow ? arrow.offsetHeight : 0; const elH = element.offsetHeight; const targetH = target.offsetHeight; // margin const tmtw = targetRect.top; const tmbw = clientSize.h - targetRect.bottom; const globalTop = this.getGlobal(this.#frameContext.get('topArea')).top; const wScrollY = _w.scrollY; const th = this.#context.get('toolbar_main').offsetHeight; const containerToolbar = this.#options.get('toolbar_container'); const headLess = this.#store.mode.isBalloon || this.#store.mode.isInline || containerToolbar; const toolbarH = (containerToolbar && globalTop - wScrollY - th > 0) || (!this.#$.toolbar.isSticky && headLess) ? 0 : th + (this.#$.toolbar.isSticky ? this.#options.get('_toolbar_sticky') : 0); const statusBarH = this.#frameContext.get('statusbar')?.offsetHeight || 0; // check margin const { rmt, rmb, bMargin, rt } = this.#getVMargin(tmtw, tmbw, toolbarH, clientSize, targetRect, isTextSelection, isToolbarTarget); if ((isWWTarget && (rmb - statusBarH + targetH <= 0 || rmt + rt + targetH - (this.#$.toolbar.isSticky && isInlineTarget ? toolbarH : 0) <= 0)) || rmt + targetH < 0) return; const topAreaRect = this.#frameContext.get('topArea').getBoundingClientRect(); const isStickyVisible = this.#store.mode.isBottom ? topAreaRect.bottom >= _w.innerHeight - th : topAreaRect.top <= th; const isSticky = this.#$.toolbar.isSticky && this.#context.get('toolbar_main').style.display !== 'none' && (!headLess || isStickyVisible); let t = addOffset.top; let y = 0; let arrowDir = ''; // [bottom] position if (position === 'bottom') { arrowDir = 'up'; t += targetRect.bottom + ah + wScrollY; y = rmb - (elH + ah); // change to <top> position if (y - siblingH < 0) { arrowDir = 'down'; t -= targetH + elH + ah * 2; y = rmt - (elH + ah); // sticky the <top> position if (y - siblingH < 0) { arrowDir = ''; t -= y - siblingH - Math.max(1, y + elH + ah); } } } // <top> position else { arrowDir = 'down'; t += targetRect.top - elH - ah + wScrollY; y = (isSticky && !this.#store.mode.isBottom ? targetRect.top - toolbarH : rmt) - elH - ah; // change to [bottom] position if (y - siblingH < 0) { arrowDir = 'up'; t += targetH + elH + ah * 2; y = (rmb > 0 ? bMargin : rmb) - (elH + ah) - statusBarH; // sticky the [bottom] position if (y - siblingH < 0) { arrowDir = ''; t += y - 2; } } } this.#setArrow(arrow, arrowDir); element.style.top = `${t}px`; // left ---------------------------------------------------------------------------------------------------- const radius = (element.nodeType === 1 ? numbers.get(_w.getComputedStyle(element).borderRadius) : 0) || 0; const targetW = targetOffset.width; const elW = element.offsetWidth; const aw = arrow ? arrow.offsetWidth : 0; // margin const rml = targetRect.left; const rmr = clientSize.w - targetRect.right; if (isWWTarget && (rml + targetW <= 0 || rmr + targetW <= 0)) return; if (arrow) { arrow.style.left = ''; arrow.style.right = ''; } let l = addOffset.left || (addOffset.right ? (isLTR ? addOffset.right - element.offsetWidth : element.offsetWidth - addOffset.right) : 0); let x = 0; let ax = 0; let awLimit = 0; if (isLTR) { l += targetRect.left + _w.scrollX - (rml < 0 ? rml : 0); x = targetW + rml; if (x < aw) { awLimit = aw / 2 - 1 + (radius <= 2 ? 0 : radius - 2); ax = awLimit; } x = targetW + rmr - elW; if (x < 0) { l += x; awLimit = elW - 1 - (aw / 2 + (radius <= 2 ? 0 : radius - 2)); ax = -(x - aw / 2); ax = ax > awLimit ? awLimit : ax; } if (arrow && ax > 0) arrow.style.left = ax + 'px'; } else { l += targetRect.right - elW + _w.scrollX + (rmr < 0 ? rmr : 0); x = targetW + rmr; if (x < aw) { awLimit = aw / 2 - 1 + (radius <= 2 ? 0 : radius - 2); ax = awLimit; } x = targetW + rml - elW; if (x < 0) { l -= x; awLimit = aw / 2 - 1 + (radius <= 2 ? 0 : radius - 2); ax = -(x - aw / 2); ax = ax < awLimit ? awLimit : ax > elW - awLimit ? elW - awLimit : ax; } if (arrow && ax > 0) arrow.style.right = ax + 'px'; } element.style.left = `${l}px`; inst.__offset = { left: element.offsetLeft + wwScroll.left, top: element.offsetTop + wwScroll.top, addOffset: addOffset, }; return { position: arrowDir === 'up' ? 'bottom' : 'top' }; } /** * @description Sets the position of an element relative to a range * @param {HTMLElement} element Element to position * @param {?Range} range Range to position against. * - if `null`, the current selection range is used * @param {Object} [options={}] Position options * @param {"bottom"|"top"} [options.position="bottom"] Position ('bottom'|'top') * @param {number} [options.addTop=0] Additional top offset * @returns {boolean} Success / Failure * @example * const success = editor.$.offset.setRangePosition(toolbar, null, { position: 'bottom', addTop: 0 }); * if (!success) toolbar.style.display = 'none'; */ setRangePosition(element, range, { position, addTop } = {}) { element.style.top = '-10000px'; element.style.visibility = 'hidden'; element.style.display = 'block'; let positionTop = position === 'top'; range ||= this.#$.selection.getRange(); const rectsObj = this.#$.selection.getRects(range, positionTop ? 'start' : 'end'); positionTop = rectsObj.position === 'start'; const isFullScreen = this.#frameContext.get('isFullScreen'); const topArea = this.#frameContext.get('topArea'); const isInCarrier = this.#carrierWrapper.contains(element); const rects = rectsObj.rects; const scrollLeft = isFullScreen ? 0 : rectsObj.scrollLeft; const scrollTop = isFullScreen ? 0 : rectsObj.scrollTop; const editorWidth = isInCarrier ? getClientSize(_d).w : topArea.offsetWidth; const offsets = this.getGlobal(topArea); const editorLeft = isInCarrier ? 0 : offsets.left; const toolbarWidth = element.offsetWidth; const toolbarHeight = element.offsetHeight; this.#setOffsetOnRange(positionTop, rects, element, editorLeft, editorWidth, scrollLeft, scrollTop, addTop); if (this.getGlobal(element).top - offsets.top < 0) { positionTop = !positionTop; this.#setOffsetOnRange(positionTop, rects, element, editorLeft, editorWidth, scrollLeft, scrollTop, addTop); } if (toolbarWidth !== element.offsetWidth || toolbarHeight !== element.offsetHeight) { this.#setOffsetOnRange(positionTop, rects, element, editorLeft, editorWidth, scrollLeft, scrollTop, addTop); } // check margin const isTextSelection = !this.#carrierWrapper.contains(element); const clientSize = getClientSize(_d); const targetH = rects.height; const tmtw = rects.top; const tmbw = clientSize.h - rects.bottom; const toolbarH = !this.#$.toolbar.isSticky && (this.#store.mode.isBalloon || this.#store.mode.isInline) ? 0 : this.#context.get('toolbar_main').offsetHeight; const { rmt, rmb, rt } = this.#getVMargin(tmtw, tmbw, toolbarH, clientSize, rects, isTextSelection, false); if (rmb + targetH <= 0 || rmt + rt + targetH <= 0) return; element.style.visibility = ''; return true; } /** * @description Sets the position of an element relative to the selection range in the editor. * - This method calculates the top and left offsets for the element, ensuring it * - does not overflow the editor boundaries and adjusts the arrow positioning accordingly. * @param {boolean} isDirTop - Determines whether the element should be positioned above (`true`) or below (`false`) the target. * @param {RectsInfo} rects - Bounding rectangle information of the selection range. * @param {HTMLElement} element - The element to be positioned. * @param {number} editorLeft - The left position of the editor. * @param {number} editorWidth - The width of the editor. * @param {number} scrollLeft - The horizontal scroll offset. * @param {number} scrollTop - The vertical scroll offset. * @param {number} [addTop=0] - Additional top margin adjustment. */ #setOffsetOnRange(isDirTop, rects, element, editorLeft, editorWidth, scrollLeft, scrollTop, addTop = 0) { const padding = 1; const arrow = /** @type {HTMLElement} */ (element.querySelector('.se-arrow ')); const arrowMargin = Math.round(arrow.offsetWidth / 2); const elW = element.offsetWidth; const elH = rects.noText && !isDirTop ? 0 : element.offsetHeight; const absoluteLeft = (isDirTop ? rects.left : rects.right) - editorLeft - elW / 2 + scrollLeft; const overRight = absoluteLeft + elW - editorWidth; let t = (isDirTop ? rects.top - elH - arrowMargin : rects.bottom + arrowMargin) - (rects.noText ? 0 : addTop) + scrollTop; const l = absoluteLeft < 0 ? padding : overRight < 0 ? absoluteLeft : absoluteLeft - overRight - padding - 1; let resetTop = false; const space = t + (isDirTop ? this.getGlobal(this.#frameContext.get('topArea')).top : element.offsetHeight - this.#frameContext.get('wysiwyg').offsetHeight); if (!isDirTop && space > 0 && this.#getPageBottomSpace() < space) { isDirTop = true; resetTop = true; } else if (isDirTop && _d.documentElement.offsetTop > space) { isDirTop = false; resetTop = true; } if (resetTop) t = (isDirTop ? rects.top - elH - arrowMargin : rects.bottom + arrowMargin) - (rects.noText ? 0 : addTop) + scrollTop; element.style.left = Math.floor(l) + 'px'; element.style.top = Math.floor(t) + 'px'; if (isDirTop) { removeClass(arrow, 'se-arrow-up'); addClass(arrow, 'se-arrow-down'); } else { removeClass(arrow, 'se-arrow-down'); addClass(arrow, 'se-arrow-up'); } const arrow_left = Math.floor(elW / 2 + (absoluteLeft - l)); arrow.style.left = (arrow_left + arrowMargin > element.offsetWidth ? element.offsetWidth - arrowMargin : arrow_left < arrowMargin ? arrowMargin : arrow_left) + 'px'; } /** * @description Get available space from page bottom * @returns {number} Available space */ #getPageBottomSpace() { const topArea = this.#frameContext.get('topArea'); return _d.documentElement.scrollHeight - (this.getGlobal(topArea).top + topArea.offsetHeight); } /** * @description Calculates the vertical margin offsets for the target element relative to the editor frame. * - This method determines the top and bottom margins based on various conditions such as * - fullscreen mode, iframe usage, toolbar height, and scroll positions. * @param {number} tmtw Top margin to window * @param {number} tmbw Bottom margin to window * @param {number} toolbarH Toolbar height * @param {{w: number, h: number}} clientSize documentElement.clientWidth, documentElement.clientHeight * @param {RectsInfo} targetRect Target rect object * @param {boolean} isTextSelection Is text selection or Range * @param {boolean} isToolbarTarget Indicates if the target is a toolbar element * @returns {{rmt:number, rmb:number, rt:number, tMargin:number, bMargin:number}} Margin values * - rmt: top margin to frame * - rmb: bottom margin to frame * - rt: Toolbar height offset adjustment * - tMargin: top margin * - bMargin: bottom margin */ #getVMargin(tmtw, tmbw, toolbarH, clientSize, targetRect, isTextSelection, isToolbarTarget) { const wwRects = this.#$.selection.getRects(this.#frameContext.get('wysiwyg'), 'start').rects; let rmt = 0; let rmb = 0; let rt = 0; let tMargin = 0; let bMargin = 0; const isIframe = this.#frameOptions.get('iframe'); tMargin = targetRect.top; bMargin = clientSize.h - targetRect.bottom; const editorOffset = this.getGlobal(); const isBottom = this.#store.mode.isBottom; if (!isTextSelection) { const emt = editorOffset.fixedTop > 0 ? editorOffset.fixedTop : 0; const emb = _w.innerHeight - (editorOffset.fixedTop + editorOffset.height); rt = !isToolbarTarget && (this.#$.toolbar.isSticky || !this.#$.toolbar.isBalloonMode) ? toolbarH : 0; if (isBottom) { rmt = tMargin - (!isToolbarTarget ? emt : 0); rmb = bMargin - (emb > 0 ? emb : 0) - rt; } else { rmt = tMargin - (!isToolbarTarget ? emt : 0) - rt; rmb = bMargin - (emb > 0 ? emb : 0); } } else { rt = !isToolbarTarget && !this.#$.toolbar.isSticky && !this.#options.get('toolbar_container') ? toolbarH : 0; const wst = !isIframe ? editorOffset.top - _w.scrollY + rt : 0; const wsb = !isIframe ? this.#store.get('currentViewportHeight') - (editorOffset.top + editorOffset.height - _w.scrollY) : 0; let st = wst; let sb = wsb; if (isBottom) { if (toolbarH > wsb) { if (this.#$.toolbar.isSticky) { sb = toolbarH; } else { sb = wsb + toolbarH; } } else if (this.#options.get('toolbar_container') && !this.#$.toolbar.isSticky) { toolbarH = 0; } else { sb = wsb + toolbarH; } rmt = targetRect.top - (wwRects.top - wst); rmb = wwRects.bottom - (targetRect.bottom - sb) + toolbarH; rmb = rmb > 0 ? rmb : rmb - toolbarH; } else { if (toolbarH > wst) { if (this.#$.toolbar.isSticky) { st = toolbarH; } else { st = wst + toolbarH; } } else if (this.#options.get('toolbar_container') && !this.#$.toolbar.isSticky) { toolbarH = 0; } else { st = wst + toolbarH; } rmt = targetRect.top - (wwRects.top - st) + toolbarH; rmb = wwRects.bottom - (targetRect.bottom - wsb); // display margin rmt = rmt > 0 ? rmt : rmt - toolbarH; } } return { rmt, rmb, rt, tMargin, bMargin, }; } /** * @description Sets the visibility and direction of the arrow element. * - This method applies the appropriate class (`se-arrow-up` or `se-arrow-down`) * - based on the specified direction key and adjusts the visibility of the arrow. * @param {HTMLElement} arrow - The arrow element to be updated. * @param {string} key - The direction of the arrow. ("up"|"down"|"") * - Accepts `'up'` for an upward arrow, `'down'` for a downward arrow, * - or any other value to hide the arrow. */ #setArrow(arrow, key) { if (key === 'up') { if (arrow) arrow.style.visibility = ''; addClass(arrow, 'se-arrow-up'); removeClass(arrow, 'se-arrow-down'); } else if (key === 'down') { if (arrow) arrow.style.visibility = ''; addClass(arrow, 'se-arrow-down'); removeClass(arrow, 'se-arrow-up'); } else { if (arrow) arrow.style.visibility = 'hidden'; } } /** * @description Retrieves the current window scroll position and viewport size. * - Returns an object containing the scroll offsets, viewport dimensions, and boundary rects. * @returns {{ * top: number, * left: number, * width: number, * height: number, * bottom: number, * rects: RectsInfo * }} An object with scroll and viewport information. */ #getWindowScroll() { const viewPort = getClientSize(_d); return { top: _w.scrollY, left: _w.scrollX, width: viewPort.w, height: viewPort.h, bottom: _w.scrollY + viewPort.h, rects: { left: 0, top: 0, right: _w.innerWidth, bottom: this.#store.get('currentViewportHeight') || _w.innerHeight, noText: true, }, }; } } export default Offset;