From 125c296eedf680ad7670544aa8f74d81fa9aa799 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 8 Dec 2020 20:31:02 -0500 Subject: [PATCH] Support frame resize on firefox (#1088) * Add popup functions for getting/setting the frame size * Add frontend functions for getting/setting popup frame size * Expose display mode attribute on display HTML * Disable resizer on iframe * Add custom frame resizer handle * Add support for custom frame resizer --- ext/fg/float.html | 7 ++++ ext/fg/js/frontend.js | 12 +++++- ext/fg/js/popup-factory.js | 14 ++++++- ext/fg/js/popup-proxy.js | 8 ++++ ext/fg/js/popup-window.js | 8 ++++ ext/fg/js/popup.js | 19 ++++++++- ext/mixed/css/display.css | 50 +++++++++++++++++++++++ ext/mixed/css/popup-outer.css | 3 +- ext/mixed/js/display.js | 74 +++++++++++++++++++++++++++++++++++ 9 files changed, 189 insertions(+), 6 deletions(-) diff --git a/ext/fg/float.html b/ext/fg/float.html index 5874f44d..50c3b691 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -67,6 +67,13 @@ +
+
+ + + +
+ diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 49c8a91c..cb105341 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -119,7 +119,9 @@ class Frontend { ['copySelection', {async: false, handler: this._onApiCopySelection.bind(this)}], ['getSelectionText', {async: false, handler: this._onApiGetSelectionText.bind(this)}], ['getPopupInfo', {async: false, handler: this._onApiGetPopupInfo.bind(this)}], - ['getDocumentInformation', {async: false, handler: this._onApiGetDocumentInformation.bind(this)}] + ['getDocumentInformation', {async: false, handler: this._onApiGetDocumentInformation.bind(this)}], + ['getFrameSize', {async: true, handler: this._onApiGetFrameSize.bind(this)}], + ['setFrameSize', {async: true, handler: this._onApiSetFrameSize.bind(this)}] ]); this._updateContentScale(); @@ -203,6 +205,14 @@ class Frontend { return await this._popupFactory.clearAllVisibleOverride(token); } + async _onApiGetFrameSize() { + return await this._popup.getFrameSize(); + } + + async _onApiSetFrameSize({width, height}) { + return await this._popup.setFrameSize(width, height); + } + // Private _onResize() { diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js index 252bcdf4..9a6c2ccd 100644 --- a/ext/fg/js/popup-factory.js +++ b/ext/fg/js/popup-factory.js @@ -48,7 +48,9 @@ class PopupFactory { ['clearAutoPlayTimer', {async: false, handler: this._onApiClearAutoPlayTimer.bind(this)}], ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}], ['updateTheme', {async: false, handler: this._onApiUpdateTheme.bind(this)}], - ['setCustomOuterCss', {async: false, handler: this._onApiSetCustomOuterCss.bind(this)}] + ['setCustomOuterCss', {async: false, handler: this._onApiSetCustomOuterCss.bind(this)}], + ['popup.getFrameSize', {async: true, handler: this._onApiGetFrameSize.bind(this)}], + ['popup.setFrameSize', {async: true, handler: this._onApiSetFrameSize.bind(this)}] ]); } @@ -265,6 +267,16 @@ class PopupFactory { return popup.setCustomOuterCss(css, useWebExtensionApi); } + async _onApiGetFrameSize({id}) { + const popup = this._getPopup(id); + return await popup.getFrameSize(); + } + + async _onApiSetFrameSize({id, width, height}) { + const popup = this._getPopup(id); + return await popup.setFrameSize(width, height); + } + // Private functions _getPopup(id) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 26dce0d1..a03b58e8 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -149,6 +149,14 @@ class PopupProxy extends EventDispatcher { return new DOMRect(0, 0, 0, 0); } + getFrameSize() { + return this._invokeSafe('popup.getFrameSize', {id: this._id}, {width: 0, height: 0, valid: false}); + } + + setFrameSize(width, height) { + return this._invokeSafe('popup.setFrameSize', {id: this._id, width, height}); + } + // Private _invoke(action, params={}) { diff --git a/ext/fg/js/popup-window.js b/ext/fg/js/popup-window.js index 3d707a9e..3d68beb8 100644 --- a/ext/fg/js/popup-window.js +++ b/ext/fg/js/popup-window.js @@ -132,6 +132,14 @@ class PopupWindow extends EventDispatcher { return new DOMRect(0, 0, 0, 0); } + async getFrameSize() { + return {width: 0, height: 0, valid: false}; + } + + async setFrameSize(_width, _height) { + return false; + } + // Private async _invoke(open, action, params={}, defaultReturnValue) { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index ffd72dca..c24ffb5c 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -208,6 +208,16 @@ class Popup extends EventDispatcher { return this._frame.getBoundingClientRect(); } + async getFrameSize() { + const rect = this._frame.getBoundingClientRect(); + return {width: rect.width, height: rect.height, valid: true}; + } + + async setFrameSize(width, height) { + this._setFrameSize(width, height); + return true; + } + // Private functions _onFrameMouseOver() { @@ -397,8 +407,7 @@ class Popup extends EventDispatcher { frame.style.left = `${x}px`; frame.style.top = `${y}px`; - frame.style.width = `${width}px`; - frame.style.height = `${height}px`; + this._setFrameSize(width, height); this._setVisible(true); if (this._child !== null) { @@ -406,6 +415,12 @@ class Popup extends EventDispatcher { } } + _setFrameSize(width, height) { + const {style} = this._frame; + style.width = `${width}px`; + style.height = `${height}px`; + } + _setVisible(visible) { this._visible.defaultValue = visible; } diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index 605c0148..0a676a8f 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -1119,6 +1119,52 @@ button.action-button { } +/* Frame resizer */ +.frame-resizer-container { + position: fixed; + bottom: 0; + right: 0; + z-index: 100; + background: transparent; + pointer-events: none; + user-select: none; +} +.frame-resizer-sizer1 { + padding-top: 100%; + line-height: 0; +} +.frame-resizer-sizer2 { + display: inline-block; + overflow-x: hidden; + overflow-y: scroll; + vertical-align: bottom; +} +.frame-resizer-sizer2.frame-resizer-sizer2-with-min-size { + min-width: 1em; +} +.frame-resizer-svg { + display: block; + position: absolute; + bottom: 0; + right: 0; + width: 75%; + height: 75%; +} +.frame-resizer-handle { + fill: var(--default-text-color); + opacity: 0.125; + cursor: se-resize; + pointer-events: auto; + transition: + fill var(--animation-duration) linear, + opacity var(--animation-duration) linear; +} +.frame-resizer-handle:hover { + fill: var(--accent-color); + opacity: 1; +} + + /* Conditional styles */ :root:not([data-enable-search-tags=true]) .tag[data-category=search] { display: none; @@ -1221,3 +1267,7 @@ button.action-button { :root:not([data-glossary-layout-mode=compact]) .term-glossary-image-description { display: block; } + +:root[data-popup-display-mode=full-width] .frame-resizer-container { + display: none; +} diff --git a/ext/mixed/css/popup-outer.css b/ext/mixed/css/popup-outer.css index 74307d9f..0ee8df9e 100644 --- a/ext/mixed/css/popup-outer.css +++ b/ext/mixed/css/popup-outer.css @@ -22,7 +22,7 @@ iframe.yomichan-popup { border: 1em solid #999999; box-shadow: 0 0 10em rgba(0, 0, 0, 0.5); position: fixed; - resize: both; + resize: none; visibility: hidden; z-index: 2147483647; box-sizing: border-box; @@ -38,7 +38,6 @@ iframe.yomichan-popup[data-outer-theme=auto][data-site-color=dark] { iframe.yomichan-popup[data-popup-display-mode=full-width] { border-left: none; border-right: none; - resize: none; } iframe.yomichan-popup[data-popup-display-mode=full-width][data-below=true] { border-bottom: none; diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index b9daa9a0..b19d07e2 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -109,6 +109,11 @@ class Display extends EventDispatcher { this._browser = null; this._copyTextarea = null; this._definitionTextScanner = null; + this._frameResizeToken = null; + this._frameResizeHandle = document.querySelector('#frame-resizer-handle'); + this._frameResizeStartSize = null; + this._frameResizeStartOffset = null; + this._frameResizeEventListeners = new EventListenerCollection(); this.registerActions([ ['close', () => { this.onEscape(); }], @@ -228,6 +233,10 @@ class Display extends EventDispatcher { this._navigationNextButton.addEventListener('click', this._onNextTermView.bind(this), false); } + if (this._frameResizeHandle !== null) { + this._frameResizeHandle.addEventListener('mousedown', this._onFrameResizerMouseDown.bind(this), false); + } + // Final preparation this._updateFocusedElement(); } @@ -801,6 +810,7 @@ class Display extends EventDispatcher { data.showPitchAccentPositionNotation = `${options.general.showPitchAccentPositionNotation}`; data.showPitchAccentGraph = `${options.general.showPitchAccentGraph}`; data.debug = `${options.general.debugInfo}`; + data.popupDisplayMode = `${options.general.popupDisplayMode}`; } _updateTheme(themeName) { @@ -1795,4 +1805,68 @@ class Display extends EventDispatcher { this._definitionTextScanner.clearSelection(true); this.setContent(details); } + + _onFrameResizerMouseDown(e) { + if (e.button !== 0) { return; } + // Don't do e.preventDefault() here; this allows mousemove events to be processed + // if the pointer moves out of the frame. + this._startFrameResize(e); + } + + _onFrameResizerMouseUp() { + this._stopFrameResize(); + } + + _onFrameResizerWindowBlur() { + this._stopFrameResize(); + } + + _onFrameResizerMouseMove(e) { + if ((e.buttons & 0x1) === 0x0) { + this._stopFrameResize(); + } else { + if (this._frameResizeStartSize === null) { return; } + const {clientX: x, clientY: y} = e; + this._updateFrameSize(x, y); + } + } + + _startFrameResize(e) { + if (this._frameResizeToken !== null) { return; } + + const {clientX: x, clientY: y} = e; + const token = {}; + this._frameResizeToken = token; + this._frameResizeStartOffset = {x, y}; + this._frameResizeEventListeners.addEventListener(window, 'mouseup', this._onFrameResizerMouseUp.bind(this), false); + this._frameResizeEventListeners.addEventListener(window, 'blur', this._onFrameResizerWindowBlur.bind(this), false); + this._frameResizeEventListeners.addEventListener(window, 'mousemove', this._onFrameResizerMouseMove.bind(this), false); + + this._initializeFrameResize(token); + } + + async _initializeFrameResize(token) { + const size = await this._invokeOwner('getFrameSize'); + if (this._frameResizeToken !== token) { return; } + this._frameResizeStartSize = size; + } + + _stopFrameResize() { + if (this._frameResizeToken === null) { return; } + + this._frameResizeEventListeners.removeAllEventListeners(); + this._frameResizeStartSize = null; + this._frameResizeStartOffset = null; + this._frameResizeToken = null; + } + + async _updateFrameSize(x, y) { + const handleSize = this._frameResizeHandle.getBoundingClientRect(); + let {width, height} = this._frameResizeStartSize; + width += x - this._frameResizeStartOffset.x; + height += y - this._frameResizeStartOffset.y; + width = Math.max(Math.max(0, handleSize.width), width); + height = Math.max(Math.max(0, handleSize.height), height); + await this._invokeOwner('setFrameSize', {width, height}); + } }