From 63d37c872b786abe9233d70b2eff0362582cbc3a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 16 May 2022 21:45:22 -0400 Subject: [PATCH] Popup positioning improvements (#2135) * Rename elementRect to sourceRect * Add getRects function to TextSourceElement and TextSourceRange * Add jsdocs * Remove unnecessary valid parameter * Remove default parameter * Make optionsContext optional * Remove unnecessary checks * Update sourceRect to use left/right rather than x/y * Update the return type of Popup*.getFrameRect * Rename some unrelated rect vars for disambiguation * Disambiguate between Popup.Rect and Popup.ValidRect * Move sourceRect destructuring * Pass multiple source rects * Simplify * Change Rect to use right/bottom rather than width/height * Update how popup offset is applied * Simplify frame offset * Remove _applyFrameOffset * Use right/bottom rather than width/height * Simplify some positioning settings * Update parameter names for clarity * Fix typos * Refactor data type for _getPosition* functions * Support using multiple source rects * Combine _getPosition functions * Refactor * Expose after dataset value * Consistently use this's property * Add jsdoc --- ext/js/app/frontend.js | 13 +- ext/js/app/popup-factory.js | 21 ++- ext/js/app/popup-proxy.js | 38 +++-- ext/js/app/popup-window.js | 8 +- ext/js/app/popup.js | 248 +++++++++++++++++++++--------- ext/js/display/display.js | 9 ++ ext/js/dom/popup-menu.js | 16 +- ext/js/dom/text-source-element.js | 4 + ext/js/dom/text-source-range.js | 4 + 9 files changed, 251 insertions(+), 110 deletions(-) diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index 102cd299..634408d9 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -565,7 +565,7 @@ class Frontend { textSource = this._textScanner.getCurrentTextSource(); if (textSource === null) { return; } } - this._showPopupContent(textSource, null); + this._showPopupContent(textSource, null, null); } _showContent(textSource, focus, dictionaryEntries, type, sentence, documentTitle, optionsContext) { @@ -601,14 +601,17 @@ class Frontend { this._showPopupContent(textSource, optionsContext, details); } - _showPopupContent(textSource, optionsContext, details=null) { - const {left, top, width, height} = textSource.getRect(); + _showPopupContent(textSource, optionsContext, details) { + const sourceRects = []; + for (const {left, top, right, bottom} of textSource.getRects()) { + sourceRects.push({left, top, right, bottom}); + } this._lastShowPromise = ( this._popup !== null ? this._popup.showContent( { optionsContext, - elementRect: {x: left, y: top, width, height, valid: true}, + sourceRects, writingMode: textSource.getWritingMode() }, details @@ -661,7 +664,7 @@ class Frontend { this._popup !== null && await this._popup.isVisible() ) { - this._showPopupContent(textSource, null); + this._showPopupContent(textSource, null, null); } } diff --git a/ext/js/app/popup-factory.js b/ext/js/app/popup-factory.js index fb4a7b8b..bef687ff 100644 --- a/ext/js/app/popup-factory.js +++ b/ext/js/app/popup-factory.js @@ -250,7 +250,9 @@ class PopupFactory { async _onApiContainsPoint({id, x, y}) { const popup = this._getPopup(id); - [x, y] = this._convertPopupPointToRootPagePoint(popup, x, y); + const offset = this._getPopupOffset(popup); + x += offset.x; + y += offset.y; return await popup.containsPoint(x, y); } @@ -258,9 +260,13 @@ class PopupFactory { const popup = this._getPopup(id); if (!this._popupCanShow(popup)) { return; } - const {elementRect} = details; - if (typeof elementRect !== 'undefined') { - [elementRect.x, elementRect.y] = this._convertPopupPointToRootPagePoint(popup, elementRect.x, elementRect.y); + const offset = this._getPopupOffset(popup); + const {sourceRects} = details; + for (const sourceRect of sourceRects) { + sourceRect.left += offset.x; + sourceRect.top += offset.y; + sourceRect.right += offset.x; + sourceRect.bottom += offset.y; } return await popup.showContent(details, displayDetails); @@ -311,16 +317,15 @@ class PopupFactory { return popup; } - _convertPopupPointToRootPagePoint(popup, x, y) { + _getPopupOffset(popup) { const {parent} = popup; if (parent !== null) { const popupRect = parent.getFrameRect(); if (popupRect.valid) { - x += popupRect.x; - y += popupRect.y; + return {x: popupRect.left, y: popupRect.top}; } } - return [x, y]; + return {x: 0, y: 0}; } _popupCanShow(popup) { diff --git a/ext/js/app/popup-proxy.js b/ext/js/app/popup-proxy.js index a98461c4..2bf81d0a 100644 --- a/ext/js/app/popup-proxy.js +++ b/ext/js/app/popup-proxy.js @@ -40,7 +40,8 @@ class PopupProxy extends EventDispatcher { this._frameId = frameId; this._frameOffsetForwarder = frameOffsetForwarder; - this._frameOffset = [0, 0]; + this._frameOffsetX = 0; + this._frameOffsetY = 0; this._frameOffsetPromise = null; this._frameOffsetUpdatedAt = null; this._frameOffsetExpireTimeout = 1000; @@ -178,22 +179,28 @@ class PopupProxy extends EventDispatcher { async containsPoint(x, y) { if (this._frameOffsetForwarder !== null) { await this._updateFrameOffset(); - [x, y] = this._applyFrameOffset(x, y); + x += this._frameOffsetX; + y += this._frameOffsetY; } return await this._invokeSafe('PopupFactory.containsPoint', {id: this._id, x, y}, false); } /** * Shows and updates the positioning and content of the popup. - * @param {{optionsContext: object, elementRect: {x: number, y: number, width: number, height: number}, writingMode: string}} details Settings for the outer popup. - * @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. + * @param {Popup.ContentDetails} details Settings for the outer popup. + * @param {Display.ContentDetails} displayDetails The details parameter passed to `Display.setContent`. * @returns {Promise} */ async showContent(details, displayDetails) { - const {elementRect} = details; - if (typeof elementRect !== 'undefined' && this._frameOffsetForwarder !== null) { + if (this._frameOffsetForwarder !== null) { + const {sourceRects} = details; await this._updateFrameOffset(); - [elementRect.x, elementRect.y] = this._applyFrameOffset(elementRect.x, elementRect.y); + for (const sourceRect of sourceRects) { + sourceRect.left += this._frameOffsetX; + sourceRect.top += this._frameOffsetY; + sourceRect.right += this._frameOffsetX; + sourceRect.bottom += this._frameOffsetY; + } } return await this._invokeSafe('PopupFactory.showContent', {id: this._id, details, displayDetails}); } @@ -254,11 +261,11 @@ class PopupProxy extends EventDispatcher { /** * Gets the rectangle of the DOM frame, synchronously. - * @returns {{x: number, y: number, width: number, height: number, valid: boolean}} The rect. + * @returns {Popup.ValidRect} The rect. * `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame. */ getFrameRect() { - return {x: 0, y: 0, width: 0, height: 0, valid: false}; + return {left: 0, top: 0, right: 0, bottom: 0, valid: false}; } /** @@ -317,8 +324,12 @@ class PopupProxy extends EventDispatcher { this._frameOffsetPromise = this._frameOffsetForwarder.getOffset(); try { const offset = await this._frameOffsetPromise; - this._frameOffset = offset !== null ? offset : [0, 0]; - if (offset === null) { + if (offset !== null) { + this._frameOffsetX = offset[0]; + this._frameOffsetY = offset[1]; + } else { + this._frameOffsetX = 0; + this._frameOffsetY = 0; this.trigger('offsetNotFound'); return; } @@ -329,9 +340,4 @@ class PopupProxy extends EventDispatcher { this._frameOffsetPromise = null; } } - - _applyFrameOffset(x, y) { - const [offsetX, offsetY] = this._frameOffset; - return [x + offsetX, y + offsetY]; - } } diff --git a/ext/js/app/popup-window.js b/ext/js/app/popup-window.js index edb3a181..6a63a9cb 100644 --- a/ext/js/app/popup-window.js +++ b/ext/js/app/popup-window.js @@ -168,8 +168,8 @@ class PopupWindow extends EventDispatcher { /** * Shows and updates the positioning and content of the popup. - * @param {{optionsContext: object, elementRect: {x: number, y: number, width: number, height: number}, writingMode: string}} details Settings for the outer popup. - * @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. + * @param {Popup.ContentDetails} details Settings for the outer popup. + * @param {Display.ContentDetails} displayDetails The details parameter passed to `Display.setContent`. * @returns {Promise} */ async showContent(_details, displayDetails) { @@ -229,11 +229,11 @@ class PopupWindow extends EventDispatcher { /** * Gets the rectangle of the DOM frame, synchronously. - * @returns {{x: number, y: number, width: number, height: number, valid: boolean}} The rect. + * @returns {Popup.ValidRect} The rect. * `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame. */ getFrameRect() { - return {x: 0, y: 0, width: 0, height: 0, valid: false}; + return {left: 0, top: 0, right: 0, bottom: 0, valid: false}; } /** diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index ae97093e..97808b31 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -26,6 +26,44 @@ * This class is the container which hosts the display of search results. */ class Popup extends EventDispatcher { + /** + * Information about how popup content should be shown, specifically related to the outer popup frame. + * @typedef {object} ContentDetails + * @property {?object} optionsContext The options context for the content to show. + * @property {Rect[]} sourceRects The rectangles of the source content. + * @property {'horizontal-tb' | 'vertical-rl' | 'vertical-lr' | 'sideways-rl' | 'sideways-lr'} writingMode The normalized CSS writing-mode value of the source content. + */ + + /** + * A rectangle representing a DOM region, similar to DOMRect. + * @typedef {object} Rect + * @property {number} left The left position of the rectangle. + * @property {number} top The top position of the rectangle. + * @property {number} right The right position of the rectangle. + * @property {number} bottom The bottom position of the rectangle. + */ + + /** + * A rectangle representing a DOM region, similar to DOMRect but with a `valid` property. + * @typedef {object} ValidRect + * @property {number} left The left position of the rectangle. + * @property {number} top The top position of the rectangle. + * @property {number} right The right position of the rectangle. + * @property {number} bottom The bottom position of the rectangle. + * @property {boolean} valid Whether or not the rectangle is valid. + */ + + /** + * A rectangle representing a DOM region for placing the popup frame. + * @typedef {object} SizeRect + * @property {number} left The left position of the rectangle. + * @property {number} top The top position of the rectangle. + * @property {number} width The width of the rectangle. + * @property {number} height The height of the rectangle. + * @property {boolean} after Whether or not the rectangle is positioned to the right of the source rectangle. + * @property {boolean} below Whether or not the rectangle is positioned below the source rectangle. + */ + /** * Creates a new instance. * @param {object} details @@ -63,8 +101,9 @@ class Popup extends EventDispatcher { this._horizontalOffset2 = 10; this._verticalOffset2 = 0; this._verticalTextPosition = 'before'; - this._horizontalTextPosition = 'below'; + this._horizontalTextPositionBelow = true; this._displayMode = 'default'; + this._displayModeIsFullWidth = false; this._scaleRelativeToVisualViewport = true; this._useSecureFrameUrl = true; this._useShadowDom = true; @@ -237,7 +276,7 @@ class Popup extends EventDispatcher { async containsPoint(x, y) { for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup.child) { const rect = popup.getFrameRect(); - if (rect.valid && x >= rect.x && y >= rect.y && x < rect.x + rect.width && y < rect.y + rect.height) { + if (rect.valid && x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) { return true; } } @@ -246,21 +285,19 @@ class Popup extends EventDispatcher { /** * Shows and updates the positioning and content of the popup. - * @param {{optionsContext: object, elementRect: {x: number, y: number, width: number, height: number}, writingMode: string}} details Settings for the outer popup. - * @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. + * @param {ContentDetails} details Settings for the outer popup. + * @param {Display.ContentDetails} displayDetails The details parameter passed to `Display.setContent`. * @returns {Promise} */ async showContent(details, displayDetails) { if (!this._optionsAssigned) { throw new Error('Options not assigned'); } - const {optionsContext, elementRect, writingMode} = details; + const {optionsContext, sourceRects, writingMode} = details; if (optionsContext !== null) { await this._setOptionsContextIfDifferent(optionsContext); } - if (typeof elementRect !== 'undefined' && typeof writingMode !== 'undefined') { - await this._show(elementRect, writingMode); - } + await this._show(sourceRects, writingMode); if (displayDetails !== null) { this._invokeSafe('Display.setContent', {details: displayDetails}); @@ -327,12 +364,12 @@ class Popup extends EventDispatcher { /** * Gets the rectangle of the DOM frame, synchronously. - * @returns {{x: number, y: number, width: number, height: number, valid: boolean}} The rect. + * @returns {ValidRect} The rect. * `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame. */ getFrameRect() { - const {left, top, width, height} = this._frame.getBoundingClientRect(); - return {x: left, y: top, width, height, valid: true}; + const {left, top, right, bottom} = this._frame.getBoundingClientRect(); + return {left, top, right, bottom, valid: true}; } /** @@ -523,42 +560,25 @@ class Popup extends EventDispatcher { } } - async _show(elementRect, writingMode) { + async _show(sourceRects, writingMode) { const injected = await this._inject(); if (!injected) { return; } - const frame = this._frame; - const frameRect = frame.getBoundingClientRect(); - const viewport = this._getViewport(this._scaleRelativeToVisualViewport); - const scale = this._contentScale; - const scaleRatio = this._frameSizeContentScale === null ? 1.0 : scale / this._frameSizeContentScale; - this._frameSizeContentScale = scale; - const getPositionArgs = [ - elementRect, - Math.max(frameRect.width * scaleRatio, this._initialWidth * scale), - Math.max(frameRect.height * scaleRatio, this._initialHeight * scale), - viewport, - scale, - writingMode - ]; - let [x, y, width, height, below] = ( - writingMode === 'horizontal-tb' || this._verticalTextPosition === 'default' ? - this._getPositionForHorizontalText(...getPositionArgs) : - this._getPositionForVerticalText(...getPositionArgs) - ); + let {left, top, width, height, after, below} = this._getPosition(sourceRects, writingMode, viewport); - frame.dataset.popupDisplayMode = this._displayMode; - frame.dataset.below = `${below}`; - - if (this._displayMode === 'full-width') { - x = viewport.left; - y = below ? viewport.bottom - height : viewport.top; + if (this._displayModeIsFullWidth) { + left = viewport.left; + top = below ? viewport.bottom - height : viewport.top; width = viewport.right - viewport.left; } - frame.style.left = `${x}px`; - frame.style.top = `${y}px`; + const frame = this._frame; + frame.dataset.popupDisplayMode = this._displayMode; + frame.dataset.after = `${after}`; + frame.dataset.below = `${below}`; + frame.style.left = `${left}px`; + frame.style.top = `${top}px`; this._setFrameSize(width, height); this._setVisible(true); @@ -652,52 +672,97 @@ class Popup extends EventDispatcher { return fullscreenElement; } - _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale) { - const preferBelow = (this._horizontalTextPosition === 'below'); - const horizontalOffset = this._horizontalOffset * offsetScale; - const verticalOffset = this._verticalOffset * offsetScale; + /** + * @param {Rect[]} sourceRects + * @param {string} writingMode + * @returns {SizeRect} + */ + _getPosition(sourceRects, writingMode, viewport) { + const scale = this._contentScale; + const scaleRatio = this._frameSizeContentScale === null ? 1.0 : scale / this._frameSizeContentScale; + this._frameSizeContentScale = scale; + const frameRect = this._frame.getBoundingClientRect(); + const frameWidth = Math.max(frameRect.width * scaleRatio, this._initialWidth * scale); + const frameHeight = Math.max(frameRect.height * scaleRatio, this._initialHeight * scale); - const [x, w] = this._getConstrainedPosition( - elementRect.x + elementRect.width - horizontalOffset, - elementRect.x + horizontalOffset, - width, + const horizontal = (writingMode === 'horizontal-tb' || this._verticalTextPosition === 'default'); + let preferAfter; + let horizontalOffset; + let verticalOffset; + if (horizontal) { + preferAfter = this._horizontalTextPositionBelow; + horizontalOffset = this._horizontalOffset; + verticalOffset = this._verticalOffset; + } else { + preferAfter = this._isVerticalTextPopupOnRight(this._verticalTextPosition, writingMode); + horizontalOffset = this._horizontalOffset2; + verticalOffset = this._verticalOffset2; + } + horizontalOffset *= scale; + verticalOffset *= scale; + + let best = null; + const sourceRectsLength = sourceRects.length; + for (let i = 0, ii = (sourceRectsLength > 1 ? sourceRectsLength : 0); i <= ii; ++i) { + const sourceRect = i < sourceRectsLength ? sourceRects[i] : this._getBoundingSourceRect(sourceRects); + const result = ( + horizontal ? + this._getPositionForHorizontalText(sourceRect, frameWidth, frameHeight, viewport, horizontalOffset, verticalOffset, preferAfter) : + this._getPositionForVerticalText(sourceRect, frameWidth, frameHeight, viewport, horizontalOffset, verticalOffset, preferAfter) + ); + if (i < ii && this._isOverlapping(result, sourceRects, i)) { continue; } + if (best === null || result.height > best.height) { + best = result; + if (result.height >= frameHeight) { break; } + } + } + return best; + } + + /** + * @returns {SizeRect} + */ + _getPositionForHorizontalText(sourceRect, frameWidth, frameHeight, viewport, horizontalOffset, verticalOffset, preferBelow) { + const [left, width, after] = this._getConstrainedPosition( + sourceRect.right - horizontalOffset, + sourceRect.left + horizontalOffset, + frameWidth, viewport.left, viewport.right, true ); - const [y, h, below] = this._getConstrainedPositionBinary( - elementRect.y - verticalOffset, - elementRect.y + elementRect.height + verticalOffset, - height, + const [top, height, below] = this._getConstrainedPositionBinary( + sourceRect.top - verticalOffset, + sourceRect.bottom + verticalOffset, + frameHeight, viewport.top, viewport.bottom, preferBelow ); - return [x, y, w, h, below]; + return {left, top, width, height, after, below}; } - _getPositionForVerticalText(elementRect, width, height, viewport, offsetScale, writingMode) { - const preferRight = this._isVerticalTextPopupOnRight(this._verticalTextPosition, writingMode); - const horizontalOffset = this._horizontalOffset2 * offsetScale; - const verticalOffset = this._verticalOffset2 * offsetScale; - - const [x, w] = this._getConstrainedPositionBinary( - elementRect.x - horizontalOffset, - elementRect.x + elementRect.width + horizontalOffset, - width, + /** + * @returns {SizeRect} + */ + _getPositionForVerticalText(sourceRect, frameWidth, frameHeight, viewport, horizontalOffset, verticalOffset, preferRight) { + const [left, width, after] = this._getConstrainedPositionBinary( + sourceRect.left - horizontalOffset, + sourceRect.right + horizontalOffset, + frameWidth, viewport.left, viewport.right, preferRight ); - const [y, h, below] = this._getConstrainedPosition( - elementRect.y + elementRect.height - verticalOffset, - elementRect.y + verticalOffset, - height, + const [top, height, below] = this._getConstrainedPosition( + sourceRect.bottom - verticalOffset, + sourceRect.top + verticalOffset, + frameHeight, viewport.top, viewport.bottom, true ); - return [x, y, w, h, below]; + return {left, top, width, height, after, below}; } _isVerticalTextPopupOnRight(positionPreference, writingMode) { @@ -706,10 +771,9 @@ class Popup extends EventDispatcher { return !this._isWritingModeLeftToRight(writingMode); case 'after': return this._isWritingModeLeftToRight(writingMode); - case 'left': - return false; case 'right': return true; + // case 'left': default: return false; } @@ -806,8 +870,9 @@ class Popup extends EventDispatcher { this._horizontalOffset2 = general.popupHorizontalOffset2; this._verticalOffset2 = general.popupVerticalOffset2; this._verticalTextPosition = general.popupVerticalTextPosition; - this._horizontalTextPosition = general.popupHorizontalTextPosition; + this._horizontalTextPositionBelow = (this._verticalTextPosition === 'below'); this._displayMode = general.popupDisplayMode; + this._displayModeIsFullWidth = (this._displayMode === 'full-width'); this._scaleRelativeToVisualViewport = general.popupScaleRelativeToVisualViewport; this._useSecureFrameUrl = general.useSecurePopupFrameUrl; this._useShadowDom = general.usePopupShadowDom; @@ -820,4 +885,49 @@ class Popup extends EventDispatcher { if (deepEqual(this._optionsContext, optionsContext)) { return; } await this._setOptionsContext(optionsContext); } + + /** + * @param {Rect[]} sourceRects + * @returns {Rect} + */ + _getBoundingSourceRect(sourceRects) { + switch (sourceRects.length) { + case 0: return {left: 0, top: 0, right: 0, bottom: 0}; + case 1: return sourceRects[0]; + } + let {left, top, right, bottom} = sourceRects[0]; + for (let i = 1, ii = sourceRects.length; i < ii; ++i) { + const sourceRect = sourceRects[i]; + left = Math.min(left, sourceRect.left); + top = Math.min(top, sourceRect.top); + right = Math.max(right, sourceRect.right); + bottom = Math.max(bottom, sourceRect.bottom); + } + return {left, top, right, bottom}; + } + + /** + * @param {SizeRect} sizeRect + * @param {Rect[]} sourceRects + * @param {number} ignoreIndex + * @returns {boolean} + */ + _isOverlapping(sizeRect, sourceRects, ignoreIndex) { + const {left, top} = sizeRect; + const right = left + sizeRect.width; + const bottom = top + sizeRect.height; + for (let i = 0, ii = sourceRects.length; i < ii; ++i) { + if (i === ignoreIndex) { continue; } + const sourceRect = sourceRects[i]; + if ( + left < sourceRect.right && + right > sourceRect.left && + top < sourceRect.bottom && + bottom > sourceRect.top + ) { + return true; + } + } + return false; + } } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index c16f7cae..82364233 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -36,6 +36,11 @@ */ class Display extends EventDispatcher { + /** + * Information about how popup content should be shown, specifically related to the inner popup content. + * @typedef {object} ContentDetails + */ + constructor(tabId, frameId, pageType, japaneseUtil, documentFocusController, hotkeyHandler) { super(); this._tabId = tabId; @@ -341,6 +346,10 @@ class Display extends EventDispatcher { this.trigger('optionsUpdated', {options}); } + /** + * Updates the content of the display. + * @param {ContentDetails} details + */ setContent(details) { const {focus, params, state, content} = details; const historyMode = this._historyHasChanged ? details.historyMode : 'clear'; diff --git a/ext/js/dom/popup-menu.js b/ext/js/dom/popup-menu.js index 66967002..7f5e3130 100644 --- a/ext/js/dom/popup-menu.js +++ b/ext/js/dom/popup-menu.js @@ -154,8 +154,8 @@ class PopupMenu extends EventDispatcher { // Position const menu = this._node; - const fullRect = this._containerNode.getBoundingClientRect(); - const sourceRect = this._sourceElement.getBoundingClientRect(); + const containerNodeRect = this._containerNode.getBoundingClientRect(); + const sourceElementRect = this._sourceElement.getBoundingClientRect(); const menuRect = menu.getBoundingClientRect(); let top = menuRect.top; let bottom = menuRect.bottom; @@ -166,19 +166,19 @@ class PopupMenu extends EventDispatcher { } let x = ( - sourceRect.left + - sourceRect.width * ((-horizontal * horizontalCover + 1) * 0.5) + + sourceElementRect.left + + sourceElementRect.width * ((-horizontal * horizontalCover + 1) * 0.5) + menuRect.width * ((-horizontal + 1) * -0.5) ); let y = ( - sourceRect.top + + sourceElementRect.top + (menuRect.top - top) + - sourceRect.height * ((-vertical * verticalCover + 1) * 0.5) + + sourceElementRect.height * ((-vertical * verticalCover + 1) * 0.5) + (bottom - top) * ((-vertical + 1) * -0.5) ); - x = Math.max(0.0, Math.min(fullRect.width - menuRect.width, x)); - y = Math.max(0.0, Math.min(fullRect.height - menuRect.height, y)); + x = Math.max(0.0, Math.min(containerNodeRect.width - menuRect.width, x)); + y = Math.max(0.0, Math.min(containerNodeRect.height - menuRect.height, y)); menu.style.left = `${x}px`; menu.style.top = `${y}px`; diff --git a/ext/js/dom/text-source-element.js b/ext/js/dom/text-source-element.js index 38b29a4c..1a842310 100644 --- a/ext/js/dom/text-source-element.js +++ b/ext/js/dom/text-source-element.js @@ -94,6 +94,10 @@ class TextSourceElement { return this._element.getBoundingClientRect(); } + getRects() { + return this.getClientRects(); + } + getWritingMode() { return 'horizontal-tb'; } diff --git a/ext/js/dom/text-source-range.js b/ext/js/dom/text-source-range.js index 6dfa2158..e0e2c5b0 100644 --- a/ext/js/dom/text-source-range.js +++ b/ext/js/dom/text-source-range.js @@ -94,6 +94,10 @@ class TextSourceRange { return this._range.getBoundingClientRect(); } + getRects() { + return this._range.getClientRects(); + } + getWritingMode() { return TextSourceRange.getElementWritingMode(TextSourceRange.getParentElement(this._range.startContainer)); }