Merge pull request #224 from toasted-nutbread/display-jquery-optimizations

Remove jQuery usage from Display.js
This commit is contained in:
Alex Yatskov 2019-09-28 09:12:23 -07:00 committed by GitHub
commit 64eed33e88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 297 additions and 90 deletions

View File

@ -19,20 +19,27 @@
class DisplaySearch extends Display {
constructor() {
super($('#spinner'), $('#content'));
super(document.querySelector('#spinner'), document.querySelector('#content'));
this.optionsContext = {
depth: 0,
url: window.location.href
};
this.search = $('#search').click(this.onSearch.bind(this));
this.query = $('#query').on('input', this.onSearchInput.bind(this));
this.intro = $('#intro');
this.search = document.querySelector('#search');
this.query = document.querySelector('#query');
this.intro = document.querySelector('#intro');
this.introHidden = false;
this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract});
window.wanakana.bind(this.query.get(0));
if (this.search !== null) {
this.search.addEventListener('click', (e) => this.onSearch(e), false);
}
if (this.query !== null) {
this.query.addEventListener('input', () => this.onSearchInput(), false);
window.wanakana.bind(this.query);
}
}
onError(error) {
@ -40,23 +47,50 @@ class DisplaySearch extends Display {
}
onSearchClear() {
this.query.focus().select();
if (this.query === null) {
return;
}
this.query.focus();
this.query.select();
}
onSearchInput() {
this.search.prop('disabled', this.query.val().length === 0);
this.search.disabled = (this.query === null || this.query.value.length === 0);
}
async onSearch(e) {
if (this.query === null) {
return;
}
try {
e.preventDefault();
this.intro.slideUp();
const {length, definitions} = await apiTermsFind(this.query.val(), this.optionsContext);
this.hideIntro();
const {length, definitions} = await apiTermsFind(this.query.value, this.optionsContext);
super.termsShow(definitions, await apiOptionsGet(this.optionsContext));
} catch (e) {
this.onError(e);
}
}
hideIntro() {
if (this.introHidden) {
return;
}
this.introHidden = true;
if (this.intro === null) {
return;
}
const size = this.intro.getBoundingClientRect();
this.intro.style.height = `${size.height}px`;
this.intro.style.transition = 'height 0.4s ease-in-out 0s';
window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation
this.intro.style.height = '0';
}
}
window.yomichan_search = new DisplaySearch();

View File

@ -10,21 +10,19 @@
</head>
<body>
<div class="container-fluid">
<div id="intro">
<div id="intro" style="overflow: hidden;">
<div class="page-header">
<h1>Yomichan Search</h1>
</div>
<p>Search your installed dictionaries by entering a Japanese expression into the field below.</p>
<p style="margin-bottom: 0;">Search your installed dictionaries by entering a Japanese expression into the field below.</p>
</div>
<p>
<form class="input-group">
<input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
<span class="input-group-btn">
<input type="submit" class="btn btn-default form-control" id="search" value="Search" disabled>
</span>
</form>
</p>
<form class="input-group" style="padding-top: 10px;">
<input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
<span class="input-group-btn">
<input type="submit" class="btn btn-default form-control" id="search" value="Search" disabled>
</span>
</form>
<div id="spinner">
<img src="/mixed/img/spinner.gif">
@ -34,7 +32,6 @@
</div>
<script src="/mixed/lib/handlebars.min.js"></script>
<script src="/mixed/lib/jquery.min.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
<script src="/mixed/js/extension.js"></script>
@ -49,6 +46,7 @@
<script src="/fg/js/source.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/japanese.js"></script>
<script src="/mixed/js/scroll.js"></script>
<script src="/bg/js/search.js"></script>
<script src="/bg/js/search-frontend.js"></script>

View File

@ -31,7 +31,6 @@
</div>
</div>
<script src="/mixed/lib/jquery.min.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
<script src="/mixed/js/extension.js"></script>
@ -41,6 +40,7 @@
<script src="/fg/js/document.js"></script>
<script src="/fg/js/source.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/scroll.js"></script>
<script src="/fg/js/float.js"></script>

View File

@ -19,7 +19,7 @@
class DisplayFloat extends Display {
constructor() {
super($('#spinner'), $('#definitions'));
super(document.querySelector('#spinner'), document.querySelector('#definitions'));
this.autoPlayAudioTimer = null;
this.styleNode = null;
@ -30,7 +30,7 @@ class DisplayFloat extends Display {
this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract});
$(window).on('message', utilAsync(this.onMessage.bind(this)));
window.addEventListener('message', (e) => this.onMessage(e), false);
}
onError(error) {
@ -42,8 +42,16 @@ class DisplayFloat extends Display {
}
onOrphaned() {
$('#definitions').hide();
$('#error-orphaned').show();
const definitions = document.querySelector('#definitions');
const errorOrphaned = document.querySelector('#error-orphaned');
if (definitions !== null) {
definitions.style.setProperty('display', 'none', 'important');
}
if (errorOrphaned !== null) {
errorOrphaned.style.setProperty('display', 'block', 'important');
}
}
onSearchClear() {
@ -86,7 +94,7 @@ class DisplayFloat extends Display {
}
};
const {action, params} = e.originalEvent.data;
const {action, params} = e.data;
const handler = handlers[action];
if (handler) {
handler(params);

View File

@ -230,3 +230,7 @@ div.glossary-item.compact-glossary {
.info-output td {
text-align: right;
}
.entry:not(.entry-current) .current {
display: none;
}

View File

@ -28,11 +28,14 @@ class Display {
this.index = 0;
this.audioCache = {};
this.optionsContext = {};
this.eventListeners = [];
this.dependencies = {};
$(document).keydown(this.onKeyDown.bind(this));
$(document).on('wheel', this.onWheel.bind(this));
this.windowScroll = new WindowScroll();
document.addEventListener('keydown', this.onKeyDown.bind(this));
document.addEventListener('wheel', this.onWheel.bind(this), {passive: false});
}
onError(error) {
@ -52,12 +55,13 @@ class Display {
try {
e.preventDefault();
const link = $(e.target);
const link = e.target;
this.windowScroll.toY(0);
const context = {
source: {
definitions: this.definitions,
index: Display.entryIndexFind(link),
scroll: $('html,body').scrollTop()
index: this.entryIndexFind(link),
scroll: this.windowScroll.y
}
};
@ -67,7 +71,7 @@ class Display {
context.source.source = this.context.source;
}
const kanjiDefs = await apiKanjiFind(link.text(), this.optionsContext);
const kanjiDefs = await apiKanjiFind(link.textContent, this.optionsContext);
this.kanjiShow(kanjiDefs, this.options, context);
} catch (e) {
this.onError(e);
@ -80,7 +84,7 @@ class Display {
const {docRangeFromPoint, docSentenceExtract} = this.dependencies;
const clickedElement = $(e.target);
const clickedElement = e.target;
const textSource = docRangeFromPoint(e.clientX, e.clientY, this.options);
if (textSource === null) {
return false;
@ -102,11 +106,12 @@ class Display {
textSource.cleanup();
}
this.windowScroll.toY(0);
const context = {
source: {
definitions: this.definitions,
index: Display.entryIndexFind(clickedElement),
scroll: $('html,body').scrollTop()
index: this.entryIndexFind(clickedElement),
scroll: this.windowScroll.y
}
};
@ -124,38 +129,38 @@ class Display {
onAudioPlay(e) {
e.preventDefault();
const link = $(e.currentTarget);
const definitionIndex = Display.entryIndexFind(link);
const expressionIndex = link.closest('.entry').find('.expression .action-play-audio').index(link);
const link = e.currentTarget;
const entry = link.closest('.entry');
const definitionIndex = this.entryIndexFind(entry);
const expressionIndex = Display.indexOf(entry.querySelectorAll('.expression .action-play-audio'), link);
this.audioPlay(this.definitions[definitionIndex], expressionIndex);
}
onNoteAdd(e) {
e.preventDefault();
const link = $(e.currentTarget);
const index = Display.entryIndexFind(link);
this.noteAdd(this.definitions[index], link.data('mode'));
const link = e.currentTarget;
const index = this.entryIndexFind(link);
this.noteAdd(this.definitions[index], link.dataset.mode);
}
onNoteView(e) {
e.preventDefault();
const link = $(e.currentTarget);
const index = Display.entryIndexFind(link);
apiNoteView(link.data('noteId'));
const link = e.currentTarget;
apiNoteView(link.dataset.noteId);
}
onKeyDown(e) {
const noteTryAdd = mode => {
const button = Display.adderButtonFind(this.index, mode);
if (button.length !== 0 && !button.hasClass('disabled')) {
const button = this.adderButtonFind(this.index, mode);
if (button !== null && !button.classList.contains('disabled')) {
this.noteAdd(this.definitions[this.index], mode);
}
};
const noteTryView = mode => {
const button = Display.viewerButtonFind(this.index);
if (button.length !== 0 && !button.hasClass('disabled')) {
apiNoteView(button.data('noteId'));
const button = this.viewerButtonFind(this.index);
if (button !== null && !button.classList.contains('disabled')) {
apiNoteView(button.dataset.noteId);
}
};
@ -237,7 +242,8 @@ class Display {
80: /* p */ () => {
if (e.altKey) {
if ($('.entry').eq(this.index).data('type') === 'term') {
const entry = this.getEntry(this.index);
if (entry !== null && entry.dataset.type === 'term') {
this.audioPlay(this.definitions[this.index], this.firstExpressionIndex);
}
@ -259,13 +265,12 @@ class Display {
}
onWheel(e) {
const event = e.originalEvent;
const handler = () => {
if (event.altKey) {
if (event.deltaY < 0) { // scroll up
if (e.altKey) {
if (e.deltaY < 0) { // scroll up
this.entryScrollIntoView(this.index - 1, null, true);
return true;
} else if (event.deltaY > 0) { // scroll down
} else if (e.deltaY > 0) { // scroll down
this.entryScrollIntoView(this.index + 1, null, true);
return true;
}
@ -273,12 +278,14 @@ class Display {
};
if (handler()) {
event.preventDefault();
e.preventDefault();
}
}
async termsShow(definitions, options, context) {
try {
this.clearEventListeners();
if (!context || context.focus !== false) {
window.focus();
}
@ -310,7 +317,7 @@ class Display {
}
const content = await apiTemplateRender('terms.html', params);
this.container.html(content);
this.container.innerHTML = content;
const {index, scroll} = context || {};
this.entryScrollIntoView(index || 0, scroll);
@ -318,13 +325,13 @@ class Display {
this.autoPlayAudio();
}
$('.action-add-note').click(this.onNoteAdd.bind(this));
$('.action-view-note').click(this.onNoteView.bind(this));
$('.action-play-audio').click(this.onAudioPlay.bind(this));
$('.kanji-link').click(this.onKanjiLookup.bind(this));
$('.source-term').click(this.onSourceTermView.bind(this));
this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
if (this.options.scanning.enablePopupSearch) {
$('.glossary-item').click(this.onTermLookup.bind(this));
this.addEventListeners('.glossary-item', 'click', this.onTermLookup.bind(this));
}
await this.adderButtonUpdate(['term-kanji', 'term-kana'], sequence);
@ -335,6 +342,8 @@ class Display {
async kanjiShow(definitions, options, context) {
try {
this.clearEventListeners();
if (!context || context.focus !== false) {
window.focus();
}
@ -362,13 +371,13 @@ class Display {
}
const content = await apiTemplateRender('kanji.html', params);
this.container.html(content);
this.container.innerHTML = content;
const {index, scroll} = context || {};
this.entryScrollIntoView(index || 0, scroll);
$('.action-add-note').click(this.onNoteAdd.bind(this));
$('.action-view-note').click(this.onNoteView.bind(this));
$('.source-term').click(this.onSourceTermView.bind(this));
this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
await this.adderButtonUpdate(['kanji'], sequence);
} catch (e) {
@ -390,14 +399,13 @@ class Display {
for (let i = 0; i < states.length; ++i) {
const state = states[i];
for (const mode in state) {
const button = Display.adderButtonFind(i, mode);
if (state[mode]) {
button.removeClass('disabled');
} else {
button.addClass('disabled');
const button = this.adderButtonFind(i, mode);
if (button === null) {
continue;
}
button.removeClass('pending');
button.classList.toggle('disabled', !state[mode]);
button.classList.remove('pending');
}
}
} catch (e) {
@ -409,22 +417,29 @@ class Display {
index = Math.min(index, this.definitions.length - 1);
index = Math.max(index, 0);
$('.current').hide().eq(index).show();
const entryPre = this.getEntry(this.index);
if (entryPre !== null) {
entryPre.classList.remove('entry-current');
}
const container = $('html,body').stop();
const entry = $('.entry').eq(index);
const entry = this.getEntry(index);
if (entry !== null) {
entry.classList.add('entry-current');
}
this.windowScroll.stop();
let target;
if (scroll) {
target = scroll;
} else {
target = index === 0 ? 0 : entry.offset().top;
target = index === 0 || entry === null ? 0 : Display.getElementTop(entry);
}
if (smooth) {
container.animate({scrollTop: target}, 200);
this.windowScroll.animate(this.windowScroll.x, target, 200);
} else {
container.scrollTop(target);
this.windowScroll.toY(target);
}
this.index = index;
@ -446,7 +461,7 @@ class Display {
async noteAdd(definition, mode) {
try {
this.spinner.show();
this.setSpinnerVisible(true);
const context = {};
if (this.noteUsesScreenshot()) {
@ -459,21 +474,28 @@ class Display {
const noteId = await apiDefinitionAdd(definition, mode, context, this.optionsContext);
if (noteId) {
const index = this.definitions.indexOf(definition);
Display.adderButtonFind(index, mode).addClass('disabled');
Display.viewerButtonFind(index).removeClass('pending disabled').data('noteId', noteId);
const adderButton = this.adderButtonFind(index, mode);
if (adderButton !== null) {
adderButton.classList.add('disabled');
}
const viewerButton = this.viewerButtonFind(index);
if (viewerButton !== null) {
viewerButton.classList.remove('pending', 'disabled');
viewerButton.dataset.noteId = noteId;
}
} else {
throw 'Note could note be added';
}
} catch (e) {
this.onError(e);
} finally {
this.spinner.hide();
this.setSpinnerVisible(false);
}
}
async audioPlay(definition, expressionIndex) {
try {
this.spinner.show();
this.setSpinnerVisible(true);
const expression = expressionIndex === -1 ? definition : definition.expressions[expressionIndex];
let url = await apiAudioGetUrl(expression, this.options.general.audioSource);
@ -505,7 +527,7 @@ class Display {
} catch (e) {
this.onError(e);
} finally {
this.spinner.hide();
this.setSpinnerVisible(false);
}
}
@ -542,6 +564,15 @@ class Display {
return apiForward('popupSetVisible', {visible});
}
setSpinnerVisible(visible) {
this.spinner.style.display = visible ? 'block' : '';
}
getEntry(index) {
const entries = this.container.querySelectorAll('.entry');
return index >= 0 && index < entries.length ? entries[index] : null;
}
static clozeBuild(sentence, source) {
const result = {
sentence: sentence.text.trim()
@ -556,19 +587,51 @@ class Display {
return result;
}
static entryIndexFind(element) {
return $('.entry').index(element.closest('.entry'));
entryIndexFind(element) {
const entry = element.closest('.entry');
return entry !== null ? Display.indexOf(this.container.querySelectorAll('.entry'), entry) : -1;
}
static adderButtonFind(index, mode) {
return $('.entry').eq(index).find(`.action-add-note[data-mode="${mode}"]`);
adderButtonFind(index, mode) {
const entry = this.getEntry(index);
return entry !== null ? entry.querySelector(`.action-add-note[data-mode="${mode}"]`) : null;
}
static viewerButtonFind(index) {
return $('.entry').eq(index).find('.action-view-note');
viewerButtonFind(index) {
const entry = this.getEntry(index);
return entry !== null ? entry.querySelector('.action-view-note') : null;
}
static delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
static indexOf(nodeList, node) {
for (let i = 0, ii = nodeList.length; i < ii; ++i) {
if (nodeList[i] === node) {
return i;
}
}
return -1;
}
addEventListeners(selector, type, listener, options) {
this.container.querySelectorAll(selector).forEach((node) => {
node.addEventListener(type, listener, options);
this.eventListeners.push([node, type, listener, options]);
});
}
clearEventListeners() {
for (const [node, type, listener, options] of this.eventListeners) {
node.removeEventListener(type, listener, options);
}
this.eventListeners = [];
}
static getElementTop(element) {
const elementRect = element.getBoundingClientRect();
const documentRect = document.documentElement.getBoundingClientRect();
return elementRect.top - documentRect.top;
}
}

100
ext/mixed/js/scroll.js Normal file
View File

@ -0,0 +1,100 @@
/*
* Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class WindowScroll {
constructor() {
this.animationRequestId = null;
this.animationStartTime = 0;
this.animationStartX = 0;
this.animationStartY = 0;
this.animationEndTime = 0;
this.animationEndX = 0;
this.animationEndY = 0;
this.requestAnimationFrameCallback = (t) => this.onAnimationFrame(t);
}
toY(y) {
this.to(this.x, y);
}
toX(x) {
this.to(x, this.y);
}
to(x, y) {
this.stop();
window.scroll(x, y);
}
animate(x, y, time) {
this.animationStartX = this.x;
this.animationStartY = this.y;
this.animationStartTime = window.performance.now();
this.animationEndX = x;
this.animationEndY = y;
this.animationEndTime = this.animationStartTime + time;
this.animationRequestId = window.requestAnimationFrame(this.requestAnimationFrameCallback);
}
stop() {
if (this.animationRequestId === null) {
return;
}
window.cancelAnimationFrame(this.animationRequestId);
this.animationRequestId = null;
}
onAnimationFrame(time) {
if (time >= this.animationEndTime) {
window.scroll(this.animationEndX, this.animationEndY);
this.animationRequestId = null;
return;
}
const t = WindowScroll.easeInOutCubic((time - this.animationStartTime) / (this.animationEndTime - this.animationStartTime));
window.scroll(
WindowScroll.lerp(this.animationStartX, this.animationEndX, t),
WindowScroll.lerp(this.animationStartY, this.animationEndY, t)
);
this.animationRequestId = window.requestAnimationFrame(this.requestAnimationFrameCallback);
}
get x() {
return window.scrollX || window.pageXOffset;
}
get y() {
return window.scrollY || window.pageYOffset;
}
static easeInOutCubic(t) {
if (t < 0.5) {
return (4.0 * t * t * t);
} else {
t = 1.0 - t;
return 1.0 - (4.0 * t * t * t);
}
}
static lerp(start, end, percent) {
return (end - start) * percent + start;
}
}