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
This commit is contained in:
toasted-nutbread 2022-05-16 21:45:22 -04:00 committed by GitHub
parent 96f5a06c80
commit 63d37c872b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 251 additions and 110 deletions

View File

@ -565,7 +565,7 @@ class Frontend {
textSource = this._textScanner.getCurrentTextSource(); textSource = this._textScanner.getCurrentTextSource();
if (textSource === null) { return; } if (textSource === null) { return; }
} }
this._showPopupContent(textSource, null); this._showPopupContent(textSource, null, null);
} }
_showContent(textSource, focus, dictionaryEntries, type, sentence, documentTitle, optionsContext) { _showContent(textSource, focus, dictionaryEntries, type, sentence, documentTitle, optionsContext) {
@ -601,14 +601,17 @@ class Frontend {
this._showPopupContent(textSource, optionsContext, details); this._showPopupContent(textSource, optionsContext, details);
} }
_showPopupContent(textSource, optionsContext, details=null) { _showPopupContent(textSource, optionsContext, details) {
const {left, top, width, height} = textSource.getRect(); const sourceRects = [];
for (const {left, top, right, bottom} of textSource.getRects()) {
sourceRects.push({left, top, right, bottom});
}
this._lastShowPromise = ( this._lastShowPromise = (
this._popup !== null ? this._popup !== null ?
this._popup.showContent( this._popup.showContent(
{ {
optionsContext, optionsContext,
elementRect: {x: left, y: top, width, height, valid: true}, sourceRects,
writingMode: textSource.getWritingMode() writingMode: textSource.getWritingMode()
}, },
details details
@ -661,7 +664,7 @@ class Frontend {
this._popup !== null && this._popup !== null &&
await this._popup.isVisible() await this._popup.isVisible()
) { ) {
this._showPopupContent(textSource, null); this._showPopupContent(textSource, null, null);
} }
} }

View File

@ -250,7 +250,9 @@ class PopupFactory {
async _onApiContainsPoint({id, x, y}) { async _onApiContainsPoint({id, x, y}) {
const popup = this._getPopup(id); 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); return await popup.containsPoint(x, y);
} }
@ -258,9 +260,13 @@ class PopupFactory {
const popup = this._getPopup(id); const popup = this._getPopup(id);
if (!this._popupCanShow(popup)) { return; } if (!this._popupCanShow(popup)) { return; }
const {elementRect} = details; const offset = this._getPopupOffset(popup);
if (typeof elementRect !== 'undefined') { const {sourceRects} = details;
[elementRect.x, elementRect.y] = this._convertPopupPointToRootPagePoint(popup, elementRect.x, elementRect.y); 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); return await popup.showContent(details, displayDetails);
@ -311,16 +317,15 @@ class PopupFactory {
return popup; return popup;
} }
_convertPopupPointToRootPagePoint(popup, x, y) { _getPopupOffset(popup) {
const {parent} = popup; const {parent} = popup;
if (parent !== null) { if (parent !== null) {
const popupRect = parent.getFrameRect(); const popupRect = parent.getFrameRect();
if (popupRect.valid) { if (popupRect.valid) {
x += popupRect.x; return {x: popupRect.left, y: popupRect.top};
y += popupRect.y;
} }
} }
return [x, y]; return {x: 0, y: 0};
} }
_popupCanShow(popup) { _popupCanShow(popup) {

View File

@ -40,7 +40,8 @@ class PopupProxy extends EventDispatcher {
this._frameId = frameId; this._frameId = frameId;
this._frameOffsetForwarder = frameOffsetForwarder; this._frameOffsetForwarder = frameOffsetForwarder;
this._frameOffset = [0, 0]; this._frameOffsetX = 0;
this._frameOffsetY = 0;
this._frameOffsetPromise = null; this._frameOffsetPromise = null;
this._frameOffsetUpdatedAt = null; this._frameOffsetUpdatedAt = null;
this._frameOffsetExpireTimeout = 1000; this._frameOffsetExpireTimeout = 1000;
@ -178,22 +179,28 @@ class PopupProxy extends EventDispatcher {
async containsPoint(x, y) { async containsPoint(x, y) {
if (this._frameOffsetForwarder !== null) { if (this._frameOffsetForwarder !== null) {
await this._updateFrameOffset(); 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); return await this._invokeSafe('PopupFactory.containsPoint', {id: this._id, x, y}, false);
} }
/** /**
* Shows and updates the positioning and content of the popup. * 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 {Popup.ContentDetails} details Settings for the outer popup.
* @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. * @param {Display.ContentDetails} displayDetails The details parameter passed to `Display.setContent`.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async showContent(details, displayDetails) { async showContent(details, displayDetails) {
const {elementRect} = details; if (this._frameOffsetForwarder !== null) {
if (typeof elementRect !== 'undefined' && this._frameOffsetForwarder !== null) { const {sourceRects} = details;
await this._updateFrameOffset(); 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}); 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. * 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. * `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame.
*/ */
getFrameRect() { 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(); this._frameOffsetPromise = this._frameOffsetForwarder.getOffset();
try { try {
const offset = await this._frameOffsetPromise; 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'); this.trigger('offsetNotFound');
return; return;
} }
@ -329,9 +340,4 @@ class PopupProxy extends EventDispatcher {
this._frameOffsetPromise = null; this._frameOffsetPromise = null;
} }
} }
_applyFrameOffset(x, y) {
const [offsetX, offsetY] = this._frameOffset;
return [x + offsetX, y + offsetY];
}
} }

View File

@ -168,8 +168,8 @@ class PopupWindow extends EventDispatcher {
/** /**
* Shows and updates the positioning and content of the popup. * 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 {Popup.ContentDetails} details Settings for the outer popup.
* @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. * @param {Display.ContentDetails} displayDetails The details parameter passed to `Display.setContent`.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async showContent(_details, displayDetails) { async showContent(_details, displayDetails) {
@ -229,11 +229,11 @@ class PopupWindow extends EventDispatcher {
/** /**
* Gets the rectangle of the DOM frame, synchronously. * 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. * `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame.
*/ */
getFrameRect() { getFrameRect() {
return {x: 0, y: 0, width: 0, height: 0, valid: false}; return {left: 0, top: 0, right: 0, bottom: 0, valid: false};
} }
/** /**

View File

@ -26,6 +26,44 @@
* This class is the container which hosts the display of search results. * This class is the container which hosts the display of search results.
*/ */
class Popup extends EventDispatcher { 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. * Creates a new instance.
* @param {object} details * @param {object} details
@ -63,8 +101,9 @@ class Popup extends EventDispatcher {
this._horizontalOffset2 = 10; this._horizontalOffset2 = 10;
this._verticalOffset2 = 0; this._verticalOffset2 = 0;
this._verticalTextPosition = 'before'; this._verticalTextPosition = 'before';
this._horizontalTextPosition = 'below'; this._horizontalTextPositionBelow = true;
this._displayMode = 'default'; this._displayMode = 'default';
this._displayModeIsFullWidth = false;
this._scaleRelativeToVisualViewport = true; this._scaleRelativeToVisualViewport = true;
this._useSecureFrameUrl = true; this._useSecureFrameUrl = true;
this._useShadowDom = true; this._useShadowDom = true;
@ -237,7 +276,7 @@ class Popup extends EventDispatcher {
async containsPoint(x, y) { async containsPoint(x, y) {
for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup.child) { for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup.child) {
const rect = popup.getFrameRect(); 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; return true;
} }
} }
@ -246,21 +285,19 @@ class Popup extends EventDispatcher {
/** /**
* Shows and updates the positioning and content of the popup. * 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 {ContentDetails} details Settings for the outer popup.
* @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. * @param {Display.ContentDetails} displayDetails The details parameter passed to `Display.setContent`.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async showContent(details, displayDetails) { async showContent(details, displayDetails) {
if (!this._optionsAssigned) { throw new Error('Options not assigned'); } if (!this._optionsAssigned) { throw new Error('Options not assigned'); }
const {optionsContext, elementRect, writingMode} = details; const {optionsContext, sourceRects, writingMode} = details;
if (optionsContext !== null) { if (optionsContext !== null) {
await this._setOptionsContextIfDifferent(optionsContext); await this._setOptionsContextIfDifferent(optionsContext);
} }
if (typeof elementRect !== 'undefined' && typeof writingMode !== 'undefined') { await this._show(sourceRects, writingMode);
await this._show(elementRect, writingMode);
}
if (displayDetails !== null) { if (displayDetails !== null) {
this._invokeSafe('Display.setContent', {details: displayDetails}); this._invokeSafe('Display.setContent', {details: displayDetails});
@ -327,12 +364,12 @@ class Popup extends EventDispatcher {
/** /**
* Gets the rectangle of the DOM frame, synchronously. * 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. * `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame.
*/ */
getFrameRect() { getFrameRect() {
const {left, top, width, height} = this._frame.getBoundingClientRect(); const {left, top, right, bottom} = this._frame.getBoundingClientRect();
return {x: left, y: top, width, height, valid: true}; 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(); const injected = await this._inject();
if (!injected) { return; } if (!injected) { return; }
const frame = this._frame;
const frameRect = frame.getBoundingClientRect();
const viewport = this._getViewport(this._scaleRelativeToVisualViewport); const viewport = this._getViewport(this._scaleRelativeToVisualViewport);
const scale = this._contentScale; let {left, top, width, height, after, below} = this._getPosition(sourceRects, writingMode, viewport);
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)
);
frame.dataset.popupDisplayMode = this._displayMode; if (this._displayModeIsFullWidth) {
frame.dataset.below = `${below}`; left = viewport.left;
top = below ? viewport.bottom - height : viewport.top;
if (this._displayMode === 'full-width') {
x = viewport.left;
y = below ? viewport.bottom - height : viewport.top;
width = viewport.right - viewport.left; width = viewport.right - viewport.left;
} }
frame.style.left = `${x}px`; const frame = this._frame;
frame.style.top = `${y}px`; 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._setFrameSize(width, height);
this._setVisible(true); this._setVisible(true);
@ -652,52 +672,97 @@ class Popup extends EventDispatcher {
return fullscreenElement; return fullscreenElement;
} }
_getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale) { /**
const preferBelow = (this._horizontalTextPosition === 'below'); * @param {Rect[]} sourceRects
const horizontalOffset = this._horizontalOffset * offsetScale; * @param {string} writingMode
const verticalOffset = this._verticalOffset * offsetScale; * @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( const horizontal = (writingMode === 'horizontal-tb' || this._verticalTextPosition === 'default');
elementRect.x + elementRect.width - horizontalOffset, let preferAfter;
elementRect.x + horizontalOffset, let horizontalOffset;
width, 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.left,
viewport.right, viewport.right,
true true
); );
const [y, h, below] = this._getConstrainedPositionBinary( const [top, height, below] = this._getConstrainedPositionBinary(
elementRect.y - verticalOffset, sourceRect.top - verticalOffset,
elementRect.y + elementRect.height + verticalOffset, sourceRect.bottom + verticalOffset,
height, frameHeight,
viewport.top, viewport.top,
viewport.bottom, viewport.bottom,
preferBelow 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); * @returns {SizeRect}
const horizontalOffset = this._horizontalOffset2 * offsetScale; */
const verticalOffset = this._verticalOffset2 * offsetScale; _getPositionForVerticalText(sourceRect, frameWidth, frameHeight, viewport, horizontalOffset, verticalOffset, preferRight) {
const [left, width, after] = this._getConstrainedPositionBinary(
const [x, w] = this._getConstrainedPositionBinary( sourceRect.left - horizontalOffset,
elementRect.x - horizontalOffset, sourceRect.right + horizontalOffset,
elementRect.x + elementRect.width + horizontalOffset, frameWidth,
width,
viewport.left, viewport.left,
viewport.right, viewport.right,
preferRight preferRight
); );
const [y, h, below] = this._getConstrainedPosition( const [top, height, below] = this._getConstrainedPosition(
elementRect.y + elementRect.height - verticalOffset, sourceRect.bottom - verticalOffset,
elementRect.y + verticalOffset, sourceRect.top + verticalOffset,
height, frameHeight,
viewport.top, viewport.top,
viewport.bottom, viewport.bottom,
true true
); );
return [x, y, w, h, below]; return {left, top, width, height, after, below};
} }
_isVerticalTextPopupOnRight(positionPreference, writingMode) { _isVerticalTextPopupOnRight(positionPreference, writingMode) {
@ -706,10 +771,9 @@ class Popup extends EventDispatcher {
return !this._isWritingModeLeftToRight(writingMode); return !this._isWritingModeLeftToRight(writingMode);
case 'after': case 'after':
return this._isWritingModeLeftToRight(writingMode); return this._isWritingModeLeftToRight(writingMode);
case 'left':
return false;
case 'right': case 'right':
return true; return true;
// case 'left':
default: default:
return false; return false;
} }
@ -806,8 +870,9 @@ class Popup extends EventDispatcher {
this._horizontalOffset2 = general.popupHorizontalOffset2; this._horizontalOffset2 = general.popupHorizontalOffset2;
this._verticalOffset2 = general.popupVerticalOffset2; this._verticalOffset2 = general.popupVerticalOffset2;
this._verticalTextPosition = general.popupVerticalTextPosition; this._verticalTextPosition = general.popupVerticalTextPosition;
this._horizontalTextPosition = general.popupHorizontalTextPosition; this._horizontalTextPositionBelow = (this._verticalTextPosition === 'below');
this._displayMode = general.popupDisplayMode; this._displayMode = general.popupDisplayMode;
this._displayModeIsFullWidth = (this._displayMode === 'full-width');
this._scaleRelativeToVisualViewport = general.popupScaleRelativeToVisualViewport; this._scaleRelativeToVisualViewport = general.popupScaleRelativeToVisualViewport;
this._useSecureFrameUrl = general.useSecurePopupFrameUrl; this._useSecureFrameUrl = general.useSecurePopupFrameUrl;
this._useShadowDom = general.usePopupShadowDom; this._useShadowDom = general.usePopupShadowDom;
@ -820,4 +885,49 @@ class Popup extends EventDispatcher {
if (deepEqual(this._optionsContext, optionsContext)) { return; } if (deepEqual(this._optionsContext, optionsContext)) { return; }
await this._setOptionsContext(optionsContext); 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;
}
} }

View File

@ -36,6 +36,11 @@
*/ */
class Display extends EventDispatcher { 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) { constructor(tabId, frameId, pageType, japaneseUtil, documentFocusController, hotkeyHandler) {
super(); super();
this._tabId = tabId; this._tabId = tabId;
@ -341,6 +346,10 @@ class Display extends EventDispatcher {
this.trigger('optionsUpdated', {options}); this.trigger('optionsUpdated', {options});
} }
/**
* Updates the content of the display.
* @param {ContentDetails} details
*/
setContent(details) { setContent(details) {
const {focus, params, state, content} = details; const {focus, params, state, content} = details;
const historyMode = this._historyHasChanged ? details.historyMode : 'clear'; const historyMode = this._historyHasChanged ? details.historyMode : 'clear';

View File

@ -154,8 +154,8 @@ class PopupMenu extends EventDispatcher {
// Position // Position
const menu = this._node; const menu = this._node;
const fullRect = this._containerNode.getBoundingClientRect(); const containerNodeRect = this._containerNode.getBoundingClientRect();
const sourceRect = this._sourceElement.getBoundingClientRect(); const sourceElementRect = this._sourceElement.getBoundingClientRect();
const menuRect = menu.getBoundingClientRect(); const menuRect = menu.getBoundingClientRect();
let top = menuRect.top; let top = menuRect.top;
let bottom = menuRect.bottom; let bottom = menuRect.bottom;
@ -166,19 +166,19 @@ class PopupMenu extends EventDispatcher {
} }
let x = ( let x = (
sourceRect.left + sourceElementRect.left +
sourceRect.width * ((-horizontal * horizontalCover + 1) * 0.5) + sourceElementRect.width * ((-horizontal * horizontalCover + 1) * 0.5) +
menuRect.width * ((-horizontal + 1) * -0.5) menuRect.width * ((-horizontal + 1) * -0.5)
); );
let y = ( let y = (
sourceRect.top + sourceElementRect.top +
(menuRect.top - top) + (menuRect.top - top) +
sourceRect.height * ((-vertical * verticalCover + 1) * 0.5) + sourceElementRect.height * ((-vertical * verticalCover + 1) * 0.5) +
(bottom - top) * ((-vertical + 1) * -0.5) (bottom - top) * ((-vertical + 1) * -0.5)
); );
x = Math.max(0.0, Math.min(fullRect.width - menuRect.width, x)); x = Math.max(0.0, Math.min(containerNodeRect.width - menuRect.width, x));
y = Math.max(0.0, Math.min(fullRect.height - menuRect.height, y)); y = Math.max(0.0, Math.min(containerNodeRect.height - menuRect.height, y));
menu.style.left = `${x}px`; menu.style.left = `${x}px`;
menu.style.top = `${y}px`; menu.style.top = `${y}px`;

View File

@ -94,6 +94,10 @@ class TextSourceElement {
return this._element.getBoundingClientRect(); return this._element.getBoundingClientRect();
} }
getRects() {
return this.getClientRects();
}
getWritingMode() { getWritingMode() {
return 'horizontal-tb'; return 'horizontal-tb';
} }

View File

@ -94,6 +94,10 @@ class TextSourceRange {
return this._range.getBoundingClientRect(); return this._range.getBoundingClientRect();
} }
getRects() {
return this._range.getClientRects();
}
getWritingMode() { getWritingMode() {
return TextSourceRange.getElementWritingMode(TextSourceRange.getParentElement(this._range.startContainer)); return TextSourceRange.getElementWritingMode(TextSourceRange.getParentElement(this._range.startContainer));
} }