mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-02 19:05:32 -04:00
Rewrite web reader
This commit is contained in:
parent
88394d4636
commit
a45e6ea3da
590
public/js/hyperlist.js
Normal file
590
public/js/hyperlist.js
Normal file
@ -0,0 +1,590 @@
|
|||||||
|
(function(f) {
|
||||||
|
if (typeof exports === "object" && typeof module !== "undefined") {
|
||||||
|
module.exports = f()
|
||||||
|
} else if (typeof define === "function" && define.amd) {
|
||||||
|
define([], f)
|
||||||
|
} else {
|
||||||
|
var g;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
g = window
|
||||||
|
} else if (typeof global !== "undefined") {
|
||||||
|
g = global
|
||||||
|
} else if (typeof self !== "undefined") {
|
||||||
|
g = self
|
||||||
|
} else {
|
||||||
|
g = this
|
||||||
|
}
|
||||||
|
g.HyperList = f()
|
||||||
|
}
|
||||||
|
})(function() {
|
||||||
|
var define, module, exports;
|
||||||
|
return (function() {
|
||||||
|
function r(e, n, t) {
|
||||||
|
function o(i, f) {
|
||||||
|
if (!n[i]) {
|
||||||
|
if (!e[i]) {
|
||||||
|
var c = "function" == typeof require && require;
|
||||||
|
if (!f && c) return c(i, !0);
|
||||||
|
if (u) return u(i, !0);
|
||||||
|
var a = new Error("Cannot find module '" + i + "'");
|
||||||
|
throw a.code = "MODULE_NOT_FOUND", a
|
||||||
|
}
|
||||||
|
var p = n[i] = {
|
||||||
|
exports: {}
|
||||||
|
};
|
||||||
|
e[i][0].call(p.exports, function(r) {
|
||||||
|
var n = e[i][1][r];
|
||||||
|
return o(n || r)
|
||||||
|
}, p, p.exports, r, e, n, t)
|
||||||
|
}
|
||||||
|
return n[i].exports
|
||||||
|
}
|
||||||
|
for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) o(t[i]);
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})()({
|
||||||
|
1: [function(_dereq_, module, exports) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Default configuration.
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _createClass = function() {
|
||||||
|
function defineProperties(target, props) {
|
||||||
|
for (var i = 0; i < props.length; i++) {
|
||||||
|
var descriptor = props[i];
|
||||||
|
descriptor.enumerable = descriptor.enumerable || false;
|
||||||
|
descriptor.configurable = true;
|
||||||
|
if ("value" in descriptor) descriptor.writable = true;
|
||||||
|
Object.defineProperty(target, descriptor.key, descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return function(Constructor, protoProps, staticProps) {
|
||||||
|
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
||||||
|
if (staticProps) defineProperties(Constructor, staticProps);
|
||||||
|
return Constructor;
|
||||||
|
};
|
||||||
|
}();
|
||||||
|
|
||||||
|
function _defineProperty(obj, key, value) {
|
||||||
|
if (key in obj) {
|
||||||
|
Object.defineProperty(obj, key, {
|
||||||
|
value: value,
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
obj[key] = value;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _classCallCheck(instance, Constructor) {
|
||||||
|
if (!(instance instanceof Constructor)) {
|
||||||
|
throw new TypeError("Cannot call a class as a function");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultConfig = {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
|
||||||
|
// Check for valid number.
|
||||||
|
};
|
||||||
|
var isNumber = function isNumber(input) {
|
||||||
|
return Number(input) === Number(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a class to an element.
|
||||||
|
var addClass = 'classList' in document.documentElement ? function(element, className) {
|
||||||
|
element.classList.add(className);
|
||||||
|
} : function(element, className) {
|
||||||
|
var oldClass = element.getAttribute('class') || '';
|
||||||
|
element.setAttribute('class', oldClass + ' ' + className);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HyperList instance that virtually scrolls very large amounts of
|
||||||
|
* data effortlessly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var HyperList = function() {
|
||||||
|
_createClass(HyperList, null, [{
|
||||||
|
key: 'create',
|
||||||
|
value: function create(element, userProvidedConfig) {
|
||||||
|
return new HyperList(element, userProvidedConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge given css style on an element
|
||||||
|
* @param {DOMElement} element
|
||||||
|
* @param {Object} style
|
||||||
|
*/
|
||||||
|
|
||||||
|
}, {
|
||||||
|
key: 'mergeStyle',
|
||||||
|
value: function mergeStyle(element, style) {
|
||||||
|
for (var i in style) {
|
||||||
|
if (element.style[i] !== style[i]) {
|
||||||
|
element.style[i] = style[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: 'getMaxBrowserHeight',
|
||||||
|
value: function getMaxBrowserHeight() {
|
||||||
|
// Create two elements, the wrapper is `1px` tall and is transparent and
|
||||||
|
// positioned at the top of the page. Inside that is an element that gets
|
||||||
|
// set to 1 billion pixels. Then reads the max height the browser can
|
||||||
|
// calculate.
|
||||||
|
var wrapper = document.createElement('div');
|
||||||
|
var fixture = document.createElement('div');
|
||||||
|
|
||||||
|
// As said above, these values get set to put the fixture elements into the
|
||||||
|
// right visual state.
|
||||||
|
HyperList.mergeStyle(wrapper, {
|
||||||
|
position: 'absolute',
|
||||||
|
height: '1px',
|
||||||
|
opacity: 0
|
||||||
|
});
|
||||||
|
HyperList.mergeStyle(fixture, {
|
||||||
|
height: '1e7px'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the fixture into the wrapper element.
|
||||||
|
wrapper.appendChild(fixture);
|
||||||
|
|
||||||
|
// Apply to the page, the values won't kick in unless this is attached.
|
||||||
|
document.body.appendChild(wrapper);
|
||||||
|
|
||||||
|
// Get the maximum element height in pixels.
|
||||||
|
var maxElementHeight = fixture.offsetHeight;
|
||||||
|
|
||||||
|
// Remove the element immediately after reading the value.
|
||||||
|
document.body.removeChild(wrapper);
|
||||||
|
|
||||||
|
return maxElementHeight;
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
function HyperList(element, userProvidedConfig) {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
_classCallCheck(this, HyperList);
|
||||||
|
|
||||||
|
this._config = {};
|
||||||
|
this._lastRepaint = null;
|
||||||
|
this._maxElementHeight = HyperList.getMaxBrowserHeight();
|
||||||
|
|
||||||
|
this.refresh(element, userProvidedConfig);
|
||||||
|
|
||||||
|
var config = this._config;
|
||||||
|
|
||||||
|
// Create internal render loop.
|
||||||
|
var render = function render() {
|
||||||
|
var scrollTop = _this._getScrollPosition();
|
||||||
|
var lastRepaint = _this._lastRepaint;
|
||||||
|
|
||||||
|
_this._renderAnimationFrame = window.requestAnimationFrame(render);
|
||||||
|
|
||||||
|
if (scrollTop === lastRepaint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var diff = lastRepaint ? scrollTop - lastRepaint : 0;
|
||||||
|
if (!lastRepaint || diff < 0 || diff > _this._averageHeight) {
|
||||||
|
var rendered = _this._renderChunk();
|
||||||
|
|
||||||
|
_this._lastRepaint = scrollTop;
|
||||||
|
|
||||||
|
if (rendered !== false && typeof config.afterRender === 'function') {
|
||||||
|
config.afterRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
_createClass(HyperList, [{
|
||||||
|
key: 'destroy',
|
||||||
|
value: function destroy() {
|
||||||
|
window.cancelAnimationFrame(this._renderAnimationFrame);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: 'refresh',
|
||||||
|
value: function refresh(element, userProvidedConfig) {
|
||||||
|
var _scrollerStyle;
|
||||||
|
|
||||||
|
Object.assign(this._config, defaultConfig, userProvidedConfig);
|
||||||
|
|
||||||
|
if (!element || element.nodeType !== 1) {
|
||||||
|
throw new Error('HyperList requires a valid DOM Node container');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._element = element;
|
||||||
|
|
||||||
|
var config = this._config;
|
||||||
|
|
||||||
|
var scroller = this._scroller || config.scroller || document.createElement(config.scrollerTagName || 'tr');
|
||||||
|
|
||||||
|
// Default configuration option `useFragment` to `true`.
|
||||||
|
if (typeof config.useFragment !== 'boolean') {
|
||||||
|
this._config.useFragment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.generate) {
|
||||||
|
throw new Error('Missing required `generate` function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNumber(config.total)) {
|
||||||
|
throw new Error('Invalid required `total` value, expected number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(config.itemHeight) && !isNumber(config.itemHeight)) {
|
||||||
|
throw new Error('\n Invalid required `itemHeight` value, expected number or array\n '.trim());
|
||||||
|
} else if (isNumber(config.itemHeight)) {
|
||||||
|
this._itemHeights = Array(config.total).fill(config.itemHeight);
|
||||||
|
} else {
|
||||||
|
this._itemHeights = config.itemHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width and height should be coerced to string representations. Either in
|
||||||
|
// `%` or `px`.
|
||||||
|
Object.keys(defaultConfig).filter(function(prop) {
|
||||||
|
return prop in config;
|
||||||
|
}).forEach(function(prop) {
|
||||||
|
var value = config[prop];
|
||||||
|
var isValueNumber = isNumber(value);
|
||||||
|
|
||||||
|
if (value && typeof value !== 'string' && typeof value !== 'number') {
|
||||||
|
var msg = 'Invalid optional `' + prop + '`, expected string or number';
|
||||||
|
throw new Error(msg);
|
||||||
|
} else if (isValueNumber) {
|
||||||
|
config[prop] = value + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var isHoriz = Boolean(config.horizontal);
|
||||||
|
var value = config[isHoriz ? 'width' : 'height'];
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
var isValueNumber = isNumber(value);
|
||||||
|
var isValuePercent = isValueNumber ? false : value.slice(-1) === '%';
|
||||||
|
// Compute the containerHeight as number
|
||||||
|
var numberValue = isValueNumber ? value : parseInt(value.replace(/px|%/, ''), 10);
|
||||||
|
var innerSize = window[isHoriz ? 'innerWidth' : 'innerHeight'];
|
||||||
|
|
||||||
|
if (isValuePercent) {
|
||||||
|
this._containerSize = innerSize * numberValue / 100;
|
||||||
|
} else {
|
||||||
|
this._containerSize = isNumber(value) ? value : numberValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollContainer = config.scrollContainer;
|
||||||
|
var scrollerHeight = config.itemHeight * config.total;
|
||||||
|
var maxElementHeight = this._maxElementHeight;
|
||||||
|
|
||||||
|
if (scrollerHeight > maxElementHeight) {
|
||||||
|
console.warn(['HyperList: The maximum element height', maxElementHeight + 'px has', 'been exceeded; please reduce your item height.'].join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorate the container element with styles that will match
|
||||||
|
// the user supplied configuration.
|
||||||
|
var elementStyle = {
|
||||||
|
width: '' + config.width,
|
||||||
|
height: scrollContainer ? scrollerHeight + 'px' : '' + config.height,
|
||||||
|
overflow: scrollContainer ? 'none' : 'auto',
|
||||||
|
position: 'relative'
|
||||||
|
};
|
||||||
|
|
||||||
|
HyperList.mergeStyle(element, elementStyle);
|
||||||
|
|
||||||
|
if (scrollContainer) {
|
||||||
|
HyperList.mergeStyle(config.scrollContainer, {
|
||||||
|
overflow: 'auto'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollerStyle = (_scrollerStyle = {
|
||||||
|
opacity: '0',
|
||||||
|
position: 'absolute'
|
||||||
|
}, _defineProperty(_scrollerStyle, isHoriz ? 'height' : 'width', '1px'), _defineProperty(_scrollerStyle, isHoriz ? 'width' : 'height', scrollerHeight + 'px'), _scrollerStyle);
|
||||||
|
|
||||||
|
HyperList.mergeStyle(scroller, scrollerStyle);
|
||||||
|
|
||||||
|
// Only append the scroller element once.
|
||||||
|
if (!this._scroller) {
|
||||||
|
element.appendChild(scroller);
|
||||||
|
}
|
||||||
|
|
||||||
|
var padding = this._computeScrollPadding();
|
||||||
|
this._scrollPaddingBottom = padding.bottom;
|
||||||
|
this._scrollPaddingTop = padding.top;
|
||||||
|
|
||||||
|
// Set the scroller instance.
|
||||||
|
this._scroller = scroller;
|
||||||
|
this._scrollHeight = this._computeScrollHeight();
|
||||||
|
|
||||||
|
// Reuse the item positions if refreshed, otherwise set to empty array.
|
||||||
|
this._itemPositions = this._itemPositions || Array(config.total).fill(0);
|
||||||
|
|
||||||
|
// Each index in the array should represent the position in the DOM.
|
||||||
|
this._computePositions(0);
|
||||||
|
|
||||||
|
// Render after refreshing. Force render if we're calling refresh manually.
|
||||||
|
this._renderChunk(this._lastRepaint !== null);
|
||||||
|
|
||||||
|
if (typeof config.afterRender === 'function') {
|
||||||
|
config.afterRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: '_getRow',
|
||||||
|
value: function _getRow(i) {
|
||||||
|
var config = this._config;
|
||||||
|
var item = config.generate(i);
|
||||||
|
var height = item.height;
|
||||||
|
|
||||||
|
console.log(item);
|
||||||
|
|
||||||
|
if (height !== undefined && isNumber(height)) {
|
||||||
|
item = item.element;
|
||||||
|
|
||||||
|
// The height isn't the same as predicted, compute positions again
|
||||||
|
if (height !== this._itemHeights[i]) {
|
||||||
|
this._itemHeights[i] = height;
|
||||||
|
this._computePositions(i);
|
||||||
|
this._scrollHeight = this._computeScrollHeight(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
height = this._itemHeights[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(item);
|
||||||
|
|
||||||
|
if (!item || item.nodeType !== 1) {
|
||||||
|
throw new Error('Generator did not return a DOM Node for index: ' + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
addClass(item, config.rowClassName || 'vrow');
|
||||||
|
|
||||||
|
var top = this._itemPositions[i] + this._scrollPaddingTop;
|
||||||
|
|
||||||
|
HyperList.mergeStyle(item, _defineProperty({
|
||||||
|
position: 'absolute'
|
||||||
|
}, config.horizontal ? 'left' : 'top', top + 'px'));
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: '_getScrollPosition',
|
||||||
|
value: function _getScrollPosition() {
|
||||||
|
var config = this._config;
|
||||||
|
|
||||||
|
if (typeof config.overrideScrollPosition === 'function') {
|
||||||
|
return config.overrideScrollPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._element[config.horizontal ? 'scrollLeft' : 'scrollTop'];
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: '_renderChunk',
|
||||||
|
value: function _renderChunk(force) {
|
||||||
|
var config = this._config;
|
||||||
|
var element = this._element;
|
||||||
|
var scrollTop = this._getScrollPosition();
|
||||||
|
var total = config.total;
|
||||||
|
|
||||||
|
var from = config.reverse ? this._getReverseFrom(scrollTop) : this._getFrom(scrollTop) - 1;
|
||||||
|
|
||||||
|
if (from < 0 || from - this._screenItemsLen < 0) {
|
||||||
|
from = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && this._lastFrom === from) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastFrom = from;
|
||||||
|
|
||||||
|
var to = from + this._cachedItemsLen;
|
||||||
|
|
||||||
|
if (to > total || to + this._cachedItemsLen > total) {
|
||||||
|
to = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append all the new rows in a document fragment that we will later append
|
||||||
|
// to the parent node
|
||||||
|
var fragment = config.useFragment ? document.createDocumentFragment() : []
|
||||||
|
// Sometimes you'll pass fake elements to this tool and Fragments require
|
||||||
|
// real elements.
|
||||||
|
|
||||||
|
|
||||||
|
// The element that forces the container to scroll.
|
||||||
|
;
|
||||||
|
var scroller = this._scroller;
|
||||||
|
|
||||||
|
// Keep the scroller in the list of children.
|
||||||
|
fragment[config.useFragment ? 'appendChild' : 'push'](scroller);
|
||||||
|
|
||||||
|
for (var i = from; i < to; i++) {
|
||||||
|
var row = this._getRow(i);
|
||||||
|
|
||||||
|
fragment[config.useFragment ? 'appendChild' : 'push'](row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.applyPatch) {
|
||||||
|
return config.applyPatch(element, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.innerHTML = '';
|
||||||
|
element.appendChild(fragment);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: '_computePositions',
|
||||||
|
value: function _computePositions() {
|
||||||
|
var from = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
|
||||||
|
|
||||||
|
var config = this._config;
|
||||||
|
var total = config.total;
|
||||||
|
var reverse = config.reverse;
|
||||||
|
|
||||||
|
if (from < 1 && !reverse) {
|
||||||
|
from = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = from; i < total; i++) {
|
||||||
|
if (reverse) {
|
||||||
|
if (i === 0) {
|
||||||
|
this._itemPositions[0] = this._scrollHeight - this._itemHeights[0];
|
||||||
|
} else {
|
||||||
|
this._itemPositions[i] = this._itemPositions[i - 1] - this._itemHeights[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._itemPositions[i] = this._itemHeights[i - 1] + this._itemPositions[i - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: '_computeScrollHeight',
|
||||||
|
value: function _computeScrollHeight() {
|
||||||
|
var _HyperList$mergeStyle2,
|
||||||
|
_this2 = this;
|
||||||
|
|
||||||
|
var config = this._config;
|
||||||
|
var isHoriz = Boolean(config.horizontal);
|
||||||
|
var total = config.total;
|
||||||
|
var scrollHeight = this._itemHeights.reduce(function(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}, 0) + this._scrollPaddingBottom + this._scrollPaddingTop;
|
||||||
|
|
||||||
|
HyperList.mergeStyle(this._scroller, (_HyperList$mergeStyle2 = {
|
||||||
|
opacity: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0px'
|
||||||
|
}, _defineProperty(_HyperList$mergeStyle2, isHoriz ? 'height' : 'width', '1px'), _defineProperty(_HyperList$mergeStyle2, isHoriz ? 'width' : 'height', scrollHeight + 'px'), _HyperList$mergeStyle2));
|
||||||
|
|
||||||
|
// Calculate the height median
|
||||||
|
var sortedItemHeights = this._itemHeights.slice(0).sort(function(a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
var middle = Math.floor(total / 2);
|
||||||
|
var averageHeight = total % 2 === 0 ? (sortedItemHeights[middle] + sortedItemHeights[middle - 1]) / 2 : sortedItemHeights[middle];
|
||||||
|
|
||||||
|
var clientProp = isHoriz ? 'clientWidth' : 'clientHeight';
|
||||||
|
var element = config.scrollContainer ? config.scrollContainer : this._element;
|
||||||
|
var containerHeight = element[clientProp] ? element[clientProp] : this._containerSize;
|
||||||
|
this._screenItemsLen = Math.ceil(containerHeight / averageHeight);
|
||||||
|
this._containerSize = containerHeight;
|
||||||
|
|
||||||
|
// Cache 3 times the number of items that fit in the container viewport.
|
||||||
|
this._cachedItemsLen = Math.max(this._cachedItemsLen || 0, this._screenItemsLen * 3);
|
||||||
|
this._averageHeight = averageHeight;
|
||||||
|
|
||||||
|
if (config.reverse) {
|
||||||
|
window.requestAnimationFrame(function() {
|
||||||
|
if (isHoriz) {
|
||||||
|
_this2._element.scrollLeft = scrollHeight;
|
||||||
|
} else {
|
||||||
|
_this2._element.scrollTop = scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return scrollHeight;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: '_computeScrollPadding',
|
||||||
|
value: function _computeScrollPadding() {
|
||||||
|
var config = this._config;
|
||||||
|
var isHoriz = Boolean(config.horizontal);
|
||||||
|
var isReverse = config.reverse;
|
||||||
|
var styles = window.getComputedStyle(this._element);
|
||||||
|
|
||||||
|
var padding = function padding(location) {
|
||||||
|
var cssValue = styles.getPropertyValue('padding-' + location);
|
||||||
|
return parseInt(cssValue, 10) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHoriz && isReverse) {
|
||||||
|
return {
|
||||||
|
bottom: padding('left'),
|
||||||
|
top: padding('right')
|
||||||
|
};
|
||||||
|
} else if (isHoriz) {
|
||||||
|
return {
|
||||||
|
bottom: padding('right'),
|
||||||
|
top: padding('left')
|
||||||
|
};
|
||||||
|
} else if (isReverse) {
|
||||||
|
return {
|
||||||
|
bottom: padding('top'),
|
||||||
|
top: padding('bottom')
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
bottom: padding('bottom'),
|
||||||
|
top: padding('top')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: '_getFrom',
|
||||||
|
value: function _getFrom(scrollTop) {
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
while (this._itemPositions[i] < scrollTop) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: '_getReverseFrom',
|
||||||
|
value: function _getReverseFrom(scrollTop) {
|
||||||
|
var i = this._config.total - 1;
|
||||||
|
|
||||||
|
while (i > 0 && this._itemPositions[i] < scrollTop + this._containerSize) {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
return HyperList;
|
||||||
|
}();
|
||||||
|
|
||||||
|
exports.default = HyperList;
|
||||||
|
module.exports = exports['default'];
|
||||||
|
|
||||||
|
}, {}]
|
||||||
|
}, {}, [1])(1)
|
||||||
|
});
|
@ -1,64 +1,62 @@
|
|||||||
$(function() {
|
$(() => {
|
||||||
function bind() {
|
getPages();
|
||||||
var controller = new ScrollMagic.Controller();
|
|
||||||
|
|
||||||
// replace history on scroll
|
$('#page-select').change(() => {
|
||||||
$('img').each(function(idx) {
|
const p = parseInt($('#page-select').val());
|
||||||
var scene = new ScrollMagic.Scene({
|
toPage(p);
|
||||||
triggerElement: $(this).get(),
|
});
|
||||||
triggerHook: 'onEnter',
|
});
|
||||||
reverse: true
|
|
||||||
})
|
|
||||||
.addTo(controller)
|
|
||||||
.on('enter', function(event) {
|
|
||||||
current = $(event.target.triggerElement()).attr('id');
|
|
||||||
replaceHistory(current);
|
|
||||||
})
|
|
||||||
.on('leave', function(event) {
|
|
||||||
var prev = $(event.target.triggerElement()).prev();
|
|
||||||
current = $(prev).attr('id');
|
|
||||||
replaceHistory(current);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// poor man's infinite scroll
|
const getPages = () => {
|
||||||
var scene = new ScrollMagic.Scene({
|
$.get(`${base_url}api/dimensions/${tid}/${eid}`)
|
||||||
triggerElement: $('.next-url').get(),
|
.then(data => {
|
||||||
triggerHook: 'onEnter',
|
if (!data.success && data.error)
|
||||||
offset: -500
|
throw new Error(resp.error);
|
||||||
})
|
const dimensions = data.dimensions;
|
||||||
.addTo(controller)
|
|
||||||
.on('enter', function() {
|
const items = dimensions.map((d, i) => {
|
||||||
var nextURL = $('.next-url').attr('href');
|
return {
|
||||||
$('.next-url').remove();
|
id: i + 1,
|
||||||
if (!nextURL) {
|
url: `${base_url}api/page/${tid}/${eid}/${i+1}`,
|
||||||
console.log('No .next-url found. Reached end of page');
|
width: d.width,
|
||||||
var lastURL = $('img').last().attr('id');
|
height: d.height
|
||||||
// load the reader URL for the last page to update reading progrss to 100%
|
};
|
||||||
$.get(lastURL);
|
|
||||||
$('#next-btn').removeAttr('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$('#hidden').load(encodeURI(nextURL) + ' .uk-container', function(res, status, xhr) {
|
|
||||||
if (status === 'error') console.log(xhr.statusText);
|
|
||||||
if (status === 'success') {
|
|
||||||
console.log(nextURL + ' loaded');
|
|
||||||
// new page loaded to #hidden, we now append it
|
|
||||||
$('.uk-section > .uk-container').append($('#hidden .uk-container').children());
|
|
||||||
$('#hidden').empty();
|
|
||||||
bind();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
bind();
|
setProp('items', items);
|
||||||
});
|
setProp('loading', false);
|
||||||
$('#page-select').change(function() {
|
|
||||||
jumpTo(parseInt($('#page-select').val()));
|
|
||||||
});
|
|
||||||
|
|
||||||
function showControl(idx) {
|
waitForPage(items.length, () => {
|
||||||
|
toPage(page);
|
||||||
|
setupScroller();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
const errMsg = `Failed to get the page dimensions. ${e}`;
|
||||||
|
console.error(e);
|
||||||
|
setProp('alertClass', 'uk-alert-danger');
|
||||||
|
setProp('msg', errMsg);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPage = (idx) => {
|
||||||
|
$(`#${idx}`).get(0).scrollIntoView(true);
|
||||||
|
UIkit.modal($('#modal-sections')).hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForPage = (idx, cb) => {
|
||||||
|
if ($(`#${idx}`).length > 0) return cb();
|
||||||
|
setTimeout(() => {
|
||||||
|
waitForPage(idx, cb)
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setProp = (key, prop) => {
|
||||||
|
$('#root').get(0).__x.$data[key] = prop;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showControl = (event) => {
|
||||||
|
const idx = parseInt($(event.currentTarget).attr('id'));
|
||||||
const pageCount = $('#page-select > option').length;
|
const pageCount = $('#page-select > option').length;
|
||||||
const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`;
|
const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`;
|
||||||
$('#progress-label').text(progressText);
|
$('#progress-label').text(progressText);
|
||||||
@ -66,19 +64,43 @@ function showControl(idx) {
|
|||||||
UIkit.modal($('#modal-sections')).show();
|
UIkit.modal($('#modal-sections')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function jumpTo(page) {
|
const redirect = (url) => {
|
||||||
var ary = window.location.pathname.split('/');
|
|
||||||
ary[ary.length - 1] = page;
|
|
||||||
ary.shift(); // remove leading `/`
|
|
||||||
ary.unshift(window.location.origin);
|
|
||||||
window.location.replace(ary.join('/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceHistory(url) {
|
|
||||||
history.replaceState(null, "", url);
|
|
||||||
console.log('reading ' + url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirect(url) {
|
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupScroller = () => {
|
||||||
|
$('#root img').each((idx, el) => {
|
||||||
|
$(el).on('inview', (event, inView) => {
|
||||||
|
if (inView) {
|
||||||
|
const current = $(event.currentTarget).attr('id');
|
||||||
|
replaceHistory(current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastSavedPage = page;
|
||||||
|
const saveProgress = (idx) => {
|
||||||
|
if (Math.abs(idx - lastSavedPage) < 5) return;
|
||||||
|
lastSavedPage = idx;
|
||||||
|
|
||||||
|
const url = `${base_url}api/progress/${tid}/${idx}?${$.param({entry: eid})}`;
|
||||||
|
$.post(url)
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
alert('danger', e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -101,8 +101,8 @@ class Entry
|
|||||||
img
|
img
|
||||||
end
|
end
|
||||||
|
|
||||||
def page_aspect_ratios
|
def page_dimensions
|
||||||
ratios = [] of Float64
|
sizes = [] of Hash(String, Int32)
|
||||||
ArchiveFile.open @zip_path do |file|
|
ArchiveFile.open @zip_path do |file|
|
||||||
file.entries
|
file.entries
|
||||||
.select { |e|
|
.select { |e|
|
||||||
@ -116,14 +116,17 @@ class Entry
|
|||||||
begin
|
begin
|
||||||
data = file.read_entry(e).not_nil!
|
data = file.read_entry(e).not_nil!
|
||||||
size = ImageSize.get data
|
size = ImageSize.get data
|
||||||
ratios << size.height / size.width
|
sizes << {
|
||||||
|
"width" => size.width,
|
||||||
|
"height" => size.height,
|
||||||
|
}
|
||||||
rescue
|
rescue
|
||||||
Logger.warn "Failed to read page #{i} of entry #{@id}"
|
Logger.warn "Failed to read page #{i} of entry #{@id}"
|
||||||
ratios << 1_f64
|
sizes << {"width" => 1000_i32, "height" => 1000_i32}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
ratios
|
sizes
|
||||||
end
|
end
|
||||||
|
|
||||||
def next_entry(username)
|
def next_entry(username)
|
||||||
|
@ -333,7 +333,7 @@ class APIRouter < Router
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/ratios/:tid/:eid" do |env|
|
get "/api/dimensions/:tid/:eid" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
eid = env.params.url["eid"]
|
eid = env.params.url["eid"]
|
||||||
@ -343,10 +343,10 @@ class APIRouter < Router
|
|||||||
entry = title.get_entry eid
|
entry = title.get_entry eid
|
||||||
raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil?
|
raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil?
|
||||||
|
|
||||||
ratios = entry.page_aspect_ratios
|
sizes = entry.page_dimensions
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => true,
|
"success" => true,
|
||||||
"ratios" => ratios,
|
"dimensions" => sizes,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
|
@ -13,10 +13,6 @@ class ReaderRouter < Router
|
|||||||
|
|
||||||
# load progress
|
# load progress
|
||||||
page = entry.load_progress username
|
page = entry.load_progress username
|
||||||
# we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll
|
|
||||||
# library perloads a few pages in advance, and the user
|
|
||||||
# might not have actually read them
|
|
||||||
page = [page - 2 * IMGS_PER_PAGE, 1].max
|
|
||||||
|
|
||||||
# start from page 1 if the user has finished reading the entry
|
# start from page 1 if the user has finished reading the entry
|
||||||
page = 1 if entry.finished? username
|
page = 1 if entry.finished? username
|
||||||
@ -32,29 +28,17 @@ class ReaderRouter < Router
|
|||||||
begin
|
begin
|
||||||
base_url = Config.current.base_url
|
base_url = Config.current.base_url
|
||||||
|
|
||||||
|
username = get_username env
|
||||||
|
|
||||||
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
title = (@context.library.get_title env.params.url["title"]).not_nil!
|
||||||
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
entry = (title.get_entry env.params.url["entry"]).not_nil!
|
||||||
page = env.params.url["page"].to_i
|
page = env.params.url["page"].to_i
|
||||||
raise "" if page > entry.pages || page <= 0
|
raise "" if page > entry.pages || page <= 0
|
||||||
|
|
||||||
# save progress
|
|
||||||
username = get_username env
|
|
||||||
entry.save_progress username, page
|
|
||||||
|
|
||||||
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
|
|
||||||
urls = pages.map { |idx|
|
|
||||||
"#{base_url}api/page/#{title.id}/#{entry.id}/#{idx}"
|
|
||||||
}
|
|
||||||
reader_urls = pages.map { |idx|
|
|
||||||
"#{base_url}reader/#{title.id}/#{entry.id}/#{idx}"
|
|
||||||
}
|
|
||||||
next_page = page + IMGS_PER_PAGE
|
|
||||||
next_url = next_entry_url = nil
|
|
||||||
exit_url = "#{base_url}book/#{title.id}"
|
exit_url = "#{base_url}book/#{title.id}"
|
||||||
|
|
||||||
|
next_entry_url = nil
|
||||||
next_entry = entry.next_entry username
|
next_entry = entry.next_entry username
|
||||||
unless next_page > entry.pages
|
|
||||||
next_url = "#{base_url}reader/#{title.id}/#{entry.id}/#{next_page}"
|
|
||||||
end
|
|
||||||
unless next_entry.nil?
|
unless next_entry.nil?
|
||||||
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
|
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
|
||||||
end
|
end
|
||||||
|
41
src/views/list.html.ecr
Normal file
41
src/views/list.html.ecr
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="reader-bg">
|
||||||
|
|
||||||
|
<%= render_component "head" %>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="uk-section uk-section-default uk-section-small reader-bg">
|
||||||
|
<div class="uk-container uk-container-small" id="container">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const base_url = "<%= base_url %>"
|
||||||
|
</script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
|
||||||
|
<script src="<%= base_url %>js/uikit.min.js"></script>
|
||||||
|
<script src="<%= base_url %>js/uikit-icons.min.js"></script>
|
||||||
|
<script src="<%= base_url %>js/hyperlist.js"></script>
|
||||||
|
<script>
|
||||||
|
const container = $('#container').get(0);
|
||||||
|
const list = new HyperList(container, {
|
||||||
|
itemHeight: 500,
|
||||||
|
total: 38,
|
||||||
|
generate(idx) {
|
||||||
|
const el = document.createElement('img');
|
||||||
|
el.setAttribute('class', 'uk-align-center');
|
||||||
|
el.setAttribute('data-src', `/api/page/364cdcf8c17340ba95a20862dbcbe934/786fb3d0fb0c4675a34efb0019b62c4e/${idx+1}`);
|
||||||
|
el.setAttribute('data-width', '');
|
||||||
|
el.setAttribute('data-height', '');
|
||||||
|
el.setAttribute('uk-img', '');
|
||||||
|
return {
|
||||||
|
height: 500,
|
||||||
|
element: el
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -6,22 +6,40 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="uk-section uk-section-default uk-section-small reader-bg">
|
<div class="uk-section uk-section-default uk-section-small reader-bg">
|
||||||
<div class="uk-container uk-container-small">
|
<div class="uk-container uk-container-small">
|
||||||
<%- urls.each_with_index do |url, i| -%>
|
<div id="alert"></div>
|
||||||
<img class="uk-align-center" data-src="<%= url %>" src="<%= base_url %>img/loading.gif" data-width data-height uk-img id="<%= reader_urls[i] %>" onclick="showControl(<%= pages.to_a[i] %>);">
|
<div id="root" x-data="{
|
||||||
<%- end -%>
|
loading: true,
|
||||||
<%- if next_url -%>
|
msg: 'Loading the web reader. Please wait...',
|
||||||
<a class="next-url" href="<%= next_url %>"></a>
|
alertClass: 'uk-alert-primary',
|
||||||
<%- end -%>
|
items: []
|
||||||
|
}">
|
||||||
|
<div x-show="loading">
|
||||||
|
<div :class="alertClass" x-show="msg" uk-alert>
|
||||||
|
<p x-text="msg"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="!loading" x-cloak>
|
||||||
|
<template x-for="item in items">
|
||||||
|
<img
|
||||||
|
uk-img
|
||||||
|
class="uk-align-center"
|
||||||
|
:data-src="item.url"
|
||||||
|
:width="item.width"
|
||||||
|
:height="item.height"
|
||||||
|
:id="item.id"
|
||||||
|
@click="showControl($event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<%- if next_entry_url -%>
|
||||||
|
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="redirect('<%= next_entry_url %>')">Next Entry</button>
|
||||||
|
<%- else -%>
|
||||||
|
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="redirect('<%= exit_url %>')">Exit Reader</button>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- if next_entry_url -%>
|
|
||||||
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" hidden onclick="redirect('<%= next_entry_url %>')">Next Entry</button>
|
|
||||||
<%- else -%>
|
|
||||||
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" hidden onclick="redirect('<%= exit_url %>')">Exit Reader</button>
|
|
||||||
<%- end -%>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hidden" hidden></div>
|
|
||||||
|
|
||||||
<div id="modal-sections" class="uk-flex-top" uk-modal>
|
<div id="modal-sections" class="uk-flex-top" uk-modal>
|
||||||
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
||||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
@ -49,14 +67,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const base_url = "<%= base_url %>"
|
const base_url = "<%= base_url %>";
|
||||||
|
const page = <%= page %>;
|
||||||
|
const tid = "<%= title.id %>";
|
||||||
|
const eid = "<%= entry.id %>";
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/protonet-jquery.inview/1.1.2/jquery.inview.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/uikit.min.js"></script>
|
<script src="<%= base_url %>js/uikit.min.js"></script>
|
||||||
<script src="<%= base_url %>js/uikit-icons.min.js"></script>
|
<script src="<%= base_url %>js/uikit-icons.min.js"></script>
|
||||||
<script src="<%= base_url %>js/reader.js"></script>
|
<script src="<%= base_url %>js/reader.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
img[data-src][src*='data:image'] { background: white; }
|
||||||
|
#root img { width: 100%; }
|
||||||
|
</style>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user