Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 229 lines 7.6 kB view raw
1/* 2 * Copyright (C) 2023-2025 Yomitan Authors 3 * Copyright (C) 2021-2022 Yomichan Authors 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19import {EventListenerCollection} from '../core/event-listener-collection.js'; 20 21export class DisplayResizer { 22 /** 23 * @param {import('./display.js').Display} display 24 */ 25 constructor(display) { 26 /** @type {import('./display.js').Display} */ 27 this._display = display; 28 /** @type {?import('core').TokenObject} */ 29 this._token = null; 30 /** @type {?HTMLElement} */ 31 this._handle = null; 32 /** @type {?number} */ 33 this._touchIdentifier = null; 34 /** @type {?{width: number, height: number}} */ 35 this._startSize = null; 36 /** @type {?{x: number, y: number}} */ 37 this._startOffset = null; 38 /** @type {EventListenerCollection} */ 39 this._eventListeners = new EventListenerCollection(); 40 } 41 42 /** */ 43 prepare() { 44 this._handle = document.querySelector('#frame-resizer-handle'); 45 if (this._handle === null) { return; } 46 47 this._handle.addEventListener('mousedown', this._onFrameResizerMouseDown.bind(this), false); 48 this._handle.addEventListener('touchstart', this._onFrameResizerTouchStart.bind(this), {passive: false, capture: false}); 49 } 50 51 // Private 52 53 /** 54 * @param {MouseEvent} e 55 */ 56 _onFrameResizerMouseDown(e) { 57 if (e.button !== 0) { return; } 58 // Don't do e.preventDefault() here; this allows mousemove events to be processed 59 // if the pointer moves out of the frame. 60 this._startFrameResize(e); 61 } 62 63 /** 64 * @param {TouchEvent} e 65 */ 66 _onFrameResizerTouchStart(e) { 67 e.preventDefault(); 68 this._startFrameResizeTouch(e); 69 } 70 71 /** */ 72 _onFrameResizerMouseUp() { 73 this._stopFrameResize(); 74 } 75 76 /** */ 77 _onFrameResizerWindowBlur() { 78 this._stopFrameResize(); 79 } 80 81 /** 82 * @param {MouseEvent} e 83 */ 84 _onFrameResizerMouseMove(e) { 85 if ((e.buttons & 0x1) === 0x0) { 86 this._stopFrameResize(); 87 } else { 88 if (this._startSize === null) { return; } 89 const {clientX: x, clientY: y} = e; 90 void this._updateFrameSize(x, y); 91 } 92 } 93 94 /** 95 * @param {TouchEvent} e 96 */ 97 _onFrameResizerTouchEnd(e) { 98 if (this._getTouch(e.changedTouches, this._touchIdentifier) === null) { return; } 99 this._stopFrameResize(); 100 } 101 102 /** 103 * @param {TouchEvent} e 104 */ 105 _onFrameResizerTouchCancel(e) { 106 if (this._getTouch(e.changedTouches, this._touchIdentifier) === null) { return; } 107 this._stopFrameResize(); 108 } 109 110 /** 111 * @param {TouchEvent} e 112 */ 113 _onFrameResizerTouchMove(e) { 114 if (this._startSize === null) { return; } 115 const primaryTouch = this._getTouch(e.changedTouches, this._touchIdentifier); 116 if (primaryTouch === null) { return; } 117 const {clientX: x, clientY: y} = primaryTouch; 118 void this._updateFrameSize(x, y); 119 } 120 121 /** 122 * @param {MouseEvent} e 123 */ 124 _startFrameResize(e) { 125 if (this._token !== null) { return; } 126 127 const {clientX: x, clientY: y} = e; 128 /** @type {?import('core').TokenObject} */ 129 const token = {}; 130 this._token = token; 131 this._startOffset = {x, y}; 132 this._eventListeners.addEventListener(window, 'mouseup', this._onFrameResizerMouseUp.bind(this), false); 133 this._eventListeners.addEventListener(window, 'blur', this._onFrameResizerWindowBlur.bind(this), false); 134 this._eventListeners.addEventListener(window, 'mousemove', this._onFrameResizerMouseMove.bind(this), false); 135 136 const {documentElement} = document; 137 if (documentElement !== null) { 138 documentElement.dataset.isResizing = 'true'; 139 } 140 141 void this._initializeFrameResize(token); 142 } 143 144 /** 145 * @param {TouchEvent} e 146 */ 147 _startFrameResizeTouch(e) { 148 if (this._token !== null) { return; } 149 150 const {clientX: x, clientY: y, identifier} = e.changedTouches[0]; 151 /** @type {?import('core').TokenObject} */ 152 const token = {}; 153 this._token = token; 154 this._startOffset = {x, y}; 155 this._touchIdentifier = identifier; 156 this._eventListeners.addEventListener(window, 'touchend', this._onFrameResizerTouchEnd.bind(this), false); 157 this._eventListeners.addEventListener(window, 'touchcancel', this._onFrameResizerTouchCancel.bind(this), false); 158 this._eventListeners.addEventListener(window, 'blur', this._onFrameResizerWindowBlur.bind(this), false); 159 this._eventListeners.addEventListener(window, 'touchmove', this._onFrameResizerTouchMove.bind(this), false); 160 161 const {documentElement} = document; 162 if (documentElement !== null) { 163 documentElement.dataset.isResizing = 'true'; 164 } 165 166 void this._initializeFrameResize(token); 167 } 168 169 /** 170 * @param {import('core').TokenObject} token 171 */ 172 async _initializeFrameResize(token) { 173 const {parentPopupId} = this._display; 174 if (parentPopupId === null) { return; } 175 176 /** @type {import('popup').ValidSize} */ 177 const size = await this._display.invokeParentFrame('popupFactoryGetFrameSize', {id: parentPopupId}); 178 if (this._token !== token) { return; } 179 const {width, height} = size; 180 this._startSize = {width, height}; 181 } 182 183 /** */ 184 _stopFrameResize() { 185 if (this._token === null) { return; } 186 187 this._eventListeners.removeAllEventListeners(); 188 this._startSize = null; 189 this._startOffset = null; 190 this._touchIdentifier = null; 191 this._token = null; 192 193 const {documentElement} = document; 194 if (documentElement !== null) { 195 delete documentElement.dataset.isResizing; 196 } 197 } 198 199 /** 200 * @param {number} x 201 * @param {number} y 202 */ 203 async _updateFrameSize(x, y) { 204 const {parentPopupId} = this._display; 205 if (parentPopupId === null || this._handle === null || this._startOffset === null || this._startSize === null) { return; } 206 207 const handleSize = this._handle.getBoundingClientRect(); 208 let {width, height} = this._startSize; 209 width += x - this._startOffset.x; 210 height += y - this._startOffset.y; 211 width = Math.max(Math.max(0, handleSize.width), width); 212 height = Math.max(Math.max(0, handleSize.height), height); 213 await this._display.invokeParentFrame('popupFactorySetFrameSize', {id: parentPopupId, width, height}); 214 } 215 216 /** 217 * @param {TouchList} touchList 218 * @param {?number} identifier 219 * @returns {?Touch} 220 */ 221 _getTouch(touchList, identifier) { 222 for (const touch of touchList) { 223 if (touch.identifier === identifier) { 224 return touch; 225 } 226 } 227 return null; 228 } 229}