Merge pull request #224 from toasted-nutbread/display-jquery-optimizations
Remove jQuery usage from Display.js
This commit is contained in:
commit
64eed33e88
@ -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();
|
||||
|
@ -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">
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
|
@ -230,3 +230,7 @@ div.glossary-item.compact-glossary {
|
||||
.info-output td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.entry:not(.entry-current) .current {
|
||||
display: none;
|
||||
}
|
||||
|
@ -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
100
ext/mixed/js/scroll.js
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user