diff options
37 files changed, 577 insertions, 541 deletions
| diff --git a/.eslintrc.json b/.eslintrc.json index fcc6995b..4ee1f982 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,6 +22,7 @@          "dot-notation": "error",          "eqeqeq": "error",          "func-names": ["error", "always"], +        "guard-for-in": "error",          "no-case-declarations": "error",          "no-const-assign": "error",          "no-constant-condition": "off", @@ -73,7 +74,7 @@      },      "overrides": [          { -            "files": ["*.js"], +            "files": ["ext/**/*.js"],              "excludedFiles": ["ext/mixed/js/core.js"],              "globals": {                  "yomichan": "readonly", diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 0ecfd5c4..972e2b8b 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -52,7 +52,7 @@ const audioUrlBuilders = new Map([          for (const row of dom.getElementsByClassName('dc-result-row')) {              try {                  const url = row.querySelector('audio>source[src]').getAttribute('src'); -                const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText; +                const reading = row.getElementsByClassName('dc-vocab_kana').item(0).textContent;                  if (url && reading && (!definition.reading || definition.reading === reading)) {                      return audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');                  } @@ -156,8 +156,8 @@ function audioBuildFilename(definition) {  async function audioInject(definition, fields, sources, optionsContext) {      let usesAudio = false; -    for (const name in fields) { -        if (fields[name].includes('{audio}')) { +    for (const fieldValue of Object.values(fields)) { +        if (fieldValue.includes('{audio}')) {              usesAudio = true;              break;          } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 2691b7d9..238ac52c 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -48,6 +48,41 @@ class Backend {          this.apiForwarder = new BackendApiForwarder();          this.messageToken = yomichan.generateId(16); + +        this._messageHandlers = new Map([ +            ['optionsSchemaGet', this._onApiOptionsSchemaGet.bind(this)], +            ['optionsGet', this._onApiOptionsGet.bind(this)], +            ['optionsGetFull', this._onApiOptionsGetFull.bind(this)], +            ['optionsSet', this._onApiOptionsSet.bind(this)], +            ['optionsSave', this._onApiOptionsSave.bind(this)], +            ['kanjiFind', this._onApiKanjiFind.bind(this)], +            ['termsFind', this._onApiTermsFind.bind(this)], +            ['textParse', this._onApiTextParse.bind(this)], +            ['textParseMecab', this._onApiTextParseMecab.bind(this)], +            ['definitionAdd', this._onApiDefinitionAdd.bind(this)], +            ['definitionsAddable', this._onApiDefinitionsAddable.bind(this)], +            ['noteView', this._onApiNoteView.bind(this)], +            ['templateRender', this._onApiTemplateRender.bind(this)], +            ['commandExec', this._onApiCommandExec.bind(this)], +            ['audioGetUrl', this._onApiAudioGetUrl.bind(this)], +            ['screenshotGet', this._onApiScreenshotGet.bind(this)], +            ['forward', this._onApiForward.bind(this)], +            ['frameInformationGet', this._onApiFrameInformationGet.bind(this)], +            ['injectStylesheet', this._onApiInjectStylesheet.bind(this)], +            ['getEnvironmentInfo', this._onApiGetEnvironmentInfo.bind(this)], +            ['clipboardGet', this._onApiClipboardGet.bind(this)], +            ['getDisplayTemplatesHtml', this._onApiGetDisplayTemplatesHtml.bind(this)], +            ['getQueryParserTemplatesHtml', this._onApiGetQueryParserTemplatesHtml.bind(this)], +            ['getZoom', this._onApiGetZoom.bind(this)], +            ['getMessageToken', this._onApiGetMessageToken.bind(this)] +        ]); + +        this._commandHandlers = new Map([ +            ['search', this._onCommandSearch.bind(this)], +            ['help', this._onCommandHelp.bind(this)], +            ['options', this._onCommandOptions.bind(this)], +            ['toggle', this._onCommandToggle.bind(this)] +        ]);      }      async prepare() { @@ -65,10 +100,10 @@ class Backend {          this.onOptionsUpdated('background');          if (isObject(chrome.commands) && isObject(chrome.commands.onCommand)) { -            chrome.commands.onCommand.addListener((command) => this._runCommand(command)); +            chrome.commands.onCommand.addListener(this._runCommand.bind(this));          }          if (isObject(chrome.tabs) && isObject(chrome.tabs.onZoomChange)) { -            chrome.tabs.onZoomChange.addListener((info) => this._onZoomChange(info)); +            chrome.tabs.onZoomChange.addListener(this._onZoomChange.bind(this));          }          chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); @@ -81,7 +116,7 @@ class Backend {          this.isPreparedResolve = null;          this.isPreparedPromise = null; -        this.clipboardMonitor.onClipboardText = (text) => this._onClipboardText(text); +        this.clipboardMonitor.onClipboardText = this._onClipboardText.bind(this);      }      onOptionsUpdated(source) { @@ -96,11 +131,11 @@ class Backend {      }      onMessage({action, params}, sender, callback) { -        const handler = Backend._messageHandlers.get(action); +        const handler = this._messageHandlers.get(action);          if (typeof handler !== 'function') { return false; }          try { -            const promise = handler(this, params, sender); +            const promise = handler(params, sender);              promise.then(                  (result) => callback({result}),                  (error) => callback({error: errorToJson(error)}) @@ -243,10 +278,10 @@ class Backend {      }      _runCommand(command, params) { -        const handler = Backend._commandHandlers.get(command); +        const handler = this._commandHandlers.get(command);          if (typeof handler !== 'function') { return false; } -        handler(this, params); +        handler(params);          return true;      } @@ -357,11 +392,11 @@ class Backend {      async _onApiTextParseMecab({text, optionsContext}) {          const options = await this.getOptions(optionsContext); -        const results = {}; +        const results = [];          const rawResults = await this.mecab.parseText(text); -        for (const mecabName in rawResults) { +        for (const [mecabName, parsedLines] of Object.entries(rawResults)) {              const result = []; -            for (const parsedLine of rawResults[mecabName]) { +            for (const parsedLine of parsedLines) {                  for (const {expression, reading, source} of parsedLine) {                      const term = [];                      if (expression !== null && reading !== null) { @@ -381,7 +416,7 @@ class Backend {                  }                  result.push([{text: '\n'}]);              } -            results[mecabName] = result; +            results.push([mecabName, result]);          }          return results;      } @@ -693,9 +728,10 @@ class Backend {      }      _onCommandOptions(params) { -        if (!(params && params.newTab)) { +        const {mode='existingOrNewTab'} = params || {}; +        if (mode === 'existingOrNewTab') {              chrome.runtime.openOptionsPage(); -        } else { +        } else if (mode === 'newTab') {              const manifest = chrome.runtime.getManifest();              const url = chrome.runtime.getURL(manifest.options_ui.page);              chrome.tabs.create({url}); @@ -718,8 +754,8 @@ class Backend {      async _injectScreenshot(definition, fields, screenshot) {          let usesScreenshot = false; -        for (const name in fields) { -            if (fields[name].includes('{screenshot}')) { +        for (const fieldValue of Object.values(fields)) { +            if (fieldValue.includes('{screenshot}')) {                  usesScreenshot = true;                  break;              } @@ -802,7 +838,7 @@ class Backend {              chrome.tabs.update(tab.id, {active: true}, () => {                  const e = chrome.runtime.lastError;                  if (e) { -                    reject(e); +                    reject(new Error(e.message));                  } else {                      resolve();                  } @@ -819,7 +855,7 @@ class Backend {                  chrome.windows.get(tab.windowId, {}, (value) => {                      const e = chrome.runtime.lastError;                      if (e) { -                        reject(e); +                        reject(new Error(e.message));                      } else {                          resolve(value);                      } @@ -830,7 +866,7 @@ class Backend {                      chrome.windows.update(tab.windowId, {focused: true}, () => {                          const e = chrome.runtime.lastError;                          if (e) { -                            reject(e); +                            reject(new Error(e.message));                          } else {                              resolve();                          } @@ -867,40 +903,5 @@ class Backend {      }  } -Backend._messageHandlers = new Map([ -    ['optionsSchemaGet', (self, ...args) => self._onApiOptionsSchemaGet(...args)], -    ['optionsGet', (self, ...args) => self._onApiOptionsGet(...args)], -    ['optionsGetFull', (self, ...args) => self._onApiOptionsGetFull(...args)], -    ['optionsSet', (self, ...args) => self._onApiOptionsSet(...args)], -    ['optionsSave', (self, ...args) => self._onApiOptionsSave(...args)], -    ['kanjiFind', (self, ...args) => self._onApiKanjiFind(...args)], -    ['termsFind', (self, ...args) => self._onApiTermsFind(...args)], -    ['textParse', (self, ...args) => self._onApiTextParse(...args)], -    ['textParseMecab', (self, ...args) => self._onApiTextParseMecab(...args)], -    ['definitionAdd', (self, ...args) => self._onApiDefinitionAdd(...args)], -    ['definitionsAddable', (self, ...args) => self._onApiDefinitionsAddable(...args)], -    ['noteView', (self, ...args) => self._onApiNoteView(...args)], -    ['templateRender', (self, ...args) => self._onApiTemplateRender(...args)], -    ['commandExec', (self, ...args) => self._onApiCommandExec(...args)], -    ['audioGetUrl', (self, ...args) => self._onApiAudioGetUrl(...args)], -    ['screenshotGet', (self, ...args) => self._onApiScreenshotGet(...args)], -    ['forward', (self, ...args) => self._onApiForward(...args)], -    ['frameInformationGet', (self, ...args) => self._onApiFrameInformationGet(...args)], -    ['injectStylesheet', (self, ...args) => self._onApiInjectStylesheet(...args)], -    ['getEnvironmentInfo', (self, ...args) => self._onApiGetEnvironmentInfo(...args)], -    ['clipboardGet', (self, ...args) => self._onApiClipboardGet(...args)], -    ['getDisplayTemplatesHtml', (self, ...args) => self._onApiGetDisplayTemplatesHtml(...args)], -    ['getQueryParserTemplatesHtml', (self, ...args) => self._onApiGetQueryParserTemplatesHtml(...args)], -    ['getZoom', (self, ...args) => self._onApiGetZoom(...args)], -    ['getMessageToken', (self, ...args) => self._onApiGetMessageToken(...args)] -]); - -Backend._commandHandlers = new Map([ -    ['search', (self, ...args) => self._onCommandSearch(...args)], -    ['help', (self, ...args) => self._onCommandHelp(...args)], -    ['options', (self, ...args) => self._onCommandOptions(...args)], -    ['toggle', (self, ...args) => self._onCommandToggle(...args)] -]); -  window.yomichanBackend = new Backend();  window.yomichanBackend.prepare(); diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js index e2ced965..d548d271 100644 --- a/ext/bg/js/deinflector.js +++ b/ext/bg/js/deinflector.js @@ -57,9 +57,9 @@ class Deinflector {      static normalizeReasons(reasons) {          const normalizedReasons = []; -        for (const reason in reasons) { +        for (const [reason, reasonInfo] of Object.entries(reasons)) {              const variants = []; -            for (const {kanaIn, kanaOut, rulesIn, rulesOut} of reasons[reason]) { +            for (const {kanaIn, kanaOut, rulesIn, rulesOut} of reasonInfo) {                  variants.push([                      kanaIn,                      kanaOut, diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index f5c5b21b..ffeac80a 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -20,22 +20,16 @@  function dictEnabledSet(options) {      const enabledDictionaryMap = new Map(); -    const optionsDictionaries = options.dictionaries; -    for (const title in optionsDictionaries) { -        if (!hasOwn(optionsDictionaries, title)) { continue; } -        const dictionary = optionsDictionaries[title]; -        if (!dictionary.enabled) { continue; } -        enabledDictionaryMap.set(title, { -            priority: dictionary.priority || 0, -            allowSecondarySearches: !!dictionary.allowSecondarySearches -        }); +    for (const [title, {enabled, priority, allowSecondarySearches}] of Object.entries(options.dictionaries)) { +        if (!enabled) { continue; } +        enabledDictionaryMap.set(title, {priority, allowSecondarySearches});      }      return enabledDictionaryMap;  }  function dictConfigured(options) { -    for (const title in options.dictionaries) { -        if (options.dictionaries[title].enabled) { +    for (const {enabled} of Object.values(options.dictionaries)) { +        if (enabled) {              return true;          }      } @@ -388,40 +382,39 @@ dictFieldFormat.markers = new Set([  ]);  async function dictNoteFormat(definition, mode, options, templates) { -    const note = {fields: {}, tags: options.anki.tags}; -    let fields = []; - -    if (mode === 'kanji') { -        fields = options.anki.kanji.fields; -        note.deckName = options.anki.kanji.deck; -        note.modelName = options.anki.kanji.model; -    } else { -        fields = options.anki.terms.fields; -        note.deckName = options.anki.terms.deck; -        note.modelName = options.anki.terms.model; - -        if (definition.audio) { -            const audio = { -                url: definition.audio.url, -                filename: definition.audio.filename, -                skipHash: '7e2c2f954ef6051373ba916f000168dc', -                fields: [] -            }; +    const isKanji = (mode === 'kanji'); +    const tags = options.anki.tags; +    const modeOptions = isKanji ? options.anki.kanji : options.anki.terms; +    const modeOptionsFieldEntries = Object.entries(modeOptions.fields); + +    const note = { +        fields: {}, +        tags, +        deckName: modeOptions.deck, +        modelName: modeOptions.model +    }; -            for (const name in fields) { -                if (fields[name].includes('{audio}')) { -                    audio.fields.push(name); -                } -            } +    for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { +        note.fields[fieldName] = await dictFieldFormat(fieldValue, definition, mode, options, templates); +    } + +    if (!isKanji && definition.audio) { +        const audioFields = []; -            if (audio.fields.length > 0) { -                note.audio = audio; +        for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { +            if (fieldValue.includes('{audio}')) { +                audioFields.push(fieldName);              }          } -    } -    for (const name in fields) { -        note.fields[name] = await dictFieldFormat(fields[name], definition, mode, options, templates); +        if (audioFields.length > 0) { +            note.audio = { +                url: definition.audio.url, +                filename: definition.audio.filename, +                skipHash: '7e2c2f954ef6051373ba916f000168dc', +                fields: audioFields +            }; +        }      }      return note; diff --git a/ext/bg/js/search-query-parser-generator.js b/ext/bg/js/search-query-parser-generator.js index f842644e..1ab23a82 100644 --- a/ext/bg/js/search-query-parser-generator.js +++ b/ext/bg/js/search-query-parser-generator.js @@ -50,7 +50,7 @@ class QueryParserGenerator {          const segmentTextContainer = segmentContainer.querySelector('.query-parser-segment-text');          const segmentReadingContainer = segmentContainer.querySelector('.query-parser-segment-reading');          segmentTextContainer.appendChild(this.createSegmentText(segment.text)); -        segmentReadingContainer.innerText = segment.reading; +        segmentReadingContainer.textContent = segment.reading;          return segmentContainer;      } @@ -58,7 +58,7 @@ class QueryParserGenerator {          const fragment = document.createDocumentFragment();          for (const chr of text) {              const charContainer = this._templateHandler.instantiate('char'); -            charContainer.innerText = chr; +            charContainer.textContent = chr;              fragment.appendChild(charContainer);          }          return fragment; @@ -69,7 +69,7 @@ class QueryParserGenerator {          for (const parseResult of parseResults) {              const optionContainer = this._templateHandler.instantiate('select-option');              optionContainer.value = parseResult.id; -            optionContainer.innerText = parseResult.name; +            optionContainer.textContent = parseResult.name;              optionContainer.defaultSelected = selectedParser === parseResult.id;              selectContainer.appendChild(optionContainer);          } diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 8c434990..11c7baa2 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -142,11 +142,11 @@ class QueryParser extends TextScanner {          }          if (this.search.options.parsing.enableMecabParser) {              const mecabResults = await apiTextParseMecab(text, this.search.getOptionsContext()); -            for (const mecabDictName in mecabResults) { +            for (const [mecabDictName, mecabDictResults] of mecabResults) {                  results.push({                      name: `MeCab: ${mecabDictName}`,                      id: `mecab-${mecabDictName}`, -                    parsedText: mecabResults[mecabDictName] +                    parsedText: mecabDictResults                  });              }          } @@ -164,7 +164,7 @@ class QueryParser extends TextScanner {      }      renderParserSelect() { -        this.queryParserSelect.innerHTML = ''; +        this.queryParserSelect.textContent = '';          if (this.parseResults.length > 1) {              const select = this.queryParserGenerator.createParserSelect(this.parseResults, this.selectedParser);              select.addEventListener('change', this.onParserChange.bind(this)); diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 98e167ad..0a7a5fe1 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -39,6 +39,25 @@ class DisplaySearch extends Display {          this.introAnimationTimer = null;          this.clipboardMonitor = new ClipboardMonitor(); + +        this._onKeyDownIgnoreKeys = new Map([ +            ['ANY_MOD', new Set([ +                'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End', +                'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', +                'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', +                'F21', 'F22', 'F23', 'F24' +            ])], +            ['Control', new Set(['C', 'A', 'Z', 'Y', 'X', 'F', 'G'])], +            ['Meta', new Set(['C', 'A', 'Z', 'Y', 'X', 'F', 'G'])], +            ['OS', new Set()], +            ['Alt', new Set()], +            ['AltGraph', new Set()], +            ['Shift', new Set()] +        ]); + +        this._runtimeMessageHandlers = new Map([ +            ['searchQueryUpdate', ({query}) => { this.onExternalSearchUpdate(query); }] +        ]);      }      static create() { @@ -55,70 +74,37 @@ class DisplaySearch extends Display {              const {queryParams: {query='', mode=''}} = parseUrl(window.location.href); -            if (this.search !== null) { -                this.search.addEventListener('click', (e) => this.onSearch(e), false); -            } -            if (this.query !== null) { -                document.documentElement.dataset.searchMode = mode; -                this.query.addEventListener('input', () => this.onSearchInput(), false); - -                if (this.wanakanaEnable !== null) { -                    if (this.options.general.enableWanakana === true) { -                        this.wanakanaEnable.checked = true; -                        window.wanakana.bind(this.query); -                    } else { -                        this.wanakanaEnable.checked = false; -                    } -                    this.wanakanaEnable.addEventListener('change', (e) => { -                        const {queryParams: {query: query2=''}} = parseUrl(window.location.href); -                        if (e.target.checked) { -                            window.wanakana.bind(this.query); -                            apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext()); -                        } else { -                            window.wanakana.unbind(this.query); -                            apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext()); -                        } -                        this.setQuery(query2); -                        this.onSearchQueryUpdated(this.query.value, false); -                    }); -                } +            document.documentElement.dataset.searchMode = mode; -                this.setQuery(query); -                this.onSearchQueryUpdated(this.query.value, false); +            if (this.options.general.enableWanakana === true) { +                this.wanakanaEnable.checked = true; +                window.wanakana.bind(this.query); +            } else { +                this.wanakanaEnable.checked = false;              } -            if (this.clipboardMonitorEnable !== null && mode !== 'popup') { + +            this.setQuery(query); +            this.onSearchQueryUpdated(this.query.value, false); + +            if (mode !== 'popup') {                  if (this.options.general.enableClipboardMonitor === true) {                      this.clipboardMonitorEnable.checked = true;                      this.clipboardMonitor.start();                  } else {                      this.clipboardMonitorEnable.checked = false;                  } -                this.clipboardMonitorEnable.addEventListener('change', (e) => { -                    if (e.target.checked) { -                        chrome.permissions.request( -                            {permissions: ['clipboardRead']}, -                            (granted) => { -                                if (granted) { -                                    this.clipboardMonitor.start(); -                                    apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext()); -                                } else { -                                    e.target.checked = false; -                                } -                            } -                        ); -                    } else { -                        this.clipboardMonitor.stop(); -                        apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext()); -                    } -                }); +                this.clipboardMonitorEnable.addEventListener('change', this.onClipboardMonitorEnableChange.bind(this));              }              chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); -            window.addEventListener('popstate', (e) => this.onPopState(e)); -            window.addEventListener('copy', (e) => this.onCopy(e)); +            this.search.addEventListener('click', this.onSearch.bind(this), false); +            this.query.addEventListener('input', this.onSearchInput.bind(this), false); +            this.wanakanaEnable.addEventListener('change', this.onWanakanaEnableChange.bind(this)); +            window.addEventListener('popstate', this.onPopState.bind(this)); +            window.addEventListener('copy', this.onCopy.bind(this)); -            this.clipboardMonitor.onClipboardText = (text) => this.onExternalSearchUpdate(text); +            this.clipboardMonitor.onClipboardText = this.onExternalSearchUpdate.bind(this);              this.updateSearchButton();          } catch (e) { @@ -174,28 +160,30 @@ class DisplaySearch extends Display {      }      onRuntimeMessage({action, params}, sender, callback) { -        const handler = DisplaySearch._runtimeMessageHandlers.get(action); +        const handler = this._runtimeMessageHandlers.get(action);          if (typeof handler !== 'function') { return false; } -        const result = handler(this, params, sender); +        const result = handler(params, sender);          callback(result);          return false;      }      onKeyDown(e) {          const key = Display.getKeyFromEvent(e); -        const ignoreKeys = DisplaySearch.onKeyDownIgnoreKeys; +        const ignoreKeys = this._onKeyDownIgnoreKeys; -        const activeModifierMap = { -            'Control': e.ctrlKey, -            'Meta': e.metaKey, -            'ANY_MOD': true -        }; +        const activeModifierMap = new Map([ +            ['Control', e.ctrlKey], +            ['Meta', e.metaKey], +            ['Shift', e.shiftKey], +            ['Alt', e.altKey], +            ['ANY_MOD', true] +        ]);          let preventFocus = false; -        for (const [modifier, keys] of Object.entries(ignoreKeys)) { -            const modifierActive = activeModifierMap[modifier]; -            if (key === modifier || (modifierActive && keys.includes(key))) { +        for (const [modifier, keys] of ignoreKeys.entries()) { +            const modifierActive = activeModifierMap.get(modifier); +            if (key === modifier || (modifierActive && keys.has(key))) {                  preventFocus = true;                  break;              } @@ -253,6 +241,38 @@ class DisplaySearch extends Display {          }      } +    onWanakanaEnableChange(e) { +        const {queryParams: {query=''}} = parseUrl(window.location.href); +        const enableWanakana = e.target.checked; +        if (enableWanakana) { +            window.wanakana.bind(this.query); +        } else { +            window.wanakana.unbind(this.query); +        } +        this.setQuery(query); +        this.onSearchQueryUpdated(this.query.value, false); +        apiOptionsSet({general: {enableWanakana}}, this.getOptionsContext()); +    } + +    onClipboardMonitorEnableChange(e) { +        if (e.target.checked) { +            chrome.permissions.request( +                {permissions: ['clipboardRead']}, +                (granted) => { +                    if (granted) { +                        this.clipboardMonitor.start(); +                        apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext()); +                    } else { +                        e.target.checked = false; +                    } +                } +            ); +        } else { +            this.clipboardMonitor.stop(); +            apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext()); +        } +    } +      async updateOptions(options) {          await super.updateOptions(options);          this.queryParser.setOptions(this.options); @@ -346,23 +366,4 @@ class DisplaySearch extends Display {      }  } -DisplaySearch.onKeyDownIgnoreKeys = { -    'ANY_MOD': [ -        'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End', -        'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', -        'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', -        'F21', 'F22', 'F23', 'F24' -    ], -    'Control': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'], -    'Meta': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'], -    'OS': [], -    'Alt': [], -    'AltGraph': [], -    'Shift': [] -}; - -DisplaySearch._runtimeMessageHandlers = new Map([ -    ['searchQueryUpdate', (self, {query}) => { self.onExternalSearchUpdate(query); }] -]); -  DisplaySearch.instance = DisplaySearch.create(); diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index 2e80e334..770716d4 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -45,10 +45,10 @@ function ankiTemplatesInitialize() {          node.addEventListener('click', onAnkiTemplateMarkerClicked, false);      } -    $('#field-templates').on('change', (e) => onAnkiFieldTemplatesChanged(e)); -    $('#field-template-render').on('click', (e) => onAnkiTemplateRender(e)); -    $('#field-templates-reset').on('click', (e) => onAnkiFieldTemplatesReset(e)); -    $('#field-templates-reset-confirm').on('click', (e) => onAnkiFieldTemplatesResetConfirm(e)); +    $('#field-templates').on('change', onAnkiFieldTemplatesChanged); +    $('#field-template-render').on('click', onAnkiTemplateRender); +    $('#field-templates-reset').on('click', onAnkiFieldTemplatesReset); +    $('#field-templates-reset-confirm').on('click', onAnkiFieldTemplatesResetConfirm);      ankiTemplatesUpdateValue();  } diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index 4263fc51..782691ab 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -154,10 +154,10 @@ async function _ankiFieldsPopulate(tabId, options) {      container.appendChild(fragment);      for (const node of container.querySelectorAll('.anki-field-value')) { -        node.addEventListener('change', (e) => onFormOptionsChanged(e), false); +        node.addEventListener('change', onFormOptionsChanged, false);      }      for (const node of container.querySelectorAll('.marker-link')) { -        node.addEventListener('click', (e) => _onAnkiMarkerClicked(e), false); +        node.addEventListener('click', _onAnkiMarkerClicked, false);      }  } @@ -267,7 +267,7 @@ function ankiGetFieldMarkers(type) {  function ankiInitialize() {      for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { -        node.addEventListener('change', (e) => _onAnkiModelChanged(e), false); +        node.addEventListener('change', _onAnkiModelChanged, false);      }  } diff --git a/ext/bg/js/settings/audio-ui.js b/ext/bg/js/settings/audio-ui.js index 555380b4..206539a4 100644 --- a/ext/bg/js/settings/audio-ui.js +++ b/ext/bg/js/settings/audio-ui.js @@ -37,7 +37,7 @@ AudioSourceUI.Container = class Container {              this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));          } -        this._clickListener = () => this.onAddAudioSource(); +        this._clickListener = this.onAddAudioSource.bind(this);          this.addButton.addEventListener('click', this._clickListener, false);      } @@ -105,8 +105,8 @@ AudioSourceUI.AudioSource = class AudioSource {          this.select.value = audioSource; -        this._selectChangeListener = () => this.onSelectChanged(); -        this._removeClickListener = () => this.onRemoveClicked(); +        this._selectChangeListener = this.onSelectChanged.bind(this); +        this._removeClickListener = this.onRemoveClicked.bind(this);          this.select.addEventListener('change', this._selectChangeListener, false);          this.removeButton.addEventListener('click', this._removeClickListener, false); diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index 588d9a11..6d183a43 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -29,7 +29,7 @@ async function audioSettingsInitialize() {          document.querySelector('.audio-source-list'),          document.querySelector('.audio-source-add')      ); -    audioSourceUI.save = () => settingsSaveOptions(); +    audioSourceUI.save = settingsSaveOptions;      textToSpeechInitialize();  } @@ -37,11 +37,11 @@ async function audioSettingsInitialize() {  function textToSpeechInitialize() {      if (typeof speechSynthesis === 'undefined') { return; } -    speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false); +    speechSynthesis.addEventListener('voiceschanged', updateTextToSpeechVoices, false);      updateTextToSpeechVoices(); -    document.querySelector('#text-to-speech-voice').addEventListener('change', (e) => onTextToSpeechVoiceChange(e), false); -    document.querySelector('#text-to-speech-voice-test').addEventListener('click', () => textToSpeechTest(), false); +    document.querySelector('#text-to-speech-voice').addEventListener('change', onTextToSpeechVoiceChange, false); +    document.querySelector('#text-to-speech-voice-test').addEventListener('click', textToSpeechTest, false);  }  function updateTextToSpeechVoices() { diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js index 5a271321..4ca86b07 100644 --- a/ext/bg/js/settings/conditions-ui.js +++ b/ext/bg/js/settings/conditions-ui.js @@ -41,7 +41,7 @@ ConditionsUI.Container = class Container {              this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup));          } -        this.addButton.on('click', () => this.onAddConditionGroup()); +        this.addButton.on('click', this.onAddConditionGroup.bind(this));      }      cleanup() { @@ -127,7 +127,7 @@ ConditionsUI.ConditionGroup = class ConditionGroup {              this.children.push(new ConditionsUI.Condition(this, condition));          } -        this.addButton.on('click', () => this.onAddCondition()); +        this.addButton.on('click', this.onAddCondition.bind(this));      }      cleanup() { @@ -185,10 +185,10 @@ ConditionsUI.Condition = class Condition {          this.updateOperators();          this.updateInput(); -        this.input.on('change', () => this.onInputChanged()); -        this.typeSelect.on('change', () => this.onConditionTypeChanged()); -        this.operatorSelect.on('change', () => this.onConditionOperatorChanged()); -        this.removeButton.on('click', () => this.onRemoveClicked()); +        this.input.on('change', this.onInputChanged.bind(this)); +        this.typeSelect.on('change', this.onConditionTypeChanged.bind(this)); +        this.operatorSelect.on('change', this.onConditionOperatorChanged.bind(this)); +        this.removeButton.on('click', this.onRemoveClicked.bind(this));      }      cleanup() { @@ -235,10 +235,10 @@ ConditionsUI.Condition = class Condition {      updateInput() {          const conditionDescriptors = this.parent.parent.conditionDescriptors;          const {type, operator} = this.condition; -        const props = { -            placeholder: '', -            type: 'text' -        }; +        const props = new Map([ +            ['placeholder', ''], +            ['type', 'text'] +        ]);          const objects = [];          if (hasOwn(conditionDescriptors, type)) { @@ -252,20 +252,20 @@ ConditionsUI.Condition = class Condition {          for (const object of objects) {              if (hasOwn(object, 'placeholder')) { -                props.placeholder = object.placeholder; +                props.set('placeholder', object.placeholder);              }              if (object.type === 'number') { -                props.type = 'number'; +                props.set('type', 'number');                  for (const prop of ['step', 'min', 'max']) {                      if (hasOwn(object, prop)) { -                        props[prop] = object[prop]; +                        props.set(prop, object[prop]);                      }                  }              }          } -        for (const prop in props) { -            this.input.prop(prop, props[prop]); +        for (const [prop, value] of props.entries()) { +            this.input.prop(prop, value);          }          const {valid} = this.validateValue(this.condition.value); diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index 70a22a16..b9551073 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -36,7 +36,7 @@ class SettingsDictionaryListUI {          this.dictionaryEntries = [];          this.extra = null; -        document.querySelector('#dict-delete-confirm').addEventListener('click', (e) => this.onDictionaryConfirmDelete(e), false); +        document.querySelector('#dict-delete-confirm').addEventListener('click', this.onDictionaryConfirmDelete.bind(this), false);      }      setOptionsDictionaries(optionsDictionaries) { @@ -198,10 +198,10 @@ class SettingsDictionaryEntryUI {          this.applyValues(); -        this.eventListeners.addEventListener(this.enabledCheckbox, 'change', (e) => this.onEnabledChanged(e), false); -        this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', (e) => this.onAllowSecondarySearchesChanged(e), false); -        this.eventListeners.addEventListener(this.priorityInput, 'change', (e) => this.onPriorityChanged(e), false); -        this.eventListeners.addEventListener(this.deleteButton, 'click', (e) => this.onDeleteButtonClicked(e), false); +        this.eventListeners.addEventListener(this.enabledCheckbox, 'change', this.onEnabledChanged.bind(this), false); +        this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', this.onAllowSecondarySearchesChanged.bind(this), false); +        this.eventListeners.addEventListener(this.priorityInput, 'change', this.onPriorityChanged.bind(this), false); +        this.eventListeners.addEventListener(this.deleteButton, 'click', this.onDeleteButtonClicked.bind(this), false);      }      cleanup() { @@ -341,14 +341,14 @@ async function dictSettingsInitialize() {          document.querySelector('#dict-groups-extra'),          document.querySelector('#dict-extra-template')      ); -    dictionaryUI.save = () => settingsSaveOptions(); - -    document.querySelector('#dict-purge-button').addEventListener('click', (e) => onDictionaryPurgeButtonClick(e), false); -    document.querySelector('#dict-purge-confirm').addEventListener('click', (e) => onDictionaryPurge(e), false); -    document.querySelector('#dict-file-button').addEventListener('click', (e) => onDictionaryImportButtonClick(e), false); -    document.querySelector('#dict-file').addEventListener('change', (e) => onDictionaryImport(e), false); -    document.querySelector('#dict-main').addEventListener('change', (e) => onDictionaryMainChanged(e), false); -    document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', (e) => onDatabaseEnablePrefixWildcardSearchesChanged(e), false); +    dictionaryUI.save = settingsSaveOptions; + +    document.querySelector('#dict-purge-button').addEventListener('click', onDictionaryPurgeButtonClick, false); +    document.querySelector('#dict-purge-confirm').addEventListener('click', onDictionaryPurge, false); +    document.querySelector('#dict-file-button').addEventListener('click', onDictionaryImportButtonClick, false); +    document.querySelector('#dict-file').addEventListener('change', onDictionaryImport, false); +    document.querySelector('#dict-main').addEventListener('change', onDictionaryMainChanged, false); +    document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', onDatabaseEnablePrefixWildcardSearchesChanged, false);      await onDictionaryOptionsChanged();      await onDatabaseUpdated(); diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index c6683427..127a6d2b 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -68,7 +68,7 @@ async function formRead(options) {      options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);      options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0);      options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); -    options.general.popupScalingFactor = parseInt($('#popup-scaling-factor').val(), 10); +    options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val());      options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked');      options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked');      options.general.popupTheme = $('#popup-theme').val(); @@ -200,7 +200,7 @@ async function formWrite(options) {  }  function formSetupEventListeners() { -    $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change((e) => onFormOptionsChanged(e)); +    $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(onFormOptionsChanged);  }  function formUpdateVisibility(options) { diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index aa2b6100..1ceac177 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -28,6 +28,12 @@ class SettingsPopupPreview {          this.themeChangeTimeout = null;          this.textSource = null;          this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); + +        this._windowMessageHandlers = new Map([ +            ['setText', ({text}) => this.setText(text)], +            ['setCustomCss', ({css}) => this.setCustomCss(css)], +            ['setCustomOuterCss', ({css}) => this.setCustomOuterCss(css)] +        ]);      }      static create() { @@ -38,15 +44,12 @@ class SettingsPopupPreview {      async prepare() {          // Setup events -        window.addEventListener('message', (e) => this.onMessage(e), false); +        window.addEventListener('message', this.onMessage.bind(this), false); -        const themeDarkCheckbox = document.querySelector('#theme-dark-checkbox'); -        if (themeDarkCheckbox !== null) { -            themeDarkCheckbox.addEventListener('change', () => this.onThemeDarkCheckboxChanged(themeDarkCheckbox), false); -        } +        document.querySelector('#theme-dark-checkbox').addEventListener('change', this.onThemeDarkCheckboxChanged.bind(this), false);          // Overwrite API functions -        window.apiOptionsGet = (...args) => this.apiOptionsGet(...args); +        window.apiOptionsGet = this.apiOptionsGet.bind(this);          // Overwrite frontend          const popupHost = new PopupProxyHost(); @@ -56,7 +59,7 @@ class SettingsPopupPreview {          this.popup.setChildrenSupported(false);          this.popupSetCustomOuterCssOld = this.popup.setCustomOuterCss; -        this.popup.setCustomOuterCss = (...args) => this.popupSetCustomOuterCss(...args); +        this.popup.setCustomOuterCss = this.popupSetCustomOuterCss.bind(this);          this.frontend = new Frontend(this.popup); @@ -101,14 +104,14 @@ class SettingsPopupPreview {          if (e.origin !== this._targetOrigin) { return; }          const {action, params} = e.data; -        const handler = SettingsPopupPreview._messageHandlers.get(action); +        const handler = this._windowMessageHandlers.get(action);          if (typeof handler !== 'function') { return; } -        handler(this, params); +        handler(params);      } -    onThemeDarkCheckboxChanged(node) { -        document.documentElement.classList.toggle('dark', node.checked); +    onThemeDarkCheckboxChanged(e) { +        document.documentElement.classList.toggle('dark', e.target.checked);          if (this.themeChangeTimeout !== null) {              clearTimeout(this.themeChangeTimeout);          } @@ -171,12 +174,6 @@ class SettingsPopupPreview {      }  } -SettingsPopupPreview._messageHandlers = new Map([ -    ['setText', (self, {text}) => self.setText(text)], -    ['setCustomCss', (self, {css}) => self.setCustomCss(css)], -    ['setCustomOuterCss', (self, {css}) => self.setCustomOuterCss(css)] -]); -  SettingsPopupPreview.instance = SettingsPopupPreview.create(); diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index 3e589809..f946a33a 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -39,16 +39,16 @@ async function profileOptionsSetup() {  }  function profileOptionsSetupEventListeners() { -    $('#profile-target').change((e) => onTargetProfileChanged(e)); -    $('#profile-name').change((e) => onProfileNameChanged(e)); -    $('#profile-add').click((e) => onProfileAdd(e)); -    $('#profile-remove').click((e) => onProfileRemove(e)); -    $('#profile-remove-confirm').click((e) => onProfileRemoveConfirm(e)); -    $('#profile-copy').click((e) => onProfileCopy(e)); -    $('#profile-copy-confirm').click((e) => onProfileCopyConfirm(e)); +    $('#profile-target').change(onTargetProfileChanged); +    $('#profile-name').change(onProfileNameChanged); +    $('#profile-add').click(onProfileAdd); +    $('#profile-remove').click(onProfileRemove); +    $('#profile-remove-confirm').click(onProfileRemoveConfirm); +    $('#profile-copy').click(onProfileCopy); +    $('#profile-copy-confirm').click(onProfileCopyConfirm);      $('#profile-move-up').click(() => onProfileMove(-1));      $('#profile-move-down').click(() => onProfileMove(1)); -    $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change((e) => onProfileOptionsChanged(e)); +    $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(onProfileOptionsChanged);  }  function tryGetIntegerValue(selector, min, max) { diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js index cbe1bb4d..8978414e 100644 --- a/ext/bg/js/settings/storage.js +++ b/ext/bg/js/settings/storage.js @@ -57,7 +57,7 @@ async function storageInfoInitialize() {      await storageShowInfo(); -    document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false); +    document.querySelector('#storage-refresh').addEventListener('click', storageShowInfo, false);  }  async function storageUpdateStats() { diff --git a/ext/fg/float.html b/ext/fg/float.html index 082755f5..352a866a 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -51,6 +51,7 @@          <script src="/mixed/js/display.js"></script>          <script src="/mixed/js/display-generator.js"></script>          <script src="/mixed/js/scroll.js"></script> +        <script src="/mixed/js/template-handler.js"></script>          <script src="/fg/js/float.js"></script> diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index ea9ac965..35861475 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -50,7 +50,9 @@ function docImposterCreate(element, isTextarea) {      const imposter = document.createElement('div');      const imposterStyle = imposter.style; -    imposter.innerText = element.value; +    let value = element.value; +    if (value.endsWith('\n')) { value += '\n'; } +    imposter.textContent = value;      for (let i = 0, ii = elementStyle.length; i < ii; ++i) {          const property = elementStyle[i]; diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 8f21a9c5..bc459d23 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -33,8 +33,27 @@ class DisplayFloat extends Display {          this._messageToken = null;          this._messageTokenPromise = null; -        yomichan.on('orphaned', () => this.onOrphaned()); -        window.addEventListener('message', (e) => this.onMessage(e), false); +        this._onKeyDownHandlers = new Map([ +            ['C', (e) => { +                if (e.ctrlKey && !window.getSelection().toString()) { +                    this.onSelectionCopy(); +                    return true; +                } +                return false; +            }], +            ...this._onKeyDownHandlers +        ]); + +        this._windowMessageHandlers = new Map([ +            ['setContent', ({type, details}) => this.setContent(type, details)], +            ['clearAutoPlayTimer', () => this.clearAutoPlayTimer()], +            ['setCustomCss', ({css}) => this.setCustomCss(css)], +            ['prepare', ({options, popupInfo, url, childrenSupported, scale, uniqueId}) => this.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)], +            ['setContentScale', ({scale}) => this.setContentScale(scale)] +        ]); + +        yomichan.on('orphaned', this.onOrphaned.bind(this)); +        window.addEventListener('message', this.onMessage.bind(this), false);      }      async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) { @@ -96,18 +115,6 @@ class DisplayFloat extends Display {          }      } -    onKeyDown(e) { -        const key = Display.getKeyFromEvent(e); -        const handler = DisplayFloat._onKeyDownHandlers.get(key); -        if (typeof handler === 'function') { -            if (handler(this, e)) { -                e.preventDefault(); -                return true; -            } -        } -        return super.onKeyDown(e); -    } -      async getMessageToken() {          // this._messageTokenPromise is used to ensure that only one call to apiGetMessageToken is made.          if (this._messageTokenPromise === null) { @@ -126,10 +133,10 @@ class DisplayFloat extends Display {              return;          } -        const handler = DisplayFloat._messageHandlers.get(action); +        const handler = this._windowMessageHandlers.get(action);          if (typeof handler !== 'function') { return; } -        handler(this, params); +        handler(params);      }      getOptionsContext() { @@ -153,22 +160,4 @@ class DisplayFloat extends Display {      }  } -DisplayFloat._onKeyDownHandlers = new Map([ -    ['C', (self, e) => { -        if (e.ctrlKey && !window.getSelection().toString()) { -            self.onSelectionCopy(); -            return true; -        } -        return false; -    }] -]); - -DisplayFloat._messageHandlers = new Map([ -    ['setContent', (self, {type, details}) => self.setContent(type, details)], -    ['clearAutoPlayTimer', (self) => self.clearAutoPlayTimer()], -    ['setCustomCss', (self, {css}) => self.setCustomCss(css)], -    ['prepare', (self, {options, popupInfo, url, childrenSupported, scale, uniqueId}) => self.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)], -    ['setContentScale', (self, {scale}) => self.setContentScale(scale)] -]); -  DisplayFloat.instance = new DisplayFloat(); diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 67045241..929ab56a 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -39,6 +39,15 @@ class Frontend extends TextScanner {          this._contentScale = 1.0;          this._orphaned = true;          this._lastShowPromise = Promise.resolve(); + +        this._windowMessageHandlers = new Map([ +            ['popupClose', () => this.onSearchClear(true)], +            ['selectionCopy', () => document.execCommand('copy')] +        ]); + +        this._runtimeMessageHandlers = new Map([ +            ['popupSetVisibleOverride', ({visible}) => { this.popup.setVisibleOverride(visible); }] +        ]);      }      async prepare() { @@ -55,9 +64,9 @@ class Frontend extends TextScanner {                  window.visualViewport.addEventListener('resize', this.onVisualViewportResize.bind(this));              } -            yomichan.on('orphaned', () => this.onOrphaned()); -            yomichan.on('optionsUpdated', () => this.updateOptions()); -            yomichan.on('zoomChanged', (e) => this.onZoomChanged(e)); +            yomichan.on('orphaned', this.onOrphaned.bind(this)); +            yomichan.on('optionsUpdated', this.updateOptions.bind(this)); +            yomichan.on('zoomChanged', this.onZoomChanged.bind(this));              chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));              this._updateContentScale(); @@ -72,17 +81,17 @@ class Frontend extends TextScanner {      onWindowMessage(e) {          const action = e.data; -        const handler = Frontend._windowMessageHandlers.get(action); +        const handler = this._windowMessageHandlers.get(action);          if (typeof handler !== 'function') { return false; } -        handler(this); +        handler();      }      onRuntimeMessage({action, params}, sender, callback) { -        const handler = Frontend._runtimeMessageHandlers.get(action); +        const handler = this._runtimeMessageHandlers.get(action);          if (typeof handler !== 'function') { return false; } -        const result = handler(this, params, sender); +        const result = handler(params, sender);          callback(result);          return false;      } @@ -237,12 +246,3 @@ class Frontend extends TextScanner {          return visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0;      }  } - -Frontend._windowMessageHandlers = new Map([ -    ['popupClose', (self) => self.onSearchClear(true)], -    ['selectionCopy', () => document.execCommand('copy')] -]); - -Frontend._runtimeMessageHandlers = new Map([ -    ['popupSetVisibleOverride', (self, {visible}) => { self.popup.setVisibleOverride(visible); }] -]); diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index e55801ff..bef2cb16 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -34,16 +34,16 @@ class PopupProxyHost {          if (typeof frameId !== 'number') { return; }          this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([ -            ['getOrCreatePopup', ({id, parentId}) => this._onApiGetOrCreatePopup(id, parentId)], -            ['setOptions', ({id, options}) => this._onApiSetOptions(id, options)], -            ['hide', ({id, changeFocus}) => this._onApiHide(id, changeFocus)], -            ['isVisible', ({id}) => this._onApiIsVisibleAsync(id)], -            ['setVisibleOverride', ({id, visible}) => this._onApiSetVisibleOverride(id, visible)], -            ['containsPoint', ({id, x, y}) => this._onApiContainsPoint(id, x, y)], -            ['showContent', ({id, elementRect, writingMode, type, details}) => this._onApiShowContent(id, elementRect, writingMode, type, details)], -            ['setCustomCss', ({id, css}) => this._onApiSetCustomCss(id, css)], -            ['clearAutoPlayTimer', ({id}) => this._onApiClearAutoPlayTimer(id)], -            ['setContentScale', ({id, scale}) => this._onApiSetContentScale(id, scale)] +            ['getOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)], +            ['setOptions', this._onApiSetOptions.bind(this)], +            ['hide', this._onApiHide.bind(this)], +            ['isVisible', this._onApiIsVisibleAsync.bind(this)], +            ['setVisibleOverride', this._onApiSetVisibleOverride.bind(this)], +            ['containsPoint', this._onApiContainsPoint.bind(this)], +            ['showContent', this._onApiShowContent.bind(this)], +            ['setCustomCss', this._onApiSetCustomCss.bind(this)], +            ['clearAutoPlayTimer', this._onApiClearAutoPlayTimer.bind(this)], +            ['setContentScale', this._onApiSetContentScale.bind(this)]          ]));      } @@ -87,56 +87,56 @@ class PopupProxyHost {      // Message handlers -    async _onApiGetOrCreatePopup(id, parentId) { +    async _onApiGetOrCreatePopup({id, parentId}) {          const popup = this.getOrCreatePopup(id, parentId);          return {              id: popup.id          };      } -    async _onApiSetOptions(id, options) { +    async _onApiSetOptions({id, options}) {          const popup = this._getPopup(id);          return await popup.setOptions(options);      } -    async _onApiHide(id, changeFocus) { +    async _onApiHide({id, changeFocus}) {          const popup = this._getPopup(id);          return popup.hide(changeFocus);      } -    async _onApiIsVisibleAsync(id) { +    async _onApiIsVisibleAsync({id}) {          const popup = this._getPopup(id);          return await popup.isVisible();      } -    async _onApiSetVisibleOverride(id, visible) { +    async _onApiSetVisibleOverride({id, visible}) {          const popup = this._getPopup(id);          return await popup.setVisibleOverride(visible);      } -    async _onApiContainsPoint(id, x, y) { +    async _onApiContainsPoint({id, x, y}) {          const popup = this._getPopup(id);          return await popup.containsPoint(x, y);      } -    async _onApiShowContent(id, elementRect, writingMode, type, details) { +    async _onApiShowContent({id, elementRect, writingMode, type, details}) {          const popup = this._getPopup(id);          elementRect = PopupProxyHost._convertJsonRectToDOMRect(popup, elementRect);          if (!PopupProxyHost._popupCanShow(popup)) { return; }          return await popup.showContent(elementRect, writingMode, type, details);      } -    async _onApiSetCustomCss(id, css) { +    async _onApiSetCustomCss({id, css}) {          const popup = this._getPopup(id);          return popup.setCustomCss(css);      } -    async _onApiClearAutoPlayTimer(id) { +    async _onApiClearAutoPlayTimer({id}) {          const popup = this._getPopup(id);          return popup.clearAutoPlayTimer();      } -    async _onApiSetContentScale(id, scale) { +    async _onApiSetContentScale({id, scale}) {          const popup = this._getPopup(id);          return popup.setContentScale(scale);      } diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 4927f4bd..bc40a8c4 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -260,7 +260,7 @@ class Popup {              'mozfullscreenchange',              'webkitfullscreenchange'          ]; -        const onFullscreenChanged = () => this._onFullscreenChanged(); +        const onFullscreenChanged = this._onFullscreenChanged.bind(this);          for (const eventName of fullscreenEvents) {              this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false);          } diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index fa785ec4..6dc482bd 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -366,7 +366,7 @@ class TextSourceElement {      setEndOffset(length) {          switch (this.element.nodeName.toUpperCase()) {              case 'BUTTON': -                this.content = this.element.innerHTML; +                this.content = this.element.textContent;                  break;              case 'IMG':                  this.content = this.element.getAttribute('alt'); diff --git a/ext/manifest.json b/ext/manifest.json index b86459f9..09e89729 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -1,7 +1,7 @@  {      "manifest_version": 2,      "name": "Yomichan", -    "version": "20.1.26.0", +    "version": "20.2.24.0",      "description": "Japanese dictionary with Anki integration",      "icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"}, diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index 6a5383bc..c4758235 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -30,8 +30,8 @@   * General   */ -html:root[data-yomichan-page=float]:not([data-yomichan-theme]), -html:root[data-yomichan-page=float]:not([data-yomichan-theme]) body { +:root[data-yomichan-page=float]:not([data-yomichan-theme]), +:root[data-yomichan-page=float]:not([data-yomichan-theme]) body {      background-color: transparent;  } @@ -65,10 +65,6 @@ ol, ul {      height: 2.28571428em; /* 14px => 32px */  } -.invisible { -    visibility: hidden; -} -  /*   * Navigation   */ @@ -82,17 +78,18 @@ ol, ul {      padding: 0.25em 0.5em;      border-bottom-width: 0.07142857em; /* 14px => 1px */      border-bottom-style: solid; +    z-index: 10;  } -html:root[data-yomichan-page=search] .navigation-header { +:root[data-yomichan-page=search] .navigation-header {      position: sticky;  } -html:root[data-yomichan-page=float] .navigation-header { +:root[data-yomichan-page=float] .navigation-header {      position: fixed;  } -html:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation-header-spacer { +:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation-header-spacer {      height: 2.1em;  } @@ -136,7 +133,7 @@ html:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation      margin-right: 0.2em;  } -html:root[data-yomichan-page=search][data-search-mode=popup] .search-input { +:root[data-yomichan-page=search][data-search-mode=popup] .search-input {      display: none;  } @@ -150,7 +147,7 @@ html:root[data-yomichan-page=search][data-search-mode=popup] .search-input {      padding-bottom: 0.72em;  } -html:root[data-yomichan-page=float] .entry { +:root[data-yomichan-page=float] .entry {      padding-left: 0.72em;      padding-right: 0.72em;  } @@ -231,7 +228,7 @@ button.action-button {      margin-right: 0.375em;  } -html:root:not([data-enable-search-tags=true]) .tag[data-category=search] { +:root:not([data-enable-search-tags=true]) .tag[data-category=search] {      display: none;  } diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js index 46f3d17e..d7e77cc0 100644 --- a/ext/mixed/js/display-generator.js +++ b/ext/mixed/js/display-generator.js @@ -16,36 +16,20 @@   * along with this program.  If not, see <http://www.gnu.org/licenses/>.   */ -/*global apiGetDisplayTemplatesHtml*/ +/*global apiGetDisplayTemplatesHtml, TemplateHandler*/  class DisplayGenerator {      constructor() { -        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;      }      async prepare() {          const html = await apiGetDisplayTemplatesHtml(); -        const doc = new DOMParser().parseFromString(html, 'text/html'); -        this._setTemplates(doc); +        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'); @@ -78,7 +62,7 @@ class DisplayGenerator {      }      createTermExpression([details, termTags]) { -        const node = DisplayGenerator._instantiateTemplate(this._termExpressionTemplate); +        const node = this._templateHandler.instantiate('term-expression');          const expressionContainer = node.querySelector('.term-expression-text');          const tagContainer = node.querySelector('.tags'); @@ -112,7 +96,7 @@ class DisplayGenerator {      }      createTermReason(reason) { -        const fragment = DisplayGenerator._instantiateTemplateFragment(this._termReasonTemplate); +        const fragment = this._templateHandler.instantiateFragment('term-reason');          const node = fragment.querySelector('.term-reason');          node.textContent = reason;          node.dataset.reason = reason; @@ -120,7 +104,7 @@ class DisplayGenerator {      }      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'); @@ -136,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); @@ -145,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; @@ -160,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'); @@ -205,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); @@ -214,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'); @@ -236,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) { @@ -249,11 +233,11 @@ 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'); @@ -265,7 +249,7 @@ class DisplayGenerator {      }      createSearchTag(details) { -        const node = DisplayGenerator._instantiateTemplate(this._tagSearchTemplate); +        const node = this._templateHandler.instantiate('tag-search');          node.textContent = details.query; @@ -275,7 +259,7 @@ class DisplayGenerator {      }      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) { @@ -293,26 +277,6 @@ class DisplayGenerator {          return node;      } -    _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._tagSearchTemplate = doc.querySelector('#tag-search-template'); -        this._tagFrequencyTemplate = doc.querySelector('#tag-frequency-template'); -    } -      _appendKanjiLinks(container, text) {          let part = '';          for (const c of text) { @@ -382,12 +346,4 @@ class DisplayGenerator {              container.appendChild(document.createTextNode(parts[i]));          }      } - -    static _instantiateTemplate(template) { -        return document.importNode(template.content.firstChild, true); -    } - -    static _instantiateTemplateFragment(template) { -        return document.importNode(template.content, true); -    }  } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 8113260c..631f9e34 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -45,6 +45,110 @@ class Display {          this.displayGenerator = new DisplayGenerator();          this.windowScroll = new WindowScroll(); +        this._onKeyDownHandlers = new Map([ +            ['Escape', () => { +                this.onSearchClear(); +                return true; +            }], +            ['PageUp', (e) => { +                if (e.altKey) { +                    this.entryScrollIntoView(this.index - 3, null, true); +                    return true; +                } +                return false; +            }], +            ['PageDown', (e) => { +                if (e.altKey) { +                    this.entryScrollIntoView(this.index + 3, null, true); +                    return true; +                } +                return false; +            }], +            ['End', (e) => { +                if (e.altKey) { +                    this.entryScrollIntoView(this.definitions.length - 1, null, true); +                    return true; +                } +                return false; +            }], +            ['Home', (e) => { +                if (e.altKey) { +                    this.entryScrollIntoView(0, null, true); +                    return true; +                } +                return false; +            }], +            ['ArrowUp', (e) => { +                if (e.altKey) { +                    this.entryScrollIntoView(this.index - 1, null, true); +                    return true; +                } +                return false; +            }], +            ['ArrowDown', (e) => { +                if (e.altKey) { +                    this.entryScrollIntoView(this.index + 1, null, true); +                    return true; +                } +                return false; +            }], +            ['B', (e) => { +                if (e.altKey) { +                    this.sourceTermView(); +                    return true; +                } +                return false; +            }], +            ['F', (e) => { +                if (e.altKey) { +                    this.nextTermView(); +                    return true; +                } +                return false; +            }], +            ['E', (e) => { +                if (e.altKey) { +                    this.noteTryAdd('term-kanji'); +                    return true; +                } +                return false; +            }], +            ['K', (e) => { +                if (e.altKey) { +                    this.noteTryAdd('kanji'); +                    return true; +                } +                return false; +            }], +            ['R', (e) => { +                if (e.altKey) { +                    this.noteTryAdd('term-kana'); +                    return true; +                } +                return false; +            }], +            ['P', (e) => { +                if (e.altKey) { +                    const index = this.index; +                    if (index < 0 || index >= this.definitions.length) { return; } + +                    const entry = this.getEntry(index); +                    if (entry !== null && entry.dataset.type === 'term') { +                        this.audioPlay(this.definitions[index], this.firstExpressionIndex, index); +                    } +                    return true; +                } +                return false; +            }], +            ['V', (e) => { +                if (e.altKey) { +                    this.noteTryView(); +                    return true; +                } +                return false; +            }] +        ]); +          this.setInteractive(true);      } @@ -215,9 +319,9 @@ class Display {      onKeyDown(e) {          const key = Display.getKeyFromEvent(e); -        const handler = Display._onKeyDownHandlers.get(key); +        const handler = this._onKeyDownHandlers.get(key);          if (typeof handler === 'function') { -            if (handler(this, e)) { +            if (handler(e)) {                  e.preventDefault();                  return true;              } @@ -303,7 +407,7 @@ class Display {          if (interactive) {              const actionPrevious = document.querySelector('.action-previous');              const actionNext = document.querySelector('.action-next'); -            const navigationHeader = document.querySelector('.navigation-header'); +            // 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}); @@ -313,9 +417,10 @@ class Display {              if (actionNext !== null) {                  this.persistentEventListeners.addEventListener(actionNext, 'click', this.onNextTermView.bind(this));              } -            if (navigationHeader !== null) { -                this.persistentEventListeners.addEventListener(navigationHeader, 'wheel', this.onHistoryWheel.bind(this), {passive: false}); -            } +            // temporarily disabled +            // if (navigationHeader !== null) { +            //     this.persistentEventListeners.addEventListener(navigationHeader, 'wheel', this.onHistoryWheel.bind(this), {passive: false}); +            // }          } else {              this.persistentEventListeners.removeAllEventListeners();          } @@ -519,15 +624,13 @@ class Display {      updateAdderButtons(states) {          for (let i = 0; i < states.length; ++i) { -            const state = states[i];              let noteId = null; -            for (const mode in state) { +            for (const [mode, info] of Object.entries(states[i])) {                  const button = this.adderButtonFind(i, mode);                  if (button === null) {                      continue;                  } -                const info = state[mode];                  if (!info.canAdd && noteId === null && info.noteId) {                      noteId = info.noteId;                  } @@ -634,7 +737,7 @@ class Display {              this.setSpinnerVisible(true);              const context = {}; -            if (this.noteUsesScreenshot()) { +            if (this.noteUsesScreenshot(mode)) {                  const screenshot = await this.getScreenshot();                  if (screenshot) {                      context.screenshot = screenshot; @@ -704,10 +807,11 @@ class Display {          }      } -    noteUsesScreenshot() { -        const fields = this.options.anki.terms.fields; -        for (const name in fields) { -            if (fields[name].includes('{screenshot}')) { +    noteUsesScreenshot(mode) { +        const optionsAnki = this.options.anki; +        const fields = (mode === 'kanji' ? optionsAnki.kanji : optionsAnki.terms).fields; +        for (const fieldValue of Object.values(fields)) { +            if (fieldValue.includes('{screenshot}')) {                  return true;              }          } @@ -814,120 +918,3 @@ class Display {          return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');      }  } - -Display._onKeyDownHandlers = new Map([ -    ['Escape', (self) => { -        self.onSearchClear(); -        return true; -    }], - -    ['PageUp', (self, e) => { -        if (e.altKey) { -            self.entryScrollIntoView(self.index - 3, null, true); -            return true; -        } -        return false; -    }], - -    ['PageDown', (self, e) => { -        if (e.altKey) { -            self.entryScrollIntoView(self.index + 3, null, true); -            return true; -        } -        return false; -    }], - -    ['End', (self, e) => { -        if (e.altKey) { -            self.entryScrollIntoView(self.definitions.length - 1, null, true); -            return true; -        } -        return false; -    }], - -    ['Home', (self, e) => { -        if (e.altKey) { -            self.entryScrollIntoView(0, null, true); -            return true; -        } -        return false; -    }], - -    ['ArrowUp', (self, e) => { -        if (e.altKey) { -            self.entryScrollIntoView(self.index - 1, null, true); -            return true; -        } -        return false; -    }], - -    ['ArrowDown', (self, e) => { -        if (e.altKey) { -            self.entryScrollIntoView(self.index + 1, null, true); -            return true; -        } -        return false; -    }], - -    ['B', (self, e) => { -        if (e.altKey) { -            self.sourceTermView(); -            return true; -        } -        return false; -    }], - -    ['F', (self, e) => { -        if (e.altKey) { -            self.nextTermView(); -            return true; -        } -        return false; -    }], - -    ['E', (self, e) => { -        if (e.altKey) { -            self.noteTryAdd('term-kanji'); -            return true; -        } -        return false; -    }], - -    ['K', (self, e) => { -        if (e.altKey) { -            self.noteTryAdd('kanji'); -            return true; -        } -        return false; -    }], - -    ['R', (self, e) => { -        if (e.altKey) { -            self.noteTryAdd('term-kana'); -            return true; -        } -        return false; -    }], - -    ['P', (self, e) => { -        if (e.altKey) { -            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[index], self.firstExpressionIndex, index); -            } -            return true; -        } -        return false; -    }], - -    ['V', (self, e) => { -        if (e.altKey) { -            self.noteTryView(); -            return true; -        } -        return false; -    }] -]); diff --git a/ext/mixed/js/scroll.js b/ext/mixed/js/scroll.js index 5829d294..72da8b65 100644 --- a/ext/mixed/js/scroll.js +++ b/ext/mixed/js/scroll.js @@ -26,7 +26,7 @@ class WindowScroll {          this.animationEndTime = 0;          this.animationEndX = 0;          this.animationEndY = 0; -        this.requestAnimationFrameCallback = (t) => this.onAnimationFrame(t); +        this.requestAnimationFrameCallback = this.onAnimationFrame.bind(this);      }      toY(y) { diff --git a/package.json b/package.json index 04bfd4af..74d89dda 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,10 @@          "url": "git+https://github.com/FooSoft/yomichan.git"      },      "author": "FooSoft", +    "license": "GPL-3.0-or-later",      "licenses": [          { -            "type": "GPLv3", +            "type": "GPL-3.0-or-later",              "url": "https://www.gnu.org/licenses/gpl-3.0.html"          }      ], diff --git a/test/dictionary-validate.js b/test/dictionary-validate.js index 25a5de88..14eee2ed 100644 --- a/test/dictionary-validate.js +++ b/test/dictionary-validate.js @@ -1,3 +1,21 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +  const fs = require('fs');  const path = require('path');  const yomichanTest = require('./yomichan-test'); diff --git a/test/schema-validate.js b/test/schema-validate.js index 1271a611..a4f2d94c 100644 --- a/test/schema-validate.js +++ b/test/schema-validate.js @@ -1,3 +1,21 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +  const fs = require('fs');  const yomichanTest = require('./yomichan-test'); diff --git a/test/test-database.js b/test/test-database.js index 44f409dd..35f22523 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -1,3 +1,21 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +  const fs = require('fs');  const url = require('url');  const path = require('path'); @@ -72,7 +90,7 @@ class XMLHttpRequest {  const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);  const {dictFieldSplit, dictTagSanitize} = yomichanTest.requireScript('ext/bg/js/dictionary.js', ['dictFieldSplit', 'dictTagSanitize']); -const {stringReverse, hasOwn} = yomichanTest.requireScript('ext/mixed/js/core.js', ['stringReverse', 'hasOwn'], {chrome}); +const {stringReverse} = yomichanTest.requireScript('ext/mixed/js/core.js', ['stringReverse'], {chrome});  const {requestJson} = yomichanTest.requireScript('ext/bg/js/request.js', ['requestJson'], {XMLHttpRequest});  const databaseGlobals = { @@ -80,7 +98,6 @@ const databaseGlobals = {      JsonSchema,      requestJson,      stringReverse, -    hasOwn,      dictFieldSplit,      dictTagSanitize,      indexedDB: global.indexedDB, @@ -109,7 +126,8 @@ function countKanjiWithCharacter(kanji, character) {  function clearDatabase(timeout) {      return new Promise((resolve, reject) => { -        const timer = setTimeout(() => { +        let timer = setTimeout(() => { +            timer = null;              reject(new Error(`clearDatabase failed to resolve after ${timeout}ms`));          }, timeout); @@ -122,7 +140,9 @@ function clearDatabase(timeout) {                      request.onsuccess = () => resolve2();                  });              } -            clearTimeout(timer); +            if (timer !== null) { +                clearTimeout(timer); +            }              resolve();          })();      }); diff --git a/test/test-dictionary.js b/test/test-dictionary.js index b157dd5d..74f9e62b 100644 --- a/test/test-dictionary.js +++ b/test/test-dictionary.js @@ -1,3 +1,21 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +  const yomichanTest = require('./yomichan-test');  const dictionaryValidate = require('./dictionary-validate'); diff --git a/test/test-schema.js b/test/test-schema.js index 8ca63167..f4612f86 100644 --- a/test/test-schema.js +++ b/test/test-schema.js @@ -1,3 +1,21 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +  const assert = require('assert');  const yomichanTest = require('./yomichan-test'); diff --git a/test/yomichan-test.js b/test/yomichan-test.js index 939e0ad2..78bfb9c6 100644 --- a/test/yomichan-test.js +++ b/test/yomichan-test.js @@ -1,3 +1,21 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +  const fs = require('fs');  const path = require('path'); |