From a68282b4bf4d97572627e21940becbac7347b1bc Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Thu, 31 Dec 2020 16:10:04 +0000 Subject: [PATCH] Rewrite reader.js with a reusable alpine function --- public/js/reader.js | 523 ++++++++++++++++++-------------------- src/views/reader.html.ecr | 31 +-- 2 files changed, 257 insertions(+), 297 deletions(-) diff --git a/public/js/reader.js b/public/js/reader.js index 7ff46aa..e890644 100644 --- a/public/js/reader.js +++ b/public/js/reader.js @@ -1,293 +1,264 @@ -let lastSavedPage = page; -let items = []; -let longPages = false; +const readerComponent = () => { + return { + loading: true, + mode: 'continuous', // Can be 'continuous', 'height' or 'width' + msg: 'Loading the web reader. Please wait...', + alertClass: 'uk-alert-primary', + items: [], + curItem: {}, + flipAnimation: null, + longPages: false, + lastSavedPage: page, -$(() => { - getPages(); + /** + * Initialize the component by fetching the page dimensions + */ + init(nextTick) { + $.get(`${base_url}api/dimensions/${tid}/${eid}`) + .then(data => { + if (!data.success && data.error) + throw new Error(resp.error); + const dimensions = data.dimensions; - $('#page-select').change(() => { - const p = parseInt($('#page-select').val()); - toPage(p); - }); + this.items = dimensions.map((d, i) => { + return { + id: i + 1, + url: `${base_url}api/page/${tid}/${eid}/${i+1}`, + width: d.width, + height: d.height + }; + }); - $('#mode-select').change(() => { - const mode = $('#mode-select').val(); - const curIdx = parseInt($('#page-select').val()); + const avgRatio = this.items.reduce((acc, cur) => { + return acc + cur.height / cur.width + }, 0) / this.items.length; - updateMode(mode, curIdx); - }); -}); + console.log(avgRatio); + this.longPages = avgRatio > 2; + this.loading = false; + this.mode = localStorage.getItem('mode') || 'continuous'; -$(window).resize(() => { - const mode = getProp('mode'); - if (mode === 'continuous') return; + // Here we save a copy of this.mode, and use the copy as + // the model-select value. This is because `updateMode` + // might change this.mode and make it `height` or `width`, + // which are not available in mode-select + const mode = this.mode; + this.updateMode(this.mode, page, nextTick); + $('#mode-select').val(mode); + }) + .catch(e => { + const errMsg = `Failed to get the page dimensions. ${e}`; + console.error(e); + this.alertClass = 'uk-alert-danger'; + this.msg = errMsg; + }) + }, + /** + * Handles the `change` event for the page selector + */ + pageChanged() { + const p = parseInt($('#page-select').val()); + this.toPage(p); + }, + /** + * Handles the `change` event for the mode selector + * + * @param {function} nextTick - Alpine $nextTick magic property + */ + modeChanged(nextTick) { + const mode = $('#mode-select').val(); + const curIdx = parseInt($('#page-select').val()); - const wideScreen = $(window).width() > $(window).height(); - const propMode = wideScreen ? 'height' : 'width'; - setProp('mode', propMode); -}); + this.updateMode(mode, curIdx, nextTick); + }, + /** + * Handles the window `resize` event + */ + resized() { + if (this.mode === 'continuous') return; -/** - * Update the reader mode - * - * @function updateMode - * @param {string} mode - The mode. Can be one of the followings: - * {'continuous', 'paged', 'height', 'width'} - * @param {number} targetPage - The one-based index of the target page - */ -const updateMode = (mode, targetPage) => { - localStorage.setItem('mode', mode); + const wideScreen = $(window).width() > $(window).height(); + this.mode = wideScreen ? 'height' : 'width'; + }, + /** + * Handles the window `keydown` event + * + * @param {Event} event - The triggering event + */ + keyHandler(event) { + if (this.mode === 'continuous') return; - // The mode to be put into the `mode` prop. It can't be `screen` - let propMode = mode; + if (event.key === 'ArrowLeft' || event.key === 'k') + this.flipPage(false); + if (event.key === 'ArrowRight' || event.key === 'j') + this.flipPage(true); + }, + /** + * Flips to the next or the previous page + * + * @param {bool} isNext - Whether we are going to the next page + */ + flipPage(isNext) { + const idx = parseInt(this.curItem.id); + const delta = isNext ? 1 : -1; + const newIdx = idx + delta; - if (mode === 'paged') { - const wideScreen = $(window).width() > $(window).height(); - propMode = wideScreen ? 'height' : 'width'; - } + this.toPage(newIdx); - setProp('mode', propMode); + if (isNext) + this.flipAnimation = 'right'; + else + this.flipAnimation = 'left'; - if (mode === 'continuous') { - waitForPage(items.length, () => { - setupScroller(); - }); - } + setTimeout(() => { + this.flipAnimation = null; + }, 500); - waitForPage(targetPage, () => { - setTimeout(() => { - toPage(targetPage); - }, 100); - }); -}; - -/** - * Get dimension of the pages in the entry from the API and update the view - */ -const getPages = () => { - $.get(`${base_url}api/dimensions/${tid}/${eid}`) - .then(data => { - if (!data.success && data.error) - throw new Error(resp.error); - const dimensions = data.dimensions; - - items = dimensions.map((d, i) => { - return { - id: i + 1, - url: `${base_url}api/page/${tid}/${eid}/${i+1}`, - width: d.width, - height: d.height - }; - }); - - const avgRatio = items.reduce((acc, cur) => { - return acc + cur.height / cur.width - }, 0) / items.length; - - console.log(avgRatio); - longPages = avgRatio > 2; - - setProp('items', items); - setProp('loading', false); - - const storedMode = localStorage.getItem('mode') || 'continuous'; - - setProp('mode', storedMode); - updateMode(storedMode, page); - $('#mode-select').val(storedMode); - }) - .catch(e => { - const errMsg = `Failed to get the page dimensions. ${e}`; - console.error(e); - setProp('alertClass', 'uk-alert-danger'); - setProp('msg', errMsg); - }) -}; - -/** - * Jump to a specific page - * - * @function toPage - * @param {number} idx - One-based index of the page - */ -const toPage = (idx) => { - const mode = getProp('mode'); - if (mode === 'continuous') { - $(`#${idx}`).get(0).scrollIntoView(true); - } else { - if (idx >= 1 && idx <= items.length) { - setProp('curItem', items[idx - 1]); - } - } - replaceHistory(idx); - UIkit.modal($('#modal-sections')).hide(); -}; - -/** - * Check if a page exists every 100ms. If so, invoke the callback function. - * - * @function waitForPage - * @param {number} idx - One-based index of the page - * @param {function} cb - Callback function - */ -const waitForPage = (idx, cb) => { - if ($(`#${idx}`).length > 0) return cb(); - setTimeout(() => { - waitForPage(idx, cb) - }, 100); -}; - -/** - * Show the control modal - * - * @function showControl - * @param {string} idx - One-based index of the current page - */ -const showControl = (idx) => { - const pageCount = $('#page-select > option').length; - const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`; - $('#progress-label').text(progressText); - $('#page-select').val(idx); - UIkit.modal($('#modal-sections')).show(); -} - -/** - * Redirect to a URL - * - * @function redirect - * @param {string} url - The target URL - */ -const redirect = (url) => { - window.location.replace(url); -} - -/** - * Replace the address bar history and save th ereading progress if necessary - * - * @function replaceHistory - * @param {number} idx - One-based index of the current page - */ -const replaceHistory = (idx) => { - const ary = window.location.pathname.split('/'); - ary[ary.length - 1] = idx; - ary.shift(); // remove leading `/` - ary.unshift(window.location.origin); - const url = ary.join('/'); - saveProgress(idx); - history.replaceState(null, "", url); -} - -/** - * Set up the scroll handler that calls `replaceHistory` when an image - * enters the view port - * - * @function setupScroller - */ -const setupScroller = () => { - const mode = getProp('mode'); - if (mode !== 'continuous') return; - $('#root img').each((idx, el) => { - $(el).on('inview', (event, inView) => { - if (inView) { - const current = $(event.currentTarget).attr('id'); - - setProp('curItem', getProp('items')[current - 1]); - replaceHistory(current); + this.replaceHistory(newIdx); + }, + /** + * Jumps to a specific page + * + * @param {number} idx - One-based index of the page + */ + toPage(idx) { + if (this.mode === 'continuous') { + $(`#${idx}`).get(0).scrollIntoView(true); + } else { + if (idx >= 1 && idx <= this.items.length) { + this.curItem = this.items[idx - 1]; + } } - }); - }); -}; + this.replaceHistory(idx); + UIkit.modal($('#modal-sections')).hide(); + }, + /** + * Replace the address bar history and save the reading progress if necessary + * + * @param {number} idx - One-based index of the page + */ + replaceHistory(idx) { + const ary = window.location.pathname.split('/'); + ary[ary.length - 1] = idx; + ary.shift(); // remove leading `/` + ary.unshift(window.location.origin); + const url = ary.join('/'); + this.saveProgress(idx); + history.replaceState(null, "", url); + }, + /** + * Updates the backend reading progress if: + * 1) the current page is more than five pages away from the last + * saved page, or + * 2) the average height/width ratio of the pages is over 2, or + * 3) the current page is the first page, or + * 4) the current page is the last page + * + * @param {number} idx - One-based index of the page + * @param {function} cb - Callback + */ + saveProgress(idx, cb) { + idx = parseInt(idx); + if (Math.abs(idx - this.lastSavedPage) >= 5 || + this.longPages || + idx === 1 || idx === this.items.length + ) { + this.lastSavedPage = idx; + console.log('saving progress', idx); -/** - * Update the backend reading progress if: - * 1) the current page is more than five pages away from the last - * saved page, or - * 2) the average height/width ratio of the pages is over 2, or - * 3) the current page is the first page, or - * 4) the current page is the last page - * - * @function saveProgress - * @param {number} idx - One-based index of the page - * @param {function} cb - Callback - */ -const saveProgress = (idx, cb) => { - idx = parseInt(idx); - if (Math.abs(idx - lastSavedPage) >= 5 || - longPages || - idx === 1 || idx === items.length - ) { - lastSavedPage = idx; - console.log('saving progress', idx); + const url = `${base_url}api/progress/${tid}/${idx}?${$.param({eid: eid})}`; + $.ajax({ + method: 'PUT', + url: url, + dataType: 'json' + }) + .done(data => { + if (data.error) + alert('danger', data.error); + if (cb) cb(); + }) + .fail((jqXHR, status) => { + alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }); + } + }, + /** + * Updates the reader mode + * + * @param {string} mode - Either `continuous` or `paged` + * @param {number} targetPage - The one-based index of the target page + * @param {function} nextTick - Alpine $nextTick magic property + */ + updateMode(mode, targetPage, nextTick) { + localStorage.setItem('mode', mode); - const url = `${base_url}api/progress/${tid}/${idx}?${$.param({eid: eid})}`; - $.ajax({ - method: 'PUT', - url: url, - dataType: 'json' - }) - .done(data => { - if (data.error) - alert('danger', data.error); - if (cb) cb(); - }) - .fail((jqXHR, status) => { - alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); + // The mode to be put into the `mode` prop. It can't be `screen` + let propMode = mode; + + if (mode === 'paged') { + const wideScreen = $(window).width() > $(window).height(); + propMode = wideScreen ? 'height' : 'width'; + } + + this.mode = propMode; + + if (mode === 'continuous') { + nextTick(() => { + this.setupScroller(); + }); + } + + nextTick(() => { + this.toPage(targetPage); }); - } -}; + }, + /** + * Shows the control modal + * + * @param {Event} event - The triggering event + */ + showControl(event) { + const idx = event.currentTarget.id; + const pageCount = this.items.length; + const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`; + $('#progress-label').text(progressText); + $('#page-select').val(idx); + UIkit.modal($('#modal-sections')).show(); + }, + /** + * Redirects to a URL + * + * @param {string} url - The target URL + */ + redirect(url) { + window.location.replace(url); + }, + /** + * Set up the scroll handler that calls `replaceHistory` when an image + * enters the view port + */ + setupScroller() { + if (this.mode !== 'continuous') return; + $('#root img').each((idx, el) => { + $(el).on('inview', (event, inView) => { + if (inView) { + const current = $(event.currentTarget).attr('id'); -/** - * Mark progress to 100% and redirect to the next entry - * Used as the onclick handler for the "Next Entry" button - * - * @function nextEntry - * @param {string} nextUrl - URL of the next entry - */ -const nextEntry = (nextUrl) => { - saveProgress(items.length, () => { - redirect(nextUrl); - }); -}; - -/** - * Show the next or the previous page - * - * @function flipPage - * @param {bool} isNext - Whether we are going to the next page - */ -const flipPage = (isNext) => { - const curItem = getProp('curItem'); - const idx = parseInt(curItem.id); - const delta = isNext ? 1 : -1; - const newIdx = idx + delta; - - toPage(newIdx); - - if (isNext) - setProp('flipAnimation', 'right'); - else - setProp('flipAnimation', 'left'); - - setTimeout(() => { - setProp('flipAnimation', null); - }, 500); - - replaceHistory(newIdx); - saveProgress(newIdx); -}; - -/** - * Handle the global keydown events - * - * @function keyHandler - * @param {event} event - The $event object - */ -const keyHandler = (event) => { - const mode = getProp('mode'); - if (mode === 'continuous') return; - - if (event.key === 'ArrowLeft' || event.key === 'k') - flipPage(false); - if (event.key === 'ArrowRight' || event.key === 'j') - flipPage(true); -}; + this.curItem = this.items[current - 1]; + this.replaceHistory(current); + } + }); + }); + }, + /** + * Marks progress as 100% and jumps to the next entry + * + * @param {string} nextUrl - URL of the next entry + */ + nextEntry(nextUrl) { + this.saveProgress(items.length, () => { + this.redirect(nextUrl); + }); + } + }; +} diff --git a/src/views/reader.html.ecr b/src/views/reader.html.ecr index fc72e30..69dc79c 100644 --- a/src/views/reader.html.ecr +++ b/src/views/reader.html.ecr @@ -4,19 +4,8 @@ <% page = "Reader" %> <%= render_component "head" %> - -
+ +
@@ -40,7 +29,7 @@ :width="item.width" :height="item.height" :id="item.id" - :onclick="`showControl('${item.id}')`" + @click="showControl($event)" /> <%- if next_entry_url -%> @@ -56,11 +45,11 @@ 'uk-align-center': true, 'uk-animation-slide-left': flipAnimation === 'left', 'uk-animation-slide-right': flipAnimation === 'right' - }" :data-src="curItem.url" :width="curItem.width" :height="curItem.height" :id="curItem.id" :onclick="`showControl('${curItem.id}')`" :style="` - width:${mode === 'width' ? '100vw' : 'auto'}; - height:${mode === 'height' ? '100vh' : 'auto'}; - margin-bottom:0; - `" /> + }" :data-src="curItem.url" :width="curItem.width" :height="curItem.height" :id="curItem.id" @click="showControl($event)" :style="` + width:${mode === 'width' ? '100vw' : 'auto'}; + height:${mode === 'height' ? '100vh' : 'auto'}; + margin-bottom:0; + `" />
@@ -83,7 +72,7 @@
- <%- (1..entry.pages).each do |p| -%> <%- end -%> @@ -93,7 +82,7 @@
-