diff options
Diffstat (limited to 'ext/mixed/js')
| -rw-r--r-- | ext/mixed/js/api.js | 18 | ||||
| -rw-r--r-- | ext/mixed/js/audio.js | 28 | ||||
| -rw-r--r-- | ext/mixed/js/core.js | 59 | ||||
| -rw-r--r-- | ext/mixed/js/display-generator.js | 128 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 144 | ||||
| -rw-r--r-- | ext/mixed/js/japanese.js | 454 | ||||
| -rw-r--r-- | ext/mixed/js/template-handler.js | 47 | ||||
| -rw-r--r-- | ext/mixed/js/text-scanner.js | 66 | 
8 files changed, 275 insertions, 669 deletions
| diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 5ec93b01..7ea68d59 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -58,15 +58,15 @@ function apiDefinitionAdd(definition, mode, context, optionsContext) {  }  function apiDefinitionsAddable(definitions, modes, optionsContext) { -    return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext}).catch(() => null); +    return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext});  }  function apiNoteView(noteId) {      return _apiInvoke('noteView', {noteId});  } -function apiTemplateRender(template, data, dynamic) { -    return _apiInvoke('templateRender', {data, template, dynamic}); +function apiTemplateRender(template, data) { +    return _apiInvoke('templateRender', {data, template});  }  function apiAudioGetUrl(definition, source, optionsContext) { @@ -89,8 +89,8 @@ function apiFrameInformationGet() {      return _apiInvoke('frameInformationGet');  } -function apiInjectStylesheet(css) { -    return _apiInvoke('injectStylesheet', {css}); +function apiInjectStylesheet(type, value) { +    return _apiInvoke('injectStylesheet', {type, value});  }  function apiGetEnvironmentInfo() { @@ -105,10 +105,18 @@ function apiGetDisplayTemplatesHtml() {      return _apiInvoke('getDisplayTemplatesHtml');  } +function apiGetQueryParserTemplatesHtml() { +    return _apiInvoke('getQueryParserTemplatesHtml'); +} +  function apiGetZoom() {      return _apiInvoke('getZoom');  } +function apiGetMessageToken() { +    return _apiInvoke('getMessageToken'); +} +  function _apiInvoke(action, params={}) {      const data = {action, params};      return new Promise((resolve, reject) => { diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js index b0c5fa82..b5a025be 100644 --- a/ext/mixed/js/audio.js +++ b/ext/mixed/js/audio.js @@ -16,6 +16,7 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global apiAudioGetUrl*/  class TextToSpeechAudio {      constructor(text, voice) { @@ -53,7 +54,6 @@ class TextToSpeechAudio {              speechSynthesis.cancel();              speechSynthesis.speak(this._utterance); -          } catch (e) {              // NOP          } @@ -71,21 +71,16 @@ class TextToSpeechAudio {          const m = /^tts:[^#?]*\?([^#]*)/.exec(ttsUri);          if (m === null) { return null; } -        const searchParameters = {}; -        for (const group of m[1].split('&')) { -            const sep = group.indexOf('='); -            if (sep < 0) { continue; } -            searchParameters[decodeURIComponent(group.substring(0, sep))] = decodeURIComponent(group.substring(sep + 1)); -        } - -        if (!searchParameters.text) { return null; } +        const searchParameters = new URLSearchParams(m[1]); +        const text = searchParameters.get('text'); +        let voice = searchParameters.get('voice'); +        if (text === null || voice === null) { return null; } -        const voice = audioGetTextToSpeechVoice(searchParameters.voice); +        voice = audioGetTextToSpeechVoice(voice);          if (voice === null) { return null; } -        return new TextToSpeechAudio(searchParameters.text, voice); +        return new TextToSpeechAudio(text, voice);      } -  }  function audioGetFromUrl(url, willDownload) { @@ -113,8 +108,11 @@ function audioGetFromUrl(url, willDownload) {  async function audioGetFromSources(expression, sources, optionsContext, willDownload, cache=null) {      const key = `${expression.expression}:${expression.reading}`; -    if (cache !== null && hasOwn(cache, expression)) { -        return cache[key]; +    if (cache !== null) { +        const cacheValue = cache.get(expression); +        if (typeof cacheValue !== 'undefined') { +            return cacheValue; +        }      }      for (let i = 0, ii = sources.length; i < ii; ++i) { @@ -132,7 +130,7 @@ async function audioGetFromSources(expression, sources, optionsContext, willDown              }              const result = {audio, url, source};              if (cache !== null) { -                cache[key] = result; +                cache.set(key, result);              }              return result;          } catch (e) { diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 0142d594..83813796 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -113,11 +113,7 @@ function toIterable(value) {      if (value !== null && typeof value === 'object') {          const length = value.length;          if (typeof length === 'number' && Number.isFinite(length)) { -            const array = []; -            for (let i = 0; i < length; ++i) { -                array.push(value[i]); -            } -            return array; +            return Array.from(value);          }      } @@ -128,6 +124,14 @@ function stringReverse(string) {      return string.split('').reverse().join('').replace(/([\uDC00-\uDFFF])([\uD800-\uDBFF])/g, '$2$1');  } +function parseUrl(url) { +    const parsedUrl = new URL(url); +    const baseUrl = `${parsedUrl.origin}${parsedUrl.pathname}`; +    const queryParams = Array.from(parsedUrl.searchParams.entries()) +        .reduce((a, [k, v]) => Object.assign({}, a, {[k]: v}), {}); +    return {baseUrl, queryParams}; +} +  /*   * Async utilities @@ -156,9 +160,9 @@ function promiseTimeout(delay, resolveValue) {      const resolve = (value) => complete(promiseResolve, value);      const reject = (value) => complete(promiseReject, value); -    const promise = new Promise((resolve, reject) => { -        promiseResolve = resolve; -        promiseReject = reject; +    const promise = new Promise((resolve2, reject2) => { +        promiseResolve = resolve2; +        promiseReject = reject2;      });      timer = window.setTimeout(() => {          timer = null; @@ -232,6 +236,29 @@ class EventDispatcher {      }  } +class EventListenerCollection { +    constructor() { +        this._eventListeners = []; +    } + +    get size() { +        return this._eventListeners.length; +    } + +    addEventListener(node, type, listener, options) { +        node.addEventListener(type, listener, options); +        this._eventListeners.push([node, type, listener, options]); +    } + +    removeAllEventListeners() { +        if (this._eventListeners.length === 0) { return; } +        for (const [node, type, listener, options] of this._eventListeners) { +            node.removeEventListener(type, listener, options); +        } +        this._eventListeners = []; +    } +} +  /*   * Default message handlers @@ -244,7 +271,7 @@ const yomichan = (() => {              this._messageHandlers = new Map([                  ['getUrl', this._onMessageGetUrl.bind(this)], -                ['optionsUpdate', this._onMessageOptionsUpdate.bind(this)], +                ['optionsUpdated', this._onMessageOptionsUpdated.bind(this)],                  ['zoomChanged', this._onMessageZoomChanged.bind(this)]              ]); @@ -253,6 +280,16 @@ const yomichan = (() => {          // Public +        generateId(length) { +            const array = new Uint8Array(length); +            window.crypto.getRandomValues(array); +            let id = ''; +            for (const value of array) { +                id += value.toString(16).padStart(2, '0'); +            } +            return id; +        } +          triggerOrphaned(error) {              this.trigger('orphaned', {error});          } @@ -272,8 +309,8 @@ const yomichan = (() => {              return {url: window.location.href};          } -        _onMessageOptionsUpdate({source}) { -            this.trigger('optionsUpdate', {source}); +        _onMessageOptionsUpdated({source}) { +            this.trigger('optionsUpdated', {source});          }          _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) { diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js index e1710488..d7e77cc0 100644 --- a/ext/mixed/js/display-generator.js +++ b/ext/mixed/js/display-generator.js @@ -16,46 +16,20 @@   * along with this program.  If not, see <http://www.gnu.org/licenses/>.   */ +/*global apiGetDisplayTemplatesHtml, TemplateHandler*/  class DisplayGenerator {      constructor() { -        this._isInitialized = false; -        this._initializationPromise = null; - -        this._termEntryTemplate = null; -        this._termExpressionTemplate = null; -        this._termDefinitionItemTemplate = null; -        this._termDefinitionOnlyTemplate = null; -        this._termGlossaryItemTemplate = null; -        this._termReasonTemplate = null; - -        this._kanjiEntryTemplate = null; -        this._kanjiInfoTableTemplate = null; -        this._kanjiInfoTableItemTemplate = null; -        this._kanjiInfoTableEmptyTemplate = null; -        this._kanjiGlossaryItemTemplate = null; -        this._kanjiReadingTemplate = null; - -        this._tagTemplate = null; -        this._tagFrequencyTemplate = null; +        this._templateHandler = null;      } -    isInitialized() { -        return this._isInitialized; -    } - -    initialize() { -        if (this._isInitialized) { -            return Promise.resolve(); -        } -        if (this._initializationPromise === null) { -            this._initializationPromise = this._initializeInternal(); -        } -        return this._initializationPromise; +    async prepare() { +        const html = await apiGetDisplayTemplatesHtml(); +        this._templateHandler = new TemplateHandler(html);      }      createTermEntry(details) { -        const node = DisplayGenerator._instantiateTemplate(this._termEntryTemplate); +        const node = this._templateHandler.instantiate('term-entry');          const expressionsContainer = node.querySelector('.term-expression-list');          const reasonsContainer = node.querySelector('.term-reasons'); @@ -71,7 +45,11 @@ class DisplayGenerator {          node.dataset.expressionCount = `${expressionMulti ? details.expressions.length : 1}`;          node.dataset.definitionCount = `${definitionMulti ? details.definitions.length : 1}`; -        DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), details.expressions, [details]); +        const termTags = details.termTags; +        let expressions = details.expressions; +        expressions = Array.isArray(expressions) ? expressions.map((e) => [e, termTags]) : null; + +        DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), expressions, [[details, termTags]]);          DisplayGenerator._appendMultiple(reasonsContainer, this.createTermReason.bind(this), details.reasons);          DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies);          DisplayGenerator._appendMultiple(definitionsContainer, this.createTermDefinitionItem.bind(this), details.definitions, [details]); @@ -83,8 +61,8 @@ class DisplayGenerator {          return node;      } -    createTermExpression(details) { -        const node = DisplayGenerator._instantiateTemplate(this._termExpressionTemplate); +    createTermExpression([details, termTags]) { +        const node = this._templateHandler.instantiate('term-expression');          const expressionContainer = node.querySelector('.term-expression-text');          const tagContainer = node.querySelector('.tags'); @@ -103,21 +81,30 @@ class DisplayGenerator {              DisplayGenerator._appendFurigana(expressionContainer, furiganaSegments, this._appendKanjiLinks.bind(this));          } -        DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), details.termTags); +        if (!Array.isArray(termTags)) { +            // Fallback +            termTags = details.termTags; +        } +        const searchQueries = [details.expression, details.reading] +            .filter((x) => !!x) +            .map((x) => ({query: x})); +        DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), termTags); +        DisplayGenerator._appendMultiple(tagContainer, this.createSearchTag.bind(this), searchQueries);          DisplayGenerator._appendMultiple(frequencyContainer, this.createFrequencyTag.bind(this), details.frequencies);          return node;      }      createTermReason(reason) { -        const node = DisplayGenerator._instantiateTemplate(this._termReasonTemplate); +        const fragment = this._templateHandler.instantiateFragment('term-reason'); +        const node = fragment.querySelector('.term-reason');          node.textContent = reason;          node.dataset.reason = reason; -        return node; +        return fragment;      }      createTermDefinitionItem(details) { -        const node = DisplayGenerator._instantiateTemplate(this._termDefinitionItemTemplate); +        const node = this._templateHandler.instantiate('term-definition-item');          const tagListContainer = node.querySelector('.term-definition-tag-list');          const onlyListContainer = node.querySelector('.term-definition-only-list'); @@ -133,7 +120,7 @@ class DisplayGenerator {      }      createTermGlossaryItem(glossary) { -        const node = DisplayGenerator._instantiateTemplate(this._termGlossaryItemTemplate); +        const node = this._templateHandler.instantiate('term-glossary-item');          const container = node.querySelector('.term-glossary');          if (container !== null) {              DisplayGenerator._appendMultilineText(container, glossary); @@ -142,7 +129,7 @@ class DisplayGenerator {      }      createTermOnly(only) { -        const node = DisplayGenerator._instantiateTemplate(this._termDefinitionOnlyTemplate); +        const node = this._templateHandler.instantiate('term-definition-only');          node.dataset.only = only;          node.textContent = only;          return node; @@ -157,7 +144,7 @@ class DisplayGenerator {      }      createKanjiEntry(details) { -        const node = DisplayGenerator._instantiateTemplate(this._kanjiEntryTemplate); +        const node = this._templateHandler.instantiate('kanji-entry');          const glyphContainer = node.querySelector('.kanji-glyph');          const frequenciesContainer = node.querySelector('.frequencies'); @@ -202,7 +189,7 @@ class DisplayGenerator {      }      createKanjiGlossaryItem(glossary) { -        const node = DisplayGenerator._instantiateTemplate(this._kanjiGlossaryItemTemplate); +        const node = this._templateHandler.instantiate('kanji-glossary-item');          const container = node.querySelector('.kanji-glossary');          if (container !== null) {              DisplayGenerator._appendMultilineText(container, glossary); @@ -211,13 +198,13 @@ class DisplayGenerator {      }      createKanjiReading(reading) { -        const node = DisplayGenerator._instantiateTemplate(this._kanjiReadingTemplate); +        const node = this._templateHandler.instantiate('kanji-reading');          node.textContent = reading;          return node;      }      createKanjiInfoTable(details) { -        const node = DisplayGenerator._instantiateTemplate(this._kanjiInfoTableTemplate); +        const node = this._templateHandler.instantiate('kanji-info-table');          const container = node.querySelector('.kanji-info-table-body'); @@ -233,7 +220,7 @@ class DisplayGenerator {      }      createKanjiInfoTableItem(details) { -        const node = DisplayGenerator._instantiateTemplate(this._kanjiInfoTableItemTemplate); +        const node = this._templateHandler.instantiate('kanji-info-table-item');          const nameNode = node.querySelector('.kanji-info-table-item-header');          const valueNode = node.querySelector('.kanji-info-table-item-value');          if (nameNode !== null) { @@ -246,21 +233,33 @@ class DisplayGenerator {      }      createKanjiInfoTableItemEmpty() { -        return DisplayGenerator._instantiateTemplate(this._kanjiInfoTableEmptyTemplate); +        return this._templateHandler.instantiate('kanji-info-table-empty');      }      createTag(details) { -        const node = DisplayGenerator._instantiateTemplate(this._tagTemplate); +        const node = this._templateHandler.instantiate('tag'); + +        const inner = node.querySelector('.tag-inner');          node.title = details.notes; -        node.textContent = details.name; +        inner.textContent = details.name;          node.dataset.category = details.category;          return node;      } +    createSearchTag(details) { +        const node = this._templateHandler.instantiate('tag-search'); + +        node.textContent = details.query; + +        node.dataset.query = details.query; + +        return node; +    } +      createFrequencyTag(details) { -        const node = DisplayGenerator._instantiateTemplate(this._tagFrequencyTemplate); +        const node = this._templateHandler.instantiate('tag-frequency');          let n = node.querySelector('.term-frequency-dictionary-name');          if (n !== null) { @@ -278,31 +277,6 @@ class DisplayGenerator {          return node;      } -    async _initializeInternal() { -        const html = await apiGetDisplayTemplatesHtml(); -        const doc = new DOMParser().parseFromString(html, 'text/html'); -        this._setTemplates(doc); -    } - -    _setTemplates(doc) { -        this._termEntryTemplate = doc.querySelector('#term-entry-template'); -        this._termExpressionTemplate = doc.querySelector('#term-expression-template'); -        this._termDefinitionItemTemplate = doc.querySelector('#term-definition-item-template'); -        this._termDefinitionOnlyTemplate = doc.querySelector('#term-definition-only-template'); -        this._termGlossaryItemTemplate = doc.querySelector('#term-glossary-item-template'); -        this._termReasonTemplate = doc.querySelector('#term-reason-template'); - -        this._kanjiEntryTemplate = doc.querySelector('#kanji-entry-template'); -        this._kanjiInfoTableTemplate = doc.querySelector('#kanji-info-table-template'); -        this._kanjiInfoTableItemTemplate = doc.querySelector('#kanji-info-table-item-template'); -        this._kanjiInfoTableEmptyTemplate = doc.querySelector('#kanji-info-table-empty-template'); -        this._kanjiGlossaryItemTemplate = doc.querySelector('#kanji-glossary-item-template'); -        this._kanjiReadingTemplate = doc.querySelector('#kanji-reading-template'); - -        this._tagTemplate = doc.querySelector('#tag-template'); -        this._tagFrequencyTemplate = doc.querySelector('#tag-frequency-template'); -    } -      _appendKanjiLinks(container, text) {          let part = '';          for (const c of text) { @@ -372,8 +346,4 @@ class DisplayGenerator {              container.appendChild(document.createTextNode(parts[i]));          }      } - -    static _instantiateTemplate(template) { -        return document.importNode(template.content.firstChild, true); -    }  } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index c4be02f2..5d3076ee 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -16,6 +16,11 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global docRangeFromPoint, docSentenceExtract +apiKanjiFind, apiTermsFind, apiNoteView, apiOptionsGet, apiDefinitionsAddable, apiDefinitionAdd +apiScreenshotGet, apiForward +audioPrepareTextToSpeech, audioGetFromSources +DisplayGenerator, WindowScroll, DisplayContext, DOM*/  class Display {      constructor(spinner, container) { @@ -27,11 +32,11 @@ class Display {          this.index = 0;          this.audioPlaying = null;          this.audioFallback = null; -        this.audioCache = {}; +        this.audioCache = new Map();          this.styleNode = null; -        this.eventListeners = []; -        this.persistentEventListeners = []; +        this.eventListeners = new EventListenerCollection(); +        this.persistentEventListeners = new EventListenerCollection();          this.interactive = false;          this.eventListenersActive = false;          this.clickScanPrevent = false; @@ -43,6 +48,13 @@ class Display {          this.setInteractive(true);      } +    async prepare(options=null) { +        const displayGeneratorPromise = this.displayGenerator.prepare(); +        const updateOptionsPromise = this.updateOptions(options); +        await Promise.all([displayGeneratorPromise, updateOptionsPromise]); +        yomichan.on('optionsUpdated', () => this.updateOptions(null)); +    } +      onError(_error) {          throw new Error('Override me');      } @@ -174,15 +186,24 @@ class Display {          e.preventDefault();          const link = e.currentTarget;          const entry = link.closest('.entry'); -        const definitionIndex = this.entryIndexFind(entry); +        const index = this.entryIndexFind(entry); +        if (index < 0 || index >= this.definitions.length) { return; } +          const expressionIndex = Display.indexOf(entry.querySelectorAll('.term-expression .action-play-audio'), link); -        this.audioPlay(this.definitions[definitionIndex], expressionIndex, definitionIndex); +        this.audioPlay( +            this.definitions[index], +            // expressionIndex is used in audioPlay to detect result output mode +            Math.max(expressionIndex, this.options.general.resultOutputMode === 'merge' ? 0 : -1), +            index +        );      }      onNoteAdd(e) {          e.preventDefault();          const link = e.currentTarget;          const index = this.entryIndexFind(link); +        if (index < 0 || index >= this.definitions.length) { return; } +          this.noteAdd(this.definitions[index], link.dataset.mode);      } @@ -216,13 +237,16 @@ class Display {      }      onHistoryWheel(e) { +        if (e.altKey) { return; }          const delta = -e.deltaX || e.deltaY;          if (delta > 0) {              this.sourceTermView();              e.preventDefault(); +            e.stopPropagation();          } else if (delta < 0) {              this.nextTermView();              e.preventDefault(); +            e.stopPropagation();          }      } @@ -230,15 +254,6 @@ class Display {          throw new Error('Override me');      } -    isInitialized() { -        return this.options !== null; -    } - -    async initialize(options=null) { -        await this.updateOptions(options); -        yomichan.on('optionsUpdate', () => this.updateOptions(null)); -    } -      async updateOptions(options) {          this.options = options ? options : await apiOptionsGet(this.getOptionsContext());          this.updateDocumentOptions(this.options); @@ -252,6 +267,7 @@ class Display {          data.ankiEnabled = `${options.anki.enable}`;          data.audioEnabled = `${options.audio.enable}`;          data.compactGlossaries = `${options.general.compactGlossaries}`; +        data.enableSearchTags = `${options.scanning.enableSearchTags}`;          data.debug = `${options.general.debugInfo}`;      } @@ -285,13 +301,24 @@ class Display {          this.interactive = interactive;          if (interactive) { -            Display.addEventListener(this.persistentEventListeners, document, 'keydown', this.onKeyDown.bind(this), false); -            Display.addEventListener(this.persistentEventListeners, document, 'wheel', this.onWheel.bind(this), {passive: false}); -            Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-previous'), 'click', this.onSourceTermView.bind(this)); -            Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-next'), 'click', this.onNextTermView.bind(this)); -            Display.addEventListener(this.persistentEventListeners, document.querySelector('.navigation-header'), 'wheel', this.onHistoryWheel.bind(this), {passive: false}); +            const actionPrevious = document.querySelector('.action-previous'); +            const actionNext = document.querySelector('.action-next'); +            // const navigationHeader = document.querySelector('.navigation-header'); + +            this.persistentEventListeners.addEventListener(document, 'keydown', this.onKeyDown.bind(this), false); +            this.persistentEventListeners.addEventListener(document, 'wheel', this.onWheel.bind(this), {passive: false}); +            if (actionPrevious !== null) { +                this.persistentEventListeners.addEventListener(actionPrevious, 'click', this.onSourceTermView.bind(this)); +            } +            if (actionNext !== null) { +                this.persistentEventListeners.addEventListener(actionNext, 'click', this.onNextTermView.bind(this)); +            } +            // temporarily disabled +            // if (navigationHeader !== null) { +            //     this.persistentEventListeners.addEventListener(navigationHeader, 'wheel', this.onHistoryWheel.bind(this), {passive: false}); +            // }          } else { -            Display.clearEventListeners(this.persistentEventListeners); +            this.persistentEventListeners.removeAllEventListeners();          }          this.setEventListenersActive(this.eventListenersActive);      } @@ -302,23 +329,23 @@ class Display {          this.eventListenersActive = active;          if (active) { -            this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this)); -            this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this)); -            this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this)); -            this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this)); +            this.addMultipleEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this)); +            this.addMultipleEventListeners('.action-view-note', 'click', this.onNoteView.bind(this)); +            this.addMultipleEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this)); +            this.addMultipleEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));              if (this.options.scanning.enablePopupSearch) { -                this.addEventListeners('.glossary-item', 'mouseup', this.onGlossaryMouseUp.bind(this)); -                this.addEventListeners('.glossary-item', 'mousedown', this.onGlossaryMouseDown.bind(this)); -                this.addEventListeners('.glossary-item', 'mousemove', this.onGlossaryMouseMove.bind(this)); +                this.addMultipleEventListeners('.term-glossary-item, .tag', 'mouseup', this.onGlossaryMouseUp.bind(this)); +                this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousedown', this.onGlossaryMouseDown.bind(this)); +                this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousemove', this.onGlossaryMouseMove.bind(this));              }          } else { -            Display.clearEventListeners(this.eventListeners); +            this.eventListeners.removeAllEventListeners();          }      } -    addEventListeners(selector, type, listener, options) { +    addMultipleEventListeners(selector, type, listener, options) {          for (const node of this.container.querySelectorAll(selector)) { -            Display.addEventListener(this.eventListeners, node, type, listener, options); +            this.eventListeners.addEventListener(node, type, listener, options);          }      } @@ -348,7 +375,6 @@ class Display {      async setContentTerms(definitions, context, token) {          if (!context) { throw new Error('Context expected'); } -        if (!this.isInitialized()) { return; }          this.setEventListenersActive(false); @@ -356,11 +382,6 @@ class Display {              window.focus();          } -        if (!this.displayGenerator.isInitialized()) { -            await this.displayGenerator.initialize(); -            if (this.setContentToken !== token) { return; } -        } -          this.definitions = definitions;          if (context.disableHistory) {              delete context.disableHistory; @@ -404,7 +425,7 @@ class Display {          this.setEventListenersActive(true); -        const states = await apiDefinitionsAddable(definitions, ['term-kanji', 'term-kana'], this.getOptionsContext()); +        const states = await this.getDefinitionsAddable(definitions, ['term-kanji', 'term-kana']);          if (this.setContentToken !== token) { return; }          this.updateAdderButtons(states); @@ -412,7 +433,6 @@ class Display {      async setContentKanji(definitions, context, token) {          if (!context) { throw new Error('Context expected'); } -        if (!this.isInitialized()) { return; }          this.setEventListenersActive(false); @@ -420,11 +440,6 @@ class Display {              window.focus();          } -        if (!this.displayGenerator.isInitialized()) { -            await this.displayGenerator.initialize(); -            if (this.setContentToken !== token) { return; } -        } -          this.definitions = definitions;          if (context.disableHistory) {              delete context.disableHistory; @@ -446,7 +461,7 @@ class Display {          for (let i = 0, ii = definitions.length; i < ii; ++i) {              if (i > 0) { -                await promiseTimeout(0); +                await promiseTimeout(1);                  if (this.setContentToken !== token) { return; }              } @@ -459,7 +474,7 @@ class Display {          this.setEventListenersActive(true); -        const states = await apiDefinitionsAddable(definitions, ['kanji'], this.getOptionsContext()); +        const states = await this.getDefinitionsAddable(definitions, ['kanji']);          if (this.setContentToken !== token) { return; }          this.updateAdderButtons(states); @@ -498,6 +513,8 @@ class Display {      }      autoPlayAudio() { +        if (this.definitions.length === 0) { return; } +          this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0);      } @@ -597,9 +614,12 @@ class Display {      }      noteTryAdd(mode) { -        const button = this.adderButtonFind(this.index, mode); +        const index = this.index; +        if (index < 0 || index >= this.definitions.length) { return; } + +        const button = this.adderButtonFind(index, mode);          if (button !== null && !button.classList.contains('disabled')) { -            this.noteAdd(this.definitions[this.index], mode); +            this.noteAdd(this.definitions[index], mode);          }      } @@ -698,7 +718,7 @@ class Display {      async getScreenshot() {          try {              await this.setPopupVisibleOverride(false); -            await Display.delay(1); // Wait for popup to be hidden. +            await promiseTimeout(1); // Wait for popup to be hidden.              const {format, quality} = this.options.anki.screenshot;              const dataUrl = await apiScreenshotGet({format, quality}); @@ -767,8 +787,12 @@ class Display {          return entry !== null ? entry.querySelector('.action-play-audio>img') : null;      } -    static delay(time) { -        return new Promise((resolve) => setTimeout(resolve, time)); +    async getDefinitionsAddable(definitions, modes) { +        try { +            return await apiDefinitionsAddable(definitions, modes, this.getOptionsContext()); +        } catch (e) { +            return []; +        }      }      static indexOf(nodeList, node) { @@ -780,19 +804,6 @@ class Display {          return -1;      } -    static addEventListener(eventListeners, object, type, listener, options) { -        if (object === null) { return; } -        object.addEventListener(type, listener, options); -        eventListeners.push([object, type, listener, options]); -    } - -    static clearEventListeners(eventListeners) { -        for (const [object, type, listener, options] of eventListeners) { -            object.removeEventListener(type, listener, options); -        } -        eventListeners.length = 0; -    } -      static getElementTop(element) {          const elementRect = element.getBoundingClientRect();          const documentRect = document.documentElement.getBoundingClientRect(); @@ -901,9 +912,12 @@ Display._onKeyDownHandlers = new Map([      ['P', (self, e) => {          if (e.altKey) { -            const entry = self.getEntry(self.index); +            const index = self.index; +            if (index < 0 || index >= self.definitions.length) { return; } + +            const entry = self.getEntry(index);              if (entry !== null && entry.dataset.type === 'term') { -                self.audioPlay(self.definitions[self.index], self.firstExpressionIndex, self.index); +                self.audioPlay(self.definitions[index], self.firstExpressionIndex, index);              }              return true;          } diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js deleted file mode 100644 index 0da822d7..00000000 --- a/ext/mixed/js/japanese.js +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Copyright (C) 2016-2020  Alex Yatskov <alex@foosoft.net> - * Author: Alex Yatskov <alex@foosoft.net> - * - * 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/>. - */ - - -const JP_HALFWIDTH_KATAKANA_MAPPING = new Map([ -    ['ヲ', 'ヲヺ-'], -    ['ァ', 'ァ--'], -    ['ィ', 'ィ--'], -    ['ゥ', 'ゥ--'], -    ['ェ', 'ェ--'], -    ['ォ', 'ォ--'], -    ['ャ', 'ャ--'], -    ['ュ', 'ュ--'], -    ['ョ', 'ョ--'], -    ['ッ', 'ッ--'], -    ['ー', 'ー--'], -    ['ア', 'ア--'], -    ['イ', 'イ--'], -    ['ウ', 'ウヴ-'], -    ['エ', 'エ--'], -    ['オ', 'オ--'], -    ['カ', 'カガ-'], -    ['キ', 'キギ-'], -    ['ク', 'クグ-'], -    ['ケ', 'ケゲ-'], -    ['コ', 'コゴ-'], -    ['サ', 'サザ-'], -    ['シ', 'シジ-'], -    ['ス', 'スズ-'], -    ['セ', 'セゼ-'], -    ['ソ', 'ソゾ-'], -    ['タ', 'タダ-'], -    ['チ', 'チヂ-'], -    ['ツ', 'ツヅ-'], -    ['テ', 'テデ-'], -    ['ト', 'トド-'], -    ['ナ', 'ナ--'], -    ['ニ', 'ニ--'], -    ['ヌ', 'ヌ--'], -    ['ネ', 'ネ--'], -    ['ノ', 'ノ--'], -    ['ハ', 'ハバパ'], -    ['ヒ', 'ヒビピ'], -    ['フ', 'フブプ'], -    ['ヘ', 'ヘベペ'], -    ['ホ', 'ホボポ'], -    ['マ', 'マ--'], -    ['ミ', 'ミ--'], -    ['ム', 'ム--'], -    ['メ', 'メ--'], -    ['モ', 'モ--'], -    ['ヤ', 'ヤ--'], -    ['ユ', 'ユ--'], -    ['ヨ', 'ヨ--'], -    ['ラ', 'ラ--'], -    ['リ', 'リ--'], -    ['ル', 'ル--'], -    ['レ', 'レ--'], -    ['ロ', 'ロ--'], -    ['ワ', 'ワ--'], -    ['ン', 'ン--'] -]); - -const JP_HIRAGANA_RANGE = [0x3040, 0x309f]; -const JP_KATAKANA_RANGE = [0x30a0, 0x30ff]; -const JP_KANA_RANGES = [JP_HIRAGANA_RANGE, JP_KATAKANA_RANGE]; - -const JP_CJK_COMMON_RANGE = [0x4e00, 0x9fff]; -const JP_CJK_RARE_RANGE = [0x3400, 0x4dbf]; -const JP_CJK_RANGES = [JP_CJK_COMMON_RANGE, JP_CJK_RARE_RANGE]; - -const JP_ITERATION_MARK_CHAR_CODE = 0x3005; - -// Japanese character ranges, roughly ordered in order of expected frequency -const JP_JAPANESE_RANGES = [ -    JP_HIRAGANA_RANGE, -    JP_KATAKANA_RANGE, - -    JP_CJK_COMMON_RANGE, -    JP_CJK_RARE_RANGE, - -    [0xff66, 0xff9f], // Halfwidth katakana - -    [0x30fb, 0x30fc], // Katakana punctuation -    [0xff61, 0xff65], // Kana punctuation -    [0x3000, 0x303f], // CJK punctuation - -    [0xff10, 0xff19], // Fullwidth numbers -    [0xff21, 0xff3a], // Fullwidth upper case Latin letters -    [0xff41, 0xff5a], // Fullwidth lower case Latin letters - -    [0xff01, 0xff0f], // Fullwidth punctuation 1 -    [0xff1a, 0xff1f], // Fullwidth punctuation 2 -    [0xff3b, 0xff3f], // Fullwidth punctuation 3 -    [0xff5b, 0xff60], // Fullwidth punctuation 4 -    [0xffe0, 0xffee], // Currency markers -]; - - -// Helper functions - -function _jpIsCharCodeInRanges(charCode, ranges) { -    for (const [min, max] of ranges) { -        if (charCode >= min && charCode <= max) { -            return true; -        } -    } -    return false; -} - - -// Character code testing functions - -function jpIsCharCodeKanji(charCode) { -    return _jpIsCharCodeInRanges(charCode, JP_CJK_RANGES); -} - -function jpIsCharCodeKana(charCode) { -    return _jpIsCharCodeInRanges(charCode, JP_KANA_RANGES); -} - -function jpIsCharCodeJapanese(charCode) { -    return _jpIsCharCodeInRanges(charCode, JP_JAPANESE_RANGES); -} - - -// String testing functions - -function jpIsStringEntirelyKana(str) { -    if (str.length === 0) { return false; } -    for (let i = 0, ii = str.length; i < ii; ++i) { -        if (!jpIsCharCodeKana(str.charCodeAt(i))) { -            return false; -        } -    } -    return true; -} - -function jpIsStringPartiallyJapanese(str) { -    if (str.length === 0) { return false; } -    for (let i = 0, ii = str.length; i < ii; ++i) { -        if (jpIsCharCodeJapanese(str.charCodeAt(i))) { -            return true; -        } -    } -    return false; -} - - -// Conversion functions - -function jpKatakanaToHiragana(text) { -    let result = ''; -    for (const c of text) { -        if (wanakana.isKatakana(c)) { -            result += wanakana.toHiragana(c); -        } else { -            result += c; -        } -    } - -    return result; -} - -function jpHiraganaToKatakana(text) { -    let result = ''; -    for (const c of text) { -        if (wanakana.isHiragana(c)) { -            result += wanakana.toKatakana(c); -        } else { -            result += c; -        } -    } - -    return result; -} - -function jpToRomaji(text) { -    return wanakana.toRomaji(text); -} - -function jpConvertReading(expressionFragment, readingFragment, readingMode) { -    switch (readingMode) { -        case 'hiragana': -            return jpKatakanaToHiragana(readingFragment || ''); -        case 'katakana': -            return jpHiraganaToKatakana(readingFragment || ''); -        case 'romaji': -            if (readingFragment) { -                return jpToRomaji(readingFragment); -            } else { -                if (jpIsStringEntirelyKana(expressionFragment)) { -                    return jpToRomaji(expressionFragment); -                } -            } -            return readingFragment; -        case 'none': -            return null; -        default: -            return readingFragment; -    } -} - -function jpDistributeFurigana(expression, reading) { -    const fallback = [{furigana: reading, text: expression}]; -    if (!reading) { -        return fallback; -    } - -    let isAmbiguous = false; -    const segmentize = (reading, groups) => { -        if (groups.length === 0 || isAmbiguous) { -            return []; -        } - -        const group = groups[0]; -        if (group.mode === 'kana') { -            if (jpKatakanaToHiragana(reading).startsWith(jpKatakanaToHiragana(group.text))) { -                const readingLeft = reading.substring(group.text.length); -                const segs = segmentize(readingLeft, groups.splice(1)); -                if (segs) { -                    return [{text: group.text}].concat(segs); -                } -            } -        } else { -            let foundSegments = null; -            for (let i = reading.length; i >= group.text.length; --i) { -                const readingUsed = reading.substring(0, i); -                const readingLeft = reading.substring(i); -                const segs = segmentize(readingLeft, groups.slice(1)); -                if (segs) { -                    if (foundSegments !== null) { -                        // more than one way to segmentize the tail, mark as ambiguous -                        isAmbiguous = true; -                        return null; -                    } -                    foundSegments = [{text: group.text, furigana: readingUsed}].concat(segs); -                } -                // there is only one way to segmentize the last non-kana group -                if (groups.length === 1) { -                    break; -                } -            } -            return foundSegments; -        } -    }; - -    const groups = []; -    let modePrev = null; -    for (const c of expression) { -        const charCode = c.charCodeAt(0); -        const modeCurr = jpIsCharCodeKanji(charCode) || charCode === JP_ITERATION_MARK_CHAR_CODE ? 'kanji' : 'kana'; -        if (modeCurr === modePrev) { -            groups[groups.length - 1].text += c; -        } else { -            groups.push({mode: modeCurr, text: c}); -            modePrev = modeCurr; -        } -    } - -    const segments = segmentize(reading, groups); -    if (segments && !isAmbiguous) { -        return segments; -    } -    return fallback; -} - -function jpDistributeFuriganaInflected(expression, reading, source) { -    const output = []; - -    let stemLength = 0; -    const shortest = Math.min(source.length, expression.length); -    const sourceHiragana = jpKatakanaToHiragana(source); -    const expressionHiragana = jpKatakanaToHiragana(expression); -    while (stemLength < shortest && sourceHiragana[stemLength] === expressionHiragana[stemLength]) { -        ++stemLength; -    } -    const offset = source.length - stemLength; - -    const stemExpression = source.substring(0, source.length - offset); -    const stemReading = reading.substring( -        0, -        offset === 0 ? reading.length : reading.length - expression.length + stemLength -    ); -    for (const segment of jpDistributeFurigana(stemExpression, stemReading)) { -        output.push(segment); -    } - -    if (stemLength !== source.length) { -        output.push({text: source.substring(stemLength)}); -    } - -    return output; -} - -function jpConvertHalfWidthKanaToFullWidth(text, sourceMapping) { -    let result = ''; -    const ii = text.length; -    const hasSourceMapping = Array.isArray(sourceMapping); - -    for (let i = 0; i < ii; ++i) { -        const c = text[i]; -        const mapping = JP_HALFWIDTH_KATAKANA_MAPPING.get(c); -        if (typeof mapping !== 'string') { -            result += c; -            continue; -        } - -        let index = 0; -        switch (text.charCodeAt(i + 1)) { -            case 0xff9e: // dakuten -                index = 1; -                break; -            case 0xff9f: // handakuten -                index = 2; -                break; -        } - -        let c2 = mapping[index]; -        if (index > 0) { -            if (c2 === '-') { // invalid -                index = 0; -                c2 = mapping[0]; -            } else { -                ++i; -            } -        } - -        if (hasSourceMapping && index > 0) { -            index = result.length; -            const v = sourceMapping.splice(index + 1, 1)[0]; -            sourceMapping[index] += v; -        } -        result += c2; -    } - -    return result; -} - -function jpConvertNumericTofullWidth(text) { -    let result = ''; -    for (let i = 0, ii = text.length; i < ii; ++i) { -        let c = text.charCodeAt(i); -        if (c >= 0x30 && c <= 0x39) { // ['0', '9'] -            c += 0xff10 - 0x30; // 0xff10 = '0' full width -            result += String.fromCharCode(c); -        } else { -            result += text[i]; -        } -    } -    return result; -} - -function jpConvertAlphabeticToKana(text, sourceMapping) { -    let part = ''; -    let result = ''; -    const ii = text.length; - -    if (sourceMapping.length === ii) { -        sourceMapping.length = ii; -        sourceMapping.fill(1); -    } - -    for (let i = 0; i < ii; ++i) { -        // Note: 0x61 is the character code for 'a' -        let c = text.charCodeAt(i); -        if (c >= 0x41 && c <= 0x5a) { // ['A', 'Z'] -            c += (0x61 - 0x41); -        } else if (c >= 0x61 && c <= 0x7a) { // ['a', 'z'] -            // NOP; c += (0x61 - 0x61); -        } else if (c >= 0xff21 && c <= 0xff3a) { // ['A', 'Z'] fullwidth -            c += (0x61 - 0xff21); -        } else if (c >= 0xff41 && c <= 0xff5a) { // ['a', 'z'] fullwidth -            c += (0x61 - 0xff41); -        } else if (c === 0x2d || c === 0xff0d) { // '-' or fullwidth dash -            c = 0x2d; // '-' -        } else { -            if (part.length > 0) { -                result += jpToHiragana(part, sourceMapping, result.length); -                part = ''; -            } -            result += text[i]; -            continue; -        } -        part += String.fromCharCode(c); -    } - -    if (part.length > 0) { -        result += jpToHiragana(part, sourceMapping, result.length); -    } -    return result; -} - -function jpToHiragana(text, sourceMapping, sourceMappingStart) { -    const result = wanakana.toHiragana(text); - -    // Generate source mapping -    if (Array.isArray(sourceMapping)) { -        if (typeof sourceMappingStart !== 'number') { sourceMappingStart = 0; } -        let i = 0; -        let resultPos = 0; -        const ii = text.length; -        while (i < ii) { -            // Find smallest matching substring -            let iNext = i + 1; -            let resultPosNext = result.length; -            while (iNext < ii) { -                const t = wanakana.toHiragana(text.substring(0, iNext)); -                if (t === result.substring(0, t.length)) { -                    resultPosNext = t.length; -                    break; -                } -                ++iNext; -            } - -            // Merge characters -            const removals = iNext - i - 1; -            if (removals > 0) { -                let sum = 0; -                const vs = sourceMapping.splice(sourceMappingStart + 1, removals); -                for (const v of vs) { sum += v; } -                sourceMapping[sourceMappingStart] += sum; -            } -            ++sourceMappingStart; - -            // Empty elements -            const additions = resultPosNext - resultPos - 1; -            for (let j = 0; j < additions; ++j) { -                sourceMapping.splice(sourceMappingStart, 0, 0); -                ++sourceMappingStart; -            } - -            i = iNext; -            resultPos = resultPosNext; -        } -    } - -    return result; -} diff --git a/ext/mixed/js/template-handler.js b/ext/mixed/js/template-handler.js new file mode 100644 index 00000000..a5a62937 --- /dev/null +++ b/ext/mixed/js/template-handler.js @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 <http://www.gnu.org/licenses/>. + */ + + +class TemplateHandler { +    constructor(html) { +        this._templates = new Map(); + +        const doc = new DOMParser().parseFromString(html, 'text/html'); +        for (const template of doc.querySelectorAll('template')) { +            this._setTemplate(template); +        } +    } + +    _setTemplate(template) { +        const idMatch = template.id.match(/^([a-z-]+)-template$/); +        if (!idMatch) { +            throw new Error(`Invalid template ID: ${template.id}`); +        } +        this._templates.set(idMatch[1], template); +    } + +    instantiate(name) { +        const template = this._templates.get(name); +        return document.importNode(template.content.firstChild, true); +    } + +    instantiateFragment(name) { +        const template = this._templates.get(name); +        return document.importNode(template.content, true); +    } +} diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 88f1e27a..ff0eac8b 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -16,6 +16,7 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global docRangeFromPoint, TextSourceRange, DOM*/  class TextScanner {      constructor(node, ignoreNodes, ignoreElements, ignorePoints) { @@ -30,7 +31,7 @@ class TextScanner {          this.options = null;          this.enabled = false; -        this.eventListeners = []; +        this.eventListeners = new EventListenerCollection();          this.primaryTouchIdentifier = null;          this.preventNextContextMenu = false; @@ -140,24 +141,24 @@ class TextScanner {          const textSourceCurrentPrevious = this.textSourceCurrent !== null ? this.textSourceCurrent.clone() : null;          this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchStart') -        .then(() => { -            if ( -                this.textSourceCurrent === null || -                this.textSourceCurrent.equals(textSourceCurrentPrevious) -            ) { -                return; -            } +            .then(() => { +                if ( +                    this.textSourceCurrent === null || +                    this.textSourceCurrent.equals(textSourceCurrentPrevious) +                ) { +                    return; +                } -            this.preventScroll = true; -            this.preventNextContextMenu = true; -            this.preventNextMouseDown = true; -        }); +                this.preventScroll = true; +                this.preventNextContextMenu = true; +                this.preventNextMouseDown = true; +            });      }      onTouchEnd(e) {          if (              this.primaryTouchIdentifier === null || -            TextScanner.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0 +            TextScanner.getTouch(e.changedTouches, this.primaryTouchIdentifier) === null          ) {              return;          } @@ -180,13 +181,11 @@ class TextScanner {              return;          } -        const touches = e.changedTouches; -        const index = TextScanner.getIndexOfTouch(touches, this.primaryTouchIdentifier); -        if (index < 0) { +        const primaryTouch = TextScanner.getTouch(e.changedTouches, this.primaryTouchIdentifier); +        if (primaryTouch === null) {              return;          } -        const primaryTouch = touches[index];          this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove');          e.preventDefault(); // Disable scroll @@ -228,7 +227,7 @@ class TextScanner {              }          } else {              if (this.enabled) { -                this.clearEventListeners(); +                this.eventListeners.removeAllEventListeners();                  this.enabled = false;              }              this.onSearchClear(false); @@ -236,13 +235,13 @@ class TextScanner {      }      hookEvents() { -        let eventListeners = this.getMouseEventListeners(); +        let eventListenerInfos = this.getMouseEventListeners();          if (this.options.scanning.touchInputEnabled) { -            eventListeners = eventListeners.concat(this.getTouchEventListeners()); +            eventListenerInfos = eventListenerInfos.concat(this.getTouchEventListeners());          } -        for (const [node, type, listener, options] of eventListeners) { -            this.addEventListener(node, type, listener, options); +        for (const [node, type, listener, options] of eventListenerInfos) { +            this.eventListeners.addEventListener(node, type, listener, options);          }      } @@ -267,18 +266,6 @@ class TextScanner {          ];      } -    addEventListener(node, type, listener, options) { -        node.addEventListener(type, listener, options); -        this.eventListeners.push([node, type, listener, options]); -    } - -    clearEventListeners() { -        for (const [node, type, listener, options] of this.eventListeners) { -            node.removeEventListener(type, listener, options); -        } -        this.eventListeners = []; -    } -      setOptions(options) {          this.options = options;          this.setEnabled(this.options.general.enable); @@ -367,13 +354,12 @@ class TextScanner {          }      } -    static getIndexOfTouch(touchList, identifier) { -        for (const i in touchList) { -            const t = touchList[i]; -            if (t.identifier === identifier) { -                return i; +    static getTouch(touchList, identifier) { +        for (const touch of touchList) { +            if (touch.identifier === identifier) { +                return touch;              }          } -        return -1; +        return null;      }  } |