summaryrefslogtreecommitdiff
path: root/lib/epub.js/src/utils/core.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/epub.js/src/utils/core.js')
-rw-r--r--lib/epub.js/src/utils/core.js876
1 files changed, 876 insertions, 0 deletions
diff --git a/lib/epub.js/src/utils/core.js b/lib/epub.js/src/utils/core.js
new file mode 100644
index 0000000..5c83944
--- /dev/null
+++ b/lib/epub.js/src/utils/core.js
@@ -0,0 +1,876 @@
+/**
+ * Core Utilities and Helpers
+ * @module Core
+*/
+import { DOMParser as XMLDOMParser } from "xmldom";
+
+/**
+ * Vendor prefixed requestAnimationFrame
+ * @returns {function} requestAnimationFrame
+ * @memberof Core
+ */
+export const requestAnimationFrame = (typeof window != "undefined") ? (window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame) : false;
+const ELEMENT_NODE = 1;
+const TEXT_NODE = 3;
+const COMMENT_NODE = 8;
+const DOCUMENT_NODE = 9;
+const _URL = typeof URL != "undefined" ? URL : (typeof window != "undefined" ? (window.URL || window.webkitURL || window.mozURL) : undefined);
+
+/**
+ * Generates a UUID
+ * based on: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
+ * @returns {string} uuid
+ * @memberof Core
+ */
+export function uuid() {
+ var d = new Date().getTime();
+ var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
+ var r = (d + Math.random()*16)%16 | 0;
+ d = Math.floor(d/16);
+ return (c=="x" ? r : (r&0x7|0x8)).toString(16);
+ });
+ return uuid;
+}
+
+/**
+ * Gets the height of a document
+ * @returns {number} height
+ * @memberof Core
+ */
+export function documentHeight() {
+ return Math.max(
+ document.documentElement.clientHeight,
+ document.body.scrollHeight,
+ document.documentElement.scrollHeight,
+ document.body.offsetHeight,
+ document.documentElement.offsetHeight
+ );
+}
+
+/**
+ * Checks if a node is an element
+ * @param {object} obj
+ * @returns {boolean}
+ * @memberof Core
+ */
+export function isElement(obj) {
+ return !!(obj && obj.nodeType == 1);
+}
+
+/**
+ * @param {any} n
+ * @returns {boolean}
+ * @memberof Core
+ */
+export function isNumber(n) {
+ return !isNaN(parseFloat(n)) && isFinite(n);
+}
+
+/**
+ * @param {any} n
+ * @returns {boolean}
+ * @memberof Core
+ */
+export function isFloat(n) {
+ let f = parseFloat(n);
+
+ if (isNumber(n) === false) {
+ return false;
+ }
+
+ if (typeof n === "string" && n.indexOf(".") > -1) {
+ return true;
+ }
+
+ return Math.floor(f) !== f;
+}
+
+/**
+ * Get a prefixed css property
+ * @param {string} unprefixed
+ * @returns {string}
+ * @memberof Core
+ */
+export function prefixed(unprefixed) {
+ var vendors = ["Webkit", "webkit", "Moz", "O", "ms" ];
+ var prefixes = ["-webkit-", "-webkit-", "-moz-", "-o-", "-ms-"];
+ var lower = unprefixed.toLowerCase();
+ var length = vendors.length;
+
+ if (typeof(document) === "undefined" || typeof(document.body.style[lower]) != "undefined") {
+ return unprefixed;
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (typeof(document.body.style[prefixes[i] + lower]) != "undefined") {
+ return prefixes[i] + lower;
+ }
+ }
+
+ return unprefixed;
+}
+
+/**
+ * Apply defaults to an object
+ * @param {object} obj
+ * @returns {object}
+ * @memberof Core
+ */
+export function defaults(obj) {
+ for (var i = 1, length = arguments.length; i < length; i++) {
+ var source = arguments[i];
+ for (var prop in source) {
+ if (obj[prop] === void 0) obj[prop] = source[prop];
+ }
+ }
+ return obj;
+}
+
+/**
+ * Extend properties of an object
+ * @param {object} target
+ * @returns {object}
+ * @memberof Core
+ */
+export function extend(target) {
+ var sources = [].slice.call(arguments, 1);
+ sources.forEach(function (source) {
+ if(!source) return;
+ Object.getOwnPropertyNames(source).forEach(function(propName) {
+ Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
+ });
+ });
+ return target;
+}
+
+/**
+ * Fast quicksort insert for sorted array -- based on:
+ * http://stackoverflow.com/questions/1344500/efficient-way-to-insert-a-number-into-a-sorted-array-of-numbers
+ * @param {any} item
+ * @param {array} array
+ * @param {function} [compareFunction]
+ * @returns {number} location (in array)
+ * @memberof Core
+ */
+export function insert(item, array, compareFunction) {
+ var location = locationOf(item, array, compareFunction);
+ array.splice(location, 0, item);
+
+ return location;
+}
+
+/**
+ * Finds where something would fit into a sorted array
+ * @param {any} item
+ * @param {array} array
+ * @param {function} [compareFunction]
+ * @param {function} [_start]
+ * @param {function} [_end]
+ * @returns {number} location (in array)
+ * @memberof Core
+ */
+export function locationOf(item, array, compareFunction, _start, _end) {
+ var start = _start || 0;
+ var end = _end || array.length;
+ var pivot = parseInt(start + (end - start) / 2);
+ var compared;
+ if(!compareFunction){
+ compareFunction = function(a, b) {
+ if(a > b) return 1;
+ if(a < b) return -1;
+ if(a == b) return 0;
+ };
+ }
+ if(end-start <= 0) {
+ return pivot;
+ }
+
+ compared = compareFunction(array[pivot], item);
+ if(end-start === 1) {
+ return compared >= 0 ? pivot : pivot + 1;
+ }
+ if(compared === 0) {
+ return pivot;
+ }
+ if(compared === -1) {
+ return locationOf(item, array, compareFunction, pivot, end);
+ } else{
+ return locationOf(item, array, compareFunction, start, pivot);
+ }
+}
+
+/**
+ * Finds index of something in a sorted array
+ * Returns -1 if not found
+ * @param {any} item
+ * @param {array} array
+ * @param {function} [compareFunction]
+ * @param {function} [_start]
+ * @param {function} [_end]
+ * @returns {number} index (in array) or -1
+ * @memberof Core
+ */
+export function indexOfSorted(item, array, compareFunction, _start, _end) {
+ var start = _start || 0;
+ var end = _end || array.length;
+ var pivot = parseInt(start + (end - start) / 2);
+ var compared;
+ if(!compareFunction){
+ compareFunction = function(a, b) {
+ if(a > b) return 1;
+ if(a < b) return -1;
+ if(a == b) return 0;
+ };
+ }
+ if(end-start <= 0) {
+ return -1; // Not found
+ }
+
+ compared = compareFunction(array[pivot], item);
+ if(end-start === 1) {
+ return compared === 0 ? pivot : -1;
+ }
+ if(compared === 0) {
+ return pivot; // Found
+ }
+ if(compared === -1) {
+ return indexOfSorted(item, array, compareFunction, pivot, end);
+ } else{
+ return indexOfSorted(item, array, compareFunction, start, pivot);
+ }
+}
+/**
+ * Find the bounds of an element
+ * taking padding and margin into account
+ * @param {element} el
+ * @returns {{ width: Number, height: Number}}
+ * @memberof Core
+ */
+export function bounds(el) {
+
+ var style = window.getComputedStyle(el);
+ var widthProps = ["width", "paddingRight", "paddingLeft", "marginRight", "marginLeft", "borderRightWidth", "borderLeftWidth"];
+ var heightProps = ["height", "paddingTop", "paddingBottom", "marginTop", "marginBottom", "borderTopWidth", "borderBottomWidth"];
+
+ var width = 0;
+ var height = 0;
+
+ widthProps.forEach(function(prop){
+ width += parseFloat(style[prop]) || 0;
+ });
+
+ heightProps.forEach(function(prop){
+ height += parseFloat(style[prop]) || 0;
+ });
+
+ return {
+ height: height,
+ width: width
+ };
+
+}
+
+/**
+ * Find the bounds of an element
+ * taking padding, margin and borders into account
+ * @param {element} el
+ * @returns {{ width: Number, height: Number}}
+ * @memberof Core
+ */
+export function borders(el) {
+
+ var style = window.getComputedStyle(el);
+ var widthProps = ["paddingRight", "paddingLeft", "marginRight", "marginLeft", "borderRightWidth", "borderLeftWidth"];
+ var heightProps = ["paddingTop", "paddingBottom", "marginTop", "marginBottom", "borderTopWidth", "borderBottomWidth"];
+
+ var width = 0;
+ var height = 0;
+
+ widthProps.forEach(function(prop){
+ width += parseFloat(style[prop]) || 0;
+ });
+
+ heightProps.forEach(function(prop){
+ height += parseFloat(style[prop]) || 0;
+ });
+
+ return {
+ height: height,
+ width: width
+ };
+
+}
+
+/**
+ * Find the bounds of any node
+ * allows for getting bounds of text nodes by wrapping them in a range
+ * @param {node} node
+ * @returns {BoundingClientRect}
+ * @memberof Core
+ */
+export function nodeBounds(node) {
+ let elPos;
+ let doc = node.ownerDocument;
+ if(node.nodeType == Node.TEXT_NODE){
+ let elRange = doc.createRange();
+ elRange.selectNodeContents(node);
+ elPos = elRange.getBoundingClientRect();
+ } else {
+ elPos = node.getBoundingClientRect();
+ }
+ return elPos;
+}
+
+/**
+ * Find the equivelent of getBoundingClientRect of a browser window
+ * @returns {{ width: Number, height: Number, top: Number, left: Number, right: Number, bottom: Number }}
+ * @memberof Core
+ */
+export function windowBounds() {
+
+ var width = window.innerWidth;
+ var height = window.innerHeight;
+
+ return {
+ top: 0,
+ left: 0,
+ right: width,
+ bottom: height,
+ width: width,
+ height: height
+ };
+
+}
+
+/**
+ * Gets the index of a node in its parent
+ * @param {Node} node
+ * @param {string} typeId
+ * @return {number} index
+ * @memberof Core
+ */
+export function indexOfNode(node, typeId) {
+ var parent = node.parentNode;
+ var children = parent.childNodes;
+ var sib;
+ var index = -1;
+ for (var i = 0; i < children.length; i++) {
+ sib = children[i];
+ if (sib.nodeType === typeId) {
+ index++;
+ }
+ if (sib == node) break;
+ }
+
+ return index;
+}
+
+/**
+ * Gets the index of a text node in its parent
+ * @param {node} textNode
+ * @returns {number} index
+ * @memberof Core
+ */
+export function indexOfTextNode(textNode) {
+ return indexOfNode(textNode, TEXT_NODE);
+}
+
+/**
+ * Gets the index of an element node in its parent
+ * @param {element} elementNode
+ * @returns {number} index
+ * @memberof Core
+ */
+export function indexOfElementNode(elementNode) {
+ return indexOfNode(elementNode, ELEMENT_NODE);
+}
+
+/**
+ * Check if extension is xml
+ * @param {string} ext
+ * @returns {boolean}
+ * @memberof Core
+ */
+export function isXml(ext) {
+ return ["xml", "opf", "ncx"].indexOf(ext) > -1;
+}
+
+/**
+ * Create a new blob
+ * @param {any} content
+ * @param {string} mime
+ * @returns {Blob}
+ * @memberof Core
+ */
+export function createBlob(content, mime){
+ return new Blob([content], {type : mime });
+}
+
+/**
+ * Create a new blob url
+ * @param {any} content
+ * @param {string} mime
+ * @returns {string} url
+ * @memberof Core
+ */
+export function createBlobUrl(content, mime){
+ var tempUrl;
+ var blob = createBlob(content, mime);
+
+ tempUrl = _URL.createObjectURL(blob);
+
+ return tempUrl;
+}
+
+/**
+ * Remove a blob url
+ * @param {string} url
+ * @memberof Core
+ */
+export function revokeBlobUrl(url){
+ return _URL.revokeObjectURL(url);
+}
+
+/**
+ * Create a new base64 encoded url
+ * @param {any} content
+ * @param {string} mime
+ * @returns {string} url
+ * @memberof Core
+ */
+export function createBase64Url(content, mime){
+ var data;
+ var datauri;
+
+ if (typeof(content) !== "string") {
+ // Only handles strings
+ return;
+ }
+
+ data = btoa(encodeURIComponent(content));
+
+ datauri = "data:" + mime + ";base64," + data;
+
+ return datauri;
+}
+
+/**
+ * Get type of an object
+ * @param {object} obj
+ * @returns {string} type
+ * @memberof Core
+ */
+export function type(obj){
+ return Object.prototype.toString.call(obj).slice(8, -1);
+}
+
+/**
+ * Parse xml (or html) markup
+ * @param {string} markup
+ * @param {string} mime
+ * @param {boolean} forceXMLDom force using xmlDom to parse instead of native parser
+ * @returns {document} document
+ * @memberof Core
+ */
+export function parse(markup, mime, forceXMLDom) {
+ var doc;
+ var Parser;
+
+ if (typeof DOMParser === "undefined" || forceXMLDom) {
+ Parser = XMLDOMParser;
+ } else {
+ Parser = DOMParser;
+ }
+
+ // Remove byte order mark before parsing
+ // https://www.w3.org/International/questions/qa-byte-order-mark
+ if(markup.charCodeAt(0) === 0xFEFF) {
+ markup = markup.slice(1);
+ }
+
+ doc = new Parser().parseFromString(markup, mime);
+
+ return doc;
+}
+
+/**
+ * querySelector polyfill
+ * @param {element} el
+ * @param {string} sel selector string
+ * @returns {element} element
+ * @memberof Core
+ */
+export function qs(el, sel) {
+ var elements;
+ if (!el) {
+ throw new Error("No Element Provided");
+ }
+
+ if (typeof el.querySelector != "undefined") {
+ return el.querySelector(sel);
+ } else {
+ elements = el.getElementsByTagName(sel);
+ if (elements.length) {
+ return elements[0];
+ }
+ }
+}
+
+/**
+ * querySelectorAll polyfill
+ * @param {element} el
+ * @param {string} sel selector string
+ * @returns {element[]} elements
+ * @memberof Core
+ */
+export function qsa(el, sel) {
+
+ if (typeof el.querySelector != "undefined") {
+ return el.querySelectorAll(sel);
+ } else {
+ return el.getElementsByTagName(sel);
+ }
+}
+
+/**
+ * querySelector by property
+ * @param {element} el
+ * @param {string} sel selector string
+ * @param {object[]} props
+ * @returns {element[]} elements
+ * @memberof Core
+ */
+export function qsp(el, sel, props) {
+ var q, filtered;
+ if (typeof el.querySelector != "undefined") {
+ sel += "[";
+ for (var prop in props) {
+ sel += prop + "~='" + props[prop] + "'";
+ }
+ sel += "]";
+ return el.querySelector(sel);
+ } else {
+ q = el.getElementsByTagName(sel);
+ filtered = Array.prototype.slice.call(q, 0).filter(function(el) {
+ for (var prop in props) {
+ if(el.getAttribute(prop) === props[prop]){
+ return true;
+ }
+ }
+ return false;
+ });
+
+ if (filtered) {
+ return filtered[0];
+ }
+ }
+}
+
+/**
+ * Sprint through all text nodes in a document
+ * @memberof Core
+ * @param {element} root element to start with
+ * @param {function} func function to run on each element
+ */
+export function sprint(root, func) {
+ var doc = root.ownerDocument || root;
+ if (typeof(doc.createTreeWalker) !== "undefined") {
+ treeWalker(root, func, NodeFilter.SHOW_TEXT);
+ } else {
+ walk(root, function(node) {
+ if (node && node.nodeType === 3) { // Node.TEXT_NODE
+ func(node);
+ }
+ }, true);
+ }
+}
+
+/**
+ * Create a treeWalker
+ * @memberof Core
+ * @param {element} root element to start with
+ * @param {function} func function to run on each element
+ * @param {function | object} filter funtion or object to filter with
+ */
+export function treeWalker(root, func, filter) {
+ var treeWalker = document.createTreeWalker(root, filter, null, false);
+ let node;
+ while ((node = treeWalker.nextNode())) {
+ func(node);
+ }
+}
+
+/**
+ * @memberof Core
+ * @param {node} node
+ * @param {callback} return false for continue,true for break inside callback
+ */
+export function walk(node,callback){
+ if(callback(node)){
+ return true;
+ }
+ node = node.firstChild;
+ if(node){
+ do{
+ let walked = walk(node,callback);
+ if(walked){
+ return true;
+ }
+ node = node.nextSibling;
+ } while(node);
+ }
+}
+
+/**
+ * Convert a blob to a base64 encoded string
+ * @param {Blog} blob
+ * @returns {string}
+ * @memberof Core
+ */
+export function blob2base64(blob) {
+ return new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.readAsDataURL(blob);
+ reader.onloadend = function() {
+ resolve(reader.result);
+ };
+ });
+}
+
+
+/**
+ * Creates a new pending promise and provides methods to resolve or reject it.
+ * From: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred#backwards_forwards_compatible
+ * @memberof Core
+ */
+export function defer() {
+ /* A method to resolve the associated Promise with the value passed.
+ * If the promise is already settled it does nothing.
+ *
+ * @param {anything} value : This value is used to resolve the promise
+ * If the value is a Promise then the associated promise assumes the state
+ * of Promise passed as value.
+ */
+ this.resolve = null;
+
+ /* A method to reject the assocaited Promise with the value passed.
+ * If the promise is already settled it does nothing.
+ *
+ * @param {anything} reason: The reason for the rejection of the Promise.
+ * Generally its an Error object. If however a Promise is passed, then the Promise
+ * itself will be the reason for rejection no matter the state of the Promise.
+ */
+ this.reject = null;
+
+ this.id = uuid();
+
+ /* A newly created Pomise object.
+ * Initially in pending state.
+ */
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ Object.freeze(this);
+}
+
+/**
+ * querySelector with filter by epub type
+ * @param {element} html
+ * @param {string} element element type to find
+ * @param {string} type epub type to find
+ * @returns {element[]} elements
+ * @memberof Core
+ */
+export function querySelectorByType(html, element, type){
+ var query;
+ if (typeof html.querySelector != "undefined") {
+ query = html.querySelector(`${element}[*|type="${type}"]`);
+ }
+ // Handle IE not supporting namespaced epub:type in querySelector
+ if(!query || query.length === 0) {
+ query = qsa(html, element);
+ for (var i = 0; i < query.length; i++) {
+ if(query[i].getAttributeNS("http://www.idpf.org/2007/ops", "type") === type ||
+ query[i].getAttribute("epub:type") === type) {
+ return query[i];
+ }
+ }
+ } else {
+ return query;
+ }
+}
+
+/**
+ * Find direct decendents of an element
+ * @param {element} el
+ * @returns {element[]} children
+ * @memberof Core
+ */
+export function findChildren(el) {
+ var result = [];
+ var childNodes = el.childNodes;
+ for (var i = 0; i < childNodes.length; i++) {
+ let node = childNodes[i];
+ if (node.nodeType === 1) {
+ result.push(node);
+ }
+ }
+ return result;
+}
+
+/**
+ * Find all parents (ancestors) of an element
+ * @param {element} node
+ * @returns {element[]} parents
+ * @memberof Core
+ */
+export function parents(node) {
+ var nodes = [node];
+ for (; node; node = node.parentNode) {
+ nodes.unshift(node);
+ }
+ return nodes
+}
+
+/**
+ * Find all direct decendents of a specific type
+ * @param {element} el
+ * @param {string} nodeName
+ * @param {boolean} [single]
+ * @returns {element[]} children
+ * @memberof Core
+ */
+export function filterChildren(el, nodeName, single) {
+ var result = [];
+ var childNodes = el.childNodes;
+ for (var i = 0; i < childNodes.length; i++) {
+ let node = childNodes[i];
+ if (node.nodeType === 1 && node.nodeName.toLowerCase() === nodeName) {
+ if (single) {
+ return node;
+ } else {
+ result.push(node);
+ }
+ }
+ }
+ if (!single) {
+ return result;
+ }
+}
+
+/**
+ * Filter all parents (ancestors) with tag name
+ * @param {element} node
+ * @param {string} tagname
+ * @returns {element[]} parents
+ * @memberof Core
+ */
+export function getParentByTagName(node, tagname) {
+ let parent;
+ if (node === null || tagname === '') return;
+ parent = node.parentNode;
+ while (parent.nodeType === 1) {
+ if (parent.tagName.toLowerCase() === tagname) {
+ return parent;
+ }
+ parent = parent.parentNode;
+ }
+}
+
+/**
+ * Lightweight Polyfill for DOM Range
+ * @class
+ * @memberof Core
+ */
+export class RangeObject {
+ constructor() {
+ this.collapsed = false;
+ this.commonAncestorContainer = undefined;
+ this.endContainer = undefined;
+ this.endOffset = undefined;
+ this.startContainer = undefined;
+ this.startOffset = undefined;
+ }
+
+ setStart(startNode, startOffset) {
+ this.startContainer = startNode;
+ this.startOffset = startOffset;
+
+ if (!this.endContainer) {
+ this.collapse(true);
+ } else {
+ this.commonAncestorContainer = this._commonAncestorContainer();
+ }
+
+ this._checkCollapsed();
+ }
+
+ setEnd(endNode, endOffset) {
+ this.endContainer = endNode;
+ this.endOffset = endOffset;
+
+ if (!this.startContainer) {
+ this.collapse(false);
+ } else {
+ this.collapsed = false;
+ this.commonAncestorContainer = this._commonAncestorContainer();
+ }
+
+ this._checkCollapsed();
+ }
+
+ collapse(toStart) {
+ this.collapsed = true;
+ if (toStart) {
+ this.endContainer = this.startContainer;
+ this.endOffset = this.startOffset;
+ this.commonAncestorContainer = this.startContainer.parentNode;
+ } else {
+ this.startContainer = this.endContainer;
+ this.startOffset = this.endOffset;
+ this.commonAncestorContainer = this.endOffset.parentNode;
+ }
+ }
+
+ selectNode(referenceNode) {
+ let parent = referenceNode.parentNode;
+ let index = Array.prototype.indexOf.call(parent.childNodes, referenceNode);
+ this.setStart(parent, index);
+ this.setEnd(parent, index + 1);
+ }
+
+ selectNodeContents(referenceNode) {
+ let end = referenceNode.childNodes[referenceNode.childNodes - 1];
+ let endIndex = (referenceNode.nodeType === 3) ?
+ referenceNode.textContent.length : parent.childNodes.length;
+ this.setStart(referenceNode, 0);
+ this.setEnd(referenceNode, endIndex);
+ }
+
+ _commonAncestorContainer(startContainer, endContainer) {
+ var startParents = parents(startContainer || this.startContainer);
+ var endParents = parents(endContainer || this.endContainer);
+
+ if (startParents[0] != endParents[0]) return undefined;
+
+ for (var i = 0; i < startParents.length; i++) {
+ if (startParents[i] != endParents[i]) {
+ return startParents[i - 1];
+ }
+ }
+ }
+
+ _checkCollapsed() {
+ if (this.startContainer === this.endContainer &&
+ this.startOffset === this.endOffset) {
+ this.collapsed = true;
+ } else {
+ this.collapsed = false;
+ }
+ }
+
+ toString() {
+ // TODO: implement walking between start and end to find text
+ }
+}