diff options
| -rw-r--r-- | ext/bg/js/options.js | 2 | ||||
| -rw-r--r-- | ext/bg/js/settings.js | 4 | ||||
| -rw-r--r-- | ext/bg/settings.html | 12 | ||||
| -rw-r--r-- | ext/fg/css/client.css | 22 | ||||
| -rw-r--r-- | ext/fg/js/document.js | 12 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 194 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 4 | ||||
| -rw-r--r-- | ext/manifest.json | 4 | 
8 files changed, 251 insertions, 3 deletions
| diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 373a1a6b..f1e02e18 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -190,6 +190,7 @@ function optionsSetDefaults(options) {              debugInfo: false,              maxResults: 32,              showAdvanced: false, +            popupDisplayMode: 'default',              popupWidth: 400,              popupHeight: 250,              popupOffset: 10, @@ -201,6 +202,7 @@ function optionsSetDefaults(options) {          scanning: {              middleMouse: true, +            touchInputEnabled: true,              selectText: true,              alphanumeric: true,              autoHideResults: false, diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 7bc6a651..4bf7181f 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -31,11 +31,13 @@ async function formRead() {      optionsNew.general.debugInfo = $('#show-debug-info').prop('checked');      optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked');      optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10); +    optionsNew.general.popupDisplayMode = $('#popup-display-mode').val();      optionsNew.general.popupWidth = parseInt($('#popup-width').val(), 10);      optionsNew.general.popupHeight = parseInt($('#popup-height').val(), 10);      optionsNew.general.popupOffset = parseInt($('#popup-offset').val(), 10);      optionsNew.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); +    optionsNew.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');      optionsNew.scanning.selectText = $('#select-matched-text').prop('checked');      optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');      optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); @@ -161,11 +163,13 @@ async function onReady() {      $('#show-debug-info').prop('checked', options.general.debugInfo);      $('#show-advanced-options').prop('checked', options.general.showAdvanced);      $('#max-displayed-results').val(options.general.maxResults); +    $('#popup-display-mode').val(options.general.popupDisplayMode);      $('#popup-width').val(options.general.popupWidth);      $('#popup-height').val(options.general.popupHeight);      $('#popup-offset').val(options.general.popupOffset);      $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); +    $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled);      $('#select-matched-text').prop('checked', options.scanning.selectText);      $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);      $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index c77f550c..7f18a358 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -74,6 +74,14 @@                      </select>                  </div> +                <div class="form-group"> +                    <label for="popup-display-mode">Popup display mode</label> +                    <select class="form-control" id="popup-display-mode"> +                        <option value="default">Default</option> +                        <option value="full-width">Full width</option> +                    </select> +                </div> +                  <div class="form-group options-advanced">                      <label for="audio-playback-volume">Audio playback volume (percent)</label>                      <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control"> @@ -106,6 +114,10 @@                  </div>                  <div class="checkbox"> +                    <label><input type="checkbox" id="touch-input-enabled"> Touch input enabled</label> +                </div> + +                <div class="checkbox">                      <label><input type="checkbox" id="select-matched-text"> Select matched text</label>                  </div> diff --git a/ext/fg/css/client.css b/ext/fg/css/client.css index b5b1f6bd..a9b8e025 100644 --- a/ext/fg/css/client.css +++ b/ext/fg/css/client.css @@ -27,3 +27,25 @@ iframe#yomichan-float {      visibility: hidden;      z-index: 2147483647;  } + +iframe#yomichan-float.yomichan-float-full-width { +    border-left: none; +    border-right: none; +    left: 0 !important; +    right: 0 !important; +    width: 100% !important; +    box-sizing: border-box; +    resize: none; +} + +iframe#yomichan-float.yomichan-float-full-width:not(.yomichan-float-above) { +    border-bottom: none; +    top: auto !important; +    bottom: 0 !important; +} + +iframe#yomichan-float.yomichan-float-full-width.yomichan-float-above { +    border-top: none; +    top: 0 !important; +    bottom: auto !important; +} diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 9366832e..13acb036 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -85,14 +85,24 @@ function docRangeFromPoint(point) {                  range.setEnd(position.offsetNode, position.offset);                  return range;              } +            return null;          };      }      const range = document.caretRangeFromPoint(point.x, point.y); +    if (range === null) { +        return; +    }      if(imposter !== null) imposter.style.zIndex = -2147483646; -    const rect = range.getClientRects()[0]; +    const rects = range.getClientRects(); + +    if (rects.length === 0) { +        return; +    } + +    const rect = rects[0];      if (point.y > rect.bottom + 2) {          return;      } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 266f9640..a81cbef8 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -26,6 +26,14 @@ class Frontend {          this.textSourceLast = null;          this.pendingLookup = false;          this.options = null; + +        this.primaryTouchIdentifier = null; +        this.contextMenuChecking = false; +        this.contextMenuPrevent = false; +        this.contextMenuPreviousRange = null; +        this.mouseDownPrevent = false; +        this.clickPrevent = false; +        this.scrollPrevent = false;      }      async prepare() { @@ -39,6 +47,15 @@ class Frontend {              window.addEventListener('mouseup', this.onMouseUp.bind(this));              window.addEventListener('resize', this.onResize.bind(this)); +            if (this.options.scanning.touchInputEnabled) { +                window.addEventListener('click', this.onClick.bind(this)); +                window.addEventListener('touchstart', this.onTouchStart.bind(this)); +                window.addEventListener('touchend', this.onTouchEnd.bind(this)); +                window.addEventListener('touchcancel', this.onTouchCancel.bind(this)); +                window.addEventListener('touchmove', this.onTouchMove.bind(this), {passive: false}); +                window.addEventListener('contextmenu', this.onContextMenu.bind(this)); +            } +              chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this));          } catch (e) {              this.onError(e); @@ -79,7 +96,7 @@ class Frontend {          const search = async () => {              try { -                await this.searchAt({x: e.clientX, y: e.clientY}); +                await this.searchAt({x: e.clientX, y: e.clientY}, 'mouse');              } catch (e) {                  this.onError(e);              } @@ -93,6 +110,14 @@ class Frontend {      }      onMouseDown(e) { +        if (this.mouseDownPrevent) { +            this.setMouseDownPrevent(false, false); +            this.setClickPrevent(true); +            e.preventDefault(); +            e.stopPropagation(); +            return false; +        } +          this.mousePosLast = {x: e.clientX, y: e.clientY};          this.popupTimerClear();          this.searchClear(); @@ -133,6 +158,85 @@ class Frontend {          this.searchClear();      } +    onClick(e) { +        if (this.clickPrevent) { +            this.setClickPrevent(false); +            e.preventDefault(); +            e.stopPropagation(); +            return false; +        } +    } + +    onTouchStart(e) { +        if (this.primaryTouchIdentifier !== null && this.getIndexOfTouch(e.touches, this.primaryTouchIdentifier) >= 0) { +            return; +        } + +        this.setPrimaryTouch(this.getPrimaryTouch(e.changedTouches)); +    } + +    onTouchEnd(e) { +        if (this.primaryTouchIdentifier === null) { +            return; +        } + +        if (this.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0) { +            return; +        } + +        this.setPrimaryTouch(this.getPrimaryTouch(this.excludeTouches(e.touches, e.changedTouches))); +    } + +    onTouchCancel(e) { +        this.onTouchEnd(e); +    } + +    onTouchMove(e) { +        if (!this.scrollPrevent || this.primaryTouchIdentifier === null) { +            return; +        } + +        const touches = e.changedTouches; +        const index = this.getIndexOfTouch(touches, this.primaryTouchIdentifier); +        if (index < 0) { +            return; +        } + +        const touch = touches[index]; +        this.searchFromTouch(touch.clientX, touch.clientY, 'touchMove'); + +        e.preventDefault(); // Disable scroll +    } + +    onContextMenu(e) { +        if (this.contextMenuPrevent) { +            this.setContextMenuPrevent(false, false); +            e.preventDefault(); +            e.stopPropagation(); +            return false; +        } +    } + +    onAfterSearch(newRange, type, searched, success) { +        if (type === 'mouse') { +            return; +        } + +        if ( +            !this.contextMenuChecking || +            (this.contextMenuPreviousRange === null ? newRange === null : this.contextMenuPreviousRange.equals(newRange))) { +            return; +        } + +        if (type === 'touchStart' && newRange !== null) { +            this.scrollPrevent = true; +        } + +        this.setContextMenuPrevent(true, false); +        this.setMouseDownPrevent(true, false); +        this.contextMenuChecking = false; +    } +      onBgMessage({action, params}, sender, callback) {          const handlers = {              optionsSet: options => { @@ -167,18 +271,22 @@ class Frontend {          }      } -    async searchAt(point) { +    async searchAt(point, type) {          if (this.pendingLookup || this.popup.containsPoint(point)) {              return;          }          const textSource = docRangeFromPoint(point);          let hideResults = !textSource || !textSource.containsPoint(point); +        let searched = false; +        let success = false;          try {              if (!hideResults && (!this.textSourceLast || !this.textSourceLast.equals(textSource))) { +                searched = true;                  this.pendingLookup = true;                  hideResults = !await this.searchTerms(textSource) && !await this.searchKanji(textSource); +                success = true;              }          } catch (e) {              if (window.yomichan_orphaned) { @@ -196,6 +304,7 @@ class Frontend {              }              this.pendingLookup = false; +            this.onAfterSearch(this.textSourceLast, type, searched, success);          }      } @@ -262,6 +371,87 @@ class Frontend {          this.textSourceLast = null;      } + +    getPrimaryTouch(touchList) { +        return touchList.length > 0 ? touchList[0] : null; +    } + +    getIndexOfTouch(touchList, identifier) { +        for (let i in touchList) { +            let t = touchList[i]; +            if (t.identifier === identifier) { +                return i; +            } +        } +        return -1; +    } + +    excludeTouches(touchList, excludeTouchList) { +        const result = []; +        for (let r of touchList) { +            if (this.getIndexOfTouch(excludeTouchList, r.identifier) < 0) { +                result.push(r); +            } +        } +        return result; +    } + +    setPrimaryTouch(touch) { +        if (touch === null) { +            this.primaryTouchIdentifier = null; +            this.contextMenuPreviousRange = null; +            this.contextMenuChecking = false; +            this.scrollPrevent = false; +            this.setContextMenuPrevent(false, true); +            this.setMouseDownPrevent(false, true); +            this.setClickPrevent(false); +        } +        else { +            this.primaryTouchIdentifier = touch.identifier; +            this.contextMenuPreviousRange = this.textSourceLast ? this.textSourceLast.clone() : null; +            this.contextMenuChecking = true; +            this.scrollPrevent = false; +            this.setContextMenuPrevent(false, false); +            this.setMouseDownPrevent(false, false); +            this.setClickPrevent(false); + +            this.searchFromTouch(touch.clientX, touch.clientY, 'touchStart'); +        } +    } + +    setContextMenuPrevent(value, delay) { +        if (!delay) { +            this.contextMenuPrevent = value; +        } +    } + +    setMouseDownPrevent(value, delay) { +        if (!delay) { +            this.mouseDownPrevent = value; +        } +    } + +    setClickPrevent(value) { +        this.clickPrevent = value; +    } + +    searchFromTouch(x, y, type) { +        this.popupTimerClear(); + +        if (!this.options.general.enable || this.pendingLookup) { +            return; +        } + +        const search = async () => { +            try { +                await this.searchAt({x, y}, type); +            } catch (e) { +                this.onError(e); +            } +        }; + +        search(); +    }  }  window.yomichan_frontend = new Frontend(); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index a17b184a..14276efe 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -62,6 +62,7 @@ class Popup {              }          } +        let above = false;          let y = 0;          let height = Math.max(containerHeight, options.general.popupHeight);          const yBelow = elementRect.bottom + options.general.popupOffset; @@ -75,11 +76,14 @@ class Popup {              } else {                  height = Math.max(height - overflowAbove, 0);                  y = Math.max(yAbove - height, 0); +                above = true;              }          } else {              y = yBelow;          } +        this.container.classList.toggle('yomichan-float-full-width', options.general.popupDisplayMode === 'full-width'); +        this.container.classList.toggle('yomichan-float-above', above);          this.container.style.left = `${x}px`;          this.container.style.top = `${y}px`;          this.container.style.width = `${width}px`; diff --git a/ext/manifest.json b/ext/manifest.json index 7ee66f4f..ef8b84ec 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -27,6 +27,10 @@      }],      "minimum_chrome_version": "57.0.0.0",      "options_page": "bg/settings.html", +    "options_ui": { +        "page": "bg/settings.html", +        "open_in_tab": true +    },      "permissions": [          "<all_urls>",          "storage", |