2020-10-07 22:31:28 +00:00
|
|
|
/*
|
2022-02-03 01:43:10 +00:00
|
|
|
* Copyright (C) 2016-2022 Yomichan Authors
|
2020-10-07 22:31:28 +00:00
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2022-08-20 18:32:34 +00:00
|
|
|
/* global
|
2022-09-21 01:06:39 +00:00
|
|
|
* DocumentUtil
|
2022-08-20 18:32:34 +00:00
|
|
|
* StringUtil
|
|
|
|
*/
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* This class represents a text source that is attached to a HTML element, such as an <img>
|
|
|
|
* with alt text or a <button>.
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
class TextSourceElement {
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Creates a new instance of the class.
|
|
|
|
* @param {Element} element The source element.
|
|
|
|
* @param {string} fullContent The string representing the element's full text value.
|
|
|
|
* @param {number} startOffset The text start offset position within the full content.
|
|
|
|
* @param {number} endOffset The text end offset position within the full content.
|
|
|
|
*/
|
2022-09-25 13:37:33 +00:00
|
|
|
constructor(element, fullContent, startOffset, endOffset) {
|
2020-10-07 22:31:28 +00:00
|
|
|
this._element = element;
|
2022-09-25 13:37:33 +00:00
|
|
|
this._fullContent = fullContent;
|
2020-10-07 22:31:28 +00:00
|
|
|
this._startOffset = startOffset;
|
|
|
|
this._endOffset = endOffset;
|
|
|
|
this._content = this._fullContent.substring(this._startOffset, this._endOffset);
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Gets the type name of this instance.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
2021-09-27 23:07:28 +00:00
|
|
|
get type() {
|
|
|
|
return 'element';
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* The source element.
|
|
|
|
* @type {Element}
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
get element() {
|
|
|
|
return this._element;
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* The string representing the element's full text value.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
get fullContent() {
|
|
|
|
return this._fullContent;
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* The text start offset position within the full content.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
get startOffset() {
|
|
|
|
return this._startOffset;
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* The text end offset position within the full content.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
get endOffset() {
|
|
|
|
return this._endOffset;
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Creates a clone of the instance.
|
|
|
|
* @returns {TextSourceElement} The new clone.
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
clone() {
|
|
|
|
return new TextSourceElement(this._element, this._fullContent, this._startOffset, this._endOffset);
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Performs any cleanup that is necessary after the element has been used.
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
cleanup() {
|
|
|
|
// NOP
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Gets the selected text of element, which is a substring of the full content
|
|
|
|
* starting at `startOffset` and ending at `endOffset`.
|
|
|
|
* @returns {string} The text content.
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
text() {
|
|
|
|
return this._content;
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Moves the end offset of the text by a set amount of unicode codepoints.
|
|
|
|
* @param {number} length The maximum number of codepoints to move by.
|
|
|
|
* @param {boolean} fromEnd Whether to move the offset from the current end position (if `true`) or the start position (if `false`).
|
|
|
|
* @returns {number} The actual number of characters (not codepoints) that were read.
|
|
|
|
*/
|
2022-09-25 13:37:33 +00:00
|
|
|
setEndOffset(length, fromEnd) {
|
2022-08-20 18:32:34 +00:00
|
|
|
const offset = fromEnd ? this._endOffset : this._startOffset;
|
|
|
|
length = Math.min(this._fullContent.length - offset, length);
|
|
|
|
if (length > 0) {
|
|
|
|
length = StringUtil.readCodePointsForward(this._fullContent, offset, length).length;
|
2020-10-07 22:31:28 +00:00
|
|
|
}
|
2022-08-20 18:32:34 +00:00
|
|
|
this._endOffset = offset + length;
|
|
|
|
this._content = this._fullContent.substring(this._startOffset, this._endOffset);
|
|
|
|
return length;
|
2020-10-07 22:31:28 +00:00
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Moves the start offset of the text by a set amount of unicode codepoints.
|
|
|
|
* @param {number} length The maximum number of codepoints to move by.
|
|
|
|
* @returns {number} The actual number of characters (not codepoints) that were read.
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
setStartOffset(length) {
|
2022-08-20 18:32:34 +00:00
|
|
|
length = Math.min(this._startOffset, length);
|
|
|
|
if (length > 0) {
|
|
|
|
length = StringUtil.readCodePointsBackward(this._fullContent, this._startOffset - 1, length).length;
|
|
|
|
}
|
|
|
|
this._startOffset -= length;
|
2020-10-07 22:31:28 +00:00
|
|
|
this._content = this._fullContent.substring(this._startOffset, this._endOffset);
|
2022-08-20 18:32:34 +00:00
|
|
|
return length;
|
2020-10-07 22:31:28 +00:00
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Gets the rects that represent the position and bounds of the text source.
|
|
|
|
* @returns {DOMRect[]} The rects.
|
|
|
|
*/
|
2022-05-17 01:45:22 +00:00
|
|
|
getRects() {
|
2022-09-21 01:06:39 +00:00
|
|
|
return DocumentUtil.convertMultipleRectZoomCoordinates(this._element.getClientRects(), this._element);
|
2022-05-17 01:45:22 +00:00
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Gets writing mode that is used for this element.
|
|
|
|
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode.
|
|
|
|
* @returns {string} The rects.
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
getWritingMode() {
|
|
|
|
return 'horizontal-tb';
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Selects the text source in the document.
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
select() {
|
|
|
|
// NOP
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Deselects the text source in the document.
|
|
|
|
*/
|
2020-10-07 22:31:28 +00:00
|
|
|
deselect() {
|
|
|
|
// NOP
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Checks whether another text source has the same starting point.
|
|
|
|
* @param {TextSourceElement|TextSourceRange} other The other source to test.
|
|
|
|
* @returns {boolean} `true` if the starting points are equivalent, `false` otherwise.
|
|
|
|
*/
|
2020-10-21 00:54:26 +00:00
|
|
|
hasSameStart(other) {
|
2020-10-07 22:31:28 +00:00
|
|
|
return (
|
|
|
|
typeof other === 'object' &&
|
|
|
|
other !== null &&
|
|
|
|
other instanceof TextSourceElement &&
|
|
|
|
this._element === other.element &&
|
|
|
|
this._fullContent === other.fullContent &&
|
|
|
|
this._startOffset === other.startOffset
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Gets a list of the nodes in this text source's range.
|
|
|
|
* @returns {Node[]} The nodes in the range.
|
|
|
|
*/
|
2020-10-21 00:54:26 +00:00
|
|
|
getNodesInRange() {
|
2021-01-21 02:35:09 +00:00
|
|
|
return [this._element];
|
2020-10-21 00:54:26 +00:00
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Creates a new instance for a given element.
|
|
|
|
* @param {Element} element The source element.
|
|
|
|
* @returns {TextSourceElement} A new instance of the class corresponding to the element.
|
|
|
|
*/
|
2022-09-25 13:37:33 +00:00
|
|
|
static create(element) {
|
|
|
|
return new TextSourceElement(element, this._getElementContent(element), 0, 0);
|
|
|
|
}
|
|
|
|
|
2022-09-26 23:37:14 +00:00
|
|
|
/**
|
|
|
|
* Gets the full content string for a given element.
|
|
|
|
* @param {Element} element The element to get the full content of.
|
|
|
|
* @returns {string} The content string.
|
|
|
|
*/
|
2022-09-25 13:37:33 +00:00
|
|
|
static _getElementContent(element) {
|
2020-10-07 22:31:28 +00:00
|
|
|
let content;
|
|
|
|
switch (element.nodeName.toUpperCase()) {
|
|
|
|
case 'BUTTON':
|
|
|
|
content = element.textContent;
|
|
|
|
break;
|
|
|
|
case 'IMG':
|
|
|
|
content = element.getAttribute('alt') || '';
|
|
|
|
break;
|
2021-02-28 18:26:23 +00:00
|
|
|
case 'SELECT':
|
|
|
|
{
|
|
|
|
const {selectedIndex, options} = element;
|
|
|
|
const option = (selectedIndex >= 0 && selectedIndex < options.length ? options[selectedIndex] : null);
|
|
|
|
content = (option !== null ? option.textContent : '');
|
|
|
|
}
|
|
|
|
break;
|
2020-10-07 22:31:28 +00:00
|
|
|
default:
|
|
|
|
content = `${element.value}`;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-03-02 03:45:03 +00:00
|
|
|
// Remove zero-width space and zero-width non-joiner
|
|
|
|
content = content.replace(/[\u200b\u200c]/g, '');
|
2020-10-07 22:31:28 +00:00
|
|
|
|
|
|
|
return content;
|
|
|
|
}
|
|
|
|
}
|