From 7c7c554ee7acfbcb4a69526373c68be0e9bbe63c Mon Sep 17 00:00:00 2001 From: Valdrin Trena Date: Sat, 8 Jun 2024 20:14:31 +0200 Subject: [PATCH] Lint --- .babelrc.json | 21 +- .eslintrc.js | 64 +- .gitignore | 2 +- .jshintrc | 7 +- .watchmanconfig | 5 +- README.md | 47 +- bower.json | 14 +- karma.conf.js | 102 +- license | 8 +- src/annotations.js | 472 +++--- src/archive.js | 426 +++-- src/book.js | 1474 ++++++++--------- src/container.js | 64 +- src/contents.js | 2527 +++++++++++++++--------------- src/displayoptions.js | 104 +- src/epub.js | 16 +- src/epubcfi.js | 1972 +++++++++++------------ src/index.js | 14 +- src/layout.js | 432 ++--- src/locations.js | 995 ++++++------ src/managers/continuous/index.js | 1187 +++++++------- src/managers/default/index.js | 2015 ++++++++++++------------ src/managers/helpers/snap.js | 664 ++++---- src/managers/helpers/stage.js | 690 ++++---- src/managers/helpers/views.js | 264 ++-- src/managers/views/iframe.js | 1686 ++++++++++---------- src/managers/views/inline.js | 722 ++++----- src/mapping.js | 949 ++++++----- src/navigation.js | 590 +++---- src/packaging.js | 619 ++++---- src/pagelist.js | 454 +++--- src/rendition.js | 2063 ++++++++++++------------ src/resources.js | 537 +++---- src/section.js | 580 +++---- src/spine.js | 450 +++--- src/store.js | 628 ++++---- src/themes.js | 468 +++--- src/utils/constants.js | 107 +- src/utils/core.js | 952 +++++------ src/utils/hook.js | 127 +- src/utils/mime.js | 317 ++-- src/utils/path.js | 149 +- src/utils/queue.js | 380 +++-- src/utils/replacements.js | 205 ++- src/utils/request.js | 224 ++- src/utils/scrolltype.js | 80 +- src/utils/url.js | 169 +- test/old/epub.js | 371 ----- test/old/rendering.js | 16 - types/annotations.d.ts | 67 +- types/archive.d.ts | 20 +- types/book.d.ts | 190 +-- types/container.d.ts | 1 - types/contents.d.ts | 224 ++- types/core.d.ts | 116 +- types/epub.d.ts | 7 +- types/epubcfi.d.ts | 163 +- types/epubjs-tests.ts | 9 - types/index.d.ts | 16 +- types/layout.d.ts | 43 +- types/locations.d.ts | 26 +- types/managers/manager.d.ts | 99 +- types/managers/view.d.ts | 69 +- types/mapping.d.ts | 29 +- types/navigation.d.ts | 37 +- types/packaging.d.ts | 76 +- types/pagelist.d.ts | 16 +- types/rendition.d.ts | 236 ++- types/resources.d.ts | 32 +- types/section.d.ts | 42 +- types/spine.d.ts | 14 +- types/store.d.ts | 31 +- types/themes.d.ts | 43 +- types/tsconfig.json | 36 +- types/utils/constants.d.ts | 6 +- types/utils/core.d.ts | 114 +- types/utils/hook.d.ts | 6 +- types/utils/path.d.ts | 6 - types/utils/queue.d.ts | 18 +- types/utils/replacements.d.ts | 12 +- types/utils/request.d.ts | 7 +- types/utils/scrolltype.d.ts | 1 - types/utils/url.d.ts | 3 - webpack.config.js | 145 +- 84 files changed, 13879 insertions(+), 14510 deletions(-) delete mode 100644 test/old/epub.js delete mode 100644 test/old/rendering.js delete mode 100644 types/epubjs-tests.ts diff --git a/.babelrc.json b/.babelrc.json index 9c7817a..d3e0d77 100644 --- a/.babelrc.json +++ b/.babelrc.json @@ -1,11 +1,14 @@ { - "presets": [ - ["@babel/preset-env", { - "targets": "last 2 Chrome versions, last 2 Safari versions, last 2 ChromeAndroid versions, last 2 iOS versions, last 2 Firefox versions, last 2 Edge versions", - "corejs": 3, - "useBuiltIns": "usage", - "bugfixes": true, - "modules": "auto" - }] - ], + "presets": [ + [ + "@babel/preset-env", + { + "targets": "last 2 Chrome versions, last 2 Safari versions, last 2 ChromeAndroid versions, last 2 iOS versions, last 2 Firefox versions, last 2 Edge versions", + "corejs": 3, + "useBuiltIns": "usage", + "bugfixes": true, + "modules": "auto" + } + ] + ] } diff --git a/.eslintrc.js b/.eslintrc.js index 1bfd1ae..d696055 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,43 +1,25 @@ module.exports = { - "env": { - "browser": true, - "commonjs": true, - "es6": true, - "node": true - }, - "globals": { - "ePub": true, - "JSZip": true - }, - "extends": "eslint:recommended", - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "indent": [ - "error", - "tab", - { "VariableDeclarator": { "var": 2, "let": 2, "const": 3 } } - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "warn", - "double" - ], - "semi": [ - "error", - "always" - ], - "no-unused-vars" : ["warn"], - "no-console" : ["warn"], - "no-unused-vars": [ - "error", - { "vars": "all", "args": "none" } - ], - "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], - "valid-jsdoc": ["warn"] - } + env: { + browser: true, + commonjs: true, + es6: true, + node: true, + }, + globals: { + ePub: true, + JSZip: true, + }, + extends: "eslint:recommended", + parserOptions: { + sourceType: "module", + }, + rules: { + "linebreak-style": ["error", "unix"], + quotes: ["warn", "double"], + semi: ["error", "always"], + "no-console": ["warn"], + "no-unused-vars": ["error", { vars: "all", args: "none" }], + "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], + "no-prototype-builtins": "off", + }, }; diff --git a/.gitignore b/.gitignore index 689df6c..b3bfabd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ books lib dist documentation/html -types/*.js \ No newline at end of file +types/*.js diff --git a/.jshintrc b/.jshintrc index c2b6f01..ce21da1 100644 --- a/.jshintrc +++ b/.jshintrc @@ -2,10 +2,8 @@ "browser": true, "devel": true, "worker": true, - "trailing": true, "strict": false, - "boss": true, "funcscope": true, "globalstrict": true, @@ -14,10 +12,9 @@ "nonstandard": true, "sub": true, "validthis": true, - "globals": { "_": false, - "define" : false, - "module" : false + "define": false, + "module": false } } diff --git a/.watchmanconfig b/.watchmanconfig index d92d987..1582bdb 100644 --- a/.watchmanconfig +++ b/.watchmanconfig @@ -1,6 +1,3 @@ { - "ignore_dirs": [ - ".git", - "node_modules" - ] + "ignore_dirs": [".git", "node_modules"] } diff --git a/README.md b/README.md index 2953f73..6c0152a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Create the new ePub, and then render it to that element: ```html ``` @@ -65,6 +65,7 @@ The default manager only displays a single section at a time. ```js book.renderTo("area", { method: "continuous", width: "100%", height: "100%" }); ``` + [View example](http://futurepress.github.io/epub.js/examples/continuous-scrolled.html) The continuous manager will display as many sections as need to fill the screen, and preload the next section offscreen. This enables seamless swiping / scrolling between pages on mobile and desktop, but is less performant than the default method. @@ -72,6 +73,7 @@ The continuous manager will display as many sections as need to fill the screen, ## Flow Overrides ### Auto (Default) + `book.renderTo("area", { flow: "auto", width: "900", height: "600" });` Flow will be based on the settings in the OPF, defaults to `paginated`. @@ -90,7 +92,7 @@ Scrolled: `book.renderTo("area", { flow: "scrolled-doc" });` ## Scripted Content -[Scripted content](https://www.w3.org/TR/epub-33/#sec-scripted-content), JavasScript the ePub HTML content, is disabled by default due to the potential for executing malicious content. +[Scripted content](https://www.w3.org/TR/epub-33/#sec-scripted-content), JavasScript the ePub HTML content, is disabled by default due to the potential for executing malicious content. This is done by sandboxing the iframe the content is rendered into, though it is still recommended to sanitize the ePub content server-side as well. @@ -101,7 +103,7 @@ If a trusted ePub contains interactivity, it can be enabled by passing `allowScr var rendition = book.renderTo("area", { width: 600, height: 400, - allowScriptedContent: true + allowScriptedContent: true, }); ``` @@ -132,11 +134,11 @@ npm start ## Examples -+ [Spreads](http://futurepress.github.io/epub.js/examples/spreads.html) -+ [Scrolled](http://futurepress.github.io/epub.js/examples/scrolled.html) -+ [Swipe](http://futurepress.github.io/epub.js/examples/swipe.html) -+ [Input](http://futurepress.github.io/epub.js/examples/input.html) -+ [Highlights](http://futurepress.github.io/epub.js/examples/highlights.html) +- [Spreads](http://futurepress.github.io/epub.js/examples/spreads.html) +- [Scrolled](http://futurepress.github.io/epub.js/examples/scrolled.html) +- [Swipe](http://futurepress.github.io/epub.js/examples/swipe.html) +- [Input](http://futurepress.github.io/epub.js/examples/input.html) +- [Highlights](http://futurepress.github.io/epub.js/examples/highlights.html) [View All Examples](http://futurepress.github.io/epub.js/examples/) @@ -175,29 +177,28 @@ Hooks require an event to register to and a can return a promise to block until Example hook: ```javascript -rendition.hooks.content.register(function(contents, view) { +rendition.hooks.content.register(function (contents, view) { + var elements = contents.document.querySelectorAll("[video]"); + var items = Array.prototype.slice.call(elements); - var elements = contents.document.querySelectorAll('[video]'); - var items = Array.prototype.slice.call(elements); - - items.forEach(function(item){ - // do something with the video item - }); - -}) + items.forEach(function (item) { + // do something with the video item + }); +}); ``` The parts of the rendering process that can be hooked into are below. ```js -book.spine.hooks.serialize // Section is being converted to text -book.spine.hooks.content // Section has been loaded and parsed -rendition.hooks.render // Section is rendered to the screen -rendition.hooks.content // Section contents have been loaded -rendition.hooks.unloaded // Section contents are being unloaded +book.spine.hooks.serialize; // Section is being converted to text +book.spine.hooks.content; // Section has been loaded and parsed +rendition.hooks.render; // Section is rendered to the screen +rendition.hooks.content; // Section contents have been loaded +rendition.hooks.unloaded; // Section contents are being unloaded ``` ## Reader + The reader has moved to its own repo at: https://github.com/futurepress/epubjs-reader/ ## Additional Resources @@ -210,7 +211,7 @@ IRC Server: freenode.net Channel: #epub.js Follow us on twitter: @Epubjs -+ http://twitter.com/#!/Epubjs +- http://twitter.com/#!/Epubjs ## Other diff --git a/bower.json b/bower.json index b044630..01bb5fd 100644 --- a/bower.json +++ b/bower.json @@ -1,19 +1,11 @@ { "name": "epubjs", "version": "0.3.0", - "authors": [ - "Fred Chasen " - ], + "authors": ["Fred Chasen "], "description": "Enhanced eBooks in the browser.", "main": "dist/epub.js", - "moduleType": [ - "amd", - "globals", - "node" - ], - "keywords": [ - "epub" - ], + "moduleType": ["amd", "globals", "node"], + "keywords": ["epub"], "license": "MIT", "homepage": "http://futurepress.org", "ignore": [ diff --git a/karma.conf.js b/karma.conf.js index fa83072..0f03ecc 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,115 +3,75 @@ webpackConfig.mode = "development"; webpackConfig.externals = {}; webpackConfig.module.rules.push({ test: /\.xhtml$/i, - use: 'raw-loader', + use: "raw-loader", }); -module.exports = function(config) { +module.exports = function (config) { config.set({ - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - + basePath: "", // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'], - + frameworks: ["mocha"], // list of files / patterns to load in the browser files: [ + { pattern: "src/*.js", watched: true, included: false, served: false }, - {pattern: 'src/*.js', watched: true, included: false, served: false}, + { pattern: "test/*.js", watched: false }, - {pattern: 'test/*.js', watched: false}, - - {pattern: 'test/fixtures/**/*', watched: false, included: false, served: true}, + { + pattern: "test/fixtures/**/*", + watched: false, + included: false, + served: true, + }, // {pattern: 'node_modules/jszip/dist/jszip.js', watched: false, included: true, served: true}, // {pattern: 'node_modules/es6-promise/dist/es6-promise.auto.js', watched: false, included: true, served: true}, // {pattern: 'libs/url/url-polyfill.js', watched: false, included: true, served: true} - ], // list of files to exclude - exclude: [ - ], - + exclude: [], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { // add webpack as preprocessor - 'test/*.js': ['webpack', 'sourcemap'], - // 'test/**/*.js': ['webpack', 'sourcemap'] + "test/*.js": ["webpack", "sourcemap"], }, - webpack: webpackConfig, - - // { - // mode: "development", - // externals: { - // "jszip": "JSZip" - // // "xmldom": "xmldom" - // }, - // devtool: 'inline-source-map', - // resolve: { - // alias: { - // path: "path-webpack" - // } - // }, - // module: { - // rules: [ - // { - // test: /\.js$/, - // exclude: /node_modules/, - // loader: "babel-loader", - // query: { - // presets: [["@babel/preset-env", { - // targets: "defaults", - // }]], - // } - // }, - // { - // test: /\.xhtml$/i, - // use: 'raw-loader', - // } - // ] - // } - // }, + webpack: webpackConfig, webpackMiddleware: { - stats: 'errors-only' + stats: "errors-only", }, // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['mocha'], + reporters: ["mocha"], // web server port port: 9876, - // enable / disable colors in the output (reporters and logs) colors: true, - // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, - // enable / disable watching file and executing tests whenever any file changes autoWatch: true, - // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['ChromeHeadless', 'ChromeHeadlessNoSandbox'], - + browsers: ["ChromeHeadless", "ChromeHeadlessNoSandbox"], // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits @@ -121,27 +81,19 @@ module.exports = function(config) { // how many browser should be started simultaneous concurrency: Infinity, - proxies: { - "/fixtures/": "/base/test/fixtures/" - }, + proxies: { "/fixtures/": "/base/test/fixtures/" }, client: { - config: { - browserConsoleLogOptions: true - }, + config: { browserConsoleLogOptions: true }, captureConsole: true, - mocha: { - reporter: 'html' - // bail: true - } + mocha: { reporter: "html" }, }, customLaunchers: { ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - } - } - - }) -} + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, + }); +}; diff --git a/license b/license index f294d33..2c18bf6 100644 --- a/license +++ b/license @@ -3,13 +3,13 @@ Copyright (c) 2013, FuturePress All rights reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. + list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. + and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -23,5 +23,5 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those -of the authors and should not be interpreted as representing official policies, +of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. diff --git a/src/annotations.js b/src/annotations.js index ecbcb56..a8b8a33 100644 --- a/src/annotations.js +++ b/src/annotations.js @@ -3,200 +3,195 @@ import EpubCFI from "./epubcfi"; import { EVENTS } from "./utils/constants"; /** - * Handles managing adding & removing Annotations - * @param {Rendition} rendition - * @class - */ + * Handles managing adding & removing Annotations + * @param {Rendition} rendition + * @class + */ class Annotations { + constructor(rendition) { + this.rendition = rendition; + this.highlights = []; + this.underlines = []; + this.marks = []; + this._annotations = {}; + this._annotationsBySectionIndex = {}; - constructor (rendition) { - this.rendition = rendition; - this.highlights = []; - this.underlines = []; - this.marks = []; - this._annotations = {}; - this._annotationsBySectionIndex = {}; + this.rendition.hooks.render.register(this.inject.bind(this)); + this.rendition.hooks.unloaded.register(this.clear.bind(this)); + } - this.rendition.hooks.render.register(this.inject.bind(this)); - this.rendition.hooks.unloaded.register(this.clear.bind(this)); - } + /** + * Add an annotation to store + * @param {string} type Type of annotation to add: "highlight", "underline", "mark" + * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to + * @param {object} data Data to assign to annotation + * @param {function} [cb] Callback after annotation is added + * @param {string} className CSS class to assign to annotation + * @param {object} styles CSS styles to assign to annotation + * @returns {Annotation} annotation + */ + add(type, cfiRange, data, cb, className, styles) { + let hash = encodeURI(cfiRange + type); + let cfi = new EpubCFI(cfiRange); + let sectionIndex = cfi.spinePos; + let annotation = new Annotation({ + type, + cfiRange, + data, + sectionIndex, + cb, + className, + styles, + }); - /** - * Add an annotation to store - * @param {string} type Type of annotation to add: "highlight", "underline", "mark" - * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to - * @param {object} data Data to assign to annotation - * @param {function} [cb] Callback after annotation is added - * @param {string} className CSS class to assign to annotation - * @param {object} styles CSS styles to assign to annotation - * @returns {Annotation} annotation - */ - add (type, cfiRange, data, cb, className, styles) { - let hash = encodeURI(cfiRange + type); - let cfi = new EpubCFI(cfiRange); - let sectionIndex = cfi.spinePos; - let annotation = new Annotation({ - type, - cfiRange, - data, - sectionIndex, - cb, - className, - styles - }); + this._annotations[hash] = annotation; - this._annotations[hash] = annotation; + if (sectionIndex in this._annotationsBySectionIndex) { + this._annotationsBySectionIndex[sectionIndex].push(hash); + } else { + this._annotationsBySectionIndex[sectionIndex] = [hash]; + } - if (sectionIndex in this._annotationsBySectionIndex) { - this._annotationsBySectionIndex[sectionIndex].push(hash); - } else { - this._annotationsBySectionIndex[sectionIndex] = [hash]; - } + let views = this.rendition.views(); - let views = this.rendition.views(); + views.forEach((view) => { + if (annotation.sectionIndex === view.index) { + annotation.attach(view); + } + }); - views.forEach( (view) => { - if (annotation.sectionIndex === view.index) { - annotation.attach(view); - } - }); + return annotation; + } - return annotation; - } + /** + * Remove an annotation from store + * @param {EpubCFI} cfiRange EpubCFI range the annotation is attached to + * @param {string} type Type of annotation to add: "highlight", "underline", "mark" + */ + remove(cfiRange, type) { + let hash = encodeURI(cfiRange + type); - /** - * Remove an annotation from store - * @param {EpubCFI} cfiRange EpubCFI range the annotation is attached to - * @param {string} type Type of annotation to add: "highlight", "underline", "mark" - */ - remove (cfiRange, type) { - let hash = encodeURI(cfiRange + type); + if (hash in this._annotations) { + let annotation = this._annotations[hash]; - if (hash in this._annotations) { - let annotation = this._annotations[hash]; + if (type && annotation.type !== type) { + return; + } - if (type && annotation.type !== type) { - return; - } + let views = this.rendition.views(); + views.forEach((view) => { + this._removeFromAnnotationBySectionIndex(annotation.sectionIndex, hash); + if (annotation.sectionIndex === view.index) { + annotation.detach(view); + } + }); - let views = this.rendition.views(); - views.forEach( (view) => { - this._removeFromAnnotationBySectionIndex(annotation.sectionIndex, hash); - if (annotation.sectionIndex === view.index) { - annotation.detach(view); - } - }); + delete this._annotations[hash]; + } + } - delete this._annotations[hash]; - } - } + /** + * Remove an annotations by Section Index + * @private + */ + _removeFromAnnotationBySectionIndex(sectionIndex, hash) { + this._annotationsBySectionIndex[sectionIndex] = this._annotationsAt( + sectionIndex + ).filter((h) => h !== hash); + } - /** - * Remove an annotations by Section Index - * @private - */ - _removeFromAnnotationBySectionIndex (sectionIndex, hash) { - this._annotationsBySectionIndex[sectionIndex] = this._annotationsAt(sectionIndex).filter(h => h !== hash); - } + /** + * Get annotations by Section Index + * @private + */ + _annotationsAt(index) { + return this._annotationsBySectionIndex[index]; + } - /** - * Get annotations by Section Index - * @private - */ - _annotationsAt (index) { - return this._annotationsBySectionIndex[index]; - } + /** + * Add a highlight to the store + * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to + * @param {object} data Data to assign to annotation + * @param {function} cb Callback after annotation is clicked + * @param {string} className CSS class to assign to annotation + * @param {object} styles CSS styles to assign to annotation + */ + highlight(cfiRange, data, cb, className, styles) { + return this.add("highlight", cfiRange, data, cb, className, styles); + } + /** + * Add a underline to the store + * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to + * @param {object} data Data to assign to annotation + * @param {function} cb Callback after annotation is clicked + * @param {string} className CSS class to assign to annotation + * @param {object} styles CSS styles to assign to annotation + */ + underline(cfiRange, data, cb, className, styles) { + return this.add("underline", cfiRange, data, cb, className, styles); + } - /** - * Add a highlight to the store - * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to - * @param {object} data Data to assign to annotation - * @param {function} cb Callback after annotation is clicked - * @param {string} className CSS class to assign to annotation - * @param {object} styles CSS styles to assign to annotation - */ - highlight (cfiRange, data, cb, className, styles) { - return this.add("highlight", cfiRange, data, cb, className, styles); - } + /** + * Add a mark to the store + * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to + * @param {object} data Data to assign to annotation + * @param {function} cb Callback after annotation is clicked + */ + mark(cfiRange, data, cb) { + return this.add("mark", cfiRange, data, cb); + } - /** - * Add a underline to the store - * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to - * @param {object} data Data to assign to annotation - * @param {function} cb Callback after annotation is clicked - * @param {string} className CSS class to assign to annotation - * @param {object} styles CSS styles to assign to annotation - */ - underline (cfiRange, data, cb, className, styles) { - return this.add("underline", cfiRange, data, cb, className, styles); - } + /** + * iterate over annotations in the store + */ + each() { + return this._annotations.forEach.apply(this._annotations, arguments); + } - /** - * Add a mark to the store - * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to - * @param {object} data Data to assign to annotation - * @param {function} cb Callback after annotation is clicked - */ - mark (cfiRange, data, cb) { - return this.add("mark", cfiRange, data, cb); - } + /** + * Hook for injecting annotation into a view + * @param {View} view + * @private + */ + inject(view) { + let sectionIndex = view.index; + if (sectionIndex in this._annotationsBySectionIndex) { + let annotations = this._annotationsBySectionIndex[sectionIndex]; + annotations.forEach((hash) => { + let annotation = this._annotations[hash]; + annotation.attach(view); + }); + } + } - /** - * iterate over annotations in the store - */ - each () { - return this._annotations.forEach.apply(this._annotations, arguments); - } + /** + * Hook for removing annotation from a view + * @param {View} view + * @private + */ + clear(view) { + let sectionIndex = view.index; + if (sectionIndex in this._annotationsBySectionIndex) { + let annotations = this._annotationsBySectionIndex[sectionIndex]; + annotations.forEach((hash) => { + let annotation = this._annotations[hash]; + annotation.detach(view); + }); + } + } - /** - * Hook for injecting annotation into a view - * @param {View} view - * @private - */ - inject (view) { - let sectionIndex = view.index; - if (sectionIndex in this._annotationsBySectionIndex) { - let annotations = this._annotationsBySectionIndex[sectionIndex]; - annotations.forEach((hash) => { - let annotation = this._annotations[hash]; - annotation.attach(view); - }); - } - } - - /** - * Hook for removing annotation from a view - * @param {View} view - * @private - */ - clear (view) { - let sectionIndex = view.index; - if (sectionIndex in this._annotationsBySectionIndex) { - let annotations = this._annotationsBySectionIndex[sectionIndex]; - annotations.forEach((hash) => { - let annotation = this._annotations[hash]; - annotation.detach(view); - }); - } - } - - /** - * [Not Implemented] Show annotations - * @TODO: needs implementation in View - */ - show () { - - } - - /** - * [Not Implemented] Hide annotations - * @TODO: needs implementation in View - */ - hide () { - - } + /** + * [Not Implemented] Show annotations + * @TODO: needs implementation in View + */ + show() {} + /** + * [Not Implemented] Hide annotations + * @TODO: needs implementation in View + */ + hide() {} } /** @@ -213,89 +208,76 @@ class Annotations { * @returns {Annotation} annotation */ class Annotation { + constructor({ type, cfiRange, data, sectionIndex, cb, className, styles }) { + this.type = type; + this.cfiRange = cfiRange; + this.data = data; + this.sectionIndex = sectionIndex; + this.mark = undefined; + this.cb = cb; + this.className = className; + this.styles = styles; + } - constructor ({ - type, - cfiRange, - data, - sectionIndex, - cb, - className, - styles - }) { - this.type = type; - this.cfiRange = cfiRange; - this.data = data; - this.sectionIndex = sectionIndex; - this.mark = undefined; - this.cb = cb; - this.className = className; - this.styles = styles; - } + /** + * Update stored data + * @param {object} data + */ + update(data) { + this.data = data; + } - /** - * Update stored data - * @param {object} data - */ - update (data) { - this.data = data; - } + /** + * Add to a view + * @param {View} view + */ + attach(view) { + let { cfiRange, data, type, cb, className, styles } = this; + let result; - /** - * Add to a view - * @param {View} view - */ - attach (view) { - let {cfiRange, data, type, mark, cb, className, styles} = this; - let result; + if (type === "highlight") { + result = view.highlight(cfiRange, data, cb, className, styles); + } else if (type === "underline") { + result = view.underline(cfiRange, data, cb, className, styles); + } else if (type === "mark") { + result = view.mark(cfiRange, data, cb); + } - if (type === "highlight") { - result = view.highlight(cfiRange, data, cb, className, styles); - } else if (type === "underline") { - result = view.underline(cfiRange, data, cb, className, styles); - } else if (type === "mark") { - result = view.mark(cfiRange, data, cb); - } + this.mark = result; + this.emit(EVENTS.ANNOTATION.ATTACH, result); + return result; + } - this.mark = result; - this.emit(EVENTS.ANNOTATION.ATTACH, result); - return result; - } + /** + * Remove from a view + * @param {View} view + */ + detach(view) { + let { cfiRange, type } = this; + let result; - /** - * Remove from a view - * @param {View} view - */ - detach (view) { - let {cfiRange, type} = this; - let result; + if (view) { + if (type === "highlight") { + result = view.unhighlight(cfiRange); + } else if (type === "underline") { + result = view.ununderline(cfiRange); + } else if (type === "mark") { + result = view.unmark(cfiRange); + } + } - if (view) { - if (type === "highlight") { - result = view.unhighlight(cfiRange); - } else if (type === "underline") { - result = view.ununderline(cfiRange); - } else if (type === "mark") { - result = view.unmark(cfiRange); - } - } - - this.mark = undefined; - this.emit(EVENTS.ANNOTATION.DETACH, result); - return result; - } - - /** - * [Not Implemented] Get text of an annotation - * @TODO: needs implementation in contents - */ - text () { - - } + this.mark = undefined; + this.emit(EVENTS.ANNOTATION.DETACH, result); + return result; + } + /** + * [Not Implemented] Get text of an annotation + * @TODO: needs implementation in contents + */ + text() {} } EventEmitter(Annotation.prototype); - -export default Annotations +export default Annotations; diff --git a/src/archive.js b/src/archive.js index 15b3835..e431a3c 100644 --- a/src/archive.js +++ b/src/archive.js @@ -1,255 +1,245 @@ -import {defer, isXml, parse} from "./utils/core"; -import request from "./utils/request"; +import JSZip from "jszip/dist/jszip"; +import { defer, isXml, parse } from "./utils/core"; import mime from "./utils/mime"; import Path from "./utils/path"; -import JSZip from "jszip/dist/jszip"; +import request from "./utils/request"; /** * Handles Unzipping a requesting files from an Epub Archive * @class */ class Archive { + constructor() { + this.zip = undefined; + this.urlCache = {}; - constructor() { - this.zip = undefined; - this.urlCache = {}; + this.checkRequirements(); + } - this.checkRequirements(); + /** + * Checks to see if JSZip exists in global namspace, + * Requires JSZip if it isn't there + * @private + */ + checkRequirements() { + try { + this.zip = new JSZip(); + } catch (e) { + throw new Error("JSZip lib not loaded"); + } + } - } + /** + * Open an archive + * @param {binary} input + * @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded + * @return {Promise} zipfile + */ + open(input, isBase64) { + return this.zip.loadAsync(input, { base64: isBase64 }); + } - /** - * Checks to see if JSZip exists in global namspace, - * Requires JSZip if it isn't there - * @private - */ - checkRequirements(){ - try { - this.zip = new JSZip(); - } catch (e) { - throw new Error("JSZip lib not loaded"); - } - } + /** + * Load and Open an archive + * @param {string} zipUrl + * @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded + * @return {Promise} zipfile + */ + openUrl(zipUrl, isBase64) { + return request(zipUrl, "binary").then( + function (data) { + return this.zip.loadAsync(data, { base64: isBase64 }); + }.bind(this) + ); + } - /** - * Open an archive - * @param {binary} input - * @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded - * @return {Promise} zipfile - */ - open(input, isBase64){ - return this.zip.loadAsync(input, {"base64": isBase64}); - } + /** + * Request a url from the archive + * @param {string} url a url to request from the archive + * @param {string} [type] specify the type of the returned result + * @return {Promise} + */ + request(url, type) { + var deferred = new defer(); + var response; + var path = new Path(url); - /** - * Load and Open an archive - * @param {string} zipUrl - * @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded - * @return {Promise} zipfile - */ - openUrl(zipUrl, isBase64){ - return request(zipUrl, "binary") - .then(function(data){ - return this.zip.loadAsync(data, {"base64": isBase64}); - }.bind(this)); - } + // If type isn't set, determine it from the file extension + if (!type) { + type = path.extension; + } - /** - * Request a url from the archive - * @param {string} url a url to request from the archive - * @param {string} [type] specify the type of the returned result - * @return {Promise} - */ - request(url, type){ - var deferred = new defer(); - var response; - var path = new Path(url); + if (type == "blob") { + response = this.getBlob(url); + } else { + response = this.getText(url); + } - // If type isn't set, determine it from the file extension - if(!type) { - type = path.extension; - } + if (response) { + response.then( + function (r) { + let result = this.handleResponse(r, type); + deferred.resolve(result); + }.bind(this) + ); + } else { + deferred.reject({ + message: "File not found in the epub: " + url, + stack: new Error().stack, + }); + } + return deferred.promise; + } - if(type == "blob"){ - response = this.getBlob(url); - } else { - response = this.getText(url); - } + /** + * Handle the response from request + * @private + * @param {any} response + * @param {string} [type] + * @return {any} the parsed result + */ + handleResponse(response, type) { + var r; - if (response) { - response.then(function (r) { - let result = this.handleResponse(r, type); - deferred.resolve(result); - }.bind(this)); - } else { - deferred.reject({ - message : "File not found in the epub: " + url, - stack : new Error().stack - }); - } - return deferred.promise; - } + if (type == "json") { + r = JSON.parse(response); + } else if (isXml(type)) { + r = parse(response, "text/xml"); + } else if (type == "xhtml") { + r = parse(response, "application/xhtml+xml"); + } else if (type == "html" || type == "htm") { + r = parse(response, "text/html"); + } else { + r = response; + } - /** - * Handle the response from request - * @private - * @param {any} response - * @param {string} [type] - * @return {any} the parsed result - */ - handleResponse(response, type){ - var r; + return r; + } - if(type == "json") { - r = JSON.parse(response); - } - else - if(isXml(type)) { - r = parse(response, "text/xml"); - } - else - if(type == "xhtml") { - r = parse(response, "application/xhtml+xml"); - } - else - if(type == "html" || type == "htm") { - r = parse(response, "text/html"); - } else { - r = response; - } + /** + * Get a Blob from Archive by Url + * @param {string} url + * @param {string} [mimeType] + * @return {Blob} + */ + getBlob(url, mimeType) { + var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash + var entry = this.zip.file(decodededUrl); - return r; - } + if (entry) { + mimeType = mimeType || mime.lookup(entry.name); + return entry.async("uint8array").then(function (uint8array) { + return new Blob([uint8array], { type: mimeType }); + }); + } + } - /** - * Get a Blob from Archive by Url - * @param {string} url - * @param {string} [mimeType] - * @return {Blob} - */ - getBlob(url, mimeType){ - var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash - var entry = this.zip.file(decodededUrl); + /** + * Get Text from Archive by Url + * @param {string} url + * @param {string} [encoding] + * @return {string} + */ + getText(url, encoding) { + var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash + var entry = this.zip.file(decodededUrl); - if(entry) { - mimeType = mimeType || mime.lookup(entry.name); - return entry.async("uint8array").then(function(uint8array) { - return new Blob([uint8array], {type : mimeType}); - }); - } - } + if (entry) { + return entry.async("string").then(function (text) { + return text; + }); + } + } - /** - * Get Text from Archive by Url - * @param {string} url - * @param {string} [encoding] - * @return {string} - */ - getText(url, encoding){ - var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash - var entry = this.zip.file(decodededUrl); + /** + * Get a base64 encoded result from Archive by Url + * @param {string} url + * @param {string} [mimeType] + * @return {string} base64 encoded + */ + getBase64(url, mimeType) { + var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash + var entry = this.zip.file(decodededUrl); - if(entry) { - return entry.async("string").then(function(text) { - return text; - }); - } - } + if (entry) { + mimeType = mimeType || mime.lookup(entry.name); + return entry.async("base64").then(function (data) { + return "data:" + mimeType + ";base64," + data; + }); + } + } - /** - * Get a base64 encoded result from Archive by Url - * @param {string} url - * @param {string} [mimeType] - * @return {string} base64 encoded - */ - getBase64(url, mimeType){ - var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash - var entry = this.zip.file(decodededUrl); + /** + * Create a Url from an unarchived item + * @param {string} url + * @param {object} [options.base64] use base64 encoding or blob url + * @return {Promise} url promise with Url string + */ + createUrl(url, options) { + var deferred = new defer(); + var _URL = window.URL || window.webkitURL || window.mozURL; + var tempUrl; + var response; + var useBase64 = options && options.base64; - if(entry) { - mimeType = mimeType || mime.lookup(entry.name); - return entry.async("base64").then(function(data) { - return "data:" + mimeType + ";base64," + data; - }); - } - } + if (url in this.urlCache) { + deferred.resolve(this.urlCache[url]); + return deferred.promise; + } - /** - * Create a Url from an unarchived item - * @param {string} url - * @param {object} [options.base64] use base64 encoding or blob url - * @return {Promise} url promise with Url string - */ - createUrl(url, options){ - var deferred = new defer(); - var _URL = window.URL || window.webkitURL || window.mozURL; - var tempUrl; - var response; - var useBase64 = options && options.base64; + if (useBase64) { + response = this.getBase64(url); - if(url in this.urlCache) { - deferred.resolve(this.urlCache[url]); - return deferred.promise; - } + if (response) { + response.then( + function (tempUrl) { + this.urlCache[url] = tempUrl; + deferred.resolve(tempUrl); + }.bind(this) + ); + } + } else { + response = this.getBlob(url); - if (useBase64) { - response = this.getBase64(url); + if (response) { + response.then( + function (blob) { + tempUrl = _URL.createObjectURL(blob); + this.urlCache[url] = tempUrl; + deferred.resolve(tempUrl); + }.bind(this) + ); + } + } - if (response) { - response.then(function(tempUrl) { + if (!response) { + deferred.reject({ + message: "File not found in the epub: " + url, + stack: new Error().stack, + }); + } - this.urlCache[url] = tempUrl; - deferred.resolve(tempUrl); + return deferred.promise; + } - }.bind(this)); + /** + * Revoke Temp Url for a archive item + * @param {string} url url of the item in the archive + */ + revokeUrl(url) { + var _URL = window.URL || window.webkitURL || window.mozURL; + var fromCache = this.urlCache[url]; + if (fromCache) _URL.revokeObjectURL(fromCache); + } - } - - } else { - - response = this.getBlob(url); - - if (response) { - response.then(function(blob) { - - tempUrl = _URL.createObjectURL(blob); - this.urlCache[url] = tempUrl; - deferred.resolve(tempUrl); - - }.bind(this)); - - } - } - - - if (!response) { - deferred.reject({ - message : "File not found in the epub: " + url, - stack : new Error().stack - }); - } - - return deferred.promise; - } - - /** - * Revoke Temp Url for a archive item - * @param {string} url url of the item in the archive - */ - revokeUrl(url){ - var _URL = window.URL || window.webkitURL || window.mozURL; - var fromCache = this.urlCache[url]; - if(fromCache) _URL.revokeObjectURL(fromCache); - } - - destroy() { - var _URL = window.URL || window.webkitURL || window.mozURL; - for (let fromCache in this.urlCache) { - _URL.revokeObjectURL(fromCache); - } - this.zip = undefined; - this.urlCache = {}; - } + destroy() { + var _URL = window.URL || window.webkitURL || window.mozURL; + for (let fromCache in this.urlCache) { + _URL.revokeObjectURL(fromCache); + } + this.zip = undefined; + this.urlCache = {}; + } } export default Archive; diff --git a/src/book.js b/src/book.js index fb66e54..6cb0744 100644 --- a/src/book.js +++ b/src/book.js @@ -1,32 +1,33 @@ import EventEmitter from "event-emitter"; -import {extend, defer} from "./utils/core"; -import Url from "./utils/url"; -import Path from "./utils/path"; -import Spine from "./spine"; -import Locations from "./locations"; +import Archive from "./archive"; import Container from "./container"; -import Packaging from "./packaging"; +import DisplayOptions from "./displayoptions"; +import EpubCFI from "./epubcfi"; +import Locations from "./locations"; import Navigation from "./navigation"; -import Resources from "./resources"; +import Packaging from "./packaging"; import PageList from "./pagelist"; import Rendition from "./rendition"; -import Archive from "./archive"; -import request from "./utils/request"; -import EpubCFI from "./epubcfi"; +import Resources from "./resources"; +import Spine from "./spine"; import Store from "./store"; -import DisplayOptions from "./displayoptions"; import { EPUBJS_VERSION, EVENTS } from "./utils/constants"; +import { defer, extend } from "./utils/core"; +import Path from "./utils/path"; +import request from "./utils/request"; +import Url from "./utils/url"; const CONTAINER_PATH = "META-INF/container.xml"; -const IBOOKS_DISPLAY_OPTIONS_PATH = "META-INF/com.apple.ibooks.display-options.xml"; +const IBOOKS_DISPLAY_OPTIONS_PATH = + "META-INF/com.apple.ibooks.display-options.xml"; const INPUT_TYPE = { - BINARY: "binary", - BASE64: "base64", - EPUB: "epub", - OPF: "opf", - MANIFEST: "json", - DIRECTORY: "directory" + BINARY: "binary", + BASE64: "base64", + EPUB: "epub", + OPF: "opf", + MANIFEST: "json", + DIRECTORY: "directory", }; /** @@ -48,718 +49,731 @@ const INPUT_TYPE = { * @example new Book({ replacements: "blobUrl" }) */ class Book { - constructor(url, options) { - // Allow passing just options to the Book - if (typeof(options) === "undefined" && - typeof(url) !== "string" && - url instanceof Blob === false && - url instanceof ArrayBuffer === false) { - options = url; - url = undefined; - } - - this.settings = extend(this.settings || {}, { - requestMethod: undefined, - requestCredentials: undefined, - requestHeaders: undefined, - encoding: undefined, - replacements: undefined, - canonical: undefined, - openAs: undefined, - store: undefined - }); - - extend(this.settings, options); - - - // Promises - this.opening = new defer(); - /** - * @member {promise} opened returns after the book is loaded - * @memberof Book - */ - this.opened = this.opening.promise; - this.isOpen = false; - - this.loading = { - manifest: new defer(), - spine: new defer(), - metadata: new defer(), - cover: new defer(), - navigation: new defer(), - pageList: new defer(), - resources: new defer(), - displayOptions: new defer() - }; - - this.loaded = { - manifest: this.loading.manifest.promise, - spine: this.loading.spine.promise, - metadata: this.loading.metadata.promise, - cover: this.loading.cover.promise, - navigation: this.loading.navigation.promise, - pageList: this.loading.pageList.promise, - resources: this.loading.resources.promise, - displayOptions: this.loading.displayOptions.promise - }; - - /** - * @member {promise} ready returns after the book is loaded and parsed - * @memberof Book - * @private - */ - this.ready = Promise.all([ - this.loaded.manifest, - this.loaded.spine, - this.loaded.metadata, - this.loaded.cover, - this.loaded.navigation, - this.loaded.resources, - this.loaded.displayOptions - ]); - - - // Queue for methods used before opening - this.isRendered = false; - // this._q = queue(this); - - /** - * @member {method} request - * @memberof Book - * @private - */ - this.request = this.settings.requestMethod || request; - - /** - * @member {Spine} spine - * @memberof Book - */ - this.spine = new Spine(); - - /** - * @member {Locations} locations - * @memberof Book - */ - this.locations = new Locations(this.spine, this.load.bind(this)); - - /** - * @member {Navigation} navigation - * @memberof Book - */ - this.navigation = undefined; - - /** - * @member {PageList} pagelist - * @memberof Book - */ - this.pageList = undefined; - - /** - * @member {Url} url - * @memberof Book - * @private - */ - this.url = undefined; - - /** - * @member {Path} path - * @memberof Book - * @private - */ - this.path = undefined; - - /** - * @member {boolean} archived - * @memberof Book - * @private - */ - this.archived = false; - - /** - * @member {Archive} archive - * @memberof Book - * @private - */ - this.archive = undefined; - - /** - * @member {Store} storage - * @memberof Book - * @private - */ - this.storage = undefined; - - /** - * @member {Resources} resources - * @memberof Book - * @private - */ - this.resources = undefined; - - /** - * @member {Rendition} rendition - * @memberof Book - * @private - */ - this.rendition = undefined; - - /** - * @member {Container} container - * @memberof Book - * @private - */ - this.container = undefined; - - /** - * @member {Packaging} packaging - * @memberof Book - * @private - */ - this.packaging = undefined; - - /** - * @member {DisplayOptions} displayOptions - * @memberof DisplayOptions - * @private - */ - this.displayOptions = undefined; - - // this.toc = undefined; - if (this.settings.store) { - this.store(this.settings.store); - } - - if(url) { - this.open(url, this.settings.openAs).catch((error) => { - var err = new Error("Cannot load book at "+ url ); - this.emit(EVENTS.BOOK.OPEN_FAILED, err); - }); - } - } - - /** - * Open a epub or url - * @param {string | ArrayBuffer} input Url, Path or ArrayBuffer - * @param {string} [what="binary", "base64", "epub", "opf", "json", "directory"] force opening as a certain type - * @returns {Promise} of when the book has been loaded - * @example book.open("/path/to/book.epub") - */ - open(input, what) { - var opening; - var type = what || this.determineType(input); - - if (type === INPUT_TYPE.BINARY) { - this.archived = true; - this.url = new Url("/", ""); - opening = this.openEpub(input); - } else if (type === INPUT_TYPE.BASE64) { - this.archived = true; - this.url = new Url("/", ""); - opening = this.openEpub(input, type); - } else if (type === INPUT_TYPE.EPUB) { - this.archived = true; - this.url = new Url("/", ""); - opening = this.request(input, "binary", this.settings.requestCredentials, this.settings.requestHeaders) - .then(this.openEpub.bind(this)); - } else if(type == INPUT_TYPE.OPF) { - this.url = new Url(input); - opening = this.openPackaging(this.url.Path.toString()); - } else if(type == INPUT_TYPE.MANIFEST) { - this.url = new Url(input); - opening = this.openManifest(this.url.Path.toString()); - } else { - this.url = new Url(input); - opening = this.openContainer(CONTAINER_PATH) - .then(this.openPackaging.bind(this)); - } - - return opening; - } - - /** - * Open an archived epub - * @private - * @param {binary} data - * @param {string} [encoding] - * @return {Promise} - */ - openEpub(data, encoding) { - return this.unarchive(data, encoding || this.settings.encoding) - .then(() => { - return this.openContainer(CONTAINER_PATH); - }) - .then((packagePath) => { - return this.openPackaging(packagePath); - }); - } - - /** - * Open the epub container - * @private - * @param {string} url - * @return {string} packagePath - */ - openContainer(url) { - return this.load(url) - .then((xml) => { - this.container = new Container(xml); - return this.resolve(this.container.packagePath); - }); - } - - /** - * Open the Open Packaging Format Xml - * @private - * @param {string} url - * @return {Promise} - */ - openPackaging(url) { - this.path = new Path(url); - return this.load(url) - .then((xml) => { - this.packaging = new Packaging(xml); - return this.unpack(this.packaging); - }); - } - - /** - * Open the manifest JSON - * @private - * @param {string} url - * @return {Promise} - */ - openManifest(url) { - this.path = new Path(url); - return this.load(url) - .then((json) => { - this.packaging = new Packaging(); - this.packaging.load(json); - return this.unpack(this.packaging); - }); - } - - /** - * Load a resource from the Book - * @param {string} path path to the resource to load - * @return {Promise} returns a promise with the requested resource - */ - load(path) { - var resolved = this.resolve(path); - if(this.archived) { - return this.archive.request(resolved); - } else { - return this.request(resolved, null, this.settings.requestCredentials, this.settings.requestHeaders); - } - } - - /** - * Resolve a path to it's absolute position in the Book - * @param {string} path - * @param {boolean} [absolute] force resolving the full URL - * @return {string} the resolved path string - */ - resolve(path, absolute) { - if (!path) { - return; - } - var resolved = path; - var isAbsolute = (path.indexOf("://") > -1); - - if (isAbsolute) { - return path; - } - - if (this.path) { - resolved = this.path.resolve(path); - } - - if(absolute != false && this.url) { - resolved = this.url.resolve(resolved); - } - - return resolved; - } - - /** - * Get a canonical link to a path - * @param {string} path - * @return {string} the canonical path string - */ - canonical(path) { - var url = path; - - if (!path) { - return ""; - } - - if (this.settings.canonical) { - url = this.settings.canonical(path); - } else { - url = this.resolve(path, true); - } - - return url; - } - - /** - * Determine the type of they input passed to open - * @private - * @param {string} input - * @return {string} binary | directory | epub | opf - */ - determineType(input) { - var url; - var path; - var extension; - - if (this.settings.encoding === "base64") { - return INPUT_TYPE.BASE64; - } - - if(typeof(input) != "string") { - return INPUT_TYPE.BINARY; - } - - url = new Url(input); - path = url.path(); - extension = path.extension; - - // If there's a search string, remove it before determining type - if (extension) { - extension = extension.replace(/\?.*$/, ""); - } - - if (!extension) { - return INPUT_TYPE.DIRECTORY; - } - - if(extension === "epub"){ - return INPUT_TYPE.EPUB; - } - - if(extension === "opf"){ - return INPUT_TYPE.OPF; - } - - if(extension === "json"){ - return INPUT_TYPE.MANIFEST; - } - } - - - /** - * unpack the contents of the Books packaging - * @private - * @param {Packaging} packaging object - */ - unpack(packaging) { - this.package = packaging; //TODO: deprecated this - - if (this.packaging.metadata.layout === "") { - // rendition:layout not set - check display options if book is pre-paginated - this.load(this.url.resolve(IBOOKS_DISPLAY_OPTIONS_PATH)).then((xml) => { - this.displayOptions = new DisplayOptions(xml); - this.loading.displayOptions.resolve(this.displayOptions); - }).catch((err) => { - this.displayOptions = new DisplayOptions(); - this.loading.displayOptions.resolve(this.displayOptions); - }); - } else { - this.displayOptions = new DisplayOptions(); - this.loading.displayOptions.resolve(this.displayOptions); - } - - this.spine.unpack(this.packaging, this.resolve.bind(this), this.canonical.bind(this)); - - this.resources = new Resources(this.packaging.manifest, { - archive: this.archive, - resolver: this.resolve.bind(this), - request: this.request.bind(this), - replacements: this.settings.replacements || (this.archived ? "blobUrl" : "base64") - }); - - this.loadNavigation(this.packaging).then(() => { - // this.toc = this.navigation.toc; - this.loading.navigation.resolve(this.navigation); - }); - - if (this.packaging.coverPath) { - this.cover = this.resolve(this.packaging.coverPath); - } - // Resolve promises - this.loading.manifest.resolve(this.packaging.manifest); - this.loading.metadata.resolve(this.packaging.metadata); - this.loading.spine.resolve(this.spine); - this.loading.cover.resolve(this.cover); - this.loading.resources.resolve(this.resources); - this.loading.pageList.resolve(this.pageList); - - this.isOpen = true; - - if(this.archived || this.settings.replacements && this.settings.replacements != "none") { - this.replacements().then(() => { - this.loaded.displayOptions.then(() => { - this.opening.resolve(this); - }); - }) - .catch((err) => { - console.error(err); - }); - } else { - // Resolve book opened promise - this.loaded.displayOptions.then(() => { - this.opening.resolve(this); - }); - } - - } - - /** - * Load Navigation and PageList from package - * @private - * @param {Packaging} packaging - */ - loadNavigation(packaging) { - let navPath = packaging.navPath || packaging.ncxPath; - let toc = packaging.toc; - - // From json manifest - if (toc) { - return new Promise((resolve, reject) => { - this.navigation = new Navigation(toc); - - if (packaging.pageList) { - this.pageList = new PageList(packaging.pageList); // TODO: handle page lists from Manifest - } - - resolve(this.navigation); - }); - } - - if (!navPath) { - return new Promise((resolve, reject) => { - this.navigation = new Navigation(); - this.pageList = new PageList(); - - resolve(this.navigation); - }); - } - - return this.load(navPath, "xml") - .then((xml) => { - this.navigation = new Navigation(xml); - this.pageList = new PageList(xml); - return this.navigation; - }); - } - - /** - * Gets a Section of the Book from the Spine - * Alias for `book.spine.get` - * @param {string} target - * @return {Section} - */ - section(target) { - return this.spine.get(target); - } - - /** - * Sugar to render a book to an element - * @param {element | string} element element or string to add a rendition to - * @param {object} [options] - * @return {Rendition} - */ - renderTo(element, options) { - this.rendition = new Rendition(this, options); - this.rendition.attachTo(element); - - return this.rendition; - } - - /** - * Set if request should use withCredentials - * @param {boolean} credentials - */ - setRequestCredentials(credentials) { - this.settings.requestCredentials = credentials; - } - - /** - * Set headers request should use - * @param {object} headers - */ - setRequestHeaders(headers) { - this.settings.requestHeaders = headers; - } - - /** - * Unarchive a zipped epub - * @private - * @param {binary} input epub data - * @param {string} [encoding] - * @return {Archive} - */ - unarchive(input, encoding) { - this.archive = new Archive(); - return this.archive.open(input, encoding); - } - - /** - * Store the epubs contents - * @private - * @param {binary} input epub data - * @param {string} [encoding] - * @return {Store} - */ - store(name) { - // Use "blobUrl" or "base64" for replacements - let replacementsSetting = this.settings.replacements && this.settings.replacements !== "none"; - // Save original url - let originalUrl = this.url; - // Save original request method - let requester = this.settings.requestMethod || request.bind(this); - // Create new Store - this.storage = new Store(name, requester, this.resolve.bind(this)); - // Replace request method to go through store - this.request = this.storage.request.bind(this.storage); - - this.opened.then(() => { - if (this.archived) { - this.storage.requester = this.archive.request.bind(this.archive); - } - // Substitute hook - let substituteResources = (output, section) => { - section.output = this.resources.substitute(output, section.url); - }; - - // Set to use replacements - this.resources.settings.replacements = replacementsSetting || "blobUrl"; - // Create replacement urls - this.resources.replacements(). - then(() => { - return this.resources.replaceCss(); - }); - - this.storage.on("offline", () => { - // Remove url to use relative resolving for hrefs - this.url = new Url("/", ""); - // Add hook to replace resources in contents - this.spine.hooks.serialize.register(substituteResources); - }); - - this.storage.on("online", () => { - // Restore original url - this.url = originalUrl; - // Remove hook - this.spine.hooks.serialize.deregister(substituteResources); - }); - - }); - - return this.storage; - } - - /** - * Get the cover url - * @return {Promise} coverUrl - */ - coverUrl() { - return this.loaded.cover.then(() => { - if (!this.cover) { - return null; - } - - if (this.archived) { - return this.archive.createUrl(this.cover); - } else { - return this.cover; - } - }); - } - - /** - * Load replacement urls - * @private - * @return {Promise} completed loading urls - */ - replacements() { - this.spine.hooks.serialize.register((output, section) => { - section.output = this.resources.substitute(output, section.url); - }); - - return this.resources.replacements(). - then(() => { - return this.resources.replaceCss(); - }); - } - - /** - * Find a DOM Range for a given CFI Range - * @param {EpubCFI} cfiRange a epub cfi range - * @return {Promise} - */ - getRange(cfiRange) { - var cfi = new EpubCFI(cfiRange); - var item = this.spine.get(cfi.spinePos); - var _request = this.load.bind(this); - if (!item) { - return new Promise((resolve, reject) => { - reject("CFI could not be found"); - }); - } - return item.load(_request).then(function (contents) { - var range = cfi.toRange(item.document); - return range; - }); - } - - /** - * Generates the Book Key using the identifier in the manifest or other string provided - * @param {string} [identifier] to use instead of metadata identifier - * @return {string} key - */ - key(identifier) { - var ident = identifier || this.packaging.metadata.identifier || this.url.filename; - return `epubjs:${EPUBJS_VERSION}:${ident}`; - } - - /** - * Destroy the Book and all associated objects - */ - destroy() { - this.opened = undefined; - this.loading = undefined; - this.loaded = undefined; - this.ready = undefined; - - this.isOpen = false; - this.isRendered = false; - - this.spine && this.spine.destroy(); - this.locations && this.locations.destroy(); - this.pageList && this.pageList.destroy(); - this.archive && this.archive.destroy(); - this.resources && this.resources.destroy(); - this.container && this.container.destroy(); - this.packaging && this.packaging.destroy(); - this.rendition && this.rendition.destroy(); - this.displayOptions && this.displayOptions.destroy(); - - this.spine = undefined; - this.locations = undefined; - this.pageList = undefined; - this.archive = undefined; - this.resources = undefined; - this.container = undefined; - this.packaging = undefined; - this.rendition = undefined; - - this.navigation = undefined; - this.url = undefined; - this.path = undefined; - this.archived = false; - } - + constructor(url, options) { + // Allow passing just options to the Book + if ( + typeof options === "undefined" && + typeof url !== "string" && + url instanceof Blob === false && + url instanceof ArrayBuffer === false + ) { + options = url; + url = undefined; + } + + this.settings = extend(this.settings || {}, { + requestMethod: undefined, + requestCredentials: undefined, + requestHeaders: undefined, + encoding: undefined, + replacements: undefined, + canonical: undefined, + openAs: undefined, + store: undefined, + }); + + extend(this.settings, options); + + // Promises + this.opening = new defer(); + /** + * @member {promise} opened returns after the book is loaded + * @memberof Book + */ + this.opened = this.opening.promise; + this.isOpen = false; + + this.loading = { + manifest: new defer(), + spine: new defer(), + metadata: new defer(), + cover: new defer(), + navigation: new defer(), + pageList: new defer(), + resources: new defer(), + displayOptions: new defer(), + }; + + this.loaded = { + manifest: this.loading.manifest.promise, + spine: this.loading.spine.promise, + metadata: this.loading.metadata.promise, + cover: this.loading.cover.promise, + navigation: this.loading.navigation.promise, + pageList: this.loading.pageList.promise, + resources: this.loading.resources.promise, + displayOptions: this.loading.displayOptions.promise, + }; + + /** + * @member {promise} ready returns after the book is loaded and parsed + * @memberof Book + * @private + */ + this.ready = Promise.all([ + this.loaded.manifest, + this.loaded.spine, + this.loaded.metadata, + this.loaded.cover, + this.loaded.navigation, + this.loaded.resources, + this.loaded.displayOptions, + ]); + + // Queue for methods used before opening + this.isRendered = false; + // this._q = queue(this); + + /** + * @member {method} request + * @memberof Book + * @private + */ + this.request = this.settings.requestMethod || request; + + /** + * @member {Spine} spine + * @memberof Book + */ + this.spine = new Spine(); + + /** + * @member {Locations} locations + * @memberof Book + */ + this.locations = new Locations(this.spine, this.load.bind(this)); + + /** + * @member {Navigation} navigation + * @memberof Book + */ + this.navigation = undefined; + + /** + * @member {PageList} pagelist + * @memberof Book + */ + this.pageList = undefined; + + /** + * @member {Url} url + * @memberof Book + * @private + */ + this.url = undefined; + + /** + * @member {Path} path + * @memberof Book + * @private + */ + this.path = undefined; + + /** + * @member {boolean} archived + * @memberof Book + * @private + */ + this.archived = false; + + /** + * @member {Archive} archive + * @memberof Book + * @private + */ + this.archive = undefined; + + /** + * @member {Store} storage + * @memberof Book + * @private + */ + this.storage = undefined; + + /** + * @member {Resources} resources + * @memberof Book + * @private + */ + this.resources = undefined; + + /** + * @member {Rendition} rendition + * @memberof Book + * @private + */ + this.rendition = undefined; + + /** + * @member {Container} container + * @memberof Book + * @private + */ + this.container = undefined; + + /** + * @member {Packaging} packaging + * @memberof Book + * @private + */ + this.packaging = undefined; + + /** + * @member {DisplayOptions} displayOptions + * @memberof DisplayOptions + * @private + */ + this.displayOptions = undefined; + + // this.toc = undefined; + if (this.settings.store) { + this.store(this.settings.store); + } + + if (url) { + this.open(url, this.settings.openAs).catch((error) => { + var err = new Error("Cannot load book at " + url); + this.emit(EVENTS.BOOK.OPEN_FAILED, err); + }); + } + } + + /** + * Open a epub or url + * @param {string | ArrayBuffer} input Url, Path or ArrayBuffer + * @param {string} [what="binary", "base64", "epub", "opf", "json", "directory"] force opening as a certain type + * @returns {Promise} of when the book has been loaded + * @example book.open("/path/to/book.epub") + */ + open(input, what) { + var opening; + var type = what || this.determineType(input); + + if (type === INPUT_TYPE.BINARY) { + this.archived = true; + this.url = new Url("/", ""); + opening = this.openEpub(input); + } else if (type === INPUT_TYPE.BASE64) { + this.archived = true; + this.url = new Url("/", ""); + opening = this.openEpub(input, type); + } else if (type === INPUT_TYPE.EPUB) { + this.archived = true; + this.url = new Url("/", ""); + opening = this.request( + input, + "binary", + this.settings.requestCredentials, + this.settings.requestHeaders + ).then(this.openEpub.bind(this)); + } else if (type == INPUT_TYPE.OPF) { + this.url = new Url(input); + opening = this.openPackaging(this.url.Path.toString()); + } else if (type == INPUT_TYPE.MANIFEST) { + this.url = new Url(input); + opening = this.openManifest(this.url.Path.toString()); + } else { + this.url = new Url(input); + opening = this.openContainer(CONTAINER_PATH).then( + this.openPackaging.bind(this) + ); + } + + return opening; + } + + /** + * Open an archived epub + * @private + * @param {binary} data + * @param {string} [encoding] + * @return {Promise} + */ + openEpub(data, encoding) { + return this.unarchive(data, encoding || this.settings.encoding) + .then(() => { + return this.openContainer(CONTAINER_PATH); + }) + .then((packagePath) => { + return this.openPackaging(packagePath); + }); + } + + /** + * Open the epub container + * @private + * @param {string} url + * @return {string} packagePath + */ + openContainer(url) { + return this.load(url).then((xml) => { + this.container = new Container(xml); + return this.resolve(this.container.packagePath); + }); + } + + /** + * Open the Open Packaging Format Xml + * @private + * @param {string} url + * @return {Promise} + */ + openPackaging(url) { + this.path = new Path(url); + return this.load(url).then((xml) => { + this.packaging = new Packaging(xml); + return this.unpack(this.packaging); + }); + } + + /** + * Open the manifest JSON + * @private + * @param {string} url + * @return {Promise} + */ + openManifest(url) { + this.path = new Path(url); + return this.load(url).then((json) => { + this.packaging = new Packaging(); + this.packaging.load(json); + return this.unpack(this.packaging); + }); + } + + /** + * Load a resource from the Book + * @param {string} path path to the resource to load + * @return {Promise} returns a promise with the requested resource + */ + load(path) { + var resolved = this.resolve(path); + if (this.archived) { + return this.archive.request(resolved); + } else { + return this.request( + resolved, + null, + this.settings.requestCredentials, + this.settings.requestHeaders + ); + } + } + + /** + * Resolve a path to it's absolute position in the Book + * @param {string} path + * @param {boolean} [absolute] force resolving the full URL + * @return {string} the resolved path string + */ + resolve(path, absolute) { + if (!path) { + return; + } + var resolved = path; + var isAbsolute = path.indexOf("://") > -1; + + if (isAbsolute) { + return path; + } + + if (this.path) { + resolved = this.path.resolve(path); + } + + if (absolute != false && this.url) { + resolved = this.url.resolve(resolved); + } + + return resolved; + } + + /** + * Get a canonical link to a path + * @param {string} path + * @return {string} the canonical path string + */ + canonical(path) { + var url = path; + + if (!path) { + return ""; + } + + if (this.settings.canonical) { + url = this.settings.canonical(path); + } else { + url = this.resolve(path, true); + } + + return url; + } + + /** + * Determine the type of they input passed to open + * @private + * @param {string} input + * @return {string} binary | directory | epub | opf + */ + determineType(input) { + var url; + var path; + var extension; + + if (this.settings.encoding === "base64") { + return INPUT_TYPE.BASE64; + } + + if (typeof input != "string") { + return INPUT_TYPE.BINARY; + } + + url = new Url(input); + path = url.path(); + extension = path.extension; + + // If there's a search string, remove it before determining type + if (extension) { + extension = extension.replace(/\?.*$/, ""); + } + + if (!extension) { + return INPUT_TYPE.DIRECTORY; + } + + if (extension === "epub") { + return INPUT_TYPE.EPUB; + } + + if (extension === "opf") { + return INPUT_TYPE.OPF; + } + + if (extension === "json") { + return INPUT_TYPE.MANIFEST; + } + } + + /** + * unpack the contents of the Books packaging + * @private + * @param {Packaging} packaging object + */ + unpack(packaging) { + this.package = packaging; //TODO: deprecated this + + if (this.packaging.metadata.layout === "") { + // rendition:layout not set - check display options if book is pre-paginated + this.load(this.url.resolve(IBOOKS_DISPLAY_OPTIONS_PATH)) + .then((xml) => { + this.displayOptions = new DisplayOptions(xml); + this.loading.displayOptions.resolve(this.displayOptions); + }) + .catch((err) => { + this.displayOptions = new DisplayOptions(); + this.loading.displayOptions.resolve(this.displayOptions); + }); + } else { + this.displayOptions = new DisplayOptions(); + this.loading.displayOptions.resolve(this.displayOptions); + } + + this.spine.unpack( + this.packaging, + this.resolve.bind(this), + this.canonical.bind(this) + ); + + this.resources = new Resources(this.packaging.manifest, { + archive: this.archive, + resolver: this.resolve.bind(this), + request: this.request.bind(this), + replacements: + this.settings.replacements || (this.archived ? "blobUrl" : "base64"), + }); + + this.loadNavigation(this.packaging).then(() => { + // this.toc = this.navigation.toc; + this.loading.navigation.resolve(this.navigation); + }); + + if (this.packaging.coverPath) { + this.cover = this.resolve(this.packaging.coverPath); + } + // Resolve promises + this.loading.manifest.resolve(this.packaging.manifest); + this.loading.metadata.resolve(this.packaging.metadata); + this.loading.spine.resolve(this.spine); + this.loading.cover.resolve(this.cover); + this.loading.resources.resolve(this.resources); + this.loading.pageList.resolve(this.pageList); + + this.isOpen = true; + + if ( + this.archived || + (this.settings.replacements && this.settings.replacements != "none") + ) { + this.replacements() + .then(() => { + this.loaded.displayOptions.then(() => { + this.opening.resolve(this); + }); + }) + .catch((err) => { + console.error(err); + }); + } else { + // Resolve book opened promise + this.loaded.displayOptions.then(() => { + this.opening.resolve(this); + }); + } + } + + /** + * Load Navigation and PageList from package + * @private + * @param {Packaging} packaging + */ + loadNavigation(packaging) { + let navPath = packaging.navPath || packaging.ncxPath; + let toc = packaging.toc; + + // From json manifest + if (toc) { + return new Promise((resolve, reject) => { + this.navigation = new Navigation(toc); + + if (packaging.pageList) { + this.pageList = new PageList(packaging.pageList); // TODO: handle page lists from Manifest + } + + resolve(this.navigation); + }); + } + + if (!navPath) { + return new Promise((resolve, reject) => { + this.navigation = new Navigation(); + this.pageList = new PageList(); + + resolve(this.navigation); + }); + } + + return this.load(navPath, "xml").then((xml) => { + this.navigation = new Navigation(xml); + this.pageList = new PageList(xml); + return this.navigation; + }); + } + + /** + * Gets a Section of the Book from the Spine + * Alias for `book.spine.get` + * @param {string} target + * @return {Section} + */ + section(target) { + return this.spine.get(target); + } + + /** + * Sugar to render a book to an element + * @param {element | string} element element or string to add a rendition to + * @param {object} [options] + * @return {Rendition} + */ + renderTo(element, options) { + this.rendition = new Rendition(this, options); + this.rendition.attachTo(element); + + return this.rendition; + } + + /** + * Set if request should use withCredentials + * @param {boolean} credentials + */ + setRequestCredentials(credentials) { + this.settings.requestCredentials = credentials; + } + + /** + * Set headers request should use + * @param {object} headers + */ + setRequestHeaders(headers) { + this.settings.requestHeaders = headers; + } + + /** + * Unarchive a zipped epub + * @private + * @param {binary} input epub data + * @param {string} [encoding] + * @return {Archive} + */ + unarchive(input, encoding) { + this.archive = new Archive(); + return this.archive.open(input, encoding); + } + + /** + * Store the epubs contents + * @private + * @param {binary} input epub data + * @param {string} [encoding] + * @return {Store} + */ + store(name) { + // Use "blobUrl" or "base64" for replacements + let replacementsSetting = + this.settings.replacements && this.settings.replacements !== "none"; + // Save original url + let originalUrl = this.url; + // Save original request method + let requester = this.settings.requestMethod || request.bind(this); + // Create new Store + this.storage = new Store(name, requester, this.resolve.bind(this)); + // Replace request method to go through store + this.request = this.storage.request.bind(this.storage); + + this.opened.then(() => { + if (this.archived) { + this.storage.requester = this.archive.request.bind(this.archive); + } + // Substitute hook + let substituteResources = (output, section) => { + section.output = this.resources.substitute(output, section.url); + }; + + // Set to use replacements + this.resources.settings.replacements = replacementsSetting || "blobUrl"; + // Create replacement urls + this.resources.replacements().then(() => { + return this.resources.replaceCss(); + }); + + this.storage.on("offline", () => { + // Remove url to use relative resolving for hrefs + this.url = new Url("/", ""); + // Add hook to replace resources in contents + this.spine.hooks.serialize.register(substituteResources); + }); + + this.storage.on("online", () => { + // Restore original url + this.url = originalUrl; + // Remove hook + this.spine.hooks.serialize.deregister(substituteResources); + }); + }); + + return this.storage; + } + + /** + * Get the cover url + * @return {Promise} coverUrl + */ + coverUrl() { + return this.loaded.cover.then(() => { + if (!this.cover) { + return null; + } + + if (this.archived) { + return this.archive.createUrl(this.cover); + } else { + return this.cover; + } + }); + } + + /** + * Load replacement urls + * @private + * @return {Promise} completed loading urls + */ + replacements() { + this.spine.hooks.serialize.register((output, section) => { + section.output = this.resources.substitute(output, section.url); + }); + + return this.resources.replacements().then(() => { + return this.resources.replaceCss(); + }); + } + + /** + * Find a DOM Range for a given CFI Range + * @param {EpubCFI} cfiRange a epub cfi range + * @return {Promise} + */ + getRange(cfiRange) { + var cfi = new EpubCFI(cfiRange); + var item = this.spine.get(cfi.spinePos); + var _request = this.load.bind(this); + if (!item) { + return new Promise((resolve, reject) => { + reject("CFI could not be found"); + }); + } + return item.load(_request).then(function (contents) { + var range = cfi.toRange(item.document); + return range; + }); + } + + /** + * Generates the Book Key using the identifier in the manifest or other string provided + * @param {string} [identifier] to use instead of metadata identifier + * @return {string} key + */ + key(identifier) { + var ident = + identifier || this.packaging.metadata.identifier || this.url.filename; + return `epubjs:${EPUBJS_VERSION}:${ident}`; + } + + /** + * Destroy the Book and all associated objects + */ + destroy() { + this.opened = undefined; + this.loading = undefined; + this.loaded = undefined; + this.ready = undefined; + + this.isOpen = false; + this.isRendered = false; + + this.spine && this.spine.destroy(); + this.locations && this.locations.destroy(); + this.pageList && this.pageList.destroy(); + this.archive && this.archive.destroy(); + this.resources && this.resources.destroy(); + this.container && this.container.destroy(); + this.packaging && this.packaging.destroy(); + this.rendition && this.rendition.destroy(); + this.displayOptions && this.displayOptions.destroy(); + + this.spine = undefined; + this.locations = undefined; + this.pageList = undefined; + this.archive = undefined; + this.resources = undefined; + this.container = undefined; + this.packaging = undefined; + this.rendition = undefined; + + this.navigation = undefined; + this.url = undefined; + this.path = undefined; + this.archived = false; + } } //-- Enable binding events to book diff --git a/src/container.js b/src/container.js index f3a214f..f6006e3 100644 --- a/src/container.js +++ b/src/container.js @@ -1,5 +1,5 @@ import path from "path-webpack"; -import {qs} from "./utils/core"; +import { qs } from "./utils/core"; /** * Handles Parsing and Accessing an Epub Container @@ -7,44 +7,44 @@ import {qs} from "./utils/core"; * @param {document} [containerDocument] xml document */ class Container { - constructor(containerDocument) { - this.packagePath = ''; - this.directory = ''; - this.encoding = ''; + constructor(containerDocument) { + this.packagePath = ""; + this.directory = ""; + this.encoding = ""; - if (containerDocument) { - this.parse(containerDocument); - } - } + if (containerDocument) { + this.parse(containerDocument); + } + } - /** - * Parse the Container XML - * @param {document} containerDocument - */ - parse(containerDocument){ - //-- - var rootfile; + /** + * Parse the Container XML + * @param {document} containerDocument + */ + parse(containerDocument) { + //-- + var rootfile; - if(!containerDocument) { - throw new Error("Container File Not Found"); - } + if (!containerDocument) { + throw new Error("Container File Not Found"); + } - rootfile = qs(containerDocument, "rootfile"); + rootfile = qs(containerDocument, "rootfile"); - if(!rootfile) { - throw new Error("No RootFile Found"); - } + if (!rootfile) { + throw new Error("No RootFile Found"); + } - this.packagePath = rootfile.getAttribute("full-path"); - this.directory = path.dirname(this.packagePath); - this.encoding = containerDocument.xmlEncoding; - } + this.packagePath = rootfile.getAttribute("full-path"); + this.directory = path.dirname(this.packagePath); + this.encoding = containerDocument.xmlEncoding; + } - destroy() { - this.packagePath = undefined; - this.directory = undefined; - this.encoding = undefined; - } + destroy() { + this.packagePath = undefined; + this.directory = undefined; + this.encoding = undefined; + } } export default Container; diff --git a/src/contents.js b/src/contents.js index 3effe72..c4c28c5 100644 --- a/src/contents.js +++ b/src/contents.js @@ -1,1262 +1,1293 @@ import EventEmitter from "event-emitter"; -import {isNumber, prefixed, borders, defaults} from "./utils/core"; import EpubCFI from "./epubcfi"; import Mapping from "./mapping"; -import {replaceLinks} from "./utils/replacements"; -import { EPUBJS_VERSION, EVENTS, DOM_EVENTS } from "./utils/constants"; +import { DOM_EVENTS, EPUBJS_VERSION, EVENTS } from "./utils/constants"; +import { borders, defaults, isNumber, prefixed } from "./utils/core"; +import { replaceLinks } from "./utils/replacements"; -const hasNavigator = typeof (navigator) !== "undefined"; +const hasNavigator = typeof navigator !== "undefined"; const isChrome = hasNavigator && /Chrome/.test(navigator.userAgent); -const isWebkit = hasNavigator && !isChrome && /AppleWebKit/.test(navigator.userAgent); +const isWebkit = + hasNavigator && !isChrome && /AppleWebKit/.test(navigator.userAgent); const ELEMENT_NODE = 1; -const TEXT_NODE = 3; /** - * Handles DOM manipulation, queries and events for View contents - * @class - * @param {document} doc Document - * @param {element} content Parent Element (typically Body) - * @param {string} cfiBase Section component of CFIs - * @param {number} sectionIndex Index in Spine of Conntent's Section - */ + * Handles DOM manipulation, queries and events for View contents + * @class + * @param {document} doc Document + * @param {element} content Parent Element (typically Body) + * @param {string} cfiBase Section component of CFIs + * @param {number} sectionIndex Index in Spine of Conntent's Section + */ class Contents { - constructor(doc, content, cfiBase, sectionIndex) { - // Blank Cfi for Parsing - this.epubcfi = new EpubCFI(); - - this.document = doc; - this.documentElement = this.document.documentElement; - this.content = content || this.document.body; - this.window = this.document.defaultView; - - this._size = { - width: 0, - height: 0 - }; - - this.sectionIndex = sectionIndex || 0; - this.cfiBase = cfiBase || ""; - - this.epubReadingSystem("epub.js", EPUBJS_VERSION); - this.called = 0; - this.active = true; - this.listeners(); - } - - /** - * Get DOM events that are listened for and passed along - */ - static get listenedEvents() { - return DOM_EVENTS; - } - - /** - * Get or Set width - * @param {number} [w] - * @returns {number} width - */ - width(w) { - // var frame = this.documentElement; - var frame = this.content; - - if (w && isNumber(w)) { - w = w + "px"; - } - - if (w) { - frame.style.width = w; - // this.content.style.width = w; - } - - return parseInt(this.window.getComputedStyle(frame)["width"]); - - - } - - /** - * Get or Set height - * @param {number} [h] - * @returns {number} height - */ - height(h) { - // var frame = this.documentElement; - var frame = this.content; - - if (h && isNumber(h)) { - h = h + "px"; - } - - if (h) { - frame.style.height = h; - // this.content.style.height = h; - } - - return parseInt(this.window.getComputedStyle(frame)["height"]); - - } - - /** - * Get or Set width of the contents - * @param {number} [w] - * @returns {number} width - */ - contentWidth(w) { - - var content = this.content || this.document.body; - - if (w && isNumber(w)) { - w = w + "px"; - } - - if (w) { - content.style.width = w; - } - - return parseInt(this.window.getComputedStyle(content)["width"]); - - - } - - /** - * Get or Set height of the contents - * @param {number} [h] - * @returns {number} height - */ - contentHeight(h) { - - var content = this.content || this.document.body; - - if (h && isNumber(h)) { - h = h + "px"; - } - - if (h) { - content.style.height = h; - } - - return parseInt(this.window.getComputedStyle(content)["height"]); - - } - - /** - * Get the width of the text using Range - * @returns {number} width - */ - textWidth() { - let rect; - let width; - let range = this.document.createRange(); - let content = this.content || this.document.body; - let border = borders(content); - - // Select the contents of frame - range.selectNodeContents(content); - - // get the width of the text content - rect = range.getBoundingClientRect(); - width = rect.width; - - if (border && border.width) { - width += border.width; - } - - return Math.round(width); - } - - /** - * Get the height of the text using Range - * @returns {number} height - */ - textHeight() { - let rect; - let height; - let range = this.document.createRange(); - let content = this.content || this.document.body; - - range.selectNodeContents(content); - - rect = range.getBoundingClientRect(); - height = rect.bottom; - - return Math.round(height); - } - - /** - * Get documentElement scrollWidth - * @returns {number} width - */ - scrollWidth() { - var width = this.documentElement.scrollWidth; - - return width; - } - - /** - * Get documentElement scrollHeight - * @returns {number} height - */ - scrollHeight() { - var height = this.documentElement.scrollHeight; - - return height; - } - - /** - * Set overflow css style of the contents - * @param {string} [overflow] - */ - overflow(overflow) { - - if (overflow) { - this.documentElement.style.overflow = overflow; - } - - return this.window.getComputedStyle(this.documentElement)["overflow"]; - } - - /** - * Set overflowX css style of the documentElement - * @param {string} [overflow] - */ - overflowX(overflow) { - - if (overflow) { - this.documentElement.style.overflowX = overflow; - } - - return this.window.getComputedStyle(this.documentElement)["overflowX"]; - } - - /** - * Set overflowY css style of the documentElement - * @param {string} [overflow] - */ - overflowY(overflow) { - - if (overflow) { - this.documentElement.style.overflowY = overflow; - } - - return this.window.getComputedStyle(this.documentElement)["overflowY"]; - } - - /** - * Set Css styles on the contents element (typically Body) - * @param {string} property - * @param {string} value - * @param {boolean} [priority] set as "important" - */ - css(property, value, priority) { - var content = this.content || this.document.body; - - if (value) { - content.style.setProperty(property, value, priority ? "important" : ""); - } else { - content.style.removeProperty(property); - } - - return this.window.getComputedStyle(content)[property]; - } - - /** - * Get or Set the viewport element - * @param {object} [options] - * @param {string} [options.width] - * @param {string} [options.height] - * @param {string} [options.scale] - * @param {string} [options.minimum] - * @param {string} [options.maximum] - * @param {string} [options.scalable] - */ - viewport(options) { - var _width, _height, _scale, _minimum, _maximum, _scalable; - // var width, height, scale, minimum, maximum, scalable; - var $viewport = this.document.querySelector("meta[name='viewport']"); - var parsed = { - "width": undefined, - "height": undefined, - "scale": undefined, - "minimum": undefined, - "maximum": undefined, - "scalable": undefined - }; - var newContent = []; - var settings = {}; - - /* - * check for the viewport size - * - */ - if($viewport && $viewport.hasAttribute("content")) { - let content = $viewport.getAttribute("content"); - let _width = content.match(/width\s*=\s*([^,]*)/); - let _height = content.match(/height\s*=\s*([^,]*)/); - let _scale = content.match(/initial-scale\s*=\s*([^,]*)/); - let _minimum = content.match(/minimum-scale\s*=\s*([^,]*)/); - let _maximum = content.match(/maximum-scale\s*=\s*([^,]*)/); - let _scalable = content.match(/user-scalable\s*=\s*([^,]*)/); - - if(_width && _width.length && typeof _width[1] !== "undefined"){ - parsed.width = _width[1]; - } - if(_height && _height.length && typeof _height[1] !== "undefined"){ - parsed.height = _height[1]; - } - if(_scale && _scale.length && typeof _scale[1] !== "undefined"){ - parsed.scale = _scale[1]; - } - if(_minimum && _minimum.length && typeof _minimum[1] !== "undefined"){ - parsed.minimum = _minimum[1]; - } - if(_maximum && _maximum.length && typeof _maximum[1] !== "undefined"){ - parsed.maximum = _maximum[1]; - } - if(_scalable && _scalable.length && typeof _scalable[1] !== "undefined"){ - parsed.scalable = _scalable[1]; - } - } - - settings = defaults(options || {}, parsed); - - if (options) { - if (settings.width) { - newContent.push("width=" + settings.width); - } - - if (settings.height) { - newContent.push("height=" + settings.height); - } - - if (settings.scale) { - newContent.push("initial-scale=" + settings.scale); - } - - if (settings.scalable === "no") { - newContent.push("minimum-scale=" + settings.scale); - newContent.push("maximum-scale=" + settings.scale); - newContent.push("user-scalable=" + settings.scalable); - } else { - - if (settings.scalable) { - newContent.push("user-scalable=" + settings.scalable); - } - - if (settings.minimum) { - newContent.push("minimum-scale=" + settings.minimum); - } - - if (settings.maximum) { - newContent.push("minimum-scale=" + settings.maximum); - } - } - - if (!$viewport) { - $viewport = this.document.createElement("meta"); - $viewport.setAttribute("name", "viewport"); - this.document.querySelector("head").appendChild($viewport); - } - - $viewport.setAttribute("content", newContent.join(", ")); - - this.window.scrollTo(0, 0); - } - - - return settings; - } - - /** - * Event emitter for when the contents has expanded - * @private - */ - expand() { - this.emit(EVENTS.CONTENTS.EXPAND); - } - - /** - * Add DOM listeners - * @private - */ - listeners() { - this.imageLoadListeners(); - - this.mediaQueryListeners(); - - // this.fontLoadListeners(); - - this.addEventListeners(); - - this.addSelectionListeners(); - - // this.transitionListeners(); - - if (typeof ResizeObserver === "undefined") { - this.resizeListeners(); - this.visibilityListeners(); - } else { - this.resizeObservers(); - } - - // this.mutationObservers(); - - this.linksHandler(); - } - - /** - * Remove DOM listeners - * @private - */ - removeListeners() { - - this.removeEventListeners(); - - this.removeSelectionListeners(); - - if (this.observer) { - this.observer.disconnect(); - } - - clearTimeout(this.expanding); - } - - /** - * Check if size of contents has changed and - * emit 'resize' event if it has. - * @private - */ - resizeCheck() { - let width = this.textWidth(); - let height = this.textHeight(); - - if (width != this._size.width || height != this._size.height) { - - this._size = { - width: width, - height: height - }; - - this.onResize && this.onResize(this._size); - this.emit(EVENTS.CONTENTS.RESIZE, this._size); - } - } - - /** - * Poll for resize detection - * @private - */ - resizeListeners() { - var width, height; - // Test size again - clearTimeout(this.expanding); - requestAnimationFrame(this.resizeCheck.bind(this)); - this.expanding = setTimeout(this.resizeListeners.bind(this), 350); - } - - /** - * Listen for visibility of tab to change - * @private - */ - visibilityListeners() { - document.addEventListener("visibilitychange", () => { - if (document.visibilityState === "visible" && this.active === false) { - this.active = true; - this.resizeListeners(); - } else { - this.active = false; - clearTimeout(this.expanding); - } - }); - } - - /** - * Use css transitions to detect resize - * @private - */ - transitionListeners() { - let body = this.content; - - body.style['transitionProperty'] = "font, font-size, font-size-adjust, font-stretch, font-variation-settings, font-weight, width, height"; - body.style['transitionDuration'] = "0.001ms"; - body.style['transitionTimingFunction'] = "linear"; - body.style['transitionDelay'] = "0"; - - this._resizeCheck = this.resizeCheck.bind(this); - this.document.addEventListener('transitionend', this._resizeCheck); - } - - /** - * Listen for media query changes and emit 'expand' event - * Adapted from: https://github.com/tylergaw/media-query-events/blob/master/js/mq-events.js - * @private - */ - mediaQueryListeners() { - var sheets = this.document.styleSheets; - var mediaChangeHandler = function(m){ - if(m.matches && !this._expanding) { - setTimeout(this.expand.bind(this), 1); - } - }.bind(this); - - for (var i = 0; i < sheets.length; i += 1) { - var rules; - // Firefox errors if we access cssRules cross-domain - try { - rules = sheets[i].cssRules; - } catch (e) { - return; - } - if(!rules) return; // Stylesheets changed - for (var j = 0; j < rules.length; j += 1) { - //if (rules[j].constructor === CSSMediaRule) { - if(rules[j].media){ - var mql = this.window.matchMedia(rules[j].media.mediaText); - mql.addListener(mediaChangeHandler); - //mql.onchange = mediaChangeHandler; - } - } - } - } - - /** - * Use ResizeObserver to listen for changes in the DOM and check for resize - * @private - */ - resizeObservers() { - // create an observer instance - this.observer = new ResizeObserver((e) => { - requestAnimationFrame(this.resizeCheck.bind(this)); - }); - - // pass in the target node - this.observer.observe(this.document.documentElement); - } - - /** - * Use MutationObserver to listen for changes in the DOM and check for resize - * @private - */ - mutationObservers() { - // create an observer instance - this.observer = new MutationObserver((mutations) => { - this.resizeCheck(); - }); - - // configuration of the observer: - let config = { attributes: true, childList: true, characterData: true, subtree: true }; - - // pass in the target node, as well as the observer options - this.observer.observe(this.document, config); - } - - /** - * Test if images are loaded or add listener for when they load - * @private - */ - imageLoadListeners() { - var images = this.document.querySelectorAll("img"); - var img; - for (var i = 0; i < images.length; i++) { - img = images[i]; - - if (typeof img.naturalWidth !== "undefined" && - img.naturalWidth === 0) { - img.onload = this.expand.bind(this); - } - } - } - - /** - * Listen for font load and check for resize when loaded - * @private - */ - fontLoadListeners() { - if (!this.document || !this.document.fonts) { - return; - } - - this.document.fonts.ready.then(function () { - this.resizeCheck(); - }.bind(this)); - - } - - /** - * Get the documentElement - * @returns {element} documentElement - */ - root() { - if(!this.document) return null; - return this.document.documentElement; - } - - /** - * Get the location offset of a EpubCFI or an #id - * @param {string | EpubCFI} target - * @param {string} [ignoreClass] for the cfi - * @returns { {left: Number, top: Number } - */ - locationOf(target, ignoreClass) { - var position; - var targetPos = {"left": 0, "top": 0}; - - if(!this.document) return targetPos; - - if(this.epubcfi.isCfiString(target)) { - let range = new EpubCFI(target).toRange(this.document, ignoreClass); - - if(range) { - try { - if (!range.endContainer || - (range.startContainer == range.endContainer - && range.startOffset == range.endOffset)) { - // If the end for the range is not set, it results in collapsed becoming - // true. This in turn leads to inconsistent behaviour when calling - // getBoundingRect. Wrong bounds lead to the wrong page being displayed. - // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15684911/ - let pos = range.startContainer.textContent.indexOf(" ", range.startOffset); - if (pos == -1) { - pos = range.startContainer.textContent.length; - } - range.setEnd(range.startContainer, pos); - } - } catch (e) { - console.error("setting end offset to start container length failed", e); - } - - if (range.startContainer.nodeType === Node.ELEMENT_NODE) { - position = range.startContainer.getBoundingClientRect(); - targetPos.left = position.left; - targetPos.top = position.top; - } else { - // Webkit does not handle collapsed range bounds correctly - // https://bugs.webkit.org/show_bug.cgi?id=138949 - - // Construct a new non-collapsed range - if (isWebkit) { - let container = range.startContainer; - let newRange = new Range(); - try { - if (container.nodeType === ELEMENT_NODE) { - position = container.getBoundingClientRect(); - } else if (range.startOffset + 2 < container.length) { - newRange.setStart(container, range.startOffset); - newRange.setEnd(container, range.startOffset + 2); - position = newRange.getBoundingClientRect(); - } else if (range.startOffset - 2 > 0) { - newRange.setStart(container, range.startOffset - 2); - newRange.setEnd(container, range.startOffset); - position = newRange.getBoundingClientRect(); - } else { // empty, return the parent element - position = container.parentNode.getBoundingClientRect(); - } - } catch (e) { - console.error(e, e.stack); - } - } else { - position = range.getBoundingClientRect(); - } - } - } - - } else if(typeof target === "string" && - target.indexOf("#") > -1) { - - let id = target.substring(target.indexOf("#")+1); - let el = this.document.getElementById(id); - if(el) { - if (isWebkit) { - // Webkit reports incorrect bounding rects in Columns - let newRange = new Range(); - newRange.selectNode(el); - position = newRange.getBoundingClientRect(); - } else { - position = el.getBoundingClientRect(); - } - } - } - - if (position) { - targetPos.left = position.left; - targetPos.top = position.top; - } - - return targetPos; - } - - /** - * Append a stylesheet link to the document head - * @param {string} src url - */ - addStylesheet(src) { - return new Promise(function(resolve, reject){ - var $stylesheet; - var ready = false; - - if(!this.document) { - resolve(false); - return; - } - - // Check if link already exists - $stylesheet = this.document.querySelector("link[href='"+src+"']"); - if ($stylesheet) { - resolve(true); - return; // already present - } - - $stylesheet = this.document.createElement("link"); - $stylesheet.type = "text/css"; - $stylesheet.rel = "stylesheet"; - $stylesheet.href = src; - $stylesheet.onload = $stylesheet.onreadystatechange = function() { - if ( !ready && (!this.readyState || this.readyState == "complete") ) { - ready = true; - // Let apply - setTimeout(() => { - resolve(true); - }, 1); - } - }; - - this.document.head.appendChild($stylesheet); - - }.bind(this)); - } - - _getStylesheetNode(key) { - var styleEl; - key = "epubjs-inserted-css-" + (key || ''); - - if(!this.document) return false; - - // Check if link already exists - styleEl = this.document.getElementById(key); - if (!styleEl) { - styleEl = this.document.createElement("style"); - styleEl.id = key; - // Append style element to head - this.document.head.appendChild(styleEl); - } - return styleEl; - } - - /** - * Append stylesheet css - * @param {string} serializedCss - * @param {string} key If the key is the same, the CSS will be replaced instead of inserted - */ - addStylesheetCss(serializedCss, key) { - if(!this.document || !serializedCss) return false; - - var styleEl; - styleEl = this._getStylesheetNode(key); - styleEl.innerHTML = serializedCss; - - return true; - } - - /** - * Append stylesheet rules to a generate stylesheet - * Array: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule - * Object: https://github.com/desirable-objects/json-to-css - * @param {array | object} rules - * @param {string} key If the key is the same, the CSS will be replaced instead of inserted - */ - addStylesheetRules(rules, key) { - var styleSheet; - - if(!this.document || !rules || rules.length === 0) return; - - // Grab style sheet - styleSheet = this._getStylesheetNode(key).sheet; - - if (Object.prototype.toString.call(rules) === "[object Array]") { - for (var i = 0, rl = rules.length; i < rl; i++) { - var j = 1, rule = rules[i], selector = rules[i][0], propStr = ""; - // If the second argument of a rule is an array of arrays, correct our variables. - if (Object.prototype.toString.call(rule[1][0]) === "[object Array]") { - rule = rule[1]; - j = 0; - } - - for (var pl = rule.length; j < pl; j++) { - var prop = rule[j]; - propStr += prop[0] + ":" + prop[1] + (prop[2] ? " !important" : "") + ";\n"; - } - - // Insert CSS Rule - styleSheet.insertRule(selector + "{" + propStr + "}", styleSheet.cssRules.length); - } - } else { - const selectors = Object.keys(rules); - selectors.forEach((selector) => { - const definition = rules[selector]; - if (Array.isArray(definition)) { - definition.forEach((item) => { - const _rules = Object.keys(item); - const result = _rules.map((rule) => { - return `${rule}:${item[rule]}`; - }).join(';'); - styleSheet.insertRule(`${selector}{${result}}`, styleSheet.cssRules.length); - }); - } else { - const _rules = Object.keys(definition); - const result = _rules.map((rule) => { - return `${rule}:${definition[rule]}`; - }).join(';'); - styleSheet.insertRule(`${selector}{${result}}`, styleSheet.cssRules.length); - } - }); - } - } - - /** - * Append a script tag to the document head - * @param {string} src url - * @returns {Promise} loaded - */ - addScript(src) { - - return new Promise(function(resolve, reject){ - var $script; - var ready = false; - - if(!this.document) { - resolve(false); - return; - } - - $script = this.document.createElement("script"); - $script.type = "text/javascript"; - $script.async = true; - $script.src = src; - $script.onload = $script.onreadystatechange = function() { - if ( !ready && (!this.readyState || this.readyState == "complete") ) { - ready = true; - setTimeout(function(){ - resolve(true); - }, 1); - } - }; - - this.document.head.appendChild($script); - - }.bind(this)); - } - - /** - * Add a class to the contents container - * @param {string} className - */ - addClass(className) { - var content; - - if(!this.document) return; - - content = this.content || this.document.body; - - if (content) { - content.classList.add(className); - } - - } - - /** - * Remove a class from the contents container - * @param {string} removeClass - */ - removeClass(className) { - var content; - - if(!this.document) return; - - content = this.content || this.document.body; - - if (content) { - content.classList.remove(className); - } - - } - - /** - * Add DOM event listeners - * @private - */ - addEventListeners(){ - if(!this.document) { - return; - } - - this._triggerEvent = this.triggerEvent.bind(this); - - DOM_EVENTS.forEach(function(eventName){ - this.document.addEventListener(eventName, this._triggerEvent, { passive: true }); - }, this); - - } - - /** - * Remove DOM event listeners - * @private - */ - removeEventListeners(){ - if(!this.document) { - return; - } - DOM_EVENTS.forEach(function(eventName){ - this.document.removeEventListener(eventName, this._triggerEvent, { passive: true }); - }, this); - this._triggerEvent = undefined; - } - - /** - * Emit passed browser events - * @private - */ - triggerEvent(e){ - this.emit(e.type, e); - } - - /** - * Add listener for text selection - * @private - */ - addSelectionListeners(){ - if(!this.document) { - return; - } - this._onSelectionChange = this.onSelectionChange.bind(this); - this.document.addEventListener("selectionchange", this._onSelectionChange, { passive: true }); - } - - /** - * Remove listener for text selection - * @private - */ - removeSelectionListeners(){ - if(!this.document) { - return; - } - this.document.removeEventListener("selectionchange", this._onSelectionChange, { passive: true }); - this._onSelectionChange = undefined; - } - - /** - * Handle getting text on selection - * @private - */ - onSelectionChange(e){ - if (this.selectionEndTimeout) { - clearTimeout(this.selectionEndTimeout); - } - this.selectionEndTimeout = setTimeout(function() { - var selection = this.window.getSelection(); - this.triggerSelectedEvent(selection); - }.bind(this), 250); - } - - /** - * Emit event on text selection - * @private - */ - triggerSelectedEvent(selection){ - var range, cfirange; - - if (selection && selection.rangeCount > 0) { - range = selection.getRangeAt(0); - if(!range.collapsed) { - // cfirange = this.section.cfiFromRange(range); - cfirange = new EpubCFI(range, this.cfiBase).toString(); - this.emit(EVENTS.CONTENTS.SELECTED, cfirange); - this.emit(EVENTS.CONTENTS.SELECTED_RANGE, range); - } - } - } - - /** - * Get a Dom Range from EpubCFI - * @param {EpubCFI} _cfi - * @param {string} [ignoreClass] - * @returns {Range} range - */ - range(_cfi, ignoreClass){ - var cfi = new EpubCFI(_cfi); - return cfi.toRange(this.document, ignoreClass); - } - - /** - * Get an EpubCFI from a Dom Range - * @param {Range} range - * @param {string} [ignoreClass] - * @returns {EpubCFI} cfi - */ - cfiFromRange(range, ignoreClass){ - return new EpubCFI(range, this.cfiBase, ignoreClass).toString(); - } - - /** - * Get an EpubCFI from a Dom node - * @param {node} node - * @param {string} [ignoreClass] - * @returns {EpubCFI} cfi - */ - cfiFromNode(node, ignoreClass){ - return new EpubCFI(node, this.cfiBase, ignoreClass).toString(); - } - - // TODO: find where this is used - remove? - map(layout){ - var map = new Mapping(layout); - return map.section(); - } - - /** - * Size the contents to a given width and height - * @param {number} [width] - * @param {number} [height] - */ - size(width, height){ - var viewport = { scale: 1.0, scalable: "no" }; - - this.layoutStyle("scrolling"); - - if (width >= 0) { - this.width(width); - viewport.width = width; - this.css("padding", "0 "+(width/12)+"px"); - } - - if (height >= 0) { - this.height(height); - viewport.height = height; - } - - this.css("margin", "0"); - this.css("box-sizing", "border-box"); - - - this.viewport(viewport); - } - - /** - * Apply columns to the contents for pagination - * @param {number} width - * @param {number} height - * @param {number} columnWidth - * @param {number} gap - */ - columns(width, height, columnWidth, gap, dir){ - let COLUMN_AXIS = prefixed("column-axis"); - let COLUMN_GAP = prefixed("column-gap"); - let COLUMN_WIDTH = prefixed("column-width"); - let COLUMN_FILL = prefixed("column-fill"); - - let writingMode = this.writingMode(); - let axis = (writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal"; - - this.layoutStyle("paginated"); - - if (dir === "rtl" && axis === "horizontal") { - this.direction(dir); - } - - this.width(width); - this.height(height); - - // Deal with Mobile trying to scale to viewport - this.viewport({ width: width, height: height, scale: 1.0, scalable: "no" }); - - // TODO: inline-block needs more testing - // Fixes Safari column cut offs, but causes RTL issues - // this.css("display", "inline-block"); - - this.css("overflow-y", "hidden"); - this.css("margin", "0", true); - - if (axis === "vertical") { - this.css("padding-top", (gap / 2) + "px", true); - this.css("padding-bottom", (gap / 2) + "px", true); - this.css("padding-left", "20px"); - this.css("padding-right", "20px"); - this.css(COLUMN_AXIS, "vertical"); - } else { - this.css("padding-top", "20px"); - this.css("padding-bottom", "20px"); - this.css("padding-left", (gap / 2) + "px", true); - this.css("padding-right", (gap / 2) + "px", true); - this.css(COLUMN_AXIS, "horizontal"); - } - - this.css("box-sizing", "border-box"); - this.css("max-width", "inherit"); - - this.css(COLUMN_FILL, "auto"); - - this.css(COLUMN_GAP, gap+"px"); - this.css(COLUMN_WIDTH, columnWidth+"px"); - - // Fix glyph clipping in WebKit - // https://github.com/futurepress/epub.js/issues/983 - this.css("-webkit-line-box-contain", "block glyphs replaced"); - } - - /** - * Scale contents from center - * @param {number} scale - * @param {number} offsetX - * @param {number} offsetY - */ - scaler(scale, offsetX, offsetY){ - var scaleStr = "scale(" + scale + ")"; - var translateStr = ""; - // this.css("position", "absolute")); - this.css("transform-origin", "top left"); - - if (offsetX >= 0 || offsetY >= 0) { - translateStr = " translate(" + (offsetX || 0 )+ "px, " + (offsetY || 0 )+ "px )"; - } - - this.css("transform", scaleStr + translateStr); - } - - /** - * Fit contents into a fixed width and height - * @param {number} width - * @param {number} height - */ - fit(width, height, section){ - var viewport = this.viewport(); - var viewportWidth = parseInt(viewport.width); - var viewportHeight = parseInt(viewport.height); - var widthScale = width / viewportWidth; - var heightScale = height / viewportHeight; - var scale = widthScale < heightScale ? widthScale : heightScale; - - // the translate does not work as intended, elements can end up unaligned - // var offsetY = (height - (viewportHeight * scale)) / 2; - // var offsetX = 0; - // if (this.sectionIndex % 2 === 1) { - // offsetX = width - (viewportWidth * scale); - // } - - this.layoutStyle("paginated"); - - // scale needs width and height to be set - this.width(viewportWidth); - this.height(viewportHeight); - this.overflow("hidden"); - - // Scale to the correct size - this.scaler(scale, 0, 0); - // this.scaler(scale, offsetX > 0 ? offsetX : 0, offsetY); - - // background images are not scaled by transform - this.css("background-size", viewportWidth * scale + "px " + viewportHeight * scale + "px"); - - this.css("background-color", "transparent"); - if (section && section.properties.includes("page-spread-left")) { - // set margin since scale is weird - var marginLeft = width - (viewportWidth * scale); - this.css("margin-left", marginLeft + "px"); - } - } - - /** - * Set the direction of the text - * @param {string} [dir="ltr"] "rtl" | "ltr" - */ - direction(dir) { - if (this.documentElement) { - this.documentElement.style["direction"] = dir; - } - } - - mapPage(cfiBase, layout, start, end, dev) { - var mapping = new Mapping(layout, dev); - - return mapping.page(this, cfiBase, start, end); - } - - /** - * Emit event when link in content is clicked - * @private - */ - linksHandler() { - replaceLinks(this.content, (href) => { - this.emit(EVENTS.CONTENTS.LINK_CLICKED, href); - }); - } - - /** - * Set the writingMode of the text - * @param {string} [mode="horizontal-tb"] "horizontal-tb" | "vertical-rl" | "vertical-lr" - */ - writingMode(mode) { - let WRITING_MODE = prefixed("writing-mode"); - - if (mode && this.documentElement) { - this.documentElement.style[WRITING_MODE] = mode; - } - - return this.window.getComputedStyle(this.documentElement)[WRITING_MODE] || ''; - } - - /** - * Set the layoutStyle of the content - * @param {string} [style="paginated"] "scrolling" | "paginated" - * @private - */ - layoutStyle(style) { - - if (style) { - this._layoutStyle = style; - navigator.epubReadingSystem.layoutStyle = this._layoutStyle; - } - - return this._layoutStyle || "paginated"; - } - - /** - * Add the epubReadingSystem object to the navigator - * @param {string} name - * @param {string} version - * @private - */ - epubReadingSystem(name, version) { - navigator.epubReadingSystem = { - name: name, - version: version, - layoutStyle: this.layoutStyle(), - hasFeature: function (feature) { - switch (feature) { - case "dom-manipulation": - return true; - case "layout-changes": - return true; - case "touch-events": - return true; - case "mouse-events": - return true; - case "keyboard-events": - return true; - case "spine-scripting": - return false; - default: - return false; - } - } - }; - return navigator.epubReadingSystem; - } - - destroy() { - // this.document.removeEventListener('transitionend', this._resizeCheck); - - this.removeListeners(); - - } + constructor(doc, content, cfiBase, sectionIndex) { + // Blank Cfi for Parsing + this.epubcfi = new EpubCFI(); + + this.document = doc; + this.documentElement = this.document.documentElement; + this.content = content || this.document.body; + this.window = this.document.defaultView; + + this._size = { + width: 0, + height: 0, + }; + + this.sectionIndex = sectionIndex || 0; + this.cfiBase = cfiBase || ""; + + this.epubReadingSystem("epub.js", EPUBJS_VERSION); + this.called = 0; + this.active = true; + this.listeners(); + } + + /** + * Get DOM events that are listened for and passed along + */ + static get listenedEvents() { + return DOM_EVENTS; + } + + /** + * Get or Set width + * @param {number} [w] + * @returns {number} width + */ + width(w) { + // var frame = this.documentElement; + var frame = this.content; + + if (w && isNumber(w)) { + w = w + "px"; + } + + if (w) { + frame.style.width = w; + // this.content.style.width = w; + } + + return parseInt(this.window.getComputedStyle(frame)["width"]); + } + + /** + * Get or Set height + * @param {number} [h] + * @returns {number} height + */ + height(h) { + // var frame = this.documentElement; + var frame = this.content; + + if (h && isNumber(h)) { + h = h + "px"; + } + + if (h) { + frame.style.height = h; + // this.content.style.height = h; + } + + return parseInt(this.window.getComputedStyle(frame)["height"]); + } + + /** + * Get or Set width of the contents + * @param {number} [w] + * @returns {number} width + */ + contentWidth(w) { + var content = this.content || this.document.body; + + if (w && isNumber(w)) { + w = w + "px"; + } + + if (w) { + content.style.width = w; + } + + return parseInt(this.window.getComputedStyle(content)["width"]); + } + + /** + * Get or Set height of the contents + * @param {number} [h] + * @returns {number} height + */ + contentHeight(h) { + var content = this.content || this.document.body; + + if (h && isNumber(h)) { + h = h + "px"; + } + + if (h) { + content.style.height = h; + } + + return parseInt(this.window.getComputedStyle(content)["height"]); + } + + /** + * Get the width of the text using Range + * @returns {number} width + */ + textWidth() { + let rect; + let width; + let range = this.document.createRange(); + let content = this.content || this.document.body; + let border = borders(content); + + // Select the contents of frame + range.selectNodeContents(content); + + // get the width of the text content + rect = range.getBoundingClientRect(); + width = rect.width; + + if (border && border.width) { + width += border.width; + } + + return Math.round(width); + } + + /** + * Get the height of the text using Range + * @returns {number} height + */ + textHeight() { + let rect; + let height; + let range = this.document.createRange(); + let content = this.content || this.document.body; + + range.selectNodeContents(content); + + rect = range.getBoundingClientRect(); + height = rect.bottom; + + return Math.round(height); + } + + /** + * Get documentElement scrollWidth + * @returns {number} width + */ + scrollWidth() { + var width = this.documentElement.scrollWidth; + + return width; + } + + /** + * Get documentElement scrollHeight + * @returns {number} height + */ + scrollHeight() { + var height = this.documentElement.scrollHeight; + + return height; + } + + /** + * Set overflow css style of the contents + * @param {string} [overflow] + */ + overflow(overflow) { + if (overflow) { + this.documentElement.style.overflow = overflow; + } + + return this.window.getComputedStyle(this.documentElement)["overflow"]; + } + + /** + * Set overflowX css style of the documentElement + * @param {string} [overflow] + */ + overflowX(overflow) { + if (overflow) { + this.documentElement.style.overflowX = overflow; + } + + return this.window.getComputedStyle(this.documentElement)["overflowX"]; + } + + /** + * Set overflowY css style of the documentElement + * @param {string} [overflow] + */ + overflowY(overflow) { + if (overflow) { + this.documentElement.style.overflowY = overflow; + } + + return this.window.getComputedStyle(this.documentElement)["overflowY"]; + } + + /** + * Set Css styles on the contents element (typically Body) + * @param {string} property + * @param {string} value + * @param {boolean} [priority] set as "important" + */ + css(property, value, priority) { + var content = this.content || this.document.body; + + if (value) { + content.style.setProperty(property, value, priority ? "important" : ""); + } else { + content.style.removeProperty(property); + } + + return this.window.getComputedStyle(content)[property]; + } + + /** + * Get or Set the viewport element + * @param {object} [options] + * @param {string} [options.width] + * @param {string} [options.height] + * @param {string} [options.scale] + * @param {string} [options.minimum] + * @param {string} [options.maximum] + * @param {string} [options.scalable] + */ + viewport(options) { + // var width, height, scale, minimum, maximum, scalable; + var $viewport = this.document.querySelector("meta[name='viewport']"); + var parsed = { + width: undefined, + height: undefined, + scale: undefined, + minimum: undefined, + maximum: undefined, + scalable: undefined, + }; + var newContent = []; + var settings = {}; + + /* + * check for the viewport size + * + */ + if ($viewport && $viewport.hasAttribute("content")) { + let content = $viewport.getAttribute("content"); + let _width = content.match(/width\s*=\s*([^,]*)/); + let _height = content.match(/height\s*=\s*([^,]*)/); + let _scale = content.match(/initial-scale\s*=\s*([^,]*)/); + let _minimum = content.match(/minimum-scale\s*=\s*([^,]*)/); + let _maximum = content.match(/maximum-scale\s*=\s*([^,]*)/); + let _scalable = content.match(/user-scalable\s*=\s*([^,]*)/); + + if (_width && _width.length && typeof _width[1] !== "undefined") { + parsed.width = _width[1]; + } + if (_height && _height.length && typeof _height[1] !== "undefined") { + parsed.height = _height[1]; + } + if (_scale && _scale.length && typeof _scale[1] !== "undefined") { + parsed.scale = _scale[1]; + } + if (_minimum && _minimum.length && typeof _minimum[1] !== "undefined") { + parsed.minimum = _minimum[1]; + } + if (_maximum && _maximum.length && typeof _maximum[1] !== "undefined") { + parsed.maximum = _maximum[1]; + } + if ( + _scalable && + _scalable.length && + typeof _scalable[1] !== "undefined" + ) { + parsed.scalable = _scalable[1]; + } + } + + settings = defaults(options || {}, parsed); + + if (options) { + if (settings.width) { + newContent.push("width=" + settings.width); + } + + if (settings.height) { + newContent.push("height=" + settings.height); + } + + if (settings.scale) { + newContent.push("initial-scale=" + settings.scale); + } + + if (settings.scalable === "no") { + newContent.push("minimum-scale=" + settings.scale); + newContent.push("maximum-scale=" + settings.scale); + newContent.push("user-scalable=" + settings.scalable); + } else { + if (settings.scalable) { + newContent.push("user-scalable=" + settings.scalable); + } + + if (settings.minimum) { + newContent.push("minimum-scale=" + settings.minimum); + } + + if (settings.maximum) { + newContent.push("minimum-scale=" + settings.maximum); + } + } + + if (!$viewport) { + $viewport = this.document.createElement("meta"); + $viewport.setAttribute("name", "viewport"); + this.document.querySelector("head").appendChild($viewport); + } + + $viewport.setAttribute("content", newContent.join(", ")); + + this.window.scrollTo(0, 0); + } + + return settings; + } + + /** + * Event emitter for when the contents has expanded + * @private + */ + expand() { + this.emit(EVENTS.CONTENTS.EXPAND); + } + + /** + * Add DOM listeners + * @private + */ + listeners() { + this.imageLoadListeners(); + + this.mediaQueryListeners(); + + // this.fontLoadListeners(); + + this.addEventListeners(); + + this.addSelectionListeners(); + + // this.transitionListeners(); + + if (typeof ResizeObserver === "undefined") { + this.resizeListeners(); + this.visibilityListeners(); + } else { + this.resizeObservers(); + } + + // this.mutationObservers(); + + this.linksHandler(); + } + + /** + * Remove DOM listeners + * @private + */ + removeListeners() { + this.removeEventListeners(); + + this.removeSelectionListeners(); + + if (this.observer) { + this.observer.disconnect(); + } + + clearTimeout(this.expanding); + } + + /** + * Check if size of contents has changed and + * emit 'resize' event if it has. + * @private + */ + resizeCheck() { + let width = this.textWidth(); + let height = this.textHeight(); + + if (width != this._size.width || height != this._size.height) { + this._size = { + width: width, + height: height, + }; + + this.onResize && this.onResize(this._size); + this.emit(EVENTS.CONTENTS.RESIZE, this._size); + } + } + + /** + * Poll for resize detection + * @private + */ + resizeListeners() { + // Test size again + clearTimeout(this.expanding); + requestAnimationFrame(this.resizeCheck.bind(this)); + this.expanding = setTimeout(this.resizeListeners.bind(this), 350); + } + + /** + * Listen for visibility of tab to change + * @private + */ + visibilityListeners() { + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "visible" && this.active === false) { + this.active = true; + this.resizeListeners(); + } else { + this.active = false; + clearTimeout(this.expanding); + } + }); + } + + /** + * Use css transitions to detect resize + * @private + */ + transitionListeners() { + let body = this.content; + + body.style["transitionProperty"] = + "font, font-size, font-size-adjust, font-stretch, font-variation-settings, font-weight, width, height"; + body.style["transitionDuration"] = "0.001ms"; + body.style["transitionTimingFunction"] = "linear"; + body.style["transitionDelay"] = "0"; + + this._resizeCheck = this.resizeCheck.bind(this); + this.document.addEventListener("transitionend", this._resizeCheck); + } + + /** + * Listen for media query changes and emit 'expand' event + * Adapted from: https://github.com/tylergaw/media-query-events/blob/master/js/mq-events.js + * @private + */ + mediaQueryListeners() { + var sheets = this.document.styleSheets; + var mediaChangeHandler = function (m) { + if (m.matches && !this._expanding) { + setTimeout(this.expand.bind(this), 1); + } + }.bind(this); + + for (var i = 0; i < sheets.length; i += 1) { + var rules; + // Firefox errors if we access cssRules cross-domain + try { + rules = sheets[i].cssRules; + } catch (e) { + return; + } + if (!rules) return; // Stylesheets changed + for (var j = 0; j < rules.length; j += 1) { + //if (rules[j].constructor === CSSMediaRule) { + if (rules[j].media) { + var mql = this.window.matchMedia(rules[j].media.mediaText); + mql.addListener(mediaChangeHandler); + //mql.onchange = mediaChangeHandler; + } + } + } + } + + /** + * Use ResizeObserver to listen for changes in the DOM and check for resize + * @private + */ + resizeObservers() { + // create an observer instance + this.observer = new ResizeObserver((e) => { + requestAnimationFrame(this.resizeCheck.bind(this)); + }); + + // pass in the target node + this.observer.observe(this.document.documentElement); + } + + /** + * Use MutationObserver to listen for changes in the DOM and check for resize + * @private + */ + mutationObservers() { + // create an observer instance + this.observer = new MutationObserver((mutations) => { + this.resizeCheck(); + }); + + // configuration of the observer: + let config = { + attributes: true, + childList: true, + characterData: true, + subtree: true, + }; + + // pass in the target node, as well as the observer options + this.observer.observe(this.document, config); + } + + /** + * Test if images are loaded or add listener for when they load + * @private + */ + imageLoadListeners() { + var images = this.document.querySelectorAll("img"); + var img; + for (var i = 0; i < images.length; i++) { + img = images[i]; + + if (typeof img.naturalWidth !== "undefined" && img.naturalWidth === 0) { + img.onload = this.expand.bind(this); + } + } + } + + /** + * Listen for font load and check for resize when loaded + * @private + */ + fontLoadListeners() { + if (!this.document || !this.document.fonts) { + return; + } + + this.document.fonts.ready.then( + function () { + this.resizeCheck(); + }.bind(this) + ); + } + + /** + * Get the documentElement + * @returns {element} documentElement + */ + root() { + if (!this.document) return null; + return this.document.documentElement; + } + + /** + * Get the location offset of a EpubCFI or an #id + * @param {string | EpubCFI} target + * @param {string} [ignoreClass] for the cfi + * @returns { {left: Number, top: Number } + */ + locationOf(target, ignoreClass) { + var position; + var targetPos = { left: 0, top: 0 }; + + if (!this.document) return targetPos; + + if (this.epubcfi.isCfiString(target)) { + let range = new EpubCFI(target).toRange(this.document, ignoreClass); + + if (range) { + try { + if ( + !range.endContainer || + (range.startContainer == range.endContainer && + range.startOffset == range.endOffset) + ) { + // If the end for the range is not set, it results in collapsed becoming + // true. This in turn leads to inconsistent behaviour when calling + // getBoundingRect. Wrong bounds lead to the wrong page being displayed. + // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15684911/ + let pos = range.startContainer.textContent.indexOf( + " ", + range.startOffset + ); + if (pos == -1) { + pos = range.startContainer.textContent.length; + } + range.setEnd(range.startContainer, pos); + } + } catch (e) { + console.error( + "setting end offset to start container length failed", + e + ); + } + + if (range.startContainer.nodeType === Node.ELEMENT_NODE) { + position = range.startContainer.getBoundingClientRect(); + targetPos.left = position.left; + targetPos.top = position.top; + } else { + // Webkit does not handle collapsed range bounds correctly + // https://bugs.webkit.org/show_bug.cgi?id=138949 + + // Construct a new non-collapsed range + if (isWebkit) { + let container = range.startContainer; + let newRange = new Range(); + try { + if (container.nodeType === ELEMENT_NODE) { + position = container.getBoundingClientRect(); + } else if (range.startOffset + 2 < container.length) { + newRange.setStart(container, range.startOffset); + newRange.setEnd(container, range.startOffset + 2); + position = newRange.getBoundingClientRect(); + } else if (range.startOffset - 2 > 0) { + newRange.setStart(container, range.startOffset - 2); + newRange.setEnd(container, range.startOffset); + position = newRange.getBoundingClientRect(); + } else { + // empty, return the parent element + position = container.parentNode.getBoundingClientRect(); + } + } catch (e) { + console.error(e, e.stack); + } + } else { + position = range.getBoundingClientRect(); + } + } + } + } else if (typeof target === "string" && target.indexOf("#") > -1) { + let id = target.substring(target.indexOf("#") + 1); + let el = this.document.getElementById(id); + if (el) { + if (isWebkit) { + // Webkit reports incorrect bounding rects in Columns + let newRange = new Range(); + newRange.selectNode(el); + position = newRange.getBoundingClientRect(); + } else { + position = el.getBoundingClientRect(); + } + } + } + + if (position) { + targetPos.left = position.left; + targetPos.top = position.top; + } + + return targetPos; + } + + /** + * Append a stylesheet link to the document head + * @param {string} src url + */ + addStylesheet(src) { + return new Promise( + function (resolve, reject) { + var $stylesheet; + var ready = false; + + if (!this.document) { + resolve(false); + return; + } + + // Check if link already exists + $stylesheet = this.document.querySelector("link[href='" + src + "']"); + if ($stylesheet) { + resolve(true); + return; // already present + } + + $stylesheet = this.document.createElement("link"); + $stylesheet.type = "text/css"; + $stylesheet.rel = "stylesheet"; + $stylesheet.href = src; + $stylesheet.onload = $stylesheet.onreadystatechange = function () { + if (!ready && (!this.readyState || this.readyState == "complete")) { + ready = true; + // Let apply + setTimeout(() => { + resolve(true); + }, 1); + } + }; + + this.document.head.appendChild($stylesheet); + }.bind(this) + ); + } + + _getStylesheetNode(key) { + var styleEl; + key = "epubjs-inserted-css-" + (key || ""); + + if (!this.document) return false; + + // Check if link already exists + styleEl = this.document.getElementById(key); + if (!styleEl) { + styleEl = this.document.createElement("style"); + styleEl.id = key; + // Append style element to head + this.document.head.appendChild(styleEl); + } + return styleEl; + } + + /** + * Append stylesheet css + * @param {string} serializedCss + * @param {string} key If the key is the same, the CSS will be replaced instead of inserted + */ + addStylesheetCss(serializedCss, key) { + if (!this.document || !serializedCss) return false; + + var styleEl; + styleEl = this._getStylesheetNode(key); + styleEl.innerHTML = serializedCss; + + return true; + } + + /** + * Append stylesheet rules to a generate stylesheet + * Array: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule + * Object: https://github.com/desirable-objects/json-to-css + * @param {array | object} rules + * @param {string} key If the key is the same, the CSS will be replaced instead of inserted + */ + addStylesheetRules(rules, key) { + var styleSheet; + + if (!this.document || !rules || rules.length === 0) return; + + // Grab style sheet + styleSheet = this._getStylesheetNode(key).sheet; + + if (Object.prototype.toString.call(rules) === "[object Array]") { + for (var i = 0, rl = rules.length; i < rl; i++) { + var j = 1, + rule = rules[i], + selector = rules[i][0], + propStr = ""; + // If the second argument of a rule is an array of arrays, correct our variables. + if (Object.prototype.toString.call(rule[1][0]) === "[object Array]") { + rule = rule[1]; + j = 0; + } + + for (var pl = rule.length; j < pl; j++) { + var prop = rule[j]; + propStr += + prop[0] + ":" + prop[1] + (prop[2] ? " !important" : "") + ";\n"; + } + + // Insert CSS Rule + styleSheet.insertRule( + selector + "{" + propStr + "}", + styleSheet.cssRules.length + ); + } + } else { + const selectors = Object.keys(rules); + selectors.forEach((selector) => { + const definition = rules[selector]; + if (Array.isArray(definition)) { + definition.forEach((item) => { + const _rules = Object.keys(item); + const result = _rules + .map((rule) => { + return `${rule}:${item[rule]}`; + }) + .join(";"); + styleSheet.insertRule( + `${selector}{${result}}`, + styleSheet.cssRules.length + ); + }); + } else { + const _rules = Object.keys(definition); + const result = _rules + .map((rule) => { + return `${rule}:${definition[rule]}`; + }) + .join(";"); + styleSheet.insertRule( + `${selector}{${result}}`, + styleSheet.cssRules.length + ); + } + }); + } + } + + /** + * Append a script tag to the document head + * @param {string} src url + * @returns {Promise} loaded + */ + addScript(src) { + return new Promise( + function (resolve, reject) { + var $script; + var ready = false; + + if (!this.document) { + resolve(false); + return; + } + + $script = this.document.createElement("script"); + $script.type = "text/javascript"; + $script.async = true; + $script.src = src; + $script.onload = $script.onreadystatechange = function () { + if (!ready && (!this.readyState || this.readyState == "complete")) { + ready = true; + setTimeout(function () { + resolve(true); + }, 1); + } + }; + + this.document.head.appendChild($script); + }.bind(this) + ); + } + + /** + * Add a class to the contents container + * @param {string} className + */ + addClass(className) { + var content; + + if (!this.document) return; + + content = this.content || this.document.body; + + if (content) { + content.classList.add(className); + } + } + + /** + * Remove a class from the contents container + * @param {string} removeClass + */ + removeClass(className) { + var content; + + if (!this.document) return; + + content = this.content || this.document.body; + + if (content) { + content.classList.remove(className); + } + } + + /** + * Add DOM event listeners + * @private + */ + addEventListeners() { + if (!this.document) { + return; + } + + this._triggerEvent = this.triggerEvent.bind(this); + + DOM_EVENTS.forEach(function (eventName) { + this.document.addEventListener(eventName, this._triggerEvent, { + passive: true, + }); + }, this); + } + + /** + * Remove DOM event listeners + * @private + */ + removeEventListeners() { + if (!this.document) { + return; + } + DOM_EVENTS.forEach(function (eventName) { + this.document.removeEventListener(eventName, this._triggerEvent, { + passive: true, + }); + }, this); + this._triggerEvent = undefined; + } + + /** + * Emit passed browser events + * @private + */ + triggerEvent(e) { + this.emit(e.type, e); + } + + /** + * Add listener for text selection + * @private + */ + addSelectionListeners() { + if (!this.document) { + return; + } + this._onSelectionChange = this.onSelectionChange.bind(this); + this.document.addEventListener("selectionchange", this._onSelectionChange, { + passive: true, + }); + } + + /** + * Remove listener for text selection + * @private + */ + removeSelectionListeners() { + if (!this.document) { + return; + } + this.document.removeEventListener( + "selectionchange", + this._onSelectionChange, + { passive: true } + ); + this._onSelectionChange = undefined; + } + + /** + * Handle getting text on selection + * @private + */ + onSelectionChange(e) { + if (this.selectionEndTimeout) { + clearTimeout(this.selectionEndTimeout); + } + this.selectionEndTimeout = setTimeout( + function () { + var selection = this.window.getSelection(); + this.triggerSelectedEvent(selection); + }.bind(this), + 250 + ); + } + + /** + * Emit event on text selection + * @private + */ + triggerSelectedEvent(selection) { + var range, cfirange; + + if (selection && selection.rangeCount > 0) { + range = selection.getRangeAt(0); + if (!range.collapsed) { + // cfirange = this.section.cfiFromRange(range); + cfirange = new EpubCFI(range, this.cfiBase).toString(); + this.emit(EVENTS.CONTENTS.SELECTED, cfirange); + this.emit(EVENTS.CONTENTS.SELECTED_RANGE, range); + } + } + } + + /** + * Get a Dom Range from EpubCFI + * @param {EpubCFI} _cfi + * @param {string} [ignoreClass] + * @returns {Range} range + */ + range(_cfi, ignoreClass) { + var cfi = new EpubCFI(_cfi); + return cfi.toRange(this.document, ignoreClass); + } + + /** + * Get an EpubCFI from a Dom Range + * @param {Range} range + * @param {string} [ignoreClass] + * @returns {EpubCFI} cfi + */ + cfiFromRange(range, ignoreClass) { + return new EpubCFI(range, this.cfiBase, ignoreClass).toString(); + } + + /** + * Get an EpubCFI from a Dom node + * @param {node} node + * @param {string} [ignoreClass] + * @returns {EpubCFI} cfi + */ + cfiFromNode(node, ignoreClass) { + return new EpubCFI(node, this.cfiBase, ignoreClass).toString(); + } + + // TODO: find where this is used - remove? + map(layout) { + var map = new Mapping(layout); + return map.section(); + } + + /** + * Size the contents to a given width and height + * @param {number} [width] + * @param {number} [height] + */ + size(width, height) { + var viewport = { scale: 1.0, scalable: "no" }; + + this.layoutStyle("scrolling"); + + if (width >= 0) { + this.width(width); + viewport.width = width; + this.css("padding", "0 " + width / 12 + "px"); + } + + if (height >= 0) { + this.height(height); + viewport.height = height; + } + + this.css("margin", "0"); + this.css("box-sizing", "border-box"); + + this.viewport(viewport); + } + + /** + * Apply columns to the contents for pagination + * @param {number} width + * @param {number} height + * @param {number} columnWidth + * @param {number} gap + */ + columns(width, height, columnWidth, gap, dir) { + let COLUMN_AXIS = prefixed("column-axis"); + let COLUMN_GAP = prefixed("column-gap"); + let COLUMN_WIDTH = prefixed("column-width"); + let COLUMN_FILL = prefixed("column-fill"); + + let writingMode = this.writingMode(); + let axis = + writingMode.indexOf("vertical") === 0 ? "vertical" : "horizontal"; + + this.layoutStyle("paginated"); + + if (dir === "rtl" && axis === "horizontal") { + this.direction(dir); + } + + this.width(width); + this.height(height); + + // Deal with Mobile trying to scale to viewport + this.viewport({ width: width, height: height, scale: 1.0, scalable: "no" }); + + // TODO: inline-block needs more testing + // Fixes Safari column cut offs, but causes RTL issues + // this.css("display", "inline-block"); + + this.css("overflow-y", "hidden"); + this.css("margin", "0", true); + + if (axis === "vertical") { + this.css("padding-top", gap / 2 + "px", true); + this.css("padding-bottom", gap / 2 + "px", true); + this.css("padding-left", "20px"); + this.css("padding-right", "20px"); + this.css(COLUMN_AXIS, "vertical"); + } else { + this.css("padding-top", "20px"); + this.css("padding-bottom", "20px"); + this.css("padding-left", gap / 2 + "px", true); + this.css("padding-right", gap / 2 + "px", true); + this.css(COLUMN_AXIS, "horizontal"); + } + + this.css("box-sizing", "border-box"); + this.css("max-width", "inherit"); + + this.css(COLUMN_FILL, "auto"); + + this.css(COLUMN_GAP, gap + "px"); + this.css(COLUMN_WIDTH, columnWidth + "px"); + + // Fix glyph clipping in WebKit + // https://github.com/futurepress/epub.js/issues/983 + this.css("-webkit-line-box-contain", "block glyphs replaced"); + } + + /** + * Scale contents from center + * @param {number} scale + * @param {number} offsetX + * @param {number} offsetY + */ + scaler(scale, offsetX, offsetY) { + var scaleStr = "scale(" + scale + ")"; + var translateStr = ""; + // this.css("position", "absolute")); + this.css("transform-origin", "top left"); + + if (offsetX >= 0 || offsetY >= 0) { + translateStr = + " translate(" + (offsetX || 0) + "px, " + (offsetY || 0) + "px )"; + } + + this.css("transform", scaleStr + translateStr); + } + + /** + * Fit contents into a fixed width and height + * @param {number} width + * @param {number} height + */ + fit(width, height, section) { + var viewport = this.viewport(); + var viewportWidth = parseInt(viewport.width); + var viewportHeight = parseInt(viewport.height); + var widthScale = width / viewportWidth; + var heightScale = height / viewportHeight; + var scale = widthScale < heightScale ? widthScale : heightScale; + + // the translate does not work as intended, elements can end up unaligned + // var offsetY = (height - (viewportHeight * scale)) / 2; + // var offsetX = 0; + // if (this.sectionIndex % 2 === 1) { + // offsetX = width - (viewportWidth * scale); + // } + + this.layoutStyle("paginated"); + + // scale needs width and height to be set + this.width(viewportWidth); + this.height(viewportHeight); + this.overflow("hidden"); + + // Scale to the correct size + this.scaler(scale, 0, 0); + // this.scaler(scale, offsetX > 0 ? offsetX : 0, offsetY); + + // background images are not scaled by transform + this.css( + "background-size", + viewportWidth * scale + "px " + viewportHeight * scale + "px" + ); + + this.css("background-color", "transparent"); + if (section && section.properties.includes("page-spread-left")) { + // set margin since scale is weird + var marginLeft = width - viewportWidth * scale; + this.css("margin-left", marginLeft + "px"); + } + } + + /** + * Set the direction of the text + * @param {string} [dir="ltr"] "rtl" | "ltr" + */ + direction(dir) { + if (this.documentElement) { + this.documentElement.style["direction"] = dir; + } + } + + mapPage(cfiBase, layout, start, end, dev) { + var mapping = new Mapping(layout, dev); + + return mapping.page(this, cfiBase, start, end); + } + + /** + * Emit event when link in content is clicked + * @private + */ + linksHandler() { + replaceLinks(this.content, (href) => { + this.emit(EVENTS.CONTENTS.LINK_CLICKED, href); + }); + } + + /** + * Set the writingMode of the text + * @param {string} [mode="horizontal-tb"] "horizontal-tb" | "vertical-rl" | "vertical-lr" + */ + writingMode(mode) { + let WRITING_MODE = prefixed("writing-mode"); + + if (mode && this.documentElement) { + this.documentElement.style[WRITING_MODE] = mode; + } + + return ( + this.window.getComputedStyle(this.documentElement)[WRITING_MODE] || "" + ); + } + + /** + * Set the layoutStyle of the content + * @param {string} [style="paginated"] "scrolling" | "paginated" + * @private + */ + layoutStyle(style) { + if (style) { + this._layoutStyle = style; + navigator.epubReadingSystem.layoutStyle = this._layoutStyle; + } + + return this._layoutStyle || "paginated"; + } + + /** + * Add the epubReadingSystem object to the navigator + * @param {string} name + * @param {string} version + * @private + */ + epubReadingSystem(name, version) { + navigator.epubReadingSystem = { + name: name, + version: version, + layoutStyle: this.layoutStyle(), + hasFeature: function (feature) { + switch (feature) { + case "dom-manipulation": + return true; + case "layout-changes": + return true; + case "touch-events": + return true; + case "mouse-events": + return true; + case "keyboard-events": + return true; + case "spine-scripting": + return false; + default: + return false; + } + }, + }; + return navigator.epubReadingSystem; + } + + destroy() { + // this.document.removeEventListener('transitionend', this._resizeCheck); + + this.removeListeners(); + } } EventEmitter(Contents.prototype); diff --git a/src/displayoptions.js b/src/displayoptions.js index a2793e2..aa04f78 100644 --- a/src/displayoptions.js +++ b/src/displayoptions.js @@ -1,4 +1,4 @@ -import {qs, qsa } from "./utils/core"; +import { qs, qsa } from "./utils/core"; /** * Open DisplayOptions Format Parser @@ -6,65 +6,65 @@ import {qs, qsa } from "./utils/core"; * @param {document} displayOptionsDocument XML */ class DisplayOptions { - constructor(displayOptionsDocument) { - this.interactive = ""; - this.fixedLayout = ""; - this.openToSpread = ""; - this.orientationLock = ""; + constructor(displayOptionsDocument) { + this.interactive = ""; + this.fixedLayout = ""; + this.openToSpread = ""; + this.orientationLock = ""; - if (displayOptionsDocument) { - this.parse(displayOptionsDocument); - } - } + if (displayOptionsDocument) { + this.parse(displayOptionsDocument); + } + } - /** - * Parse XML - * @param {document} displayOptionsDocument XML - * @return {DisplayOptions} self - */ - parse(displayOptionsDocument) { - if(!displayOptionsDocument) { - return this; - } + /** + * Parse XML + * @param {document} displayOptionsDocument XML + * @return {DisplayOptions} self + */ + parse(displayOptionsDocument) { + if (!displayOptionsDocument) { + return this; + } - const displayOptionsNode = qs(displayOptionsDocument, "display_options"); - if(!displayOptionsNode) { - return this; - } + const displayOptionsNode = qs(displayOptionsDocument, "display_options"); + if (!displayOptionsNode) { + return this; + } - const options = qsa(displayOptionsNode, "option"); - options.forEach((el) => { - let value = ""; + const options = qsa(displayOptionsNode, "option"); + options.forEach((el) => { + let value = ""; - if (el.childNodes.length) { - value = el.childNodes[0].nodeValue; - } + if (el.childNodes.length) { + value = el.childNodes[0].nodeValue; + } - switch (el.attributes.name.value) { - case "interactive": - this.interactive = value; - break; - case "fixed-layout": - this.fixedLayout = value; - break; - case "open-to-spread": - this.openToSpread = value; - break; - case "orientation-lock": - this.orientationLock = value; - break; - } - }); + switch (el.attributes.name.value) { + case "interactive": + this.interactive = value; + break; + case "fixed-layout": + this.fixedLayout = value; + break; + case "open-to-spread": + this.openToSpread = value; + break; + case "orientation-lock": + this.orientationLock = value; + break; + } + }); - return this; - } + return this; + } - destroy() { - this.interactive = undefined; - this.fixedLayout = undefined; - this.openToSpread = undefined; - this.orientationLock = undefined; - } + destroy() { + this.interactive = undefined; + this.fixedLayout = undefined; + this.openToSpread = undefined; + this.orientationLock = undefined; + } } export default DisplayOptions; diff --git a/src/epub.js b/src/epub.js index e6fcdfb..cd02fd1 100644 --- a/src/epub.js +++ b/src/epub.js @@ -1,13 +1,9 @@ import Book from "./book"; -import Rendition from "./rendition"; -import CFI from "./epubcfi"; import Contents from "./contents"; -import * as utils from "./utils/core"; +import CFI from "./epubcfi"; +import Rendition from "./rendition"; import { EPUBJS_VERSION } from "./utils/constants"; - -import IframeView from "./managers/views/iframe"; -import DefaultViewManager from "./managers/default"; -import ContinuousViewManager from "./managers/continuous"; +import * as utils from "./utils/core"; /** * Creates a new Book @@ -17,13 +13,13 @@ import ContinuousViewManager from "./managers/continuous"; * @example ePub("/path/to/book.epub", {}) */ function ePub(url, options) { - return new Book(url, options); + return new Book(url, options); } ePub.VERSION = EPUBJS_VERSION; -if (typeof(global) !== "undefined") { - global.EPUBJS_VERSION = EPUBJS_VERSION; +if (typeof global !== "undefined") { + global.EPUBJS_VERSION = EPUBJS_VERSION; } ePub.Book = Book; diff --git a/src/epubcfi.js b/src/epubcfi.js index 98a6b90..e7dd442 100644 --- a/src/epubcfi.js +++ b/src/epubcfi.js @@ -1,8 +1,13 @@ -import {extend, type, findChildren, RangeObject, isNumber} from "./utils/core"; +import { + RangeObject, + extend, + findChildren, + isNumber, + type, +} from "./utils/core"; const ELEMENT_NODE = 1; const TEXT_NODE = 3; -const COMMENT_NODE = 8; const DOCUMENT_NODE = 9; /** @@ -23,771 +28,759 @@ const DOCUMENT_NODE = 9; @param {string} [ignoreClass] class to ignore when parsing DOM */ class EpubCFI { - constructor(cfiFrom, base, ignoreClass){ - var type; - - this.str = ""; - - this.base = {}; - this.spinePos = 0; // For compatibility - - this.range = false; // true || false; - - this.path = {}; - this.start = null; - this.end = null; - - // Allow instantiation without the "new" keyword - if (!(this instanceof EpubCFI)) { - return new EpubCFI(cfiFrom, base, ignoreClass); - } - - if(typeof base === "string") { - this.base = this.parseComponent(base); - } else if(typeof base === "object" && base.steps) { - this.base = base; - } - - type = this.checkType(cfiFrom); - - - if(type === "string") { - this.str = cfiFrom; - return extend(this, this.parse(cfiFrom)); - } else if (type === "range") { - return extend(this, this.fromRange(cfiFrom, this.base, ignoreClass)); - } else if (type === "node") { - return extend(this, this.fromNode(cfiFrom, this.base, ignoreClass)); - } else if (type === "EpubCFI" && cfiFrom.path) { - return cfiFrom; - } else if (!cfiFrom) { - return this; - } else { - throw new TypeError("not a valid argument for EpubCFI"); - } - - } - - /** - * Check the type of constructor input - * @private - */ - checkType(cfi) { - - if (this.isCfiString(cfi)) { - return "string"; - // Is a range object - } else if (cfi && typeof cfi === "object" && (type(cfi) === "Range" || typeof(cfi.startContainer) != "undefined")){ - return "range"; - } else if (cfi && typeof cfi === "object" && typeof(cfi.nodeType) != "undefined" ){ // || typeof cfi === "function" - return "node"; - } else if (cfi && typeof cfi === "object" && cfi instanceof EpubCFI){ - return "EpubCFI"; - } else { - return false; - } - } - - /** - * Parse a cfi string to a CFI object representation - * @param {string} cfiStr - * @returns {object} cfi - */ - parse(cfiStr) { - var cfi = { - spinePos: -1, - range: false, - base: {}, - path: {}, - start: null, - end: null - }; - var baseComponent, pathComponent, range; - - if(typeof cfiStr !== "string") { - return {spinePos: -1}; - } - - if(cfiStr.indexOf("epubcfi(") === 0 && cfiStr[cfiStr.length-1] === ")") { - // Remove initial epubcfi( and ending ) - cfiStr = cfiStr.slice(8, cfiStr.length-1); - } - - baseComponent = this.getChapterComponent(cfiStr); - - // Make sure this is a valid cfi or return - if(!baseComponent) { - return {spinePos: -1}; - } - - cfi.base = this.parseComponent(baseComponent); - - pathComponent = this.getPathComponent(cfiStr); - cfi.path = this.parseComponent(pathComponent); - - range = this.getRange(cfiStr); - - if(range) { - cfi.range = true; - cfi.start = this.parseComponent(range[0]); - cfi.end = this.parseComponent(range[1]); - } - - // Get spine node position - // cfi.spineSegment = cfi.base.steps[1]; - - // Chapter segment is always the second step - cfi.spinePos = cfi.base.steps[1].index; - - return cfi; - } - - parseComponent(componentStr){ - var component = { - steps: [], - terminal: { - offset: null, - assertion: null - } - }; - var parts = componentStr.split(":"); - var steps = parts[0].split("/"); - var terminal; - - if(parts.length > 1) { - terminal = parts[1]; - component.terminal = this.parseTerminal(terminal); - } - - if (steps[0] === "") { - steps.shift(); // Ignore the first slash - } - - component.steps = steps.map(function(step){ - return this.parseStep(step); - }.bind(this)); - - return component; - } - - parseStep(stepStr){ - var type, num, index, has_brackets, id; - - has_brackets = stepStr.match(/\[(.*)\]/); - if(has_brackets && has_brackets[1]){ - id = has_brackets[1]; - } - - //-- Check if step is a text node or element - num = parseInt(stepStr); - - if(isNaN(num)) { - return; - } - - if(num % 2 === 0) { // Even = is an element - type = "element"; - index = num / 2 - 1; - } else { - type = "text"; - index = (num - 1 ) / 2; - } - - return { - "type" : type, - "index" : index, - "id" : id || null - }; - } - - parseTerminal(termialStr){ - var characterOffset, textLocationAssertion; - var assertion = termialStr.match(/\[(.*)\]/); - - if(assertion && assertion[1]){ - characterOffset = parseInt(termialStr.split("[")[0]); - textLocationAssertion = assertion[1]; - } else { - characterOffset = parseInt(termialStr); - } - - if (!isNumber(characterOffset)) { - characterOffset = null; - } - - return { - "offset": characterOffset, - "assertion": textLocationAssertion - }; - - } - - getChapterComponent(cfiStr) { - - var indirection = cfiStr.split("!"); - - return indirection[0]; - } - - getPathComponent(cfiStr) { - - var indirection = cfiStr.split("!"); - - if(indirection[1]) { - let ranges = indirection[1].split(","); - return ranges[0]; - } - - } - - getRange(cfiStr) { - - var ranges = cfiStr.split(","); - - if(ranges.length === 3){ - return [ - ranges[1], - ranges[2] - ]; - } - - return false; - } - - getCharecterOffsetComponent(cfiStr) { - var splitStr = cfiStr.split(":"); - return splitStr[1] || ""; - } - - joinSteps(steps) { - if(!steps) { - return ""; - } - - return steps.map(function(part){ - var segment = ""; - - if(part.type === "element") { - segment += (part.index + 1) * 2; - } - - if(part.type === "text") { - segment += 1 + (2 * part.index); // TODO: double check that this is odd - } - - if(part.id) { - segment += "[" + part.id + "]"; - } - - return segment; - - }).join("/"); - - } - - segmentString(segment) { - var segmentString = "/"; - - segmentString += this.joinSteps(segment.steps); - - if(segment.terminal && segment.terminal.offset != null){ - segmentString += ":" + segment.terminal.offset; - } - - if(segment.terminal && segment.terminal.assertion != null){ - segmentString += "[" + segment.terminal.assertion + "]"; - } - - return segmentString; - } - - /** - * Convert CFI to a epubcfi(...) string - * @returns {string} epubcfi - */ - toString() { - var cfiString = "epubcfi("; - - cfiString += this.segmentString(this.base); - - cfiString += "!"; - cfiString += this.segmentString(this.path); - - // Add Range, if present - if(this.range && this.start) { - cfiString += ","; - cfiString += this.segmentString(this.start); - } - - if(this.range && this.end) { - cfiString += ","; - cfiString += this.segmentString(this.end); - } - - cfiString += ")"; - - return cfiString; - } - - - /** - * Compare which of two CFIs is earlier in the text - * @returns {number} First is earlier = -1, Second is earlier = 1, They are equal = 0 - */ - compare(cfiOne, cfiTwo) { - var stepsA, stepsB; - var terminalA, terminalB; - - var rangeAStartSteps, rangeAEndSteps; - var rangeBEndSteps, rangeBEndSteps; - var rangeAStartTerminal, rangeAEndTerminal; - var rangeBStartTerminal, rangeBEndTerminal; - - if(typeof cfiOne === "string") { - cfiOne = new EpubCFI(cfiOne); - } - if(typeof cfiTwo === "string") { - cfiTwo = new EpubCFI(cfiTwo); - } - // Compare Spine Positions - if(cfiOne.spinePos > cfiTwo.spinePos) { - return 1; - } - if(cfiOne.spinePos < cfiTwo.spinePos) { - return -1; - } - - if (cfiOne.range) { - stepsA = cfiOne.path.steps.concat(cfiOne.start.steps); - terminalA = cfiOne.start.terminal; - } else { - stepsA = cfiOne.path.steps; - terminalA = cfiOne.path.terminal; - } - - if (cfiTwo.range) { - stepsB = cfiTwo.path.steps.concat(cfiTwo.start.steps); - terminalB = cfiTwo.start.terminal; - } else { - stepsB = cfiTwo.path.steps; - terminalB = cfiTwo.path.terminal; - } - - // Compare Each Step in the First item - for (var i = 0; i < stepsA.length; i++) { - if(!stepsA[i]) { - return -1; - } - if(!stepsB[i]) { - return 1; - } - if(stepsA[i].index > stepsB[i].index) { - return 1; - } - if(stepsA[i].index < stepsB[i].index) { - return -1; - } - // Otherwise continue checking - } - - // All steps in First equal to Second and First is Less Specific - if(stepsA.length < stepsB.length) { - return -1; - } - - // Compare the character offset of the text node - if(terminalA.offset > terminalB.offset) { - return 1; - } - if(terminalA.offset < terminalB.offset) { - return -1; - } - - // CFI's are equal - return 0; - } - - step(node) { - var nodeType = (node.nodeType === TEXT_NODE) ? "text" : "element"; - - return { - "id" : node.id, - "tagName" : node.tagName, - "type" : nodeType, - "index" : this.position(node) - }; - } - - filteredStep(node, ignoreClass) { - var filteredNode = this.filter(node, ignoreClass); - var nodeType; - - // Node filtered, so ignore - if (!filteredNode) { - return; - } - - // Otherwise add the filter node in - nodeType = (filteredNode.nodeType === TEXT_NODE) ? "text" : "element"; - - return { - "id" : filteredNode.id, - "tagName" : filteredNode.tagName, - "type" : nodeType, - "index" : this.filteredPosition(filteredNode, ignoreClass) - }; - } - - pathTo(node, offset, ignoreClass) { - var segment = { - steps: [], - terminal: { - offset: null, - assertion: null - } - }; - var currentNode = node; - var step; - - while(currentNode && currentNode.parentNode && - currentNode.parentNode.nodeType != DOCUMENT_NODE) { - - if (ignoreClass) { - step = this.filteredStep(currentNode, ignoreClass); - } else { - step = this.step(currentNode); - } - - if (step) { - segment.steps.unshift(step); - } - - currentNode = currentNode.parentNode; - - } - - if (offset != null && offset >= 0) { - - segment.terminal.offset = offset; - - // Make sure we are getting to a textNode if there is an offset - if(segment.steps[segment.steps.length-1].type != "text") { - segment.steps.push({ - "type" : "text", - "index" : 0 - }); - } - - } - - - return segment; - } - - equalStep(stepA, stepB) { - if (!stepA || !stepB) { - return false; - } - - if(stepA.index === stepB.index && - stepA.id === stepB.id && - stepA.type === stepB.type) { - return true; - } - - return false; - } - - /** - * Create a CFI object from a Range - * @param {Range} range - * @param {string | object} base - * @param {string} [ignoreClass] - * @returns {object} cfi - */ - fromRange(range, base, ignoreClass) { - var cfi = { - range: false, - base: {}, - path: {}, - start: null, - end: null - }; - - var start = range.startContainer; - var end = range.endContainer; - - var startOffset = range.startOffset; - var endOffset = range.endOffset; - - var needsIgnoring = false; - - if (ignoreClass) { - // Tell pathTo if / what to ignore - needsIgnoring = (start.ownerDocument.querySelector("." + ignoreClass) != null); - } - - - if (typeof base === "string") { - cfi.base = this.parseComponent(base); - cfi.spinePos = cfi.base.steps[1].index; - } else if (typeof base === "object") { - cfi.base = base; - } - - if (range.collapsed) { - if (needsIgnoring) { - startOffset = this.patchOffset(start, startOffset, ignoreClass); - } - cfi.path = this.pathTo(start, startOffset, ignoreClass); - } else { - cfi.range = true; - - if (needsIgnoring) { - startOffset = this.patchOffset(start, startOffset, ignoreClass); - } - - cfi.start = this.pathTo(start, startOffset, ignoreClass); - if (needsIgnoring) { - endOffset = this.patchOffset(end, endOffset, ignoreClass); - } - - cfi.end = this.pathTo(end, endOffset, ignoreClass); - - // Create a new empty path - cfi.path = { - steps: [], - terminal: null - }; - - // Push steps that are shared between start and end to the common path - var len = cfi.start.steps.length; - var i; - - for (i = 0; i < len; i++) { - if (this.equalStep(cfi.start.steps[i], cfi.end.steps[i])) { - if(i === len-1) { - // Last step is equal, check terminals - if(cfi.start.terminal === cfi.end.terminal) { - // CFI's are equal - cfi.path.steps.push(cfi.start.steps[i]); - // Not a range - cfi.range = false; - } - } else { - cfi.path.steps.push(cfi.start.steps[i]); - } - - } else { - break; - } - } - - cfi.start.steps = cfi.start.steps.slice(cfi.path.steps.length); - cfi.end.steps = cfi.end.steps.slice(cfi.path.steps.length); - - // TODO: Add Sanity check to make sure that the end if greater than the start - } - - return cfi; - } - - /** - * Create a CFI object from a Node - * @param {Node} anchor - * @param {string | object} base - * @param {string} [ignoreClass] - * @returns {object} cfi - */ - fromNode(anchor, base, ignoreClass) { - var cfi = { - range: false, - base: {}, - path: {}, - start: null, - end: null - }; - - if (typeof base === "string") { - cfi.base = this.parseComponent(base); - cfi.spinePos = cfi.base.steps[1].index; - } else if (typeof base === "object") { - cfi.base = base; - } - - cfi.path = this.pathTo(anchor, null, ignoreClass); - - return cfi; - } - - filter(anchor, ignoreClass) { - var needsIgnoring; - var sibling; // to join with - var parent, previousSibling, nextSibling; - var isText = false; - - if (anchor.nodeType === TEXT_NODE) { - isText = true; - parent = anchor.parentNode; - needsIgnoring = anchor.parentNode.classList.contains(ignoreClass); - } else { - isText = false; - needsIgnoring = anchor.classList.contains(ignoreClass); - } - - if (needsIgnoring && isText) { - previousSibling = parent.previousSibling; - nextSibling = parent.nextSibling; - - // If the sibling is a text node, join the nodes - if (previousSibling && previousSibling.nodeType === TEXT_NODE) { - sibling = previousSibling; - } else if (nextSibling && nextSibling.nodeType === TEXT_NODE) { - sibling = nextSibling; - } - - if (sibling) { - return sibling; - } else { - // Parent will be ignored on next step - return anchor; - } - - } else if (needsIgnoring && !isText) { - // Otherwise just skip the element node - return false; - } else { - // No need to filter - return anchor; - } - - } - - patchOffset(anchor, offset, ignoreClass) { - if (anchor.nodeType != TEXT_NODE) { - throw new Error("Anchor must be a text node"); - } - - var curr = anchor; - var totalOffset = offset; - - // If the parent is a ignored node, get offset from it's start - if (anchor.parentNode.classList.contains(ignoreClass)) { - curr = anchor.parentNode; - } - - while (curr.previousSibling) { - if(curr.previousSibling.nodeType === ELEMENT_NODE) { - // Originally a text node, so join - if(curr.previousSibling.classList.contains(ignoreClass)){ - totalOffset += curr.previousSibling.textContent.length; - } else { - break; // Normal node, dont join - } - } else { - // If the previous sibling is a text node, join the nodes - totalOffset += curr.previousSibling.textContent.length; - } - - curr = curr.previousSibling; - } - - return totalOffset; - - } - - normalizedMap(children, nodeType, ignoreClass) { - var output = {}; - var prevIndex = -1; - var i, len = children.length; - var currNodeType; - var prevNodeType; - - for (i = 0; i < len; i++) { - - currNodeType = children[i].nodeType; - - // Check if needs ignoring - if (currNodeType === ELEMENT_NODE && - children[i].classList.contains(ignoreClass)) { - currNodeType = TEXT_NODE; - } - - if (i > 0 && - currNodeType === TEXT_NODE && - prevNodeType === TEXT_NODE) { - // join text nodes - output[i] = prevIndex; - } else if (nodeType === currNodeType){ - prevIndex = prevIndex + 1; - output[i] = prevIndex; - } - - prevNodeType = currNodeType; - - } - - return output; - } - - position(anchor) { - var children, index; - if (anchor.nodeType === ELEMENT_NODE) { - children = anchor.parentNode.children; - if (!children) { - children = findChildren(anchor.parentNode); - } - index = Array.prototype.indexOf.call(children, anchor); - } else { - children = this.textNodes(anchor.parentNode); - index = children.indexOf(anchor); - } - - return index; - } - - filteredPosition(anchor, ignoreClass) { - var children, index, map; - - if (anchor.nodeType === ELEMENT_NODE) { - children = anchor.parentNode.children; - map = this.normalizedMap(children, ELEMENT_NODE, ignoreClass); - } else { - children = anchor.parentNode.childNodes; - // Inside an ignored node - if(anchor.parentNode.classList.contains(ignoreClass)) { - anchor = anchor.parentNode; - children = anchor.parentNode.childNodes; - } - map = this.normalizedMap(children, TEXT_NODE, ignoreClass); - } - - - index = Array.prototype.indexOf.call(children, anchor); - - return map[index]; - } - - stepsToXpath(steps) { - var xpath = [".", "*"]; - - steps.forEach(function(step){ - var position = step.index + 1; - - if(step.id){ - xpath.push("*[position()=" + position + " and @id='" + step.id + "']"); - } else if(step.type === "text") { - xpath.push("text()[" + position + "]"); - } else { - xpath.push("*[" + position + "]"); - } - }); - - return xpath.join("/"); - } - - - /* + constructor(cfiFrom, base, ignoreClass) { + var type; + + this.str = ""; + + this.base = {}; + this.spinePos = 0; // For compatibility + + this.range = false; // true || false; + + this.path = {}; + this.start = null; + this.end = null; + + // Allow instantiation without the "new" keyword + if (!(this instanceof EpubCFI)) { + return new EpubCFI(cfiFrom, base, ignoreClass); + } + + if (typeof base === "string") { + this.base = this.parseComponent(base); + } else if (typeof base === "object" && base.steps) { + this.base = base; + } + + type = this.checkType(cfiFrom); + + if (type === "string") { + this.str = cfiFrom; + return extend(this, this.parse(cfiFrom)); + } else if (type === "range") { + return extend(this, this.fromRange(cfiFrom, this.base, ignoreClass)); + } else if (type === "node") { + return extend(this, this.fromNode(cfiFrom, this.base, ignoreClass)); + } else if (type === "EpubCFI" && cfiFrom.path) { + return cfiFrom; + } else if (!cfiFrom) { + return this; + } else { + throw new TypeError("not a valid argument for EpubCFI"); + } + } + + /** + * Check the type of constructor input + * @private + */ + checkType(cfi) { + if (this.isCfiString(cfi)) { + return "string"; + // Is a range object + } else if ( + cfi && + typeof cfi === "object" && + (type(cfi) === "Range" || typeof cfi.startContainer != "undefined") + ) { + return "range"; + } else if ( + cfi && + typeof cfi === "object" && + typeof cfi.nodeType != "undefined" + ) { + // || typeof cfi === "function" + return "node"; + } else if (cfi && typeof cfi === "object" && cfi instanceof EpubCFI) { + return "EpubCFI"; + } else { + return false; + } + } + + /** + * Parse a cfi string to a CFI object representation + * @param {string} cfiStr + * @returns {object} cfi + */ + parse(cfiStr) { + var cfi = { + spinePos: -1, + range: false, + base: {}, + path: {}, + start: null, + end: null, + }; + var baseComponent, pathComponent, range; + + if (typeof cfiStr !== "string") { + return { spinePos: -1 }; + } + + if (cfiStr.indexOf("epubcfi(") === 0 && cfiStr[cfiStr.length - 1] === ")") { + // Remove initial epubcfi( and ending ) + cfiStr = cfiStr.slice(8, cfiStr.length - 1); + } + + baseComponent = this.getChapterComponent(cfiStr); + + // Make sure this is a valid cfi or return + if (!baseComponent) { + return { spinePos: -1 }; + } + + cfi.base = this.parseComponent(baseComponent); + + pathComponent = this.getPathComponent(cfiStr); + cfi.path = this.parseComponent(pathComponent); + + range = this.getRange(cfiStr); + + if (range) { + cfi.range = true; + cfi.start = this.parseComponent(range[0]); + cfi.end = this.parseComponent(range[1]); + } + + // Get spine node position + // cfi.spineSegment = cfi.base.steps[1]; + + // Chapter segment is always the second step + cfi.spinePos = cfi.base.steps[1].index; + + return cfi; + } + + parseComponent(componentStr) { + var component = { + steps: [], + terminal: { + offset: null, + assertion: null, + }, + }; + var parts = componentStr.split(":"); + var steps = parts[0].split("/"); + var terminal; + + if (parts.length > 1) { + terminal = parts[1]; + component.terminal = this.parseTerminal(terminal); + } + + if (steps[0] === "") { + steps.shift(); // Ignore the first slash + } + + component.steps = steps.map( + function (step) { + return this.parseStep(step); + }.bind(this) + ); + + return component; + } + + parseStep(stepStr) { + var type, num, index, has_brackets, id; + + has_brackets = stepStr.match(/\[(.*)\]/); + if (has_brackets && has_brackets[1]) { + id = has_brackets[1]; + } + + //-- Check if step is a text node or element + num = parseInt(stepStr); + + if (isNaN(num)) { + return; + } + + if (num % 2 === 0) { + // Even = is an element + type = "element"; + index = num / 2 - 1; + } else { + type = "text"; + index = (num - 1) / 2; + } + + return { + type: type, + index: index, + id: id || null, + }; + } + + parseTerminal(termialStr) { + var characterOffset, textLocationAssertion; + var assertion = termialStr.match(/\[(.*)\]/); + + if (assertion && assertion[1]) { + characterOffset = parseInt(termialStr.split("[")[0]); + textLocationAssertion = assertion[1]; + } else { + characterOffset = parseInt(termialStr); + } + + if (!isNumber(characterOffset)) { + characterOffset = null; + } + + return { + offset: characterOffset, + assertion: textLocationAssertion, + }; + } + + getChapterComponent(cfiStr) { + var indirection = cfiStr.split("!"); + + return indirection[0]; + } + + getPathComponent(cfiStr) { + var indirection = cfiStr.split("!"); + + if (indirection[1]) { + let ranges = indirection[1].split(","); + return ranges[0]; + } + } + + getRange(cfiStr) { + var ranges = cfiStr.split(","); + + if (ranges.length === 3) { + return [ranges[1], ranges[2]]; + } + + return false; + } + + getCharecterOffsetComponent(cfiStr) { + var splitStr = cfiStr.split(":"); + return splitStr[1] || ""; + } + + joinSteps(steps) { + if (!steps) { + return ""; + } + + return steps + .map(function (part) { + var segment = ""; + + if (part.type === "element") { + segment += (part.index + 1) * 2; + } + + if (part.type === "text") { + segment += 1 + 2 * part.index; // TODO: double check that this is odd + } + + if (part.id) { + segment += "[" + part.id + "]"; + } + + return segment; + }) + .join("/"); + } + + segmentString(segment) { + var segmentString = "/"; + + segmentString += this.joinSteps(segment.steps); + + if (segment.terminal && segment.terminal.offset != null) { + segmentString += ":" + segment.terminal.offset; + } + + if (segment.terminal && segment.terminal.assertion != null) { + segmentString += "[" + segment.terminal.assertion + "]"; + } + + return segmentString; + } + + /** + * Convert CFI to a epubcfi(...) string + * @returns {string} epubcfi + */ + toString() { + var cfiString = "epubcfi("; + + cfiString += this.segmentString(this.base); + + cfiString += "!"; + cfiString += this.segmentString(this.path); + + // Add Range, if present + if (this.range && this.start) { + cfiString += ","; + cfiString += this.segmentString(this.start); + } + + if (this.range && this.end) { + cfiString += ","; + cfiString += this.segmentString(this.end); + } + + cfiString += ")"; + + return cfiString; + } + + /** + * Compare which of two CFIs is earlier in the text + * @returns {number} First is earlier = -1, Second is earlier = 1, They are equal = 0 + */ + compare(cfiOne, cfiTwo) { + var stepsA, stepsB; + var terminalA, terminalB; + + if (typeof cfiOne === "string") { + cfiOne = new EpubCFI(cfiOne); + } + if (typeof cfiTwo === "string") { + cfiTwo = new EpubCFI(cfiTwo); + } + // Compare Spine Positions + if (cfiOne.spinePos > cfiTwo.spinePos) { + return 1; + } + if (cfiOne.spinePos < cfiTwo.spinePos) { + return -1; + } + + if (cfiOne.range) { + stepsA = cfiOne.path.steps.concat(cfiOne.start.steps); + terminalA = cfiOne.start.terminal; + } else { + stepsA = cfiOne.path.steps; + terminalA = cfiOne.path.terminal; + } + + if (cfiTwo.range) { + stepsB = cfiTwo.path.steps.concat(cfiTwo.start.steps); + terminalB = cfiTwo.start.terminal; + } else { + stepsB = cfiTwo.path.steps; + terminalB = cfiTwo.path.terminal; + } + + // Compare Each Step in the First item + for (var i = 0; i < stepsA.length; i++) { + if (!stepsA[i]) { + return -1; + } + if (!stepsB[i]) { + return 1; + } + if (stepsA[i].index > stepsB[i].index) { + return 1; + } + if (stepsA[i].index < stepsB[i].index) { + return -1; + } + // Otherwise continue checking + } + + // All steps in First equal to Second and First is Less Specific + if (stepsA.length < stepsB.length) { + return -1; + } + + // Compare the character offset of the text node + if (terminalA.offset > terminalB.offset) { + return 1; + } + if (terminalA.offset < terminalB.offset) { + return -1; + } + + // CFI's are equal + return 0; + } + + step(node) { + var nodeType = node.nodeType === TEXT_NODE ? "text" : "element"; + + return { + id: node.id, + tagName: node.tagName, + type: nodeType, + index: this.position(node), + }; + } + + filteredStep(node, ignoreClass) { + var filteredNode = this.filter(node, ignoreClass); + var nodeType; + + // Node filtered, so ignore + if (!filteredNode) { + return; + } + + // Otherwise add the filter node in + nodeType = filteredNode.nodeType === TEXT_NODE ? "text" : "element"; + + return { + id: filteredNode.id, + tagName: filteredNode.tagName, + type: nodeType, + index: this.filteredPosition(filteredNode, ignoreClass), + }; + } + + pathTo(node, offset, ignoreClass) { + var segment = { + steps: [], + terminal: { + offset: null, + assertion: null, + }, + }; + var currentNode = node; + var step; + + while ( + currentNode && + currentNode.parentNode && + currentNode.parentNode.nodeType != DOCUMENT_NODE + ) { + if (ignoreClass) { + step = this.filteredStep(currentNode, ignoreClass); + } else { + step = this.step(currentNode); + } + + if (step) { + segment.steps.unshift(step); + } + + currentNode = currentNode.parentNode; + } + + if (offset != null && offset >= 0) { + segment.terminal.offset = offset; + + // Make sure we are getting to a textNode if there is an offset + if (segment.steps[segment.steps.length - 1].type != "text") { + segment.steps.push({ + type: "text", + index: 0, + }); + } + } + + return segment; + } + + equalStep(stepA, stepB) { + if (!stepA || !stepB) { + return false; + } + + if ( + stepA.index === stepB.index && + stepA.id === stepB.id && + stepA.type === stepB.type + ) { + return true; + } + + return false; + } + + /** + * Create a CFI object from a Range + * @param {Range} range + * @param {string | object} base + * @param {string} [ignoreClass] + * @returns {object} cfi + */ + fromRange(range, base, ignoreClass) { + var cfi = { + range: false, + base: {}, + path: {}, + start: null, + end: null, + }; + + var start = range.startContainer; + var end = range.endContainer; + + var startOffset = range.startOffset; + var endOffset = range.endOffset; + + var needsIgnoring = false; + + if (ignoreClass) { + // Tell pathTo if / what to ignore + needsIgnoring = + start.ownerDocument.querySelector("." + ignoreClass) != null; + } + + if (typeof base === "string") { + cfi.base = this.parseComponent(base); + cfi.spinePos = cfi.base.steps[1].index; + } else if (typeof base === "object") { + cfi.base = base; + } + + if (range.collapsed) { + if (needsIgnoring) { + startOffset = this.patchOffset(start, startOffset, ignoreClass); + } + cfi.path = this.pathTo(start, startOffset, ignoreClass); + } else { + cfi.range = true; + + if (needsIgnoring) { + startOffset = this.patchOffset(start, startOffset, ignoreClass); + } + + cfi.start = this.pathTo(start, startOffset, ignoreClass); + if (needsIgnoring) { + endOffset = this.patchOffset(end, endOffset, ignoreClass); + } + + cfi.end = this.pathTo(end, endOffset, ignoreClass); + + // Create a new empty path + cfi.path = { + steps: [], + terminal: null, + }; + + // Push steps that are shared between start and end to the common path + var len = cfi.start.steps.length; + var i; + + for (i = 0; i < len; i++) { + if (this.equalStep(cfi.start.steps[i], cfi.end.steps[i])) { + if (i === len - 1) { + // Last step is equal, check terminals + if (cfi.start.terminal === cfi.end.terminal) { + // CFI's are equal + cfi.path.steps.push(cfi.start.steps[i]); + // Not a range + cfi.range = false; + } + } else { + cfi.path.steps.push(cfi.start.steps[i]); + } + } else { + break; + } + } + + cfi.start.steps = cfi.start.steps.slice(cfi.path.steps.length); + cfi.end.steps = cfi.end.steps.slice(cfi.path.steps.length); + + // TODO: Add Sanity check to make sure that the end if greater than the start + } + + return cfi; + } + + /** + * Create a CFI object from a Node + * @param {Node} anchor + * @param {string | object} base + * @param {string} [ignoreClass] + * @returns {object} cfi + */ + fromNode(anchor, base, ignoreClass) { + var cfi = { + range: false, + base: {}, + path: {}, + start: null, + end: null, + }; + + if (typeof base === "string") { + cfi.base = this.parseComponent(base); + cfi.spinePos = cfi.base.steps[1].index; + } else if (typeof base === "object") { + cfi.base = base; + } + + cfi.path = this.pathTo(anchor, null, ignoreClass); + + return cfi; + } + + filter(anchor, ignoreClass) { + var needsIgnoring; + var sibling; // to join with + var parent, previousSibling, nextSibling; + var isText = false; + + if (anchor.nodeType === TEXT_NODE) { + isText = true; + parent = anchor.parentNode; + needsIgnoring = anchor.parentNode.classList.contains(ignoreClass); + } else { + isText = false; + needsIgnoring = anchor.classList.contains(ignoreClass); + } + + if (needsIgnoring && isText) { + previousSibling = parent.previousSibling; + nextSibling = parent.nextSibling; + + // If the sibling is a text node, join the nodes + if (previousSibling && previousSibling.nodeType === TEXT_NODE) { + sibling = previousSibling; + } else if (nextSibling && nextSibling.nodeType === TEXT_NODE) { + sibling = nextSibling; + } + + if (sibling) { + return sibling; + } else { + // Parent will be ignored on next step + return anchor; + } + } else if (needsIgnoring && !isText) { + // Otherwise just skip the element node + return false; + } else { + // No need to filter + return anchor; + } + } + + patchOffset(anchor, offset, ignoreClass) { + if (anchor.nodeType != TEXT_NODE) { + throw new Error("Anchor must be a text node"); + } + + var curr = anchor; + var totalOffset = offset; + + // If the parent is a ignored node, get offset from it's start + if (anchor.parentNode.classList.contains(ignoreClass)) { + curr = anchor.parentNode; + } + + while (curr.previousSibling) { + if (curr.previousSibling.nodeType === ELEMENT_NODE) { + // Originally a text node, so join + if (curr.previousSibling.classList.contains(ignoreClass)) { + totalOffset += curr.previousSibling.textContent.length; + } else { + break; // Normal node, dont join + } + } else { + // If the previous sibling is a text node, join the nodes + totalOffset += curr.previousSibling.textContent.length; + } + + curr = curr.previousSibling; + } + + return totalOffset; + } + + normalizedMap(children, nodeType, ignoreClass) { + var output = {}; + var prevIndex = -1; + var i, + len = children.length; + var currNodeType; + var prevNodeType; + + for (i = 0; i < len; i++) { + currNodeType = children[i].nodeType; + + // Check if needs ignoring + if ( + currNodeType === ELEMENT_NODE && + children[i].classList.contains(ignoreClass) + ) { + currNodeType = TEXT_NODE; + } + + if (i > 0 && currNodeType === TEXT_NODE && prevNodeType === TEXT_NODE) { + // join text nodes + output[i] = prevIndex; + } else if (nodeType === currNodeType) { + prevIndex = prevIndex + 1; + output[i] = prevIndex; + } + + prevNodeType = currNodeType; + } + + return output; + } + + position(anchor) { + var children, index; + if (anchor.nodeType === ELEMENT_NODE) { + children = anchor.parentNode.children; + if (!children) { + children = findChildren(anchor.parentNode); + } + index = Array.prototype.indexOf.call(children, anchor); + } else { + children = this.textNodes(anchor.parentNode); + index = children.indexOf(anchor); + } + + return index; + } + + filteredPosition(anchor, ignoreClass) { + var children, index, map; + + if (anchor.nodeType === ELEMENT_NODE) { + children = anchor.parentNode.children; + map = this.normalizedMap(children, ELEMENT_NODE, ignoreClass); + } else { + children = anchor.parentNode.childNodes; + // Inside an ignored node + if (anchor.parentNode.classList.contains(ignoreClass)) { + anchor = anchor.parentNode; + children = anchor.parentNode.childNodes; + } + map = this.normalizedMap(children, TEXT_NODE, ignoreClass); + } + + index = Array.prototype.indexOf.call(children, anchor); + + return map[index]; + } + + stepsToXpath(steps) { + var xpath = [".", "*"]; + + steps.forEach(function (step) { + var position = step.index + 1; + + if (step.id) { + xpath.push("*[position()=" + position + " and @id='" + step.id + "']"); + } else if (step.type === "text") { + xpath.push("text()[" + position + "]"); + } else { + xpath.push("*[" + position + "]"); + } + }); + + return xpath.join("/"); + } + + /* To get the last step if needed: @@ -802,247 +795,270 @@ class EpubCFI { container = startContainerParent.childNodes[lastStep.index]; } */ - stepsToQuerySelector(steps) { - var query = ["html"]; + stepsToQuerySelector(steps) { + var query = ["html"]; - steps.forEach(function(step){ - var position = step.index + 1; + steps.forEach(function (step) { + var position = step.index + 1; - if(step.id){ - query.push("#" + step.id); - } else if(step.type === "text") { - // unsupported in querySelector - // query.push("text()[" + position + "]"); - } else { - query.push("*:nth-child(" + position + ")"); - } - }); + if (step.id) { + query.push("#" + step.id); + } else if (step.type === "text") { + // unsupported in querySelector + // query.push("text()[" + position + "]"); + } else { + query.push("*:nth-child(" + position + ")"); + } + }); - return query.join(">"); + return query.join(">"); + } - } + textNodes(container, ignoreClass) { + return Array.prototype.slice + .call(container.childNodes) + .filter(function (node) { + if (node.nodeType === TEXT_NODE) { + return true; + } else if (ignoreClass && node.classList.contains(ignoreClass)) { + return true; + } + return false; + }); + } - textNodes(container, ignoreClass) { - return Array.prototype.slice.call(container.childNodes). - filter(function (node) { - if (node.nodeType === TEXT_NODE) { - return true; - } else if (ignoreClass && node.classList.contains(ignoreClass)) { - return true; - } - return false; - }); - } + walkToNode(steps, _doc, ignoreClass) { + var doc = _doc || document; + var container = doc.documentElement; + var children; + var step; + var len = steps.length; + var i; - walkToNode(steps, _doc, ignoreClass) { - var doc = _doc || document; - var container = doc.documentElement; - var children; - var step; - var len = steps.length; - var i; + for (i = 0; i < len; i++) { + step = steps[i]; - for (i = 0; i < len; i++) { - step = steps[i]; + if (step.type === "element") { + //better to get a container using id as some times step.index may not be correct + //For ex.https://github.com/futurepress/epub.js/issues/561 + if (step.id) { + container = doc.getElementById(step.id); + } else { + children = container.children || findChildren(container); + container = children[step.index]; + } + } else if (step.type === "text") { + container = this.textNodes(container, ignoreClass)[step.index]; + } + if (!container) { + //Break the for loop as due to incorrect index we can get error if + //container is undefined so that other functionailties works fine + //like navigation + break; + } + } - if(step.type === "element") { - //better to get a container using id as some times step.index may not be correct - //For ex.https://github.com/futurepress/epub.js/issues/561 - if(step.id) { - container = doc.getElementById(step.id); - } - else { - children = container.children || findChildren(container); - container = children[step.index]; - } - } else if(step.type === "text") { - container = this.textNodes(container, ignoreClass)[step.index]; - } - if(!container) { - //Break the for loop as due to incorrect index we can get error if - //container is undefined so that other functionailties works fine - //like navigation - break; - } + return container; + } - } + findNode(steps, _doc, ignoreClass) { + var doc = _doc || document; + var container; + var xpath; - return container; - } + if (!ignoreClass && typeof doc.evaluate != "undefined") { + xpath = this.stepsToXpath(steps); + container = doc.evaluate( + xpath, + doc, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue; + } else if (ignoreClass) { + container = this.walkToNode(steps, doc, ignoreClass); + } else { + container = this.walkToNode(steps, doc); + } - findNode(steps, _doc, ignoreClass) { - var doc = _doc || document; - var container; - var xpath; + return container; + } - if(!ignoreClass && typeof doc.evaluate != "undefined") { - xpath = this.stepsToXpath(steps); - container = doc.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; - } else if(ignoreClass) { - container = this.walkToNode(steps, doc, ignoreClass); - } else { - container = this.walkToNode(steps, doc); - } + fixMiss(steps, offset, _doc, ignoreClass) { + var container = this.findNode(steps.slice(0, -1), _doc, ignoreClass); + var children = container.childNodes; + var map = this.normalizedMap(children, TEXT_NODE, ignoreClass); + var child; + var len; + var lastStepIndex = steps[steps.length - 1].index; - return container; - } + for (let childIndex in map) { + if (!map.hasOwnProperty(childIndex)) return; - fixMiss(steps, offset, _doc, ignoreClass) { - var container = this.findNode(steps.slice(0,-1), _doc, ignoreClass); - var children = container.childNodes; - var map = this.normalizedMap(children, TEXT_NODE, ignoreClass); - var child; - var len; - var lastStepIndex = steps[steps.length-1].index; + if (map[childIndex] === lastStepIndex) { + child = children[childIndex]; + len = child.textContent.length; + if (offset > len) { + offset = offset - len; + } else { + if (child.nodeType === ELEMENT_NODE) { + container = child.childNodes[0]; + } else { + container = child; + } + break; + } + } + } - for (let childIndex in map) { - if (!map.hasOwnProperty(childIndex)) return; + return { + container: container, + offset: offset, + }; + } - if(map[childIndex] === lastStepIndex) { - child = children[childIndex]; - len = child.textContent.length; - if(offset > len) { - offset = offset - len; - } else { - if (child.nodeType === ELEMENT_NODE) { - container = child.childNodes[0]; - } else { - container = child; - } - break; - } - } - } + /** + * Creates a DOM range representing a CFI + * @param {document} _doc document referenced in the base + * @param {string} [ignoreClass] + * @return {Range} + */ + toRange(_doc, ignoreClass) { + var doc = _doc || document; + var range; + var start, end, startContainer, endContainer; + var cfi = this; + var startSteps, endSteps; + var needsIgnoring = ignoreClass + ? doc.querySelector("." + ignoreClass) != null + : false; + var missed; - return { - container: container, - offset: offset - }; + if (typeof doc.createRange !== "undefined") { + range = doc.createRange(); + } else { + range = new RangeObject(); + } - } + if (cfi.range) { + start = cfi.start; + startSteps = cfi.path.steps.concat(start.steps); + startContainer = this.findNode( + startSteps, + doc, + needsIgnoring ? ignoreClass : null + ); + end = cfi.end; + endSteps = cfi.path.steps.concat(end.steps); + endContainer = this.findNode( + endSteps, + doc, + needsIgnoring ? ignoreClass : null + ); + } else { + start = cfi.path; + startSteps = cfi.path.steps; + startContainer = this.findNode( + cfi.path.steps, + doc, + needsIgnoring ? ignoreClass : null + ); + } - /** - * Creates a DOM range representing a CFI - * @param {document} _doc document referenced in the base - * @param {string} [ignoreClass] - * @return {Range} - */ - toRange(_doc, ignoreClass) { - var doc = _doc || document; - var range; - var start, end, startContainer, endContainer; - var cfi = this; - var startSteps, endSteps; - var needsIgnoring = ignoreClass ? (doc.querySelector("." + ignoreClass) != null) : false; - var missed; + if (startContainer) { + try { + if (start.terminal.offset != null) { + range.setStart(startContainer, start.terminal.offset); + } else { + range.setStart(startContainer, 0); + } + } catch (e) { + missed = this.fixMiss( + startSteps, + start.terminal.offset, + doc, + needsIgnoring ? ignoreClass : null + ); + range.setStart(missed.container, missed.offset); + } + } else { + console.log("No startContainer found for", this.toString()); + // No start found + return null; + } - if (typeof(doc.createRange) !== "undefined") { - range = doc.createRange(); - } else { - range = new RangeObject(); - } + if (endContainer) { + try { + if (end.terminal.offset != null) { + range.setEnd(endContainer, end.terminal.offset); + } else { + range.setEnd(endContainer, 0); + } + } catch (e) { + missed = this.fixMiss( + endSteps, + cfi.end.terminal.offset, + doc, + needsIgnoring ? ignoreClass : null + ); + range.setEnd(missed.container, missed.offset); + } + } - if (cfi.range) { - start = cfi.start; - startSteps = cfi.path.steps.concat(start.steps); - startContainer = this.findNode(startSteps, doc, needsIgnoring ? ignoreClass : null); - end = cfi.end; - endSteps = cfi.path.steps.concat(end.steps); - endContainer = this.findNode(endSteps, doc, needsIgnoring ? ignoreClass : null); - } else { - start = cfi.path; - startSteps = cfi.path.steps; - startContainer = this.findNode(cfi.path.steps, doc, needsIgnoring ? ignoreClass : null); - } + // doc.defaultView.getSelection().addRange(range); + return range; + } - if(startContainer) { - try { + /** + * Check if a string is wrapped with "epubcfi()" + * @param {string} str + * @returns {boolean} + */ + isCfiString(str) { + if ( + typeof str === "string" && + str.indexOf("epubcfi(") === 0 && + str[str.length - 1] === ")" + ) { + return true; + } - if(start.terminal.offset != null) { - range.setStart(startContainer, start.terminal.offset); - } else { - range.setStart(startContainer, 0); - } + return false; + } - } catch (e) { - missed = this.fixMiss(startSteps, start.terminal.offset, doc, needsIgnoring ? ignoreClass : null); - range.setStart(missed.container, missed.offset); - } - } else { - console.log("No startContainer found for", this.toString()); - // No start found - return null; - } + generateChapterComponent(_spineNodeIndex, _pos, id) { + var pos = parseInt(_pos), + spineNodeIndex = (_spineNodeIndex + 1) * 2, + cfi = "/" + spineNodeIndex + "/"; - if (endContainer) { - try { + cfi += (pos + 1) * 2; - if(end.terminal.offset != null) { - range.setEnd(endContainer, end.terminal.offset); - } else { - range.setEnd(endContainer, 0); - } + if (id) { + cfi += "[" + id + "]"; + } - } catch (e) { - missed = this.fixMiss(endSteps, cfi.end.terminal.offset, doc, needsIgnoring ? ignoreClass : null); - range.setEnd(missed.container, missed.offset); - } - } + return cfi; + } + /** + * Collapse a CFI Range to a single CFI Position + * @param {boolean} [toStart=false] + */ + collapse(toStart) { + if (!this.range) { + return; + } - // doc.defaultView.getSelection().addRange(range); - return range; - } + this.range = false; - /** - * Check if a string is wrapped with "epubcfi()" - * @param {string} str - * @returns {boolean} - */ - isCfiString(str) { - if(typeof str === "string" && - str.indexOf("epubcfi(") === 0 && - str[str.length-1] === ")") { - return true; - } - - return false; - } - - generateChapterComponent(_spineNodeIndex, _pos, id) { - var pos = parseInt(_pos), - spineNodeIndex = (_spineNodeIndex + 1) * 2, - cfi = "/"+spineNodeIndex+"/"; - - cfi += (pos + 1) * 2; - - if(id) { - cfi += "[" + id + "]"; - } - - return cfi; - } - - /** - * Collapse a CFI Range to a single CFI Position - * @param {boolean} [toStart=false] - */ - collapse(toStart) { - if (!this.range) { - return; - } - - this.range = false; - - if (toStart) { - this.path.steps = this.path.steps.concat(this.start.steps); - this.path.terminal = this.start.terminal; - } else { - this.path.steps = this.path.steps.concat(this.end.steps); - this.path.terminal = this.end.terminal; - } - - } + if (toStart) { + this.path.steps = this.path.steps.concat(this.start.steps); + this.path.terminal = this.start.terminal; + } else { + this.path.steps = this.path.steps.concat(this.end.steps); + this.path.terminal = this.end.terminal; + } + } } export default EpubCFI; diff --git a/src/index.js b/src/index.js index 16ef8b6..0651953 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,9 @@ import Book from "./book"; -import EpubCFI from "./epubcfi"; -import Rendition from "./rendition"; import Contents from "./contents"; -import Layout from "./layout"; import ePub from "./epub"; +import EpubCFI from "./epubcfi"; +import Layout from "./layout"; +import Rendition from "./rendition"; export default ePub; -export { - Book, - EpubCFI, - Rendition, - Contents, - Layout -}; +export { Book, Contents, EpubCFI, Layout, Rendition }; diff --git a/src/layout.js b/src/layout.js index 4f16a0f..dce98e2 100644 --- a/src/layout.js +++ b/src/layout.js @@ -1,6 +1,6 @@ -import { extend } from "./utils/core"; -import { EVENTS } from "./utils/constants"; import EventEmitter from "event-emitter"; +import { EVENTS } from "./utils/constants"; +import { extend } from "./utils/core"; /** * Figures out the CSS values to apply for a layout @@ -12,247 +12,255 @@ import EventEmitter from "event-emitter"; * @param {boolean} [settings.evenSpreads=false] */ class Layout { - constructor(settings) { - this.settings = settings; - this.name = settings.layout || "reflowable"; - this._spread = (settings.spread === "none") ? false : true; - this._minSpreadWidth = settings.minSpreadWidth || 800; - this._evenSpreads = settings.evenSpreads || false; + constructor(settings) { + this.settings = settings; + this.name = settings.layout || "reflowable"; + this._spread = settings.spread === "none" ? false : true; + this._minSpreadWidth = settings.minSpreadWidth || 800; + this._evenSpreads = settings.evenSpreads || false; - if (settings.flow === "scrolled" || - settings.flow === "scrolled-continuous" || - settings.flow === "scrolled-doc") { - this._flow = "scrolled"; - } else { - this._flow = "paginated"; - } + if ( + settings.flow === "scrolled" || + settings.flow === "scrolled-continuous" || + settings.flow === "scrolled-doc" + ) { + this._flow = "scrolled"; + } else { + this._flow = "paginated"; + } + this.width = 0; + this.height = 0; + this.spreadWidth = 0; + this.delta = 0; - this.width = 0; - this.height = 0; - this.spreadWidth = 0; - this.delta = 0; + this.columnWidth = 0; + this.gap = 0; + this.divisor = 1; - this.columnWidth = 0; - this.gap = 0; - this.divisor = 1; + this.props = { + name: this.name, + spread: this._spread, + flow: this._flow, + width: 0, + height: 0, + spreadWidth: 0, + delta: 0, + columnWidth: 0, + gap: 0, + divisor: 1, + }; + } - this.props = { - name: this.name, - spread: this._spread, - flow: this._flow, - width: 0, - height: 0, - spreadWidth: 0, - delta: 0, - columnWidth: 0, - gap: 0, - divisor: 1 - }; + /** + * Switch the flow between paginated and scrolled + * @param {string} flow paginated | scrolled + * @return {string} simplified flow + */ + flow(flow) { + if (typeof flow != "undefined") { + if ( + flow === "scrolled" || + flow === "scrolled-continuous" || + flow === "scrolled-doc" + ) { + this._flow = "scrolled"; + } else { + this._flow = "paginated"; + } + // this.props.flow = this._flow; + this.update({ flow: this._flow }); + } + return this._flow; + } - } + /** + * Switch between using spreads or not, and set the + * width at which they switch to single. + * @param {string} spread "none" | "always" | "auto" + * @param {number} min integer in pixels + * @return {boolean} spread true | false + */ + spread(spread, min) { + if (spread) { + this._spread = spread === "none" ? false : true; + // this.props.spread = this._spread; + this.update({ spread: this._spread }); + } - /** - * Switch the flow between paginated and scrolled - * @param {string} flow paginated | scrolled - * @return {string} simplified flow - */ - flow(flow) { - if (typeof(flow) != "undefined") { - if (flow === "scrolled" || - flow === "scrolled-continuous" || - flow === "scrolled-doc") { - this._flow = "scrolled"; - } else { - this._flow = "paginated"; - } - // this.props.flow = this._flow; - this.update({flow: this._flow}); - } - return this._flow; - } + if (min >= 0) { + this._minSpreadWidth = min; + } - /** - * Switch between using spreads or not, and set the - * width at which they switch to single. - * @param {string} spread "none" | "always" | "auto" - * @param {number} min integer in pixels - * @return {boolean} spread true | false - */ - spread(spread, min) { + return this._spread; + } - if (spread) { - this._spread = (spread === "none") ? false : true; - // this.props.spread = this._spread; - this.update({spread: this._spread}); - } + /** + * Calculate the dimensions of the pagination + * @param {number} _width width of the rendering + * @param {number} _height height of the rendering + * @param {number} _gap width of the gap between columns + */ + calculate(_width, _height, _gap) { + var divisor = 1; + var gap = _gap || 0; - if (min >= 0) { - this._minSpreadWidth = min; - } + //-- Check the width and create even width columns + // var fullWidth = Math.floor(_width); + var width = _width; + var height = _height; - return this._spread; - } + var section = Math.floor(width / 12); - /** - * Calculate the dimensions of the pagination - * @param {number} _width width of the rendering - * @param {number} _height height of the rendering - * @param {number} _gap width of the gap between columns - */ - calculate(_width, _height, _gap){ + var columnWidth; + var spreadWidth; + var pageWidth; + var delta; - var divisor = 1; - var gap = _gap || 0; + if (this._spread && width >= this._minSpreadWidth) { + divisor = 2; + } else { + divisor = 1; + } - //-- Check the width and create even width columns - // var fullWidth = Math.floor(_width); - var width = _width; - var height = _height; + if ( + this.name === "reflowable" && + this._flow === "paginated" && + !(_gap >= 0) + ) { + gap = section % 2 === 0 ? section : section - 1; + } - var section = Math.floor(width / 12); + if (this.name === "pre-paginated") { + gap = 0; + } - var columnWidth; - var spreadWidth; - var pageWidth; - var delta; + //-- Double Page + if (divisor > 1) { + // width = width - gap; + // columnWidth = (width - gap) / divisor; + // gap = gap / divisor; + columnWidth = width / divisor - gap; + pageWidth = columnWidth + gap; + } else { + columnWidth = width; + pageWidth = width; + } - if (this._spread && width >= this._minSpreadWidth) { - divisor = 2; - } else { - divisor = 1; - } + if (this.name === "pre-paginated" && divisor > 1) { + width = columnWidth; + } - if (this.name === "reflowable" && this._flow === "paginated" && !(_gap >= 0)) { - gap = ((section % 2 === 0) ? section : section - 1); - } + spreadWidth = columnWidth * divisor + gap; - if (this.name === "pre-paginated" ) { - gap = 0; - } + delta = width; - //-- Double Page - if(divisor > 1) { - // width = width - gap; - // columnWidth = (width - gap) / divisor; - // gap = gap / divisor; - columnWidth = (width / divisor) - gap; - pageWidth = columnWidth + gap; - } else { - columnWidth = width; - pageWidth = width; - } + this.width = width; + this.height = height; + this.spreadWidth = spreadWidth; + this.pageWidth = pageWidth; + this.delta = delta; - if (this.name === "pre-paginated" && divisor > 1) { - width = columnWidth; - } + this.columnWidth = columnWidth; + this.gap = gap; + this.divisor = divisor; - spreadWidth = (columnWidth * divisor) + gap; + // this.props.width = width; + // this.props.height = _height; + // this.props.spreadWidth = spreadWidth; + // this.props.pageWidth = pageWidth; + // this.props.delta = delta; + // + // this.props.columnWidth = colWidth; + // this.props.gap = gap; + // this.props.divisor = divisor; - delta = width; + this.update({ + width, + height, + spreadWidth, + pageWidth, + delta, + columnWidth, + gap, + divisor, + }); + } - this.width = width; - this.height = height; - this.spreadWidth = spreadWidth; - this.pageWidth = pageWidth; - this.delta = delta; + /** + * Apply Css to a Document + * @param {Contents} contents + * @return {Promise} + */ + format(contents, section, axis) { + var formating; - this.columnWidth = columnWidth; - this.gap = gap; - this.divisor = divisor; + if (this.name === "pre-paginated") { + formating = contents.fit(this.columnWidth, this.height, section); + } else if (this._flow === "paginated") { + formating = contents.columns( + this.width, + this.height, + this.columnWidth, + this.gap, + this.settings.direction + ); + } else if (axis && axis === "horizontal") { + formating = contents.size(null, this.height); + } else { + formating = contents.size(this.width, null); + } - // this.props.width = width; - // this.props.height = _height; - // this.props.spreadWidth = spreadWidth; - // this.props.pageWidth = pageWidth; - // this.props.delta = delta; - // - // this.props.columnWidth = colWidth; - // this.props.gap = gap; - // this.props.divisor = divisor; + return formating; // might be a promise in some View Managers + } - this.update({ - width, - height, - spreadWidth, - pageWidth, - delta, - columnWidth, - gap, - divisor - }); + /** + * Count number of pages + * @param {number} totalLength + * @param {number} pageLength + * @return {{spreads: Number, pages: Number}} + */ + count(totalLength, pageLength) { + let spreads, pages; - } + if (this.name === "pre-paginated") { + spreads = 1; + pages = 1; + } else if (this._flow === "paginated") { + pageLength = pageLength || this.delta; + spreads = Math.ceil(totalLength / pageLength); + pages = spreads * this.divisor; + } else { + // scrolled + pageLength = pageLength || this.height; + spreads = Math.ceil(totalLength / pageLength); + pages = spreads; + } - /** - * Apply Css to a Document - * @param {Contents} contents - * @return {Promise} - */ - format(contents, section, axis){ - var formating; + return { + spreads, + pages, + }; + } - if (this.name === "pre-paginated") { - formating = contents.fit(this.columnWidth, this.height, section); - } else if (this._flow === "paginated") { - formating = contents.columns(this.width, this.height, this.columnWidth, this.gap, this.settings.direction); - } else if (axis && axis === "horizontal") { - formating = contents.size(null, this.height); - } else { - formating = contents.size(this.width, null); - } + /** + * Update props that have changed + * @private + * @param {object} props + */ + update(props) { + // Remove props that haven't changed + Object.keys(props).forEach((propName) => { + if (this.props[propName] === props[propName]) { + delete props[propName]; + } + }); - return formating; // might be a promise in some View Managers - } - - /** - * Count number of pages - * @param {number} totalLength - * @param {number} pageLength - * @return {{spreads: Number, pages: Number}} - */ - count(totalLength, pageLength) { - - let spreads, pages; - - if (this.name === "pre-paginated") { - spreads = 1; - pages = 1; - } else if (this._flow === "paginated") { - pageLength = pageLength || this.delta; - spreads = Math.ceil( totalLength / pageLength); - pages = spreads * this.divisor; - } else { // scrolled - pageLength = pageLength || this.height; - spreads = Math.ceil( totalLength / pageLength); - pages = spreads; - } - - return { - spreads, - pages - }; - - } - - /** - * Update props that have changed - * @private - * @param {object} props - */ - update(props) { - // Remove props that haven't changed - Object.keys(props).forEach((propName) => { - if (this.props[propName] === props[propName]) { - delete props[propName]; - } - }); - - if(Object.keys(props).length > 0) { - let newProps = extend(this.props, props); - this.emit(EVENTS.LAYOUT.UPDATED, newProps, props); - } - } + if (Object.keys(props).length > 0) { + let newProps = extend(this.props, props); + this.emit(EVENTS.LAYOUT.UPDATED, newProps, props); + } + } } EventEmitter(Layout.prototype); diff --git a/src/locations.js b/src/locations.js index 2749890..3556cc1 100644 --- a/src/locations.js +++ b/src/locations.js @@ -1,8 +1,8 @@ -import {qs, sprint, locationOf, defer} from "./utils/core"; -import Queue from "./utils/queue"; +import EventEmitter from "event-emitter"; import EpubCFI from "./epubcfi"; import { EVENTS } from "./utils/constants"; -import EventEmitter from "event-emitter"; +import { defer, locationOf, qs, sprint } from "./utils/core"; +import Queue from "./utils/queue"; /** * Find Locations for a Book @@ -11,489 +11,512 @@ import EventEmitter from "event-emitter"; * @param {number} [pause=100] */ class Locations { - constructor(spine, request, pause) { - this.spine = spine; - this.request = request; - this.pause = pause || 100; - - this.q = new Queue(this); - this.epubcfi = new EpubCFI(); - - this._locations = []; - this._locationsWords = []; - this.total = 0; - - this.break = 150; - - this._current = 0; - - this._wordCounter = 0; - - this.currentLocation = ''; - this._currentCfi =''; - this.processingTimeout = undefined; - } - - /** - * Load all of sections in the book to generate locations - * @param {int} chars how many chars to split on - * @return {Promise>} locations - */ - generate(chars) { - - if (chars) { - this.break = chars; - } - - this.q.pause(); - - this.spine.each(function(section) { - if (section.linear) { - this.q.enqueue(this.process.bind(this), section); - } - }.bind(this)); - - return this.q.run().then(function() { - this.total = this._locations.length - 1; - - if (this._currentCfi) { - this.currentLocation = this._currentCfi; - } - - return this._locations; - // console.log(this.percentage(this.book.rendition.location.start), this.percentage(this.book.rendition.location.end)); - }.bind(this)); - - } - - createRange () { - return { - startContainer: undefined, - startOffset: undefined, - endContainer: undefined, - endOffset: undefined - }; - } - - process(section) { - - return section.load(this.request) - .then(function(contents) { - var completed = new defer(); - var locations = this.parse(contents, section.cfiBase); - this._locations = this._locations.concat(locations); - - section.unload(); - - this.processingTimeout = setTimeout(() => completed.resolve(locations), this.pause); - return completed.promise; - }.bind(this)); - - } - - parse(contents, cfiBase, chars) { - var locations = []; - var range; - var doc = contents.ownerDocument; - var body = qs(doc, "body"); - var counter = 0; - var prev; - var _break = chars || this.break; - var parser = function(node) { - var len = node.length; - var dist; - var pos = 0; - - if (node.textContent.trim().length === 0) { - return false; // continue - } - - // Start range - if (counter == 0) { - range = this.createRange(); - range.startContainer = node; - range.startOffset = 0; - } - - dist = _break - counter; - - // Node is smaller than a break, - // skip over it - if(dist > len){ - counter += len; - pos = len; - } - - - while (pos < len) { - dist = _break - counter; - - if (counter === 0) { - // Start new range - pos += 1; - range = this.createRange(); - range.startContainer = node; - range.startOffset = pos; - } - - // pos += dist; - - // Gone over - if(pos + dist >= len){ - // Continue counter for next node - counter += len - pos; - // break - pos = len; - // At End - } else { - // Advance pos - pos += dist; - - // End the previous range - range.endContainer = node; - range.endOffset = pos; - // cfi = section.cfiFromRange(range); - let cfi = new EpubCFI(range, cfiBase).toString(); - locations.push(cfi); - counter = 0; - } - } - prev = node; - }; - - sprint(body, parser.bind(this)); - - // Close remaining - if (range && range.startContainer && prev) { - range.endContainer = prev; - range.endOffset = prev.length; - let cfi = new EpubCFI(range, cfiBase).toString(); - locations.push(cfi); - counter = 0; - } - - return locations; - } - - - /** - * Load all of sections in the book to generate locations - * @param {string} startCfi start position - * @param {int} wordCount how many words to split on - * @param {int} count result count - * @return {object} locations - */ - generateFromWords(startCfi, wordCount, count) { - var start = startCfi ? new EpubCFI(startCfi) : undefined; - this.q.pause(); - this._locationsWords = []; - this._wordCounter = 0; - - this.spine.each(function(section) { - if (section.linear) { - if (start) { - if (section.index >= start.spinePos) { - this.q.enqueue(this.processWords.bind(this), section, wordCount, start, count); - } - } else { - this.q.enqueue(this.processWords.bind(this), section, wordCount, start, count); - } - } - }.bind(this)); - - return this.q.run().then(function() { - if (this._currentCfi) { - this.currentLocation = this._currentCfi; - } - - return this._locationsWords; - }.bind(this)); - - } - - processWords(section, wordCount, startCfi, count) { - if (count && this._locationsWords.length >= count) { - return Promise.resolve(); - } - - return section.load(this.request) - .then(function(contents) { - var completed = new defer(); - var locations = this.parseWords(contents, section, wordCount, startCfi); - var remainingCount = count - this._locationsWords.length; - this._locationsWords = this._locationsWords.concat(locations.length >= count ? locations.slice(0, remainingCount) : locations); - - section.unload(); - - this.processingTimeout = setTimeout(() => completed.resolve(locations), this.pause); - return completed.promise; - }.bind(this)); - } - - //http://stackoverflow.com/questions/18679576/counting-words-in-string - countWords(s) { - s = s.replace(/(^\s*)|(\s*$)/gi, "");//exclude start and end white-space - s = s.replace(/[ ]{2,}/gi, " ");//2 or more space to 1 - s = s.replace(/\n /, "\n"); // exclude newline with a start spacing - return s.split(" ").length; - } - - parseWords(contents, section, wordCount, startCfi) { - var cfiBase = section.cfiBase; - var locations = []; - var doc = contents.ownerDocument; - var body = qs(doc, "body"); - var prev; - var _break = wordCount; - var foundStartNode = startCfi ? startCfi.spinePos !== section.index : true; - var startNode; - if (startCfi && section.index === startCfi.spinePos) { - startNode = startCfi.findNode(startCfi.range ? startCfi.path.steps.concat(startCfi.start.steps) : startCfi.path.steps, contents.ownerDocument); - } - var parser = function(node) { - if (!foundStartNode) { - if (node === startNode) { - foundStartNode = true; - } else { - return false; - } - } - if (node.textContent.length < 10) { - if (node.textContent.trim().length === 0) { - return false; - } - } - var len = this.countWords(node.textContent); - var dist; - var pos = 0; - - if (len === 0) { - return false; // continue - } - - dist = _break - this._wordCounter; - - // Node is smaller than a break, - // skip over it - if (dist > len) { - this._wordCounter += len; - pos = len; - } - - - while (pos < len) { - dist = _break - this._wordCounter; - - // Gone over - if (pos + dist >= len) { - // Continue counter for next node - this._wordCounter += len - pos; - // break - pos = len; - // At End - } else { - // Advance pos - pos += dist; - - let cfi = new EpubCFI(node, cfiBase); - locations.push({ cfi: cfi.toString(), wordCount: this._wordCounter }); - this._wordCounter = 0; - } - } - prev = node; - }; - - sprint(body, parser.bind(this)); - - return locations; - } - - /** - * Get a location from an EpubCFI - * @param {EpubCFI} cfi - * @return {number} - */ - locationFromCfi(cfi){ - let loc; - if (EpubCFI.prototype.isCfiString(cfi)) { - cfi = new EpubCFI(cfi); - } - // Check if the location has not been set yet - if(this._locations.length === 0) { - return -1; - } - - loc = locationOf(cfi, this._locations, this.epubcfi.compare); - - if (loc > this.total) { - return this.total; - } - - return loc; - } - - /** - * Get a percentage position in locations from an EpubCFI - * @param {EpubCFI} cfi - * @return {number} - */ - percentageFromCfi(cfi) { - if(this._locations.length === 0) { - return null; - } - // Find closest cfi - var loc = this.locationFromCfi(cfi); - // Get percentage in total - return this.percentageFromLocation(loc); - } - - /** - * Get a percentage position from a location index - * @param {number} location - * @return {number} - */ - percentageFromLocation(loc) { - if (!loc || !this.total) { - return 0; - } - - return (loc / this.total); - } - - /** - * Get an EpubCFI from location index - * @param {number} loc - * @return {EpubCFI} cfi - */ - cfiFromLocation(loc){ - var cfi = -1; - // check that pg is an int - if(typeof loc != "number"){ - loc = parseInt(loc); - } - - if(loc >= 0 && loc < this._locations.length) { - cfi = this._locations[loc]; - } - - return cfi; - } - - /** - * Get an EpubCFI from location percentage - * @param {number} percentage - * @return {EpubCFI} cfi - */ - cfiFromPercentage(percentage){ - let loc; - if (percentage > 1) { - console.warn("Normalize cfiFromPercentage value to between 0 - 1"); - } - - // Make sure 1 goes to very end - if (percentage >= 1) { - let cfi = new EpubCFI(this._locations[this.total]); - cfi.collapse(); - return cfi.toString(); - } - - loc = Math.ceil(this.total * percentage); - return this.cfiFromLocation(loc); - } - - /** - * Load locations from JSON - * @param {json} locations - */ - load(locations){ - if (typeof locations === "string") { - this._locations = JSON.parse(locations); - } else { - this._locations = locations; - } - this.total = this._locations.length - 1; - return this._locations; - } - - /** - * Save locations to JSON - * @return {json} - */ - save(){ - return JSON.stringify(this._locations); - } - - getCurrent(){ - return this._current; - } - - setCurrent(curr){ - var loc; - - if(typeof curr == "string"){ - this._currentCfi = curr; - } else if (typeof curr == "number") { - this._current = curr; - } else { - return; - } - - if(this._locations.length === 0) { - return; - } - - if(typeof curr == "string"){ - loc = this.locationFromCfi(curr); - this._current = loc; - } else { - loc = curr; - } - - this.emit(EVENTS.LOCATIONS.CHANGED, { - percentage: this.percentageFromLocation(loc) - }); - } - - /** - * Get the current location - */ - get currentLocation() { - return this._current; - } - - /** - * Set the current location - */ - set currentLocation(curr) { - this.setCurrent(curr); - } - - /** - * Locations length - */ - length () { - return this._locations.length; - } - - destroy () { - this.spine = undefined; - this.request = undefined; - this.pause = undefined; - - this.q.stop(); - this.q = undefined; - this.epubcfi = undefined; - - this._locations = undefined - this.total = undefined; - - this.break = undefined; - this._current = undefined; - - this.currentLocation = undefined; - this._currentCfi = undefined; - clearTimeout(this.processingTimeout); - } + constructor(spine, request, pause) { + this.spine = spine; + this.request = request; + this.pause = pause || 100; + + this.q = new Queue(this); + this.epubcfi = new EpubCFI(); + + this._locations = []; + this._locationsWords = []; + this.total = 0; + this.break = 150; + this._current = 0; + this._wordCounter = 0; + this.currentLocation = ""; + this._currentCfi = ""; + this.processingTimeout = undefined; + } + + /** + * Load all of sections in the book to generate locations + * @param {int} chars how many chars to split on + * @return {Promise>} locations + */ + generate(chars) { + if (chars) { + this.break = chars; + } + + this.q.pause(); + + this.spine.each( + function (section) { + if (section.linear) { + this.q.enqueue(this.process.bind(this), section); + } + }.bind(this) + ); + + return this.q.run().then( + function () { + this.total = this._locations.length - 1; + + if (this._currentCfi) { + this.currentLocation = this._currentCfi; + } + + return this._locations; + // console.log(this.percentage(this.book.rendition.location.start), this.percentage(this.book.rendition.location.end)); + }.bind(this) + ); + } + + createRange() { + return { + startContainer: undefined, + startOffset: undefined, + endContainer: undefined, + endOffset: undefined, + }; + } + + process(section) { + return section.load(this.request).then( + function (contents) { + var completed = new defer(); + var locations = this.parse(contents, section.cfiBase); + this._locations = this._locations.concat(locations); + + section.unload(); + + this.processingTimeout = setTimeout( + () => completed.resolve(locations), + this.pause + ); + return completed.promise; + }.bind(this) + ); + } + + parse(contents, cfiBase, chars) { + var locations = []; + var range; + var doc = contents.ownerDocument; + var body = qs(doc, "body"); + var counter = 0; + var prev; + var _break = chars || this.break; + var parser = function (node) { + var len = node.length; + var dist; + var pos = 0; + + if (node.textContent.trim().length === 0) { + return false; // continue + } + + // Start range + if (counter == 0) { + range = this.createRange(); + range.startContainer = node; + range.startOffset = 0; + } + + dist = _break - counter; + + // Node is smaller than a break, + // skip over it + if (dist > len) { + counter += len; + pos = len; + } + + while (pos < len) { + dist = _break - counter; + + if (counter === 0) { + // Start new range + pos += 1; + range = this.createRange(); + range.startContainer = node; + range.startOffset = pos; + } + + // pos += dist; + + // Gone over + if (pos + dist >= len) { + // Continue counter for next node + counter += len - pos; + // break + pos = len; + // At End + } else { + // Advance pos + pos += dist; + + // End the previous range + range.endContainer = node; + range.endOffset = pos; + // cfi = section.cfiFromRange(range); + let cfi = new EpubCFI(range, cfiBase).toString(); + locations.push(cfi); + counter = 0; + } + } + prev = node; + }; + + sprint(body, parser.bind(this)); + + // Close remaining + if (range && range.startContainer && prev) { + range.endContainer = prev; + range.endOffset = prev.length; + let cfi = new EpubCFI(range, cfiBase).toString(); + locations.push(cfi); + counter = 0; + } + + return locations; + } + + /** + * Load all of sections in the book to generate locations + * @param {string} startCfi start position + * @param {int} wordCount how many words to split on + * @param {int} count result count + * @return {object} locations + */ + generateFromWords(startCfi, wordCount, count) { + var start = startCfi ? new EpubCFI(startCfi) : undefined; + this.q.pause(); + this._locationsWords = []; + this._wordCounter = 0; + + this.spine.each( + function (section) { + if (section.linear) { + if (start) { + if (section.index >= start.spinePos) { + this.q.enqueue( + this.processWords.bind(this), + section, + wordCount, + start, + count + ); + } + } else { + this.q.enqueue( + this.processWords.bind(this), + section, + wordCount, + start, + count + ); + } + } + }.bind(this) + ); + + return this.q.run().then( + function () { + if (this._currentCfi) { + this.currentLocation = this._currentCfi; + } + + return this._locationsWords; + }.bind(this) + ); + } + + processWords(section, wordCount, startCfi, count) { + if (count && this._locationsWords.length >= count) { + return Promise.resolve(); + } + + return section.load(this.request).then( + function (contents) { + var completed = new defer(); + var locations = this.parseWords(contents, section, wordCount, startCfi); + var remainingCount = count - this._locationsWords.length; + this._locationsWords = this._locationsWords.concat( + locations.length >= count + ? locations.slice(0, remainingCount) + : locations + ); + + section.unload(); + + this.processingTimeout = setTimeout( + () => completed.resolve(locations), + this.pause + ); + return completed.promise; + }.bind(this) + ); + } + + //http://stackoverflow.com/questions/18679576/counting-words-in-string + countWords(s) { + s = s.replace(/(^\s*)|(\s*$)/gi, ""); //exclude start and end white-space + s = s.replace(/[ ]{2,}/gi, " "); //2 or more space to 1 + s = s.replace(/\n /, "\n"); // exclude newline with a start spacing + return s.split(" ").length; + } + + parseWords(contents, section, wordCount, startCfi) { + var cfiBase = section.cfiBase; + var locations = []; + var doc = contents.ownerDocument; + var body = qs(doc, "body"); + var _break = wordCount; + var foundStartNode = startCfi ? startCfi.spinePos !== section.index : true; + var startNode; + if (startCfi && section.index === startCfi.spinePos) { + startNode = startCfi.findNode( + startCfi.range + ? startCfi.path.steps.concat(startCfi.start.steps) + : startCfi.path.steps, + contents.ownerDocument + ); + } + var parser = function (node) { + if (!foundStartNode) { + if (node === startNode) { + foundStartNode = true; + } else { + return false; + } + } + if (node.textContent.length < 10) { + if (node.textContent.trim().length === 0) { + return false; + } + } + var len = this.countWords(node.textContent); + var dist; + var pos = 0; + + if (len === 0) { + return false; // continue + } + + dist = _break - this._wordCounter; + + // Node is smaller than a break, + // skip over it + if (dist > len) { + this._wordCounter += len; + pos = len; + } + + while (pos < len) { + dist = _break - this._wordCounter; + + // Gone over + if (pos + dist >= len) { + // Continue counter for next node + this._wordCounter += len - pos; + // break + pos = len; + // At End + } else { + // Advance pos + pos += dist; + + let cfi = new EpubCFI(node, cfiBase); + locations.push({ cfi: cfi.toString(), wordCount: this._wordCounter }); + this._wordCounter = 0; + } + } + }; + + sprint(body, parser.bind(this)); + + return locations; + } + + /** + * Get a location from an EpubCFI + * @param {EpubCFI} cfi + * @return {number} + */ + locationFromCfi(cfi) { + let loc; + if (EpubCFI.prototype.isCfiString(cfi)) { + cfi = new EpubCFI(cfi); + } + // Check if the location has not been set yet + if (this._locations.length === 0) { + return -1; + } + + loc = locationOf(cfi, this._locations, this.epubcfi.compare); + + if (loc > this.total) { + return this.total; + } + + return loc; + } + + /** + * Get a percentage position in locations from an EpubCFI + * @param {EpubCFI} cfi + * @return {number} + */ + percentageFromCfi(cfi) { + if (this._locations.length === 0) { + return null; + } + // Find closest cfi + var loc = this.locationFromCfi(cfi); + // Get percentage in total + return this.percentageFromLocation(loc); + } + + /** + * Get a percentage position from a location index + * @param {number} location + * @return {number} + */ + percentageFromLocation(loc) { + if (!loc || !this.total) { + return 0; + } + + return loc / this.total; + } + + /** + * Get an EpubCFI from location index + * @param {number} loc + * @return {EpubCFI} cfi + */ + cfiFromLocation(loc) { + var cfi = -1; + // check that pg is an int + if (typeof loc != "number") { + loc = parseInt(loc); + } + + if (loc >= 0 && loc < this._locations.length) { + cfi = this._locations[loc]; + } + + return cfi; + } + + /** + * Get an EpubCFI from location percentage + * @param {number} percentage + * @return {EpubCFI} cfi + */ + cfiFromPercentage(percentage) { + let loc; + if (percentage > 1) { + console.warn("Normalize cfiFromPercentage value to between 0 - 1"); + } + + // Make sure 1 goes to very end + if (percentage >= 1) { + let cfi = new EpubCFI(this._locations[this.total]); + cfi.collapse(); + return cfi.toString(); + } + + loc = Math.ceil(this.total * percentage); + return this.cfiFromLocation(loc); + } + + /** + * Load locations from JSON + * @param {json} locations + */ + load(locations) { + if (typeof locations === "string") { + this._locations = JSON.parse(locations); + } else { + this._locations = locations; + } + this.total = this._locations.length - 1; + return this._locations; + } + + /** + * Save locations to JSON + * @return {json} + */ + save() { + return JSON.stringify(this._locations); + } + + getCurrent() { + return this._current; + } + + setCurrent(curr) { + var loc; + + if (typeof curr == "string") { + this._currentCfi = curr; + } else if (typeof curr == "number") { + this._current = curr; + } else { + return; + } + + if (this._locations.length === 0) { + return; + } + + if (typeof curr == "string") { + loc = this.locationFromCfi(curr); + this._current = loc; + } else { + loc = curr; + } + + this.emit(EVENTS.LOCATIONS.CHANGED, { + percentage: this.percentageFromLocation(loc), + }); + } + + /** + * Get the current location + */ + get currentLocation() { + return this._current; + } + + /** + * Set the current location + */ + set currentLocation(curr) { + this.setCurrent(curr); + } + + /** + * Locations length + */ + length() { + return this._locations.length; + } + + destroy() { + this.spine = undefined; + this.request = undefined; + this.pause = undefined; + + this.q.stop(); + this.q = undefined; + this.epubcfi = undefined; + + this._locations = undefined; + this.total = undefined; + + this.break = undefined; + this._current = undefined; + + this.currentLocation = undefined; + this._currentCfi = undefined; + clearTimeout(this.processingTimeout); + } } EventEmitter(Locations.prototype); diff --git a/src/managers/continuous/index.js b/src/managers/continuous/index.js index d61f8f7..b79dc5f 100644 --- a/src/managers/continuous/index.js +++ b/src/managers/continuous/index.js @@ -1,592 +1,609 @@ -import {extend, defer, requestAnimationFrame} from "../../utils/core"; +import debounce from "lodash/debounce"; +import { EVENTS } from "../../utils/constants"; +import { defer, extend, requestAnimationFrame } from "../../utils/core"; import DefaultViewManager from "../default"; import Snap from "../helpers/snap"; -import { EVENTS } from "../../utils/constants"; -import debounce from "lodash/debounce"; class ContinuousViewManager extends DefaultViewManager { - constructor(options) { - super(options); - - this.name = "continuous"; - - this.settings = extend(this.settings || {}, { - infinite: true, - overflow: undefined, - axis: undefined, - writingMode: undefined, - flow: "scrolled", - offset: 500, - offsetDelta: 250, - width: undefined, - height: undefined, - snap: false, - afterScrolledTimeout: 10, - allowScriptedContent: false, - allowPopups: false - }); - - extend(this.settings, options.settings || {}); - - // Gap can be 0, but defaults doesn't handle that - if (options.settings.gap != "undefined" && options.settings.gap === 0) { - this.settings.gap = options.settings.gap; - } - - this.viewSettings = { - ignoreClass: this.settings.ignoreClass, - axis: this.settings.axis, - flow: this.settings.flow, - layout: this.layout, - width: 0, - height: 0, - forceEvenPages: false, - allowScriptedContent: this.settings.allowScriptedContent, - allowPopups: this.settings.allowPopups - }; - - this.scrollTop = 0; - this.scrollLeft = 0; - } - - display(section, target){ - return DefaultViewManager.prototype.display.call(this, section, target) - .then(function () { - return this.fill(); - }.bind(this)); - } - - fill(_full){ - var full = _full || new defer(); - - this.q.enqueue(() => { - return this.check(); - }).then((result) => { - if (result) { - this.fill(full); - } else { - full.resolve(); - } - }); - - return full.promise; - } - - moveTo(offset){ - // var bounds = this.stage.bounds(); - // var dist = Math.floor(offset.top / bounds.height) * bounds.height; - var distX = 0, - distY = 0; - - var offsetX = 0, - offsetY = 0; - - if(!this.isPaginated) { - distY = offset.top; - offsetY = offset.top+this.settings.offsetDelta; - } else { - distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; - offsetX = distX+this.settings.offsetDelta; - } - - if (distX > 0 || distY > 0) { - this.scrollBy(distX, distY, true); - } - } - - afterResized(view){ - this.emit(EVENTS.MANAGERS.RESIZE, view.section); - } - - // Remove Previous Listeners if present - removeShownListeners(view){ - - // view.off("shown", this.afterDisplayed); - // view.off("shown", this.afterDisplayedAbove); - view.onDisplayed = function(){}; - - } - - add(section){ - var view = this.createView(section); - - this.views.append(view); - - view.on(EVENTS.VIEWS.RESIZED, (bounds) => { - view.expanded = true; - }); - - view.on(EVENTS.VIEWS.AXIS, (axis) => { - this.updateAxis(axis); - }); - - view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { - this.updateWritingMode(mode); - }); - - // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this)); - view.onDisplayed = this.afterDisplayed.bind(this); - view.onResize = this.afterResized.bind(this); - - return view.display(this.request); - } - - append(section){ - var view = this.createView(section); - - view.on(EVENTS.VIEWS.RESIZED, (bounds) => { - view.expanded = true; - }); - - view.on(EVENTS.VIEWS.AXIS, (axis) => { - this.updateAxis(axis); - }); - - view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { - this.updateWritingMode(mode); - }); - - this.views.append(view); - - view.onDisplayed = this.afterDisplayed.bind(this); - - return view; - } - - prepend(section){ - var view = this.createView(section); - - view.on(EVENTS.VIEWS.RESIZED, (bounds) => { - this.counter(bounds); - view.expanded = true; - }); - - view.on(EVENTS.VIEWS.AXIS, (axis) => { - this.updateAxis(axis); - }); - - view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { - this.updateWritingMode(mode); - }); - - this.views.prepend(view); - - view.onDisplayed = this.afterDisplayed.bind(this); - - return view; - } - - counter(bounds){ - if(this.settings.axis === "vertical") { - this.scrollBy(0, bounds.heightDelta, true); - } else { - this.scrollBy(bounds.widthDelta, 0, true); - } - } - - update(_offset){ - var container = this.bounds(); - var views = this.views.all(); - var viewsLength = views.length; - var visible = []; - var offset = typeof _offset != "undefined" ? _offset : (this.settings.offset || 0); - var isVisible; - var view; - - var updating = new defer(); - var promises = []; - for (var i = 0; i < viewsLength; i++) { - view = views[i]; - - isVisible = this.isVisible(view, offset, offset, container); - - if(isVisible === true) { - // console.log("visible " + view.index, view.displayed); - - if (!view.displayed) { - let displayed = view.display(this.request) - .then(function (view) { - view.show(); - }, (err) => { - view.hide(); - }); - promises.push(displayed); - } else { - view.show(); - } - visible.push(view); - } else { - this.q.enqueue(view.destroy.bind(view)); - // console.log("hidden " + view.index, view.displayed); - - clearTimeout(this.trimTimeout); - this.trimTimeout = setTimeout(function(){ - this.q.enqueue(this.trim.bind(this)); - }.bind(this), 250); - } - - } - - if(promises.length){ - return Promise.all(promises) - .catch((err) => { - updating.reject(err); - }); - } else { - updating.resolve(); - return updating.promise; - } - - } - - check(_offsetLeft, _offsetTop){ - var checking = new defer(); - var newViews = []; - - var horizontal = (this.settings.axis === "horizontal"); - var delta = this.settings.offset || 0; - - if (_offsetLeft && horizontal) { - delta = _offsetLeft; - } - - if (_offsetTop && !horizontal) { - delta = _offsetTop; - } - - var bounds = this._bounds; // bounds saved this until resize - - let offset = horizontal ? this.scrollLeft : this.scrollTop; - let visibleLength = horizontal ? Math.floor(bounds.width) : bounds.height; - let contentLength = horizontal ? this.container.scrollWidth : this.container.scrollHeight; - let writingMode = (this.writingMode && this.writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal"; - let rtlScrollType = this.settings.rtlScrollType; - let rtl = this.settings.direction === "rtl"; - - if (!this.settings.fullsize) { - // Scroll offset starts at width of element - if (rtl && rtlScrollType === "default" && writingMode === "horizontal") { - offset = contentLength - visibleLength - offset; - } - // Scroll offset starts at 0 and goes negative - if (rtl && rtlScrollType === "negative" && writingMode === "horizontal") { - offset = offset * -1; - } - } else { - // Scroll offset starts at 0 and goes negative - if ((horizontal && rtl && rtlScrollType === "negative") || - (!horizontal && rtl && rtlScrollType === "default")) { - offset = offset * -1; - } - } - - let prepend = () => { - let first = this.views.first(); - let prev = first && first.section.prev(); - - if(prev) { - newViews.push(this.prepend(prev)); - } - }; - - let append = () => { - let last = this.views.last(); - let next = last && last.section.next(); - - if(next) { - newViews.push(this.append(next)); - } - - }; - - let end = offset + visibleLength + delta; - let start = offset - delta; - - if (end >= contentLength) { - append(); - } - - if (start < 0) { - prepend(); - } - - - let promises = newViews.map((view) => { - return view.display(this.request); - }); - - if(newViews.length){ - return Promise.all(promises) - .then(() => { - return this.check(); - }) - .then(() => { - // Check to see if anything new is on screen after rendering - return this.update(delta); - }, (err) => { - return err; - }); - } else { - this.q.enqueue(function(){ - this.update(); - }.bind(this)); - checking.resolve(false); - return checking.promise; - } - - - } - - trim(){ - var task = new defer(); - var displayed = this.views.displayed(); - var first = displayed[0]; - var last = displayed[displayed.length-1]; - var firstIndex = this.views.indexOf(first); - var lastIndex = this.views.indexOf(last); - var above = this.views.slice(0, firstIndex); - var below = this.views.slice(lastIndex+1); - - // Erase all but last above - for (var i = 0; i < above.length-1; i++) { - this.erase(above[i], above); - } - - // Erase all except first below - for (var j = 1; j < below.length; j++) { - this.erase(below[j]); - } - - task.resolve(); - return task.promise; - } - - erase(view, above){ //Trim - - var prevTop; - var prevLeft; - - if(!this.settings.fullsize) { - prevTop = this.container.scrollTop; - prevLeft = this.container.scrollLeft; - } else { - prevTop = window.scrollY; - prevLeft = window.scrollX; - } - - var bounds = view.bounds(); - - this.views.remove(view); - - if(above) { - if (this.settings.axis === "vertical") { - this.scrollTo(0, prevTop - bounds.height, true); - } else { - if(this.settings.direction === 'rtl') { - if (!this.settings.fullsize) { - this.scrollTo(prevLeft, 0, true); - } else { - this.scrollTo(prevLeft + Math.floor(bounds.width), 0, true); - } - } else { - this.scrollTo(prevLeft - Math.floor(bounds.width), 0, true); - } - } - } - - } - - addEventListeners(stage){ - - window.addEventListener("unload", function(e){ - this.ignore = true; - // this.scrollTo(0,0); - this.destroy(); - }.bind(this)); - - this.addScrollListeners(); - - if (this.isPaginated && this.settings.snap) { - this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap); - } - } - - addScrollListeners() { - var scroller; - - this.tick = requestAnimationFrame; - - let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1; - - this.scrollDeltaVert = 0; - this.scrollDeltaHorz = 0; - - if(!this.settings.fullsize) { - scroller = this.container; - this.scrollTop = this.container.scrollTop; - this.scrollLeft = this.container.scrollLeft; - } else { - scroller = window; - this.scrollTop = window.scrollY * dir; - this.scrollLeft = window.scrollX * dir; - } - - this._onScroll = this.onScroll.bind(this); - scroller.addEventListener("scroll", this._onScroll); - this._scrolled = debounce(this.scrolled.bind(this), 30); - // this.tick.call(window, this.onScroll.bind(this)); - - this.didScroll = false; - - } - - removeEventListeners(){ - var scroller; - - if(!this.settings.fullsize) { - scroller = this.container; - } else { - scroller = window; - } - - scroller.removeEventListener("scroll", this._onScroll); - this._onScroll = undefined; - } - - onScroll(){ - let scrollTop; - let scrollLeft; - let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1; - - if(!this.settings.fullsize) { - scrollTop = this.container.scrollTop; - scrollLeft = this.container.scrollLeft; - } else { - scrollTop = window.scrollY * dir; - scrollLeft = window.scrollX * dir; - } - - this.scrollTop = scrollTop; - this.scrollLeft = scrollLeft; - - if(!this.ignore) { - - this._scrolled(); - - } else { - this.ignore = false; - } - - this.scrollDeltaVert += Math.abs(scrollTop-this.prevScrollTop); - this.scrollDeltaHorz += Math.abs(scrollLeft-this.prevScrollLeft); - - this.prevScrollTop = scrollTop; - this.prevScrollLeft = scrollLeft; - - clearTimeout(this.scrollTimeout); - this.scrollTimeout = setTimeout(function(){ - this.scrollDeltaVert = 0; - this.scrollDeltaHorz = 0; - }.bind(this), 150); - - clearTimeout(this.afterScrolled); - - this.didScroll = false; - - } - - scrolled() { - - this.q.enqueue(function() { - return this.check(); - }.bind(this)); - - this.emit(EVENTS.MANAGERS.SCROLL, { - top: this.scrollTop, - left: this.scrollLeft - }); - - clearTimeout(this.afterScrolled); - this.afterScrolled = setTimeout(function () { - - // Don't report scroll if we are about the snap - if (this.snapper && this.snapper.supportsTouch && this.snapper.needsSnap()) { - return; - } - - this.emit(EVENTS.MANAGERS.SCROLLED, { - top: this.scrollTop, - left: this.scrollLeft - }); - - }.bind(this), this.settings.afterScrolledTimeout); - } - - next(){ - - let delta = this.layout.props.name === "pre-paginated" && - this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta; - - if(!this.views.length) return; - - if(this.isPaginated && this.settings.axis === "horizontal") { - - this.scrollBy(delta, 0, true); - - } else { - - this.scrollBy(0, this.layout.height, true); - - } - - this.q.enqueue(function() { - return this.check(); - }.bind(this)); - } - - prev(){ - - let delta = this.layout.props.name === "pre-paginated" && - this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta; - - if(!this.views.length) return; - - if(this.isPaginated && this.settings.axis === "horizontal") { - - this.scrollBy(-delta, 0, true); - - } else { - - this.scrollBy(0, -this.layout.height, true); - - } - - this.q.enqueue(function() { - return this.check(); - }.bind(this)); - } - - updateFlow(flow){ - if (this.rendered && this.snapper) { - this.snapper.destroy(); - this.snapper = undefined; - } - - super.updateFlow(flow, "scroll"); - - if (this.rendered && this.isPaginated && this.settings.snap) { - this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap); - } - } - - destroy(){ - super.destroy(); - - if (this.snapper) { - this.snapper.destroy(); - } - } - + constructor(options) { + super(options); + + this.name = "continuous"; + + this.settings = extend(this.settings || {}, { + infinite: true, + overflow: undefined, + axis: undefined, + writingMode: undefined, + flow: "scrolled", + offset: 500, + offsetDelta: 250, + width: undefined, + height: undefined, + snap: false, + afterScrolledTimeout: 10, + allowScriptedContent: false, + allowPopups: false, + }); + + extend(this.settings, options.settings || {}); + + // Gap can be 0, but defaults doesn't handle that + if (options.settings.gap != "undefined" && options.settings.gap === 0) { + this.settings.gap = options.settings.gap; + } + + this.viewSettings = { + ignoreClass: this.settings.ignoreClass, + axis: this.settings.axis, + flow: this.settings.flow, + layout: this.layout, + width: 0, + height: 0, + forceEvenPages: false, + allowScriptedContent: this.settings.allowScriptedContent, + allowPopups: this.settings.allowPopups, + }; + + this.scrollTop = 0; + this.scrollLeft = 0; + } + + display(section, target) { + return DefaultViewManager.prototype.display + .call(this, section, target) + .then( + function () { + return this.fill(); + }.bind(this) + ); + } + + fill(_full) { + var full = _full || new defer(); + + this.q + .enqueue(() => { + return this.check(); + }) + .then((result) => { + if (result) { + this.fill(full); + } else { + full.resolve(); + } + }); + + return full.promise; + } + + moveTo(offset) { + var distX = 0, + distY = 0; + + if (!this.isPaginated) { + distY = offset.top; + } else { + distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; + } + + if (distX > 0 || distY > 0) { + this.scrollBy(distX, distY, true); + } + } + + afterResized(view) { + this.emit(EVENTS.MANAGERS.RESIZE, view.section); + } + + // Remove Previous Listeners if present + removeShownListeners(view) { + view.onDisplayed = function () {}; + } + + add(section) { + var view = this.createView(section); + + this.views.append(view); + + view.on(EVENTS.VIEWS.RESIZED, (bounds) => { + view.expanded = true; + }); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + return view.display(this.request); + } + + append(section) { + var view = this.createView(section); + + view.on(EVENTS.VIEWS.RESIZED, (bounds) => { + view.expanded = true; + }); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + this.views.append(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + + return view; + } + + prepend(section) { + var view = this.createView(section); + + view.on(EVENTS.VIEWS.RESIZED, (bounds) => { + this.counter(bounds); + view.expanded = true; + }); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + this.views.prepend(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + + return view; + } + + counter(bounds) { + if (this.settings.axis === "vertical") { + this.scrollBy(0, bounds.heightDelta, true); + } else { + this.scrollBy(bounds.widthDelta, 0, true); + } + } + + update(_offset) { + var container = this.bounds(); + var views = this.views.all(); + var viewsLength = views.length; + var visible = []; + var offset = + typeof _offset != "undefined" ? _offset : this.settings.offset || 0; + var isVisible; + var view; + + var updating = new defer(); + var promises = []; + for (var i = 0; i < viewsLength; i++) { + view = views[i]; + + isVisible = this.isVisible(view, offset, offset, container); + + if (isVisible === true) { + if (!view.displayed) { + let displayed = view.display(this.request).then( + function (view) { + view.show(); + }, + (err) => { + view.hide(); + } + ); + promises.push(displayed); + } else { + view.show(); + } + visible.push(view); + } else { + this.q.enqueue(view.destroy.bind(view)); + + clearTimeout(this.trimTimeout); + this.trimTimeout = setTimeout( + function () { + this.q.enqueue(this.trim.bind(this)); + }.bind(this), + 250 + ); + } + } + + if (promises.length) { + return Promise.all(promises).catch((err) => { + updating.reject(err); + }); + } else { + updating.resolve(); + return updating.promise; + } + } + + check(_offsetLeft, _offsetTop) { + var checking = new defer(); + var newViews = []; + + var horizontal = this.settings.axis === "horizontal"; + var delta = this.settings.offset || 0; + + if (_offsetLeft && horizontal) { + delta = _offsetLeft; + } + + if (_offsetTop && !horizontal) { + delta = _offsetTop; + } + + // bounds saved this until resize + var bounds = this._bounds; + + let offset = horizontal ? this.scrollLeft : this.scrollTop; + let visibleLength = horizontal ? Math.floor(bounds.width) : bounds.height; + let contentLength = horizontal + ? this.container.scrollWidth + : this.container.scrollHeight; + let writingMode = + this.writingMode && this.writingMode.indexOf("vertical") === 0 + ? "vertical" + : "horizontal"; + let rtlScrollType = this.settings.rtlScrollType; + let rtl = this.settings.direction === "rtl"; + + if (!this.settings.fullsize) { + // Scroll offset starts at width of element + if (rtl && rtlScrollType === "default" && writingMode === "horizontal") { + offset = contentLength - visibleLength - offset; + } + // Scroll offset starts at 0 and goes negative + if (rtl && rtlScrollType === "negative" && writingMode === "horizontal") { + offset = offset * -1; + } + } else { + // Scroll offset starts at 0 and goes negative + if ( + (horizontal && rtl && rtlScrollType === "negative") || + (!horizontal && rtl && rtlScrollType === "default") + ) { + offset = offset * -1; + } + } + + let prepend = () => { + let first = this.views.first(); + let prev = first && first.section.prev(); + + if (prev) { + newViews.push(this.prepend(prev)); + } + }; + + let append = () => { + let last = this.views.last(); + let next = last && last.section.next(); + + if (next) { + newViews.push(this.append(next)); + } + }; + + let end = offset + visibleLength + delta; + let start = offset - delta; + + if (end >= contentLength) { + append(); + } + + if (start < 0) { + prepend(); + } + + let promises = newViews.map((view) => { + return view.display(this.request); + }); + + if (newViews.length) { + return Promise.all(promises) + .then(() => { + return this.check(); + }) + .then( + () => { + // Check to see if anything new is on screen after rendering + return this.update(delta); + }, + (err) => { + return err; + } + ); + } else { + this.q.enqueue( + function () { + this.update(); + }.bind(this) + ); + checking.resolve(false); + return checking.promise; + } + } + + trim() { + var task = new defer(); + var displayed = this.views.displayed(); + var first = displayed[0]; + var last = displayed[displayed.length - 1]; + var firstIndex = this.views.indexOf(first); + var lastIndex = this.views.indexOf(last); + var above = this.views.slice(0, firstIndex); + var below = this.views.slice(lastIndex + 1); + + // Erase all but last above + for (var i = 0; i < above.length - 1; i++) { + this.erase(above[i], above); + } + + // Erase all except first below + for (var j = 1; j < below.length; j++) { + this.erase(below[j]); + } + + task.resolve(); + return task.promise; + } + + erase(view, above) { + var prevTop; + var prevLeft; + + if (!this.settings.fullsize) { + prevTop = this.container.scrollTop; + prevLeft = this.container.scrollLeft; + } else { + prevTop = window.scrollY; + prevLeft = window.scrollX; + } + + var bounds = view.bounds(); + + this.views.remove(view); + + if (above) { + if (this.settings.axis === "vertical") { + this.scrollTo(0, prevTop - bounds.height, true); + } else { + if (this.settings.direction === "rtl") { + if (!this.settings.fullsize) { + this.scrollTo(prevLeft, 0, true); + } else { + this.scrollTo(prevLeft + Math.floor(bounds.width), 0, true); + } + } else { + this.scrollTo(prevLeft - Math.floor(bounds.width), 0, true); + } + } + } + } + + addEventListeners(stage) { + window.addEventListener( + "unload", + function (e) { + this.ignore = true; + this.destroy(); + }.bind(this) + ); + + this.addScrollListeners(); + + if (this.isPaginated && this.settings.snap) { + this.snapper = new Snap( + this, + this.settings.snap && + typeof this.settings.snap === "object" && + this.settings.snap + ); + } + } + + addScrollListeners() { + var scroller; + this.tick = requestAnimationFrame; + let dir = + this.settings.direction === "rtl" && + this.settings.rtlScrollType === "default" + ? -1 + : 1; + + this.scrollDeltaVert = 0; + this.scrollDeltaHorz = 0; + + if (!this.settings.fullsize) { + scroller = this.container; + this.scrollTop = this.container.scrollTop; + this.scrollLeft = this.container.scrollLeft; + } else { + scroller = window; + this.scrollTop = window.scrollY * dir; + this.scrollLeft = window.scrollX * dir; + } + + this._onScroll = this.onScroll.bind(this); + scroller.addEventListener("scroll", this._onScroll); + this._scrolled = debounce(this.scrolled.bind(this), 30); + this.didScroll = false; + } + + removeEventListeners() { + var scroller; + + if (!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } + + scroller.removeEventListener("scroll", this._onScroll); + this._onScroll = undefined; + } + + onScroll() { + let scrollTop; + let scrollLeft; + let dir = + this.settings.direction === "rtl" && + this.settings.rtlScrollType === "default" + ? -1 + : 1; + + if (!this.settings.fullsize) { + scrollTop = this.container.scrollTop; + scrollLeft = this.container.scrollLeft; + } else { + scrollTop = window.scrollY * dir; + scrollLeft = window.scrollX * dir; + } + + this.scrollTop = scrollTop; + this.scrollLeft = scrollLeft; + + if (!this.ignore) { + this._scrolled(); + } else { + this.ignore = false; + } + + this.scrollDeltaVert += Math.abs(scrollTop - this.prevScrollTop); + this.scrollDeltaHorz += Math.abs(scrollLeft - this.prevScrollLeft); + + this.prevScrollTop = scrollTop; + this.prevScrollLeft = scrollLeft; + + clearTimeout(this.scrollTimeout); + this.scrollTimeout = setTimeout( + function () { + this.scrollDeltaVert = 0; + this.scrollDeltaHorz = 0; + }.bind(this), + 150 + ); + + clearTimeout(this.afterScrolled); + + this.didScroll = false; + } + + scrolled() { + this.q.enqueue( + function () { + return this.check(); + }.bind(this) + ); + + this.emit(EVENTS.MANAGERS.SCROLL, { + top: this.scrollTop, + left: this.scrollLeft, + }); + + clearTimeout(this.afterScrolled); + this.afterScrolled = setTimeout( + function () { + // Don't report scroll if we are about the snap + if ( + this.snapper && + this.snapper.supportsTouch && + this.snapper.needsSnap() + ) { + return; + } + + this.emit(EVENTS.MANAGERS.SCROLLED, { + top: this.scrollTop, + left: this.scrollLeft, + }); + }.bind(this), + this.settings.afterScrolledTimeout + ); + } + + next() { + let delta = + this.layout.props.name === "pre-paginated" && this.layout.props.spread + ? this.layout.props.delta * 2 + : this.layout.props.delta; + + if (!this.views.length) return; + + if (this.isPaginated && this.settings.axis === "horizontal") { + this.scrollBy(delta, 0, true); + } else { + this.scrollBy(0, this.layout.height, true); + } + + this.q.enqueue( + function () { + return this.check(); + }.bind(this) + ); + } + + prev() { + let delta = + this.layout.props.name === "pre-paginated" && this.layout.props.spread + ? this.layout.props.delta * 2 + : this.layout.props.delta; + + if (!this.views.length) return; + + if (this.isPaginated && this.settings.axis === "horizontal") { + this.scrollBy(-delta, 0, true); + } else { + this.scrollBy(0, -this.layout.height, true); + } + + this.q.enqueue( + function () { + return this.check(); + }.bind(this) + ); + } + + updateFlow(flow) { + if (this.rendered && this.snapper) { + this.snapper.destroy(); + this.snapper = undefined; + } + + super.updateFlow(flow, "scroll"); + + if (this.rendered && this.isPaginated && this.settings.snap) { + this.snapper = new Snap( + this, + this.settings.snap && + typeof this.settings.snap === "object" && + this.settings.snap + ); + } + } + + destroy() { + super.destroy(); + + if (this.snapper) { + this.snapper.destroy(); + } + } } export default ContinuousViewManager; diff --git a/src/managers/default/index.js b/src/managers/default/index.js index 2812d87..36f605a 100644 --- a/src/managers/default/index.js +++ b/src/managers/default/index.js @@ -1,163 +1,165 @@ import EventEmitter from "event-emitter"; -import {extend, defer, windowBounds, isNumber} from "../../utils/core"; -import scrollType from "../../utils/scrolltype"; import Mapping from "../../mapping"; +import { EVENTS } from "../../utils/constants"; +import { defer, extend, isNumber, windowBounds } from "../../utils/core"; import Queue from "../../utils/queue"; +import scrollType from "../../utils/scrolltype"; import Stage from "../helpers/stage"; import Views from "../helpers/views"; -import { EVENTS } from "../../utils/constants"; class DefaultViewManager { - constructor(options) { + constructor(options) { + this.name = "default"; + this.optsSettings = options.settings; + this.View = options.view; + this.request = options.request; + this.renditionQueue = options.queue; + this.q = new Queue(this); - this.name = "default"; - this.optsSettings = options.settings; - this.View = options.view; - this.request = options.request; - this.renditionQueue = options.queue; - this.q = new Queue(this); + this.settings = extend(this.settings || {}, { + infinite: true, + hidden: false, + width: undefined, + height: undefined, + axis: undefined, + writingMode: undefined, + flow: "scrolled", + ignoreClass: "", + fullsize: undefined, + allowScriptedContent: false, + allowPopups: false, + }); - this.settings = extend(this.settings || {}, { - infinite: true, - hidden: false, - width: undefined, - height: undefined, - axis: undefined, - writingMode: undefined, - flow: "scrolled", - ignoreClass: "", - fullsize: undefined, - allowScriptedContent: false, - allowPopups: false - }); + extend(this.settings, options.settings || {}); - extend(this.settings, options.settings || {}); + this.viewSettings = { + ignoreClass: this.settings.ignoreClass, + axis: this.settings.axis, + flow: this.settings.flow, + layout: this.layout, + method: this.settings.method, // srcdoc, blobUrl, write + width: 0, + height: 0, + forceEvenPages: true, + allowScriptedContent: this.settings.allowScriptedContent, + allowPopups: this.settings.allowPopups, + }; - this.viewSettings = { - ignoreClass: this.settings.ignoreClass, - axis: this.settings.axis, - flow: this.settings.flow, - layout: this.layout, - method: this.settings.method, // srcdoc, blobUrl, write - width: 0, - height: 0, - forceEvenPages: true, - allowScriptedContent: this.settings.allowScriptedContent, - allowPopups: this.settings.allowPopups - }; + this.rendered = false; + } - this.rendered = false; + render(element, size) { + let tag = element.tagName; - } + if ( + typeof this.settings.fullsize === "undefined" && + tag && + (tag.toLowerCase() == "body" || tag.toLowerCase() == "html") + ) { + this.settings.fullsize = true; + } - render(element, size){ - let tag = element.tagName; + if (this.settings.fullsize) { + this.settings.overflow = "visible"; + this.overflow = this.settings.overflow; + } - if (typeof this.settings.fullsize === "undefined" && - tag && (tag.toLowerCase() == "body" || - tag.toLowerCase() == "html")) { - this.settings.fullsize = true; - } + this.settings.size = size; - if (this.settings.fullsize) { - this.settings.overflow = "visible"; - this.overflow = this.settings.overflow; - } + this.settings.rtlScrollType = scrollType(); - this.settings.size = size; + // Save the stage + this.stage = new Stage({ + width: size.width, + height: size.height, + overflow: this.overflow, + hidden: this.settings.hidden, + axis: this.settings.axis, + fullsize: this.settings.fullsize, + direction: this.settings.direction, + }); - this.settings.rtlScrollType = scrollType(); + this.stage.attachTo(element); - // Save the stage - this.stage = new Stage({ - width: size.width, - height: size.height, - overflow: this.overflow, - hidden: this.settings.hidden, - axis: this.settings.axis, - fullsize: this.settings.fullsize, - direction: this.settings.direction - }); + // Get this stage container div + this.container = this.stage.getContainer(); - this.stage.attachTo(element); + // Views array methods + this.views = new Views(this.container); - // Get this stage container div - this.container = this.stage.getContainer(); + // Calculate Stage Size + this._bounds = this.bounds(); + this._stageSize = this.stage.size(); - // Views array methods - this.views = new Views(this.container); + // Set the dimensions for views + this.viewSettings.width = this._stageSize.width; + this.viewSettings.height = this._stageSize.height; - // Calculate Stage Size - this._bounds = this.bounds(); - this._stageSize = this.stage.size(); + // Function to handle a resize event. + // Will only attach if width and height are both fixed. + this.stage.onResize(this.onResized.bind(this)); - // Set the dimensions for views - this.viewSettings.width = this._stageSize.width; - this.viewSettings.height = this._stageSize.height; + this.stage.onOrientationChange(this.onOrientationChange.bind(this)); - // Function to handle a resize event. - // Will only attach if width and height are both fixed. - this.stage.onResize(this.onResized.bind(this)); + // Add Event Listeners + this.addEventListeners(); - this.stage.onOrientationChange(this.onOrientationChange.bind(this)); + // Add Layout method + // this.applyLayoutMethod(); + if (this.layout) { + this.updateLayout(); + } - // Add Event Listeners - this.addEventListeners(); + this.rendered = true; + } - // Add Layout method - // this.applyLayoutMethod(); - if (this.layout) { - this.updateLayout(); - } + addEventListeners() { + var scroller; - this.rendered = true; + window.addEventListener( + "unload", + function (e) { + this.destroy(); + }.bind(this) + ); - } + if (!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } - addEventListeners(){ - var scroller; + this._onScroll = this.onScroll.bind(this); + scroller.addEventListener("scroll", this._onScroll); + } - window.addEventListener("unload", function(e){ - this.destroy(); - }.bind(this)); + removeEventListeners() { + var scroller; - if(!this.settings.fullsize) { - scroller = this.container; - } else { - scroller = window; - } + if (!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } - this._onScroll = this.onScroll.bind(this); - scroller.addEventListener("scroll", this._onScroll); - } + scroller.removeEventListener("scroll", this._onScroll); + this._onScroll = undefined; + } - removeEventListeners(){ - var scroller; + destroy() { + clearTimeout(this.orientationTimeout); + clearTimeout(this.resizeTimeout); + clearTimeout(this.afterScrolled); - if(!this.settings.fullsize) { - scroller = this.container; - } else { - scroller = window; - } + this.clear(); - scroller.removeEventListener("scroll", this._onScroll); - this._onScroll = undefined; - } + this.removeEventListeners(); - destroy(){ - clearTimeout(this.orientationTimeout); - clearTimeout(this.resizeTimeout); - clearTimeout(this.afterScrolled); + this.stage.destroy(); - this.clear(); + this.rendered = false; - this.removeEventListeners(); - - this.stage.destroy(); - - this.rendered = false; - - /* + /* clearTimeout(this.trimTimeout); if(this.settings.hidden) { @@ -166,909 +168,958 @@ class DefaultViewManager { this.element.removeChild(this.container); } */ - } + } - onOrientationChange(e) { - let {orientation} = window; + onOrientationChange(e) { + let { orientation } = window; - if(this.optsSettings.resizeOnOrientationChange) { - this.resize(); - } + if (this.optsSettings.resizeOnOrientationChange) { + this.resize(); + } - // Per ampproject: - // In IOS 10.3, the measured size of an element is incorrect if the - // element size depends on window size directly and the measurement - // happens in window.resize event. Adding a timeout for correct - // measurement. See https://github.com/ampproject/amphtml/issues/8479 - clearTimeout(this.orientationTimeout); - this.orientationTimeout = setTimeout(function(){ - this.orientationTimeout = undefined; + // Per ampproject: + // In IOS 10.3, the measured size of an element is incorrect if the + // element size depends on window size directly and the measurement + // happens in window.resize event. Adding a timeout for correct + // measurement. See https://github.com/ampproject/amphtml/issues/8479 + clearTimeout(this.orientationTimeout); + this.orientationTimeout = setTimeout( + function () { + this.orientationTimeout = undefined; - if(this.optsSettings.resizeOnOrientationChange) { - this.resize(); - } + if (this.optsSettings.resizeOnOrientationChange) { + this.resize(); + } - this.emit(EVENTS.MANAGERS.ORIENTATION_CHANGE, orientation); - }.bind(this), 500); + this.emit(EVENTS.MANAGERS.ORIENTATION_CHANGE, orientation); + }.bind(this), + 500 + ); + } - } + onResized(e) { + this.resize(); + } - onResized(e) { - this.resize(); - } + resize(width, height, epubcfi) { + let stageSize = this.stage.size(width, height); - resize(width, height, epubcfi){ - let stageSize = this.stage.size(width, height); + // For Safari, wait for orientation to catch up + // if the window is a square + this.winBounds = windowBounds(); + if ( + this.orientationTimeout && + this.winBounds.width === this.winBounds.height + ) { + // reset the stage size for next resize + this._stageSize = undefined; + return; + } - // For Safari, wait for orientation to catch up - // if the window is a square - this.winBounds = windowBounds(); - if (this.orientationTimeout && - this.winBounds.width === this.winBounds.height) { - // reset the stage size for next resize - this._stageSize = undefined; - return; - } + if ( + this._stageSize && + this._stageSize.width === stageSize.width && + this._stageSize.height === stageSize.height + ) { + // Size is the same, no need to resize + return; + } - if (this._stageSize && - this._stageSize.width === stageSize.width && - this._stageSize.height === stageSize.height ) { - // Size is the same, no need to resize - return; - } + this._stageSize = stageSize; - this._stageSize = stageSize; + this._bounds = this.bounds(); - this._bounds = this.bounds(); + // Clear current views + this.clear(); - // Clear current views - this.clear(); + // Update for new views + this.viewSettings.width = this._stageSize.width; + this.viewSettings.height = this._stageSize.height; - // Update for new views - this.viewSettings.width = this._stageSize.width; - this.viewSettings.height = this._stageSize.height; + this.updateLayout(); - this.updateLayout(); + this.emit( + EVENTS.MANAGERS.RESIZED, + { + width: this._stageSize.width, + height: this._stageSize.height, + }, + epubcfi + ); + } - this.emit(EVENTS.MANAGERS.RESIZED, { - width: this._stageSize.width, - height: this._stageSize.height - }, epubcfi); - } + createView(section, forceRight) { + return new this.View(section, extend(this.viewSettings, { forceRight })); + } - createView(section, forceRight) { - return new this.View(section, extend(this.viewSettings, { forceRight }) ); - } + handleNextPrePaginated(forceRight, section, action) { + let next; - handleNextPrePaginated(forceRight, section, action) { - let next; + if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) { + if (forceRight || section.index === 0) { + // First page (cover) should stand alone for pre-paginated books + return; + } + next = section.next(); + if (next && !next.properties.includes("page-spread-left")) { + return action.call(this, next); + } + } + } - if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) { - if (forceRight || section.index === 0) { - // First page (cover) should stand alone for pre-paginated books - return; - } - next = section.next(); - if (next && !next.properties.includes("page-spread-left")) { - return action.call(this, next); - } - } - } + display(section, target) { + var displaying = new defer(); + var displayed = displaying.promise; - display(section, target){ + // Check if moving to target is needed + if (target === section.href || isNumber(target)) { + target = undefined; + } - var displaying = new defer(); - var displayed = displaying.promise; + // Check to make sure the section we want isn't already shown + var visible = this.views.find(section); - // Check if moving to target is needed - if (target === section.href || isNumber(target)) { - target = undefined; - } + // View is already shown, just move to correct location in view + if (visible && section && this.layout.name !== "pre-paginated") { + let offset = visible.offset(); - // Check to make sure the section we want isn't already shown - var visible = this.views.find(section); + if (this.settings.direction === "ltr") { + this.scrollTo(offset.left, offset.top, true); + } else { + let width = visible.width(); + this.scrollTo(offset.left + width, offset.top, true); + } - // View is already shown, just move to correct location in view - if(visible && section && this.layout.name !== "pre-paginated") { - let offset = visible.offset(); + if (target) { + let offset = visible.locationOf(target); + let width = visible.width(); + this.moveTo(offset, width); + } - if (this.settings.direction === "ltr") { - this.scrollTo(offset.left, offset.top, true); - } else { - let width = visible.width(); - this.scrollTo(offset.left + width, offset.top, true); - } + displaying.resolve(); + return displayed; + } - if(target) { - let offset = visible.locationOf(target); - let width = visible.width(); - this.moveTo(offset, width); - } + // Hide all current views + this.clear(); - displaying.resolve(); - return displayed; - } + let forceRight = false; + if ( + this.layout.name === "pre-paginated" && + this.layout.divisor === 2 && + section.properties.includes("page-spread-right") + ) { + forceRight = true; + } - // Hide all current views - this.clear(); + this.add(section, forceRight) + .then( + function (view) { + // Move to correct place within the section, if needed + if (target) { + let offset = view.locationOf(target); + let width = view.width(); + this.moveTo(offset, width); + } + }.bind(this), + (err) => { + displaying.reject(err); + } + ) + .then( + function () { + return this.handleNextPrePaginated(forceRight, section, this.add); + }.bind(this) + ) + .then( + function () { + this.views.show(); - let forceRight = false; - if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && section.properties.includes("page-spread-right")) { - forceRight = true; - } + displaying.resolve(); + }.bind(this) + ); + // .then(function(){ + // return this.hooks.display.trigger(view); + // }.bind(this)) + // .then(function(){ + // this.views.show(); + // }.bind(this)); + return displayed; + } - this.add(section, forceRight) - .then(function(view){ + afterDisplayed(view) { + this.emit(EVENTS.MANAGERS.ADDED, view); + } - // Move to correct place within the section, if needed - if(target) { - let offset = view.locationOf(target); - let width = view.width(); - this.moveTo(offset, width); - } + afterResized(view) { + this.emit(EVENTS.MANAGERS.RESIZE, view.section); + } - }.bind(this), (err) => { - displaying.reject(err); - }) - .then(function(){ - return this.handleNextPrePaginated(forceRight, section, this.add); - }.bind(this)) - .then(function(){ + moveTo(offset, width) { + var distX = 0, + distY = 0; - this.views.show(); + if (!this.isPaginated) { + distY = offset.top; + } else { + distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; - displaying.resolve(); + if (distX + this.layout.delta > this.container.scrollWidth) { + distX = this.container.scrollWidth - this.layout.delta; + } - }.bind(this)); - // .then(function(){ - // return this.hooks.display.trigger(view); - // }.bind(this)) - // .then(function(){ - // this.views.show(); - // }.bind(this)); - return displayed; - } + distY = Math.floor(offset.top / this.layout.delta) * this.layout.delta; - afterDisplayed(view){ - this.emit(EVENTS.MANAGERS.ADDED, view); - } - - afterResized(view){ - this.emit(EVENTS.MANAGERS.RESIZE, view.section); - } - - moveTo(offset, width){ - var distX = 0, - distY = 0; - - if(!this.isPaginated) { - distY = offset.top; - } else { - distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; - - if (distX + this.layout.delta > this.container.scrollWidth) { - distX = this.container.scrollWidth - this.layout.delta; - } - - distY = Math.floor(offset.top / this.layout.delta) * this.layout.delta; - - if (distY + this.layout.delta > this.container.scrollHeight) { - distY = this.container.scrollHeight - this.layout.delta; - } - } - if(this.settings.direction === 'rtl'){ - /*** + if (distY + this.layout.delta > this.container.scrollHeight) { + distY = this.container.scrollHeight - this.layout.delta; + } + } + if (this.settings.direction === "rtl") { + /*** the `floor` function above (L343) is on positive values, so we should add one `layout.delta` to distX or use `Math.ceil` function, or multiply offset.left by -1 before `Math.floor` */ - distX = distX + this.layout.delta - distX = distX - width - } - this.scrollTo(distX, distY, true); - } + distX = distX + this.layout.delta; + distX = distX - width; + } + this.scrollTo(distX, distY, true); + } - add(section, forceRight){ - var view = this.createView(section, forceRight); - - this.views.append(view); - - // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this)); - view.onDisplayed = this.afterDisplayed.bind(this); - view.onResize = this.afterResized.bind(this); - - view.on(EVENTS.VIEWS.AXIS, (axis) => { - this.updateAxis(axis); - }); - - view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { - this.updateWritingMode(mode); - }); - - return view.display(this.request); - } - - append(section, forceRight){ - var view = this.createView(section, forceRight); - this.views.append(view); - - view.onDisplayed = this.afterDisplayed.bind(this); - view.onResize = this.afterResized.bind(this); - - view.on(EVENTS.VIEWS.AXIS, (axis) => { - this.updateAxis(axis); - }); - - view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { - this.updateWritingMode(mode); - }); - - return view.display(this.request); - } - - prepend(section, forceRight){ - var view = this.createView(section, forceRight); - - view.on(EVENTS.VIEWS.RESIZED, (bounds) => { - this.counter(bounds); - }); - - this.views.prepend(view); - - view.onDisplayed = this.afterDisplayed.bind(this); - view.onResize = this.afterResized.bind(this); - - view.on(EVENTS.VIEWS.AXIS, (axis) => { - this.updateAxis(axis); - }); - - view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { - this.updateWritingMode(mode); - }); - - return view.display(this.request); - } - - counter(bounds){ - if(this.settings.axis === "vertical") { - this.scrollBy(0, bounds.heightDelta, true); - } else { - this.scrollBy(bounds.widthDelta, 0, true); - } - - } - - // resizeView(view) { - // - // if(this.settings.globalLayoutProperties.layout === "pre-paginated") { - // view.lock("both", this.bounds.width, this.bounds.height); - // } else { - // view.lock("width", this.bounds.width, this.bounds.height); - // } - // - // }; - - next(){ - var next; - var left; - - let dir = this.settings.direction; - - if(!this.views.length) return; - - if(this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) { - - this.scrollLeft = this.container.scrollLeft; - - left = this.container.scrollLeft + this.container.offsetWidth + this.layout.delta; - - if(left <= this.container.scrollWidth) { - this.scrollBy(this.layout.delta, 0, true); - } else { - next = this.views.last().section.next(); - } - } else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") { - - this.scrollLeft = this.container.scrollLeft; - - if (this.settings.rtlScrollType === "default"){ - left = this.container.scrollLeft; - - if (left > 0) { - this.scrollBy(this.layout.delta, 0, true); - } else { - next = this.views.last().section.next(); - } - } else { - left = this.container.scrollLeft + ( this.layout.delta * -1 ); - - if (left > this.container.scrollWidth * -1){ - this.scrollBy(this.layout.delta, 0, true); - } else { - next = this.views.last().section.next(); - } - } - - } else if (this.isPaginated && this.settings.axis === "vertical") { - - this.scrollTop = this.container.scrollTop; - - let top = this.container.scrollTop + this.container.offsetHeight; - - if(top < this.container.scrollHeight) { - this.scrollBy(0, this.layout.height, true); - } else { - next = this.views.last().section.next(); - } - - } else { - next = this.views.last().section.next(); - } - - if(next) { - this.clear(); - // The new section may have a different writing-mode from the old section. Thus, we need to update layout. - this.updateLayout(); - - let forceRight = false; - if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && next.properties.includes("page-spread-right")) { - forceRight = true; - } - - return this.append(next, forceRight) - .then(function(){ - return this.handleNextPrePaginated(forceRight, next, this.append); - }.bind(this), (err) => { - return err; - }) - .then(function(){ - - // Reset position to start for scrolled-doc vertical-rl in default mode - if (!this.isPaginated && - this.settings.axis === "horizontal" && - this.settings.direction === "rtl" && - this.settings.rtlScrollType === "default") { - - this.scrollTo(this.container.scrollWidth, 0, true); - } - this.views.show(); - }.bind(this)); - } - - - } - - prev(){ - var prev; - var left; - let dir = this.settings.direction; - - if(!this.views.length) return; - - if(this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) { - - this.scrollLeft = this.container.scrollLeft; - - left = this.container.scrollLeft; - - if(left > 0) { - this.scrollBy(-this.layout.delta, 0, true); - } else { - prev = this.views.first().section.prev(); - } - - } else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") { - - this.scrollLeft = this.container.scrollLeft; - - if (this.settings.rtlScrollType === "default"){ - left = this.container.scrollLeft + this.container.offsetWidth; - - if (left < this.container.scrollWidth) { - this.scrollBy(-this.layout.delta, 0, true); - } else { - prev = this.views.first().section.prev(); - } - } - else{ - left = this.container.scrollLeft; - - if (left < 0) { - this.scrollBy(-this.layout.delta, 0, true); - } else { - prev = this.views.first().section.prev(); - } - } - - } else if (this.isPaginated && this.settings.axis === "vertical") { - - this.scrollTop = this.container.scrollTop; - - let top = this.container.scrollTop; - - if(top > 0) { - this.scrollBy(0, -(this.layout.height), true); - } else { - prev = this.views.first().section.prev(); - } - - } else { - - prev = this.views.first().section.prev(); - - } - - if(prev) { - this.clear(); - // The new section may have a different writing-mode from the old section. Thus, we need to update layout. - this.updateLayout(); - - let forceRight = false; - if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && typeof prev.prev() !== "object") { - forceRight = true; - } - - return this.prepend(prev, forceRight) - .then(function(){ - var left; - if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) { - left = prev.prev(); - if (left) { - return this.prepend(left); - } - } - }.bind(this), (err) => { - return err; - }) - .then(function(){ - if(this.isPaginated && this.settings.axis === "horizontal") { - if (this.settings.direction === "rtl") { - if (this.settings.rtlScrollType === "default"){ - this.scrollTo(0, 0, true); - } - else{ - this.scrollTo((this.container.scrollWidth * -1) + this.layout.delta, 0, true); - } - } else { - this.scrollTo(this.container.scrollWidth - this.layout.delta, 0, true); - } - } - this.views.show(); - }.bind(this)); - } - } - - current(){ - var visible = this.visible(); - if(visible.length){ - // Current is the last visible view - return visible[visible.length-1]; - } - return null; - } - - clear () { - - // this.q.clear(); - - if (this.views) { - this.views.hide(); - this.scrollTo(0,0, true); - this.views.clear(); - } - } - - currentLocation(){ - this.updateLayout(); - if (this.isPaginated && this.settings.axis === "horizontal") { - this.location = this.paginatedLocation(); - } else { - this.location = this.scrolledLocation(); - } - return this.location; - } - - scrolledLocation() { - let visible = this.visible(); - let container = this.container.getBoundingClientRect(); - let pageHeight = (container.height < window.innerHeight) ? container.height : window.innerHeight; - let pageWidth = (container.width < window.innerWidth) ? container.width : window.innerWidth; - let vertical = (this.settings.axis === "vertical"); - let rtl = (this.settings.direction === "rtl"); - - let offset = 0; - let used = 0; - - if(this.settings.fullsize) { - offset = vertical ? window.scrollY : window.scrollX; - } - - let sections = visible.map((view) => { - let {index, href} = view.section; - let position = view.position(); - let width = view.width(); - let height = view.height(); - - let startPos; - let endPos; - let stopPos; - let totalPages; - - if (vertical) { - startPos = offset + container.top - position.top + used; - endPos = startPos + pageHeight - used; - totalPages = this.layout.count(height, pageHeight).pages; - stopPos = pageHeight; - } else { - startPos = offset + container.left - position.left + used; - endPos = startPos + pageWidth - used; - totalPages = this.layout.count(width, pageWidth).pages; - stopPos = pageWidth; - } - - let currPage = Math.ceil(startPos / stopPos); - let pages = []; - let endPage = Math.ceil(endPos / stopPos); - - // Reverse page counts for horizontal rtl - if (this.settings.direction === "rtl" && !vertical) { - let tempStartPage = currPage; - currPage = totalPages - endPage; - endPage = totalPages - tempStartPage; - } - - pages = []; - for (var i = currPage; i <= endPage; i++) { - let pg = i + 1; - pages.push(pg); - } - - let mapping = this.mapping.page(view.contents, view.section.cfiBase, startPos, endPos); - - return { - index, - href, - pages, - totalPages, - mapping - }; - }); - - return sections; - } - - paginatedLocation(){ - let visible = this.visible(); - let container = this.container.getBoundingClientRect(); - - let left = 0; - let used = 0; - - if(this.settings.fullsize) { - left = window.scrollX; - } - - let sections = visible.map((view) => { - let {index, href} = view.section; - let offset; - let position = view.position(); - let width = view.width(); - - // Find mapping - let start; - let end; - let pageWidth; - - if (this.settings.direction === "rtl") { - offset = container.right - left; - pageWidth = Math.min(Math.abs(offset - position.left), this.layout.width) - used; - end = position.width - (position.right - offset) - used; - start = end - pageWidth; - } else { - offset = container.left + left; - pageWidth = Math.min(position.right - offset, this.layout.width) - used; - start = offset - position.left + used; - end = start + pageWidth; - } - - used += pageWidth; - - let mapping = this.mapping.page(view.contents, view.section.cfiBase, start, end); - - let totalPages = this.layout.count(width).pages; - let startPage = Math.floor(start / this.layout.pageWidth); - let pages = []; - let endPage = Math.floor(end / this.layout.pageWidth); - - // start page should not be negative - if (startPage < 0) { - startPage = 0; - endPage = endPage + 1; - } - - // Reverse page counts for rtl - if (this.settings.direction === "rtl") { - let tempStartPage = startPage; - startPage = totalPages - endPage; - endPage = totalPages - tempStartPage; - } - - - for (var i = startPage + 1; i <= endPage; i++) { - let pg = i; - pages.push(pg); - } - - return { - index, - href, - pages, - totalPages, - mapping - }; - }); - - return sections; - } - - isVisible(view, offsetPrev, offsetNext, _container){ - var position = view.position(); - var container = _container || this.bounds(); - - if(this.settings.axis === "horizontal" && - position.right > container.left - offsetPrev && - position.left < container.right + offsetNext) { - - return true; - - } else if(this.settings.axis === "vertical" && - position.bottom > container.top - offsetPrev && - position.top < container.bottom + offsetNext) { - - return true; - } - - return false; - - } - - visible(){ - var container = this.bounds(); - var views = this.views.displayed(); - var viewsLength = views.length; - var visible = []; - var isVisible; - var view; - - for (var i = 0; i < viewsLength; i++) { - view = views[i]; - isVisible = this.isVisible(view, 0, 0, container); - - if(isVisible === true) { - visible.push(view); - } - - } - return visible; - } - - scrollBy(x, y, silent){ - let dir = this.settings.direction === "rtl" ? -1 : 1; - - if(silent) { - this.ignore = true; - } - - if(!this.settings.fullsize) { - if(x) this.container.scrollLeft += x * dir; - if(y) this.container.scrollTop += y; - } else { - window.scrollBy(x * dir, y * dir); - } - this.scrolled = true; - } - - scrollTo(x, y, silent){ - if(silent) { - this.ignore = true; - } - - if(!this.settings.fullsize) { - this.container.scrollLeft = x; - this.container.scrollTop = y; - } else { - window.scrollTo(x,y); - } - this.scrolled = true; - } - - onScroll(){ - let scrollTop; - let scrollLeft; - - if(!this.settings.fullsize) { - scrollTop = this.container.scrollTop; - scrollLeft = this.container.scrollLeft; - } else { - scrollTop = window.scrollY; - scrollLeft = window.scrollX; - } - - this.scrollTop = scrollTop; - this.scrollLeft = scrollLeft; - - if(!this.ignore) { - this.emit(EVENTS.MANAGERS.SCROLL, { - top: scrollTop, - left: scrollLeft - }); - - clearTimeout(this.afterScrolled); - this.afterScrolled = setTimeout(function () { - this.emit(EVENTS.MANAGERS.SCROLLED, { - top: this.scrollTop, - left: this.scrollLeft - }); - }.bind(this), 20); - - - - } else { - this.ignore = false; - } - - } - - bounds() { - var bounds; - - bounds = this.stage.bounds(); - - return bounds; - } - - applyLayout(layout) { - - this.layout = layout; - this.updateLayout(); - if (this.views && this.views.length > 0 && this.layout.name === "pre-paginated") { - this.display(this.views.first().section); - } - // this.manager.layout(this.layout.format); - } - - updateLayout() { - - if (!this.stage) { - return; - } - - this._stageSize = this.stage.size(); - - if(!this.isPaginated) { - this.layout.calculate(this._stageSize.width, this._stageSize.height); - } else { - this.layout.calculate( - this._stageSize.width, - this._stageSize.height, - this.settings.gap - ); - - // Set the look ahead offset for what is visible - this.settings.offset = this.layout.delta / this.layout.divisor; - - // this.stage.addStyleRules("iframe", [{"margin-right" : this.layout.gap + "px"}]); - - } - - // Set the dimensions for views - this.viewSettings.width = this.layout.width; - this.viewSettings.height = this.layout.height; - - this.setLayout(this.layout); - } - - setLayout(layout){ - - this.viewSettings.layout = layout; - - this.mapping = new Mapping(layout.props, this.settings.direction, this.settings.axis); - - if(this.views) { - - this.views.forEach(function(view){ - if (view) { - view.setLayout(layout); - } - }); - - } - - } - - updateWritingMode(mode) { - this.writingMode = mode; - } - - updateAxis(axis, forceUpdate){ - - if (!forceUpdate && axis === this.settings.axis) { - return; - } - - this.settings.axis = axis; - - this.stage && this.stage.axis(axis); - - this.viewSettings.axis = axis; - - if (this.mapping) { - this.mapping = new Mapping(this.layout.props, this.settings.direction, this.settings.axis); - } - - if (this.layout) { - if (axis === "vertical") { - this.layout.spread("none"); - } else { - this.layout.spread(this.layout.settings.spread); - } - } - } - - updateFlow(flow, defaultScrolledOverflow="auto"){ - let isPaginated = (flow === "paginated" || flow === "auto"); - - this.isPaginated = isPaginated; - - if (flow === "scrolled-doc" || - flow === "scrolled-continuous" || - flow === "scrolled") { - this.updateAxis("vertical"); - } else { - this.updateAxis("horizontal"); - } - - this.viewSettings.flow = flow; - - if (!this.settings.overflow) { - this.overflow = isPaginated ? "hidden" : defaultScrolledOverflow; - } else { - this.overflow = this.settings.overflow; - } - - this.stage && this.stage.overflow(this.overflow); - - this.updateLayout(); - - } - - getContents(){ - var contents = []; - if (!this.views) { - return contents; - } - this.views.forEach(function(view){ - const viewContents = view && view.contents; - if (viewContents) { - contents.push(viewContents); - } - }); - return contents; - } - - direction(dir="ltr") { - this.settings.direction = dir; - - this.stage && this.stage.direction(dir); - - this.viewSettings.direction = dir; - - this.updateLayout(); - } - - isRendered() { - return this.rendered; - } + add(section, forceRight) { + var view = this.createView(section, forceRight); + + this.views.append(view); + + // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this)); + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + return view.display(this.request); + } + + append(section, forceRight) { + var view = this.createView(section, forceRight); + this.views.append(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + return view.display(this.request); + } + + prepend(section, forceRight) { + var view = this.createView(section, forceRight); + + view.on(EVENTS.VIEWS.RESIZED, (bounds) => { + this.counter(bounds); + }); + + this.views.prepend(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + return view.display(this.request); + } + + counter(bounds) { + if (this.settings.axis === "vertical") { + this.scrollBy(0, bounds.heightDelta, true); + } else { + this.scrollBy(bounds.widthDelta, 0, true); + } + } + + // resizeView(view) { + // + // if(this.settings.globalLayoutProperties.layout === "pre-paginated") { + // view.lock("both", this.bounds.width, this.bounds.height); + // } else { + // view.lock("width", this.bounds.width, this.bounds.height); + // } + // + // }; + + next() { + var next; + var left; + + let dir = this.settings.direction; + + if (!this.views.length) return; + + if ( + this.isPaginated && + this.settings.axis === "horizontal" && + (!dir || dir === "ltr") + ) { + this.scrollLeft = this.container.scrollLeft; + + left = + this.container.scrollLeft + + this.container.offsetWidth + + this.layout.delta; + + if (left <= this.container.scrollWidth) { + this.scrollBy(this.layout.delta, 0, true); + } else { + next = this.views.last().section.next(); + } + } else if ( + this.isPaginated && + this.settings.axis === "horizontal" && + dir === "rtl" + ) { + this.scrollLeft = this.container.scrollLeft; + + if (this.settings.rtlScrollType === "default") { + left = this.container.scrollLeft; + + if (left > 0) { + this.scrollBy(this.layout.delta, 0, true); + } else { + next = this.views.last().section.next(); + } + } else { + left = this.container.scrollLeft + this.layout.delta * -1; + + if (left > this.container.scrollWidth * -1) { + this.scrollBy(this.layout.delta, 0, true); + } else { + next = this.views.last().section.next(); + } + } + } else if (this.isPaginated && this.settings.axis === "vertical") { + this.scrollTop = this.container.scrollTop; + + let top = this.container.scrollTop + this.container.offsetHeight; + + if (top < this.container.scrollHeight) { + this.scrollBy(0, this.layout.height, true); + } else { + next = this.views.last().section.next(); + } + } else { + next = this.views.last().section.next(); + } + + if (next) { + this.clear(); + // The new section may have a different writing-mode from the old section. Thus, we need to update layout. + this.updateLayout(); + + let forceRight = false; + if ( + this.layout.name === "pre-paginated" && + this.layout.divisor === 2 && + next.properties.includes("page-spread-right") + ) { + forceRight = true; + } + + return this.append(next, forceRight) + .then( + function () { + return this.handleNextPrePaginated(forceRight, next, this.append); + }.bind(this), + (err) => { + return err; + } + ) + .then( + function () { + // Reset position to start for scrolled-doc vertical-rl in default mode + if ( + !this.isPaginated && + this.settings.axis === "horizontal" && + this.settings.direction === "rtl" && + this.settings.rtlScrollType === "default" + ) { + this.scrollTo(this.container.scrollWidth, 0, true); + } + this.views.show(); + }.bind(this) + ); + } + } + + prev() { + var prev; + var left; + let dir = this.settings.direction; + + if (!this.views.length) return; + + if ( + this.isPaginated && + this.settings.axis === "horizontal" && + (!dir || dir === "ltr") + ) { + this.scrollLeft = this.container.scrollLeft; + + left = this.container.scrollLeft; + + if (left > 0) { + this.scrollBy(-this.layout.delta, 0, true); + } else { + prev = this.views.first().section.prev(); + } + } else if ( + this.isPaginated && + this.settings.axis === "horizontal" && + dir === "rtl" + ) { + this.scrollLeft = this.container.scrollLeft; + + if (this.settings.rtlScrollType === "default") { + left = this.container.scrollLeft + this.container.offsetWidth; + + if (left < this.container.scrollWidth) { + this.scrollBy(-this.layout.delta, 0, true); + } else { + prev = this.views.first().section.prev(); + } + } else { + left = this.container.scrollLeft; + + if (left < 0) { + this.scrollBy(-this.layout.delta, 0, true); + } else { + prev = this.views.first().section.prev(); + } + } + } else if (this.isPaginated && this.settings.axis === "vertical") { + this.scrollTop = this.container.scrollTop; + + let top = this.container.scrollTop; + + if (top > 0) { + this.scrollBy(0, -this.layout.height, true); + } else { + prev = this.views.first().section.prev(); + } + } else { + prev = this.views.first().section.prev(); + } + + if (prev) { + this.clear(); + // The new section may have a different writing-mode from the old section. Thus, we need to update layout. + this.updateLayout(); + + let forceRight = false; + if ( + this.layout.name === "pre-paginated" && + this.layout.divisor === 2 && + typeof prev.prev() !== "object" + ) { + forceRight = true; + } + + return this.prepend(prev, forceRight) + .then( + function () { + var left; + if ( + this.layout.name === "pre-paginated" && + this.layout.divisor > 1 + ) { + left = prev.prev(); + if (left) { + return this.prepend(left); + } + } + }.bind(this), + (err) => { + return err; + } + ) + .then( + function () { + if (this.isPaginated && this.settings.axis === "horizontal") { + if (this.settings.direction === "rtl") { + if (this.settings.rtlScrollType === "default") { + this.scrollTo(0, 0, true); + } else { + this.scrollTo( + this.container.scrollWidth * -1 + this.layout.delta, + 0, + true + ); + } + } else { + this.scrollTo( + this.container.scrollWidth - this.layout.delta, + 0, + true + ); + } + } + this.views.show(); + }.bind(this) + ); + } + } + + current() { + var visible = this.visible(); + if (visible.length) { + // Current is the last visible view + return visible[visible.length - 1]; + } + return null; + } + + clear() { + if (this.views) { + this.views.hide(); + this.scrollTo(0, 0, true); + this.views.clear(); + } + } + + currentLocation() { + this.updateLayout(); + if (this.isPaginated && this.settings.axis === "horizontal") { + this.location = this.paginatedLocation(); + } else { + this.location = this.scrolledLocation(); + } + return this.location; + } + + scrolledLocation() { + let visible = this.visible(); + let container = this.container.getBoundingClientRect(); + let pageHeight = + container.height < window.innerHeight + ? container.height + : window.innerHeight; + let pageWidth = + container.width < window.innerWidth ? container.width : window.innerWidth; + let vertical = this.settings.axis === "vertical"; + + let offset = 0; + let used = 0; + + if (this.settings.fullsize) { + offset = vertical ? window.scrollY : window.scrollX; + } + + let sections = visible.map((view) => { + let { index, href } = view.section; + let position = view.position(); + let width = view.width(); + let height = view.height(); + + let startPos; + let endPos; + let stopPos; + let totalPages; + + if (vertical) { + startPos = offset + container.top - position.top + used; + endPos = startPos + pageHeight - used; + totalPages = this.layout.count(height, pageHeight).pages; + stopPos = pageHeight; + } else { + startPos = offset + container.left - position.left + used; + endPos = startPos + pageWidth - used; + totalPages = this.layout.count(width, pageWidth).pages; + stopPos = pageWidth; + } + + let currPage = Math.ceil(startPos / stopPos); + let pages = []; + let endPage = Math.ceil(endPos / stopPos); + + // Reverse page counts for horizontal rtl + if (this.settings.direction === "rtl" && !vertical) { + let tempStartPage = currPage; + currPage = totalPages - endPage; + endPage = totalPages - tempStartPage; + } + + pages = []; + for (var i = currPage; i <= endPage; i++) { + let pg = i + 1; + pages.push(pg); + } + + let mapping = this.mapping.page( + view.contents, + view.section.cfiBase, + startPos, + endPos + ); + + return { + index, + href, + pages, + totalPages, + mapping, + }; + }); + + return sections; + } + + paginatedLocation() { + let visible = this.visible(); + let container = this.container.getBoundingClientRect(); + + let left = 0; + let used = 0; + + if (this.settings.fullsize) { + left = window.scrollX; + } + + let sections = visible.map((view) => { + let { index, href } = view.section; + let offset; + let position = view.position(); + let width = view.width(); + + // Find mapping + let start; + let end; + let pageWidth; + + if (this.settings.direction === "rtl") { + offset = container.right - left; + pageWidth = + Math.min(Math.abs(offset - position.left), this.layout.width) - used; + end = position.width - (position.right - offset) - used; + start = end - pageWidth; + } else { + offset = container.left + left; + pageWidth = Math.min(position.right - offset, this.layout.width) - used; + start = offset - position.left + used; + end = start + pageWidth; + } + + used += pageWidth; + + let mapping = this.mapping.page( + view.contents, + view.section.cfiBase, + start, + end + ); + + let totalPages = this.layout.count(width).pages; + let startPage = Math.floor(start / this.layout.pageWidth); + let pages = []; + let endPage = Math.floor(end / this.layout.pageWidth); + + // start page should not be negative + if (startPage < 0) { + startPage = 0; + endPage = endPage + 1; + } + + // Reverse page counts for rtl + if (this.settings.direction === "rtl") { + let tempStartPage = startPage; + startPage = totalPages - endPage; + endPage = totalPages - tempStartPage; + } + + for (var i = startPage + 1; i <= endPage; i++) { + let pg = i; + pages.push(pg); + } + + return { + index, + href, + pages, + totalPages, + mapping, + }; + }); + + return sections; + } + + isVisible(view, offsetPrev, offsetNext, _container) { + var position = view.position(); + var container = _container || this.bounds(); + + if ( + this.settings.axis === "horizontal" && + position.right > container.left - offsetPrev && + position.left < container.right + offsetNext + ) { + return true; + } else if ( + this.settings.axis === "vertical" && + position.bottom > container.top - offsetPrev && + position.top < container.bottom + offsetNext + ) { + return true; + } + + return false; + } + + visible() { + var container = this.bounds(); + var views = this.views.displayed(); + var viewsLength = views.length; + var visible = []; + var isVisible; + var view; + + for (var i = 0; i < viewsLength; i++) { + view = views[i]; + isVisible = this.isVisible(view, 0, 0, container); + + if (isVisible === true) { + visible.push(view); + } + } + return visible; + } + + scrollBy(x, y, silent) { + let dir = this.settings.direction === "rtl" ? -1 : 1; + + if (silent) { + this.ignore = true; + } + + if (!this.settings.fullsize) { + if (x) this.container.scrollLeft += x * dir; + if (y) this.container.scrollTop += y; + } else { + window.scrollBy(x * dir, y * dir); + } + this.scrolled = true; + } + + scrollTo(x, y, silent) { + if (silent) { + this.ignore = true; + } + + if (!this.settings.fullsize) { + this.container.scrollLeft = x; + this.container.scrollTop = y; + } else { + window.scrollTo(x, y); + } + this.scrolled = true; + } + + onScroll() { + let scrollTop; + let scrollLeft; + + if (!this.settings.fullsize) { + scrollTop = this.container.scrollTop; + scrollLeft = this.container.scrollLeft; + } else { + scrollTop = window.scrollY; + scrollLeft = window.scrollX; + } + + this.scrollTop = scrollTop; + this.scrollLeft = scrollLeft; + + if (!this.ignore) { + this.emit(EVENTS.MANAGERS.SCROLL, { + top: scrollTop, + left: scrollLeft, + }); + + clearTimeout(this.afterScrolled); + this.afterScrolled = setTimeout( + function () { + this.emit(EVENTS.MANAGERS.SCROLLED, { + top: this.scrollTop, + left: this.scrollLeft, + }); + }.bind(this), + 20 + ); + } else { + this.ignore = false; + } + } + + bounds() { + var bounds; + + bounds = this.stage.bounds(); + + return bounds; + } + + applyLayout(layout) { + this.layout = layout; + this.updateLayout(); + if ( + this.views && + this.views.length > 0 && + this.layout.name === "pre-paginated" + ) { + this.display(this.views.first().section); + } + } + + updateLayout() { + if (!this.stage) { + return; + } + + this._stageSize = this.stage.size(); + + if (!this.isPaginated) { + this.layout.calculate(this._stageSize.width, this._stageSize.height); + } else { + this.layout.calculate( + this._stageSize.width, + this._stageSize.height, + this.settings.gap + ); + + // Set the look ahead offset for what is visible + this.settings.offset = this.layout.delta / this.layout.divisor; + } + + // Set the dimensions for views + this.viewSettings.width = this.layout.width; + this.viewSettings.height = this.layout.height; + + this.setLayout(this.layout); + } + + setLayout(layout) { + this.viewSettings.layout = layout; + + this.mapping = new Mapping( + layout.props, + this.settings.direction, + this.settings.axis + ); + + if (this.views) { + this.views.forEach(function (view) { + if (view) { + view.setLayout(layout); + } + }); + } + } + + updateWritingMode(mode) { + this.writingMode = mode; + } + + updateAxis(axis, forceUpdate) { + if (!forceUpdate && axis === this.settings.axis) { + return; + } + + this.settings.axis = axis; + this.stage && this.stage.axis(axis); + this.viewSettings.axis = axis; + + if (this.mapping) { + this.mapping = new Mapping( + this.layout.props, + this.settings.direction, + this.settings.axis + ); + } + + if (this.layout) { + if (axis === "vertical") { + this.layout.spread("none"); + } else { + this.layout.spread(this.layout.settings.spread); + } + } + } + + updateFlow(flow, defaultScrolledOverflow = "auto") { + let isPaginated = flow === "paginated" || flow === "auto"; + this.isPaginated = isPaginated; + + if ( + flow === "scrolled-doc" || + flow === "scrolled-continuous" || + flow === "scrolled" + ) { + this.updateAxis("vertical"); + } else { + this.updateAxis("horizontal"); + } + + this.viewSettings.flow = flow; + + if (!this.settings.overflow) { + this.overflow = isPaginated ? "hidden" : defaultScrolledOverflow; + } else { + this.overflow = this.settings.overflow; + } + + this.stage && this.stage.overflow(this.overflow); + this.updateLayout(); + } + + getContents() { + var contents = []; + if (!this.views) { + return contents; + } + this.views.forEach(function (view) { + const viewContents = view && view.contents; + if (viewContents) { + contents.push(viewContents); + } + }); + return contents; + } + + direction(dir = "ltr") { + this.settings.direction = dir; + this.stage && this.stage.direction(dir); + this.viewSettings.direction = dir; + this.updateLayout(); + } + + isRendered() { + return this.rendered; + } } //-- Enable binding events to Manager diff --git a/src/managers/helpers/snap.js b/src/managers/helpers/snap.js index db0aaff..21ee178 100644 --- a/src/managers/helpers/snap.js +++ b/src/managers/helpers/snap.js @@ -1,336 +1,348 @@ -import {extend, defer, requestAnimationFrame, prefixed} from "../../utils/core"; -import { EVENTS, DOM_EVENTS } from "../../utils/constants"; import EventEmitter from "event-emitter"; +import { EVENTS } from "../../utils/constants"; +import { defer, extend } from "../../utils/core"; // easing equations from https://github.com/danro/easing-js/blob/master/easing.js -const PI_D2 = (Math.PI / 2); +const PI_D2 = Math.PI / 2; const EASING_EQUATIONS = { - easeOutSine: function (pos) { - return Math.sin(pos * PI_D2); - }, - easeInOutSine: function (pos) { - return (-0.5 * (Math.cos(Math.PI * pos) - 1)); - }, - easeInOutQuint: function (pos) { - if ((pos /= 0.5) < 1) { - return 0.5 * Math.pow(pos, 5); - } - return 0.5 * (Math.pow((pos - 2), 5) + 2); - }, - easeInCubic: function(pos) { - return Math.pow(pos, 3); - } + easeOutSine: function (pos) { + return Math.sin(pos * PI_D2); + }, + easeInOutSine: function (pos) { + return -0.5 * (Math.cos(Math.PI * pos) - 1); + }, + easeInOutQuint: function (pos) { + if ((pos /= 0.5) < 1) { + return 0.5 * Math.pow(pos, 5); + } + return 0.5 * (Math.pow(pos - 2, 5) + 2); + }, + easeInCubic: function (pos) { + return Math.pow(pos, 3); + }, }; class Snap { - constructor(manager, options) { - - this.settings = extend({ - duration: 80, - minVelocity: 0.2, - minDistance: 10, - easing: EASING_EQUATIONS['easeInCubic'] - }, options || {}); - - this.supportsTouch = this.supportsTouch(); - - if (this.supportsTouch) { - this.setup(manager); - } - } - - setup(manager) { - this.manager = manager; - - this.layout = this.manager.layout; - - this.fullsize = this.manager.settings.fullsize; - if (this.fullsize) { - this.element = this.manager.stage.element; - this.scroller = window; - this.disableScroll(); - } else { - this.element = this.manager.stage.container; - this.scroller = this.element; - this.element.style["WebkitOverflowScrolling"] = "touch"; - } - - // this.overflow = this.manager.overflow; - - // set lookahead offset to page width - this.manager.settings.offset = this.layout.width; - this.manager.settings.afterScrolledTimeout = this.settings.duration * 2; - - this.isVertical = this.manager.settings.axis === "vertical"; - - // disable snapping if not paginated or axis in not horizontal - if (!this.manager.isPaginated || this.isVertical) { - return; - } - - this.touchCanceler = false; - this.resizeCanceler = false; - this.snapping = false; - - - this.scrollLeft; - this.scrollTop; - - this.startTouchX = undefined; - this.startTouchY = undefined; - this.startTime = undefined; - this.endTouchX = undefined; - this.endTouchY = undefined; - this.endTime = undefined; - - this.addListeners(); - } - - supportsTouch() { - if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { - return true; - } - - return false; - } - - disableScroll() { - this.element.style.overflow = "hidden"; - } - - enableScroll() { - this.element.style.overflow = ""; - } - - addListeners() { - this._onResize = this.onResize.bind(this); - window.addEventListener('resize', this._onResize); - - this._onScroll = this.onScroll.bind(this); - this.scroller.addEventListener('scroll', this._onScroll); - - this._onTouchStart = this.onTouchStart.bind(this); - this.scroller.addEventListener('touchstart', this._onTouchStart, { passive: true }); - this.on('touchstart', this._onTouchStart); - - this._onTouchMove = this.onTouchMove.bind(this); - this.scroller.addEventListener('touchmove', this._onTouchMove, { passive: true }); - this.on('touchmove', this._onTouchMove); - - this._onTouchEnd = this.onTouchEnd.bind(this); - this.scroller.addEventListener('touchend', this._onTouchEnd, { passive: true }); - this.on('touchend', this._onTouchEnd); - - this._afterDisplayed = this.afterDisplayed.bind(this); - this.manager.on(EVENTS.MANAGERS.ADDED, this._afterDisplayed); - } - - removeListeners() { - window.removeEventListener('resize', this._onResize); - this._onResize = undefined; - - this.scroller.removeEventListener('scroll', this._onScroll); - this._onScroll = undefined; - - this.scroller.removeEventListener('touchstart', this._onTouchStart, { passive: true }); - this.off('touchstart', this._onTouchStart); - this._onTouchStart = undefined; - - this.scroller.removeEventListener('touchmove', this._onTouchMove, { passive: true }); - this.off('touchmove', this._onTouchMove); - this._onTouchMove = undefined; - - this.scroller.removeEventListener('touchend', this._onTouchEnd, { passive: true }); - this.off('touchend', this._onTouchEnd); - this._onTouchEnd = undefined; - - this.manager.off(EVENTS.MANAGERS.ADDED, this._afterDisplayed); - this._afterDisplayed = undefined; - } - - afterDisplayed(view) { - let contents = view.contents; - ["touchstart", "touchmove", "touchend"].forEach((e) => { - contents.on(e, (ev) => this.triggerViewEvent(ev, contents)); - }); - } - - triggerViewEvent(e, contents){ - this.emit(e.type, e, contents); - } - - onScroll(e) { - this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft; - this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop; - } - - onResize(e) { - this.resizeCanceler = true; - } - - onTouchStart(e) { - let { screenX, screenY } = e.touches[0]; - - if (this.fullsize) { - this.enableScroll(); - } - - this.touchCanceler = true; - - if (!this.startTouchX) { - this.startTouchX = screenX; - this.startTouchY = screenY; - this.startTime = this.now(); - } - - this.endTouchX = screenX; - this.endTouchY = screenY; - this.endTime = this.now(); - } - - onTouchMove(e) { - let { screenX, screenY } = e.touches[0]; - let deltaY = Math.abs(screenY - this.endTouchY); - - this.touchCanceler = true; - - - if (!this.fullsize && deltaY < 10) { - this.element.scrollLeft -= screenX - this.endTouchX; - } - - this.endTouchX = screenX; - this.endTouchY = screenY; - this.endTime = this.now(); - } - - onTouchEnd(e) { - if (this.fullsize) { - this.disableScroll(); - } - - this.touchCanceler = false; - - let swipped = this.wasSwiped(); - - if (swipped !== 0) { - this.snap(swipped); - } else { - this.snap(); - } - - this.startTouchX = undefined; - this.startTouchY = undefined; - this.startTime = undefined; - this.endTouchX = undefined; - this.endTouchY = undefined; - this.endTime = undefined; - } - - wasSwiped() { - let snapWidth = this.layout.pageWidth * this.layout.divisor; - let distance = (this.endTouchX - this.startTouchX); - let absolute = Math.abs(distance); - let time = this.endTime - this.startTime; - let velocity = (distance / time); - let minVelocity = this.settings.minVelocity; - - if (absolute <= this.settings.minDistance || absolute >= snapWidth) { - return 0; - } - - if (velocity > minVelocity) { - // previous - return -1; - } else if (velocity < -minVelocity) { - // next - return 1; - } - } - - needsSnap() { - let left = this.scrollLeft; - let snapWidth = this.layout.pageWidth * this.layout.divisor; - return (left % snapWidth) !== 0; - } - - snap(howMany=0) { - let left = this.scrollLeft; - let snapWidth = this.layout.pageWidth * this.layout.divisor; - let snapTo = Math.round(left / snapWidth) * snapWidth; - - if (howMany) { - snapTo += (howMany * snapWidth); - } - - return this.smoothScrollTo(snapTo); - } - - smoothScrollTo(destination) { - const deferred = new defer(); - const start = this.scrollLeft; - const startTime = this.now(); - - const duration = this.settings.duration; - const easing = this.settings.easing; - - this.snapping = true; - - // add animation loop - function tick() { - const now = this.now(); - const time = Math.min(1, ((now - startTime) / duration)); - const timeFunction = easing(time); - - - if (this.touchCanceler || this.resizeCanceler) { - this.resizeCanceler = false; - this.snapping = false; - deferred.resolve(); - return; - } - - if (time < 1) { - window.requestAnimationFrame(tick.bind(this)); - this.scrollTo(start + ((destination - start) * time), 0); - } else { - this.scrollTo(destination, 0); - this.snapping = false; - deferred.resolve(); - } - } - - tick.call(this); - - return deferred.promise; - } - - scrollTo(left=0, top=0) { - if (this.fullsize) { - window.scroll(left, top); - } else { - this.scroller.scrollLeft = left; - this.scroller.scrollTop = top; - } - } - - now() { - return ('now' in window.performance) ? performance.now() : new Date().getTime(); - } - - destroy() { - if (!this.scroller) { - return; - } - - if (this.fullsize) { - this.enableScroll(); - } - - this.removeListeners(); - - this.scroller = undefined; - } + constructor(manager, options) { + this.settings = extend( + { + duration: 80, + minVelocity: 0.2, + minDistance: 10, + easing: EASING_EQUATIONS["easeInCubic"], + }, + options || {} + ); + + this.supportsTouch = this.supportsTouch(); + + if (this.supportsTouch) { + this.setup(manager); + } + } + + setup(manager) { + this.manager = manager; + + this.layout = this.manager.layout; + + this.fullsize = this.manager.settings.fullsize; + if (this.fullsize) { + this.element = this.manager.stage.element; + this.scroller = window; + this.disableScroll(); + } else { + this.element = this.manager.stage.container; + this.scroller = this.element; + this.element.style["WebkitOverflowScrolling"] = "touch"; + } + + // set lookahead offset to page width + this.manager.settings.offset = this.layout.width; + this.manager.settings.afterScrolledTimeout = this.settings.duration * 2; + + this.isVertical = this.manager.settings.axis === "vertical"; + + // disable snapping if not paginated or axis in not horizontal + if (!this.manager.isPaginated || this.isVertical) { + return; + } + + this.touchCanceler = false; + this.resizeCanceler = false; + this.snapping = false; + + this.scrollLeft; + this.scrollTop; + + this.startTouchX = undefined; + this.startTouchY = undefined; + this.startTime = undefined; + this.endTouchX = undefined; + this.endTouchY = undefined; + this.endTime = undefined; + + this.addListeners(); + } + + supportsTouch() { + if ( + "ontouchstart" in window || + (window.DocumentTouch && document instanceof DocumentTouch) + ) { + return true; + } + + return false; + } + + disableScroll() { + this.element.style.overflow = "hidden"; + } + + enableScroll() { + this.element.style.overflow = ""; + } + + addListeners() { + this._onResize = this.onResize.bind(this); + window.addEventListener("resize", this._onResize); + + this._onScroll = this.onScroll.bind(this); + this.scroller.addEventListener("scroll", this._onScroll); + + this._onTouchStart = this.onTouchStart.bind(this); + this.scroller.addEventListener("touchstart", this._onTouchStart, { + passive: true, + }); + this.on("touchstart", this._onTouchStart); + + this._onTouchMove = this.onTouchMove.bind(this); + this.scroller.addEventListener("touchmove", this._onTouchMove, { + passive: true, + }); + this.on("touchmove", this._onTouchMove); + + this._onTouchEnd = this.onTouchEnd.bind(this); + this.scroller.addEventListener("touchend", this._onTouchEnd, { + passive: true, + }); + this.on("touchend", this._onTouchEnd); + + this._afterDisplayed = this.afterDisplayed.bind(this); + this.manager.on(EVENTS.MANAGERS.ADDED, this._afterDisplayed); + } + + removeListeners() { + window.removeEventListener("resize", this._onResize); + this._onResize = undefined; + + this.scroller.removeEventListener("scroll", this._onScroll); + this._onScroll = undefined; + + this.scroller.removeEventListener("touchstart", this._onTouchStart, { + passive: true, + }); + this.off("touchstart", this._onTouchStart); + this._onTouchStart = undefined; + + this.scroller.removeEventListener("touchmove", this._onTouchMove, { + passive: true, + }); + this.off("touchmove", this._onTouchMove); + this._onTouchMove = undefined; + + this.scroller.removeEventListener("touchend", this._onTouchEnd, { + passive: true, + }); + this.off("touchend", this._onTouchEnd); + this._onTouchEnd = undefined; + + this.manager.off(EVENTS.MANAGERS.ADDED, this._afterDisplayed); + this._afterDisplayed = undefined; + } + + afterDisplayed(view) { + let contents = view.contents; + ["touchstart", "touchmove", "touchend"].forEach((e) => { + contents.on(e, (ev) => this.triggerViewEvent(ev, contents)); + }); + } + + triggerViewEvent(e, contents) { + this.emit(e.type, e, contents); + } + + onScroll(e) { + this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft; + this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop; + } + + onResize(e) { + this.resizeCanceler = true; + } + + onTouchStart(e) { + let { screenX, screenY } = e.touches[0]; + + if (this.fullsize) { + this.enableScroll(); + } + + this.touchCanceler = true; + + if (!this.startTouchX) { + this.startTouchX = screenX; + this.startTouchY = screenY; + this.startTime = this.now(); + } + + this.endTouchX = screenX; + this.endTouchY = screenY; + this.endTime = this.now(); + } + + onTouchMove(e) { + let { screenX, screenY } = e.touches[0]; + let deltaY = Math.abs(screenY - this.endTouchY); + + this.touchCanceler = true; + + if (!this.fullsize && deltaY < 10) { + this.element.scrollLeft -= screenX - this.endTouchX; + } + + this.endTouchX = screenX; + this.endTouchY = screenY; + this.endTime = this.now(); + } + + onTouchEnd(e) { + if (this.fullsize) { + this.disableScroll(); + } + + this.touchCanceler = false; + + let swipped = this.wasSwiped(); + + if (swipped !== 0) { + this.snap(swipped); + } else { + this.snap(); + } + + this.startTouchX = undefined; + this.startTouchY = undefined; + this.startTime = undefined; + this.endTouchX = undefined; + this.endTouchY = undefined; + this.endTime = undefined; + } + + wasSwiped() { + let snapWidth = this.layout.pageWidth * this.layout.divisor; + let distance = this.endTouchX - this.startTouchX; + let absolute = Math.abs(distance); + let time = this.endTime - this.startTime; + let velocity = distance / time; + let minVelocity = this.settings.minVelocity; + + if (absolute <= this.settings.minDistance || absolute >= snapWidth) { + return 0; + } + + if (velocity > minVelocity) { + // previous + return -1; + } else if (velocity < -minVelocity) { + // next + return 1; + } + } + + needsSnap() { + let left = this.scrollLeft; + let snapWidth = this.layout.pageWidth * this.layout.divisor; + return left % snapWidth !== 0; + } + + snap(howMany = 0) { + let left = this.scrollLeft; + let snapWidth = this.layout.pageWidth * this.layout.divisor; + let snapTo = Math.round(left / snapWidth) * snapWidth; + + if (howMany) { + snapTo += howMany * snapWidth; + } + + return this.smoothScrollTo(snapTo); + } + + smoothScrollTo(destination) { + const deferred = new defer(); + const start = this.scrollLeft; + const startTime = this.now(); + + const duration = this.settings.duration; + + this.snapping = true; + + // add animation loop + function tick() { + const now = this.now(); + const time = Math.min(1, (now - startTime) / duration); + + if (this.touchCanceler || this.resizeCanceler) { + this.resizeCanceler = false; + this.snapping = false; + deferred.resolve(); + return; + } + + if (time < 1) { + window.requestAnimationFrame(tick.bind(this)); + this.scrollTo(start + (destination - start) * time, 0); + } else { + this.scrollTo(destination, 0); + this.snapping = false; + deferred.resolve(); + } + } + + tick.call(this); + + return deferred.promise; + } + + scrollTo(left = 0, top = 0) { + if (this.fullsize) { + window.scroll(left, top); + } else { + this.scroller.scrollLeft = left; + this.scroller.scrollTop = top; + } + } + + now() { + return "now" in window.performance + ? performance.now() + : new Date().getTime(); + } + + destroy() { + if (!this.scroller) { + return; + } + + if (this.fullsize) { + this.enableScroll(); + } + + this.removeListeners(); + + this.scroller = undefined; + } } EventEmitter(Snap.prototype); diff --git a/src/managers/helpers/stage.js b/src/managers/helpers/stage.js index d0f67e6..62f5990 100644 --- a/src/managers/helpers/stage.js +++ b/src/managers/helpers/stage.js @@ -1,363 +1,337 @@ -import {uuid, isNumber, isElement, windowBounds, extend} from "../../utils/core"; -import throttle from 'lodash/throttle' +import throttle from "lodash/throttle"; +import { + extend, + isElement, + isNumber, + uuid, + windowBounds, +} from "../../utils/core"; class Stage { - constructor(_options) { - this.settings = _options || {}; - this.id = "epubjs-container-" + uuid(); - - this.container = this.create(this.settings); - - if(this.settings.hidden) { - this.wrapper = this.wrap(this.container); - } - - } - - /* - * Creates an element to render to. - * Resizes to passed width and height or to the elements size - */ - create(options){ - let height = options.height;// !== false ? options.height : "100%"; - let width = options.width;// !== false ? options.width : "100%"; - let overflow = options.overflow || false; - let axis = options.axis || "vertical"; - let direction = options.direction; - - extend(this.settings, options); - - if(options.height && isNumber(options.height)) { - height = options.height + "px"; - } - - if(options.width && isNumber(options.width)) { - width = options.width + "px"; - } - - // Create new container element - let container = document.createElement("div"); - - container.id = this.id; - container.classList.add("epub-container"); - - // Style Element - // container.style.fontSize = "0"; - container.style.wordSpacing = "0"; - container.style.lineHeight = "0"; - container.style.verticalAlign = "top"; - container.style.position = "relative"; - - if(axis === "horizontal") { - // container.style.whiteSpace = "nowrap"; - container.style.display = "flex"; - container.style.flexDirection = "row"; - container.style.flexWrap = "nowrap"; - } - - if(width){ - container.style.width = width; - } - - if(height){ - container.style.height = height; - } - - if (overflow) { - if (overflow === "scroll" && axis === "vertical") { - container.style["overflow-y"] = overflow; - container.style["overflow-x"] = "hidden"; - } else if (overflow === "scroll" && axis === "horizontal") { - container.style["overflow-y"] = "hidden"; - container.style["overflow-x"] = overflow; - } else { - container.style["overflow"] = overflow; - } - } - - if (direction) { - container.dir = direction; - container.style["direction"] = direction; - } - - if (direction && this.settings.fullsize) { - document.body.style["direction"] = direction; - } - - return container; - } - - wrap(container) { - var wrapper = document.createElement("div"); - - wrapper.style.visibility = "hidden"; - wrapper.style.overflow = "hidden"; - wrapper.style.width = "0"; - wrapper.style.height = "0"; - - wrapper.appendChild(container); - return wrapper; - } - - - getElement(_element){ - var element; - - if(isElement(_element)) { - element = _element; - } else if (typeof _element === "string") { - element = document.getElementById(_element); - } - - if(!element){ - throw new Error("Not an Element"); - } - - return element; - } - - attachTo(what){ - - var element = this.getElement(what); - var base; - - if(!element){ - return; - } - - if(this.settings.hidden) { - base = this.wrapper; - } else { - base = this.container; - } - - element.appendChild(base); - - this.element = element; - - return element; - - } - - getContainer() { - return this.container; - } - - onResize(func){ - // Only listen to window for resize event if width and height are not fixed. - // This applies if it is set to a percent or auto. - if(!isNumber(this.settings.width) || - !isNumber(this.settings.height) ) { - this.resizeFunc = throttle(func, 50); - window.addEventListener("resize", this.resizeFunc, false); - } - - } - - onOrientationChange(func){ - this.orientationChangeFunc = func; - window.addEventListener("orientationchange", this.orientationChangeFunc, false); - } - - size(width, height){ - var bounds; - let _width = width || this.settings.width; - let _height = height || this.settings.height; - - // If width or height are set to false, inherit them from containing element - if(width === null) { - bounds = this.element.getBoundingClientRect(); - - if(bounds.width) { - width = Math.floor(bounds.width); - this.container.style.width = width + "px"; - } - } else { - if (isNumber(width)) { - this.container.style.width = width + "px"; - } else { - this.container.style.width = width; - } - } - - if(height === null) { - bounds = bounds || this.element.getBoundingClientRect(); - - if(bounds.height) { - height = bounds.height; - this.container.style.height = height + "px"; - } - - } else { - if (isNumber(height)) { - this.container.style.height = height + "px"; - } else { - this.container.style.height = height; - } - } - - if(!isNumber(width)) { - width = this.container.clientWidth; - } - - if(!isNumber(height)) { - height = this.container.clientHeight; - } - - this.containerStyles = window.getComputedStyle(this.container); - - this.containerPadding = { - left: parseFloat(this.containerStyles["padding-left"]) || 0, - right: parseFloat(this.containerStyles["padding-right"]) || 0, - top: parseFloat(this.containerStyles["padding-top"]) || 0, - bottom: parseFloat(this.containerStyles["padding-bottom"]) || 0 - }; - - // Bounds not set, get them from window - let _windowBounds = windowBounds(); - let bodyStyles = window.getComputedStyle(document.body); - let bodyPadding = { - left: parseFloat(bodyStyles["padding-left"]) || 0, - right: parseFloat(bodyStyles["padding-right"]) || 0, - top: parseFloat(bodyStyles["padding-top"]) || 0, - bottom: parseFloat(bodyStyles["padding-bottom"]) || 0 - }; - - if (!_width) { - width = _windowBounds.width - - bodyPadding.left - - bodyPadding.right; - } - - if ((this.settings.fullsize && !_height) || !_height) { - height = _windowBounds.height - - bodyPadding.top - - bodyPadding.bottom; - } - - return { - width: width - - this.containerPadding.left - - this.containerPadding.right, - height: height - - this.containerPadding.top - - this.containerPadding.bottom - }; - - } - - bounds(){ - let box; - if (this.container.style.overflow !== "visible") { - box = this.container && this.container.getBoundingClientRect(); - } - - if(!box || !box.width || !box.height) { - return windowBounds(); - } else { - return box; - } - - } - - getSheet(){ - var style = document.createElement("style"); - - // WebKit hack --> https://davidwalsh.name/add-rules-stylesheets - style.appendChild(document.createTextNode("")); - - document.head.appendChild(style); - - return style.sheet; - } - - addStyleRules(selector, rulesArray){ - var scope = "#" + this.id + " "; - var rules = ""; - - if(!this.sheet){ - this.sheet = this.getSheet(); - } - - rulesArray.forEach(function(set) { - for (var prop in set) { - if(set.hasOwnProperty(prop)) { - rules += prop + ":" + set[prop] + ";"; - } - } - }); - - this.sheet.insertRule(scope + selector + " {" + rules + "}", 0); - } - - axis(axis) { - if(axis === "horizontal") { - this.container.style.display = "flex"; - this.container.style.flexDirection = "row"; - this.container.style.flexWrap = "nowrap"; - } else { - this.container.style.display = "block"; - } - this.settings.axis = axis; - } - - // orientation(orientation) { - // if (orientation === "landscape") { - // - // } else { - // - // } - // - // this.orientation = orientation; - // } - - direction(dir) { - if (this.container) { - this.container.dir = dir; - this.container.style["direction"] = dir; - } - - if (this.settings.fullsize) { - document.body.style["direction"] = dir; - } - this.settings.dir = dir; - } - - overflow(overflow) { - if (this.container) { - if (overflow === "scroll" && this.settings.axis === "vertical") { - this.container.style["overflow-y"] = overflow; - this.container.style["overflow-x"] = "hidden"; - } else if (overflow === "scroll" && this.settings.axis === "horizontal") { - this.container.style["overflow-y"] = "hidden"; - this.container.style["overflow-x"] = overflow; - } else { - this.container.style["overflow"] = overflow; - } - } - this.settings.overflow = overflow; - } - - destroy() { - var base; - - if (this.element) { - - if(this.settings.hidden) { - base = this.wrapper; - } else { - base = this.container; - } - - if(this.element.contains(this.container)) { - this.element.removeChild(this.container); - } - - window.removeEventListener("resize", this.resizeFunc); - window.removeEventListener("orientationChange", this.orientationChangeFunc); - - } - } + constructor(_options) { + this.settings = _options || {}; + this.id = "epubjs-container-" + uuid(); + + this.container = this.create(this.settings); + + if (this.settings.hidden) { + this.wrapper = this.wrap(this.container); + } + } + + /* + * Creates an element to render to. + * Resizes to passed width and height or to the elements size + */ + create(options) { + let height = options.height; + let width = options.width; + let overflow = options.overflow || false; + let axis = options.axis || "vertical"; + let direction = options.direction; + + extend(this.settings, options); + + if (options.height && isNumber(options.height)) { + height = options.height + "px"; + } + + if (options.width && isNumber(options.width)) { + width = options.width + "px"; + } + + // Create new container element + let container = document.createElement("div"); + + container.id = this.id; + container.classList.add("epub-container"); + + // Style Element + container.style.wordSpacing = "0"; + container.style.lineHeight = "0"; + container.style.verticalAlign = "top"; + container.style.position = "relative"; + + if (axis === "horizontal") { + container.style.display = "flex"; + container.style.flexDirection = "row"; + container.style.flexWrap = "nowrap"; + } + + if (width) { + container.style.width = width; + } + + if (height) { + container.style.height = height; + } + + if (overflow) { + if (overflow === "scroll" && axis === "vertical") { + container.style["overflow-y"] = overflow; + container.style["overflow-x"] = "hidden"; + } else if (overflow === "scroll" && axis === "horizontal") { + container.style["overflow-y"] = "hidden"; + container.style["overflow-x"] = overflow; + } else { + container.style["overflow"] = overflow; + } + } + + if (direction) { + container.dir = direction; + container.style["direction"] = direction; + } + + if (direction && this.settings.fullsize) { + document.body.style["direction"] = direction; + } + + return container; + } + + wrap(container) { + var wrapper = document.createElement("div"); + + wrapper.style.visibility = "hidden"; + wrapper.style.overflow = "hidden"; + wrapper.style.width = "0"; + wrapper.style.height = "0"; + + wrapper.appendChild(container); + return wrapper; + } + + getElement(_element) { + var element; + + if (isElement(_element)) { + element = _element; + } else if (typeof _element === "string") { + element = document.getElementById(_element); + } + + if (!element) { + throw new Error("Not an Element"); + } + + return element; + } + + attachTo(what) { + var element = this.getElement(what); + var base; + + if (!element) { + return; + } + + if (this.settings.hidden) { + base = this.wrapper; + } else { + base = this.container; + } + + element.appendChild(base); + + this.element = element; + + return element; + } + + getContainer() { + return this.container; + } + + onResize(func) { + // Only listen to window for resize event if width and height are not fixed. + // This applies if it is set to a percent or auto. + if (!isNumber(this.settings.width) || !isNumber(this.settings.height)) { + this.resizeFunc = throttle(func, 50); + window.addEventListener("resize", this.resizeFunc, false); + } + } + + onOrientationChange(func) { + this.orientationChangeFunc = func; + window.addEventListener( + "orientationchange", + this.orientationChangeFunc, + false + ); + } + + size(width, height) { + var bounds; + let _width = width || this.settings.width; + let _height = height || this.settings.height; + + // If width or height are set to false, inherit them from containing element + if (width === null) { + bounds = this.element.getBoundingClientRect(); + + if (bounds.width) { + width = Math.floor(bounds.width); + this.container.style.width = width + "px"; + } + } else { + if (isNumber(width)) { + this.container.style.width = width + "px"; + } else { + this.container.style.width = width; + } + } + + if (height === null) { + bounds = bounds || this.element.getBoundingClientRect(); + + if (bounds.height) { + height = bounds.height; + this.container.style.height = height + "px"; + } + } else { + if (isNumber(height)) { + this.container.style.height = height + "px"; + } else { + this.container.style.height = height; + } + } + + if (!isNumber(width)) { + width = this.container.clientWidth; + } + + if (!isNumber(height)) { + height = this.container.clientHeight; + } + + this.containerStyles = window.getComputedStyle(this.container); + + this.containerPadding = { + left: parseFloat(this.containerStyles["padding-left"]) || 0, + right: parseFloat(this.containerStyles["padding-right"]) || 0, + top: parseFloat(this.containerStyles["padding-top"]) || 0, + bottom: parseFloat(this.containerStyles["padding-bottom"]) || 0, + }; + + // Bounds not set, get them from window + let _windowBounds = windowBounds(); + let bodyStyles = window.getComputedStyle(document.body); + let bodyPadding = { + left: parseFloat(bodyStyles["padding-left"]) || 0, + right: parseFloat(bodyStyles["padding-right"]) || 0, + top: parseFloat(bodyStyles["padding-top"]) || 0, + bottom: parseFloat(bodyStyles["padding-bottom"]) || 0, + }; + + if (!_width) { + width = _windowBounds.width - bodyPadding.left - bodyPadding.right; + } + + if ((this.settings.fullsize && !_height) || !_height) { + height = _windowBounds.height - bodyPadding.top - bodyPadding.bottom; + } + + return { + width: width - this.containerPadding.left - this.containerPadding.right, + height: height - this.containerPadding.top - this.containerPadding.bottom, + }; + } + + bounds() { + let box; + if (this.container.style.overflow !== "visible") { + box = this.container && this.container.getBoundingClientRect(); + } + + if (!box || !box.width || !box.height) { + return windowBounds(); + } else { + return box; + } + } + + getSheet() { + var style = document.createElement("style"); + + // WebKit hack --> https://davidwalsh.name/add-rules-stylesheets + style.appendChild(document.createTextNode("")); + + document.head.appendChild(style); + + return style.sheet; + } + + addStyleRules(selector, rulesArray) { + var scope = "#" + this.id + " "; + var rules = ""; + + if (!this.sheet) { + this.sheet = this.getSheet(); + } + + rulesArray.forEach(function (set) { + for (var prop in set) { + if (set.hasOwnProperty(prop)) { + rules += prop + ":" + set[prop] + ";"; + } + } + }); + + this.sheet.insertRule(scope + selector + " {" + rules + "}", 0); + } + + axis(axis) { + if (axis === "horizontal") { + this.container.style.display = "flex"; + this.container.style.flexDirection = "row"; + this.container.style.flexWrap = "nowrap"; + } else { + this.container.style.display = "block"; + } + this.settings.axis = axis; + } + + direction(dir) { + if (this.container) { + this.container.dir = dir; + this.container.style["direction"] = dir; + } + + if (this.settings.fullsize) { + document.body.style["direction"] = dir; + } + this.settings.dir = dir; + } + + overflow(overflow) { + if (this.container) { + if (overflow === "scroll" && this.settings.axis === "vertical") { + this.container.style["overflow-y"] = overflow; + this.container.style["overflow-x"] = "hidden"; + } else if (overflow === "scroll" && this.settings.axis === "horizontal") { + this.container.style["overflow-y"] = "hidden"; + this.container.style["overflow-x"] = overflow; + } else { + this.container.style["overflow"] = overflow; + } + } + this.settings.overflow = overflow; + } + + destroy() { + if (this.element) { + if (this.element.contains(this.container)) { + this.element.removeChild(this.container); + } + + window.removeEventListener("resize", this.resizeFunc); + window.removeEventListener( + "orientationChange", + this.orientationChangeFunc + ); + } + } } export default Stage; diff --git a/src/managers/helpers/views.js b/src/managers/helpers/views.js index 4368da2..cfa12c3 100644 --- a/src/managers/helpers/views.js +++ b/src/managers/helpers/views.js @@ -1,167 +1,167 @@ class Views { - constructor(container) { - this.container = container; - this._views = []; - this.length = 0; - this.hidden = false; - } + constructor(container) { + this.container = container; + this._views = []; + this.length = 0; + this.hidden = false; + } - all() { - return this._views; - } + all() { + return this._views; + } - first() { - return this._views[0]; - } + first() { + return this._views[0]; + } - last() { - return this._views[this._views.length-1]; - } + last() { + return this._views[this._views.length - 1]; + } - indexOf(view) { - return this._views.indexOf(view); - } + indexOf(view) { + return this._views.indexOf(view); + } - slice() { - return this._views.slice.apply(this._views, arguments); - } + slice() { + return this._views.slice.apply(this._views, arguments); + } - get(i) { - return this._views[i]; - } + get(i) { + return this._views[i]; + } - append(view){ - this._views.push(view); - if(this.container){ - this.container.appendChild(view.element); - } - this.length++; - return view; - } + append(view) { + this._views.push(view); + if (this.container) { + this.container.appendChild(view.element); + } + this.length++; + return view; + } - prepend(view){ - this._views.unshift(view); - if(this.container){ - this.container.insertBefore(view.element, this.container.firstChild); - } - this.length++; - return view; - } + prepend(view) { + this._views.unshift(view); + if (this.container) { + this.container.insertBefore(view.element, this.container.firstChild); + } + this.length++; + return view; + } - insert(view, index) { - this._views.splice(index, 0, view); + insert(view, index) { + this._views.splice(index, 0, view); - if(this.container){ - if(index < this.container.children.length){ - this.container.insertBefore(view.element, this.container.children[index]); - } else { - this.container.appendChild(view.element); - } - } + if (this.container) { + if (index < this.container.children.length) { + this.container.insertBefore( + view.element, + this.container.children[index] + ); + } else { + this.container.appendChild(view.element); + } + } - this.length++; - return view; - } + this.length++; + return view; + } - remove(view) { - var index = this._views.indexOf(view); + remove(view) { + var index = this._views.indexOf(view); - if(index > -1) { - this._views.splice(index, 1); - } + if (index > -1) { + this._views.splice(index, 1); + } + this.destroy(view); - this.destroy(view); + this.length--; + } - this.length--; - } + destroy(view) { + if (view.displayed) { + view.destroy(); + } - destroy(view) { - if(view.displayed){ - view.destroy(); - } - - if(this.container){ - this.container.removeChild(view.element); - } - view = null; - } + if (this.container) { + this.container.removeChild(view.element); + } + view = null; + } - // Iterators + // Iterators - forEach() { - return this._views.forEach.apply(this._views, arguments); - } + forEach() { + return this._views.forEach.apply(this._views, arguments); + } - clear(){ - // Remove all views - var view; - var len = this.length; + clear() { + // Remove all views + var view; + var len = this.length; - if(!this.length) return; + if (!this.length) return; - for (var i = 0; i < len; i++) { - view = this._views[i]; - this.destroy(view); - } + for (var i = 0; i < len; i++) { + view = this._views[i]; + this.destroy(view); + } - this._views = []; - this.length = 0; - } + this._views = []; + this.length = 0; + } - find(section){ + find(section) { + var view; + var len = this.length; - var view; - var len = this.length; + for (var i = 0; i < len; i++) { + view = this._views[i]; + if (view.displayed && view.section.index == section.index) { + return view; + } + } + } - for (var i = 0; i < len; i++) { - view = this._views[i]; - if(view.displayed && view.section.index == section.index) { - return view; - } - } + displayed() { + var displayed = []; + var view; + var len = this.length; - } + for (var i = 0; i < len; i++) { + view = this._views[i]; + if (view.displayed) { + displayed.push(view); + } + } + return displayed; + } - displayed(){ - var displayed = []; - var view; - var len = this.length; + show() { + var view; + var len = this.length; - for (var i = 0; i < len; i++) { - view = this._views[i]; - if(view.displayed){ - displayed.push(view); - } - } - return displayed; - } + for (var i = 0; i < len; i++) { + view = this._views[i]; + if (view.displayed) { + view.show(); + } + } + this.hidden = false; + } - show(){ - var view; - var len = this.length; + hide() { + var view; + var len = this.length; - for (var i = 0; i < len; i++) { - view = this._views[i]; - if(view.displayed){ - view.show(); - } - } - this.hidden = false; - } - - hide(){ - var view; - var len = this.length; - - for (var i = 0; i < len; i++) { - view = this._views[i]; - if(view.displayed){ - view.hide(); - } - } - this.hidden = true; - } + for (var i = 0; i < len; i++) { + view = this._views[i]; + if (view.displayed) { + view.hide(); + } + } + this.hidden = true; + } } export default Views; diff --git a/src/managers/views/iframe.js b/src/managers/views/iframe.js index e20e54d..fd124ac 100644 --- a/src/managers/views/iframe.js +++ b/src/managers/views/iframe.js @@ -1,849 +1,853 @@ import EventEmitter from "event-emitter"; -import {extend, borders, uuid, isNumber, bounds, defer, createBlobUrl, revokeBlobUrl} from "../../utils/core"; -import EpubCFI from "../../epubcfi"; +import { Highlight, Pane, Underline } from "marks-pane"; import Contents from "../../contents"; +import EpubCFI from "../../epubcfi"; import { EVENTS } from "../../utils/constants"; -import { Pane, Highlight, Underline } from "marks-pane"; +import { + borders, + bounds, + createBlobUrl, + defer, + extend, + isNumber, + revokeBlobUrl, + uuid, +} from "../../utils/core"; class IframeView { - constructor(section, options) { - this.settings = extend({ - ignoreClass : "", - axis: undefined, //options.layout && options.layout.props.flow === "scrolled" ? "vertical" : "horizontal", - direction: undefined, - width: 0, - height: 0, - layout: undefined, - globalLayoutProperties: {}, - method: undefined, - forceRight: false, - allowScriptedContent: false, - allowPopups: false - }, options || {}); - - this.id = "epubjs-view-" + uuid(); - this.section = section; - this.index = section.index; - - this.element = this.container(this.settings.axis); - - this.added = false; - this.displayed = false; - this.rendered = false; - - // this.width = this.settings.width; - // this.height = this.settings.height; - - this.fixedWidth = 0; - this.fixedHeight = 0; - - // Blank Cfi for Parsing - this.epubcfi = new EpubCFI(); - - this.layout = this.settings.layout; - // Dom events to listen for - // this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"]; - - this.pane = undefined; - this.highlights = {}; - this.underlines = {}; - this.marks = {}; - - } - - container(axis) { - var element = document.createElement("div"); - - element.classList.add("epub-view"); - - // this.element.style.minHeight = "100px"; - element.style.height = "0px"; - element.style.width = "0px"; - element.style.overflow = "hidden"; - element.style.position = "relative"; - element.style.display = "block"; - - if(axis && axis == "horizontal"){ - element.style.flex = "none"; - } else { - element.style.flex = "initial"; - } - - return element; - } - - create() { - - if(this.iframe) { - return this.iframe; - } - - if(!this.element) { - this.element = this.createContainer(); - } - - this.iframe = document.createElement("iframe"); - this.iframe.id = this.id; - this.iframe.scrolling = "no"; // Might need to be removed: breaks ios width calculations - this.iframe.style.overflow = "hidden"; - this.iframe.seamless = "seamless"; - // Back up if seamless isn't supported - this.iframe.style.border = "none"; - - // sandbox - this.iframe.sandbox = "allow-same-origin"; - if (this.settings.allowScriptedContent) { - this.iframe.sandbox += " allow-scripts"; - } - if (this.settings.allowPopups) { - this.iframe.sandbox += " allow-popups"; - } - - this.iframe.setAttribute("enable-annotation", "true"); - - this.resizing = true; - - // this.iframe.style.display = "none"; - this.element.style.visibility = "hidden"; - this.iframe.style.visibility = "hidden"; - - this.iframe.style.width = "0"; - this.iframe.style.height = "0"; - this._width = 0; - this._height = 0; - - this.element.setAttribute("ref", this.index); - - this.added = true; - - this.elementBounds = bounds(this.element); - - // if(width || height){ - // this.resize(width, height); - // } else if(this.width && this.height){ - // this.resize(this.width, this.height); - // } else { - // this.iframeBounds = bounds(this.iframe); - // } - - - if(("srcdoc" in this.iframe)) { - this.supportsSrcdoc = true; - } else { - this.supportsSrcdoc = false; - } - - if (!this.settings.method) { - this.settings.method = this.supportsSrcdoc ? "srcdoc" : "write"; - } - - return this.iframe; - } - - render(request, show) { - - // view.onLayout = this.layout.format.bind(this.layout); - this.create(); - - // Fit to size of the container, apply padding - this.size(); - - if(!this.sectionRender) { - this.sectionRender = this.section.render(request); - } - - // Render Chain - return this.sectionRender - .then(function(contents){ - return this.load(contents); - }.bind(this)) - .then(function(){ - - // find and report the writingMode axis - let writingMode = this.contents.writingMode(); - - // Set the axis based on the flow and writing mode - let axis; - if (this.settings.flow === "scrolled") { - axis = (writingMode.indexOf("vertical") === 0) ? "horizontal" : "vertical"; - } else { - axis = (writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal"; - } - - if (writingMode.indexOf("vertical") === 0 && this.settings.flow === "paginated") { - this.layout.delta = this.layout.height; - } - - this.setAxis(axis); - this.emit(EVENTS.VIEWS.AXIS, axis); - - this.setWritingMode(writingMode); - this.emit(EVENTS.VIEWS.WRITING_MODE, writingMode); - - - // apply the layout function to the contents - this.layout.format(this.contents, this.section, this.axis); - - // Listen for events that require an expansion of the iframe - this.addListeners(); - - return new Promise((resolve, reject) => { - // Expand the iframe to the full size of the content - this.expand(); - - if (this.settings.forceRight) { - this.element.style.marginLeft = this.width() + "px"; - } - resolve(); - }); - - }.bind(this), function(e){ - this.emit(EVENTS.VIEWS.LOAD_ERROR, e); - return new Promise((resolve, reject) => { - reject(e); - }); - }.bind(this)) - .then(function() { - this.emit(EVENTS.VIEWS.RENDERED, this.section); - }.bind(this)); - - } - - reset () { - if (this.iframe) { - this.iframe.style.width = "0"; - this.iframe.style.height = "0"; - this._width = 0; - this._height = 0; - this._textWidth = undefined; - this._contentWidth = undefined; - this._textHeight = undefined; - this._contentHeight = undefined; - } - this._needsReframe = true; - } - - // Determine locks base on settings - size(_width, _height) { - var width = _width || this.settings.width; - var height = _height || this.settings.height; - - if(this.layout.name === "pre-paginated") { - this.lock("both", width, height); - } else if(this.settings.axis === "horizontal") { - this.lock("height", width, height); - } else { - this.lock("width", width, height); - } - - this.settings.width = width; - this.settings.height = height; - } - - // Lock an axis to element dimensions, taking borders into account - lock(what, width, height) { - var elBorders = borders(this.element); - var iframeBorders; - - if(this.iframe) { - iframeBorders = borders(this.iframe); - } else { - iframeBorders = {width: 0, height: 0}; - } - - if(what == "width" && isNumber(width)){ - this.lockedWidth = width - elBorders.width - iframeBorders.width; - // this.resize(this.lockedWidth, width); // width keeps ratio correct - } - - if(what == "height" && isNumber(height)){ - this.lockedHeight = height - elBorders.height - iframeBorders.height; - // this.resize(width, this.lockedHeight); - } - - if(what === "both" && - isNumber(width) && - isNumber(height)){ - - this.lockedWidth = width - elBorders.width - iframeBorders.width; - this.lockedHeight = height - elBorders.height - iframeBorders.height; - // this.resize(this.lockedWidth, this.lockedHeight); - } - - if(this.displayed && this.iframe) { - - // this.contents.layout(); - this.expand(); - } - - - - } - - // Resize a single axis based on content dimensions - expand(force) { - var width = this.lockedWidth; - var height = this.lockedHeight; - var columns; - - var textWidth, textHeight; - - if(!this.iframe || this._expanding) return; - - this._expanding = true; - - if(this.layout.name === "pre-paginated") { - width = this.layout.columnWidth; - height = this.layout.height; - } - // Expand Horizontally - else if(this.settings.axis === "horizontal") { - // Get the width of the text - width = this.contents.textWidth(); - - if (width % this.layout.pageWidth > 0) { - width = Math.ceil(width / this.layout.pageWidth) * this.layout.pageWidth; - } - - if (this.settings.forceEvenPages) { - columns = (width / this.layout.pageWidth); - if ( this.layout.divisor > 1 && - this.layout.name === "reflowable" && - (columns % 2 > 0)) { - // add a blank page - width += this.layout.pageWidth; - } - } - - } // Expand Vertically - else if(this.settings.axis === "vertical") { - height = this.contents.textHeight(); - if (this.settings.flow === "paginated" && - height % this.layout.height > 0) { - height = Math.ceil(height / this.layout.height) * this.layout.height; - } - } - - // Only Resize if dimensions have changed or - // if Frame is still hidden, so needs reframing - if(this._needsReframe || width != this._width || height != this._height){ - this.reframe(width, height); - } - - this._expanding = false; - } - - reframe(width, height) { - var size; - - if(isNumber(width)){ - this.element.style.width = width + "px"; - this.iframe.style.width = width + "px"; - this._width = width; - } - - if(isNumber(height)){ - this.element.style.height = height + "px"; - this.iframe.style.height = height + "px"; - this._height = height; - } - - let widthDelta = this.prevBounds ? width - this.prevBounds.width : width; - let heightDelta = this.prevBounds ? height - this.prevBounds.height : height; - - size = { - width: width, - height: height, - widthDelta: widthDelta, - heightDelta: heightDelta, - }; - - this.pane && this.pane.render(); - - requestAnimationFrame(() => { - let mark; - for (let m in this.marks) { - if (this.marks.hasOwnProperty(m)) { - mark = this.marks[m]; - this.placeMark(mark.element, mark.range); - } - } - }); - - this.onResize(this, size); - - this.emit(EVENTS.VIEWS.RESIZED, size); - - this.prevBounds = size; - - this.elementBounds = bounds(this.element); - - } - - - load(contents) { - var loading = new defer(); - var loaded = loading.promise; - - if(!this.iframe) { - loading.reject(new Error("No Iframe Available")); - return loaded; - } - - this.iframe.onload = function(event) { - - this.onLoad(event, loading); - - }.bind(this); - - if (this.settings.method === "blobUrl") { - this.blobUrl = createBlobUrl(contents, "application/xhtml+xml"); - this.iframe.src = this.blobUrl; - this.element.appendChild(this.iframe); - } else if(this.settings.method === "srcdoc"){ - this.iframe.srcdoc = contents; - this.element.appendChild(this.iframe); - } else { - - this.element.appendChild(this.iframe); - - this.document = this.iframe.contentDocument; - - if(!this.document) { - loading.reject(new Error("No Document Available")); - return loaded; - } - - this.iframe.contentDocument.open(); - // For Cordova windows platform - if(window.MSApp && MSApp.execUnsafeLocalFunction) { - var outerThis = this; - MSApp.execUnsafeLocalFunction(function () { - outerThis.iframe.contentDocument.write(contents); - }); - } else { - this.iframe.contentDocument.write(contents); - } - this.iframe.contentDocument.close(); - - } - - return loaded; - } - - onLoad(event, promise) { - - this.window = this.iframe.contentWindow; - this.document = this.iframe.contentDocument; - - this.contents = new Contents(this.document, this.document.body, this.section.cfiBase, this.section.index); - - this.rendering = false; - - var link = this.document.querySelector("link[rel='canonical']"); - if (link) { - link.setAttribute("href", this.section.canonical); - } else { - link = this.document.createElement("link"); - link.setAttribute("rel", "canonical"); - link.setAttribute("href", this.section.canonical); - this.document.querySelector("head").appendChild(link); - } - - this.contents.on(EVENTS.CONTENTS.EXPAND, () => { - if(this.displayed && this.iframe) { - this.expand(); - if (this.contents) { - this.layout.format(this.contents); - } - } - }); - - this.contents.on(EVENTS.CONTENTS.RESIZE, (e) => { - if(this.displayed && this.iframe) { - this.expand(); - if (this.contents) { - this.layout.format(this.contents); - } - } - }); - - promise.resolve(this.contents); - } - - setLayout(layout) { - this.layout = layout; - - if (this.contents) { - this.layout.format(this.contents); - this.expand(); - } - } - - setAxis(axis) { - - this.settings.axis = axis; - - if(axis == "horizontal"){ - this.element.style.flex = "none"; - } else { - this.element.style.flex = "initial"; - } - - this.size(); - - } - - setWritingMode(mode) { - // this.element.style.writingMode = writingMode; - this.writingMode = mode; - } - - addListeners() { - //TODO: Add content listeners for expanding - } - - removeListeners(layoutFunc) { - //TODO: remove content listeners for expanding - } - - display(request) { - var displayed = new defer(); - - if (!this.displayed) { - - this.render(request) - .then(function () { - - this.emit(EVENTS.VIEWS.DISPLAYED, this); - this.onDisplayed(this); - - this.displayed = true; - displayed.resolve(this); - - }.bind(this), function (err) { - displayed.reject(err, this); - }); - - } else { - displayed.resolve(this); - } - - - return displayed.promise; - } - - show() { - - this.element.style.visibility = "visible"; - - if(this.iframe){ - this.iframe.style.visibility = "visible"; - - // Remind Safari to redraw the iframe - this.iframe.style.transform = "translateZ(0)"; - this.iframe.offsetWidth; - this.iframe.style.transform = null; - } - - this.emit(EVENTS.VIEWS.SHOWN, this); - } - - hide() { - // this.iframe.style.display = "none"; - this.element.style.visibility = "hidden"; - this.iframe.style.visibility = "hidden"; - - this.stopExpanding = true; - this.emit(EVENTS.VIEWS.HIDDEN, this); - } - - offset() { - return { - top: this.element.offsetTop, - left: this.element.offsetLeft - } - } - - width() { - return this._width; - } - - height() { - return this._height; - } - - position() { - return this.element.getBoundingClientRect(); - } - - locationOf(target) { - var parentPos = this.iframe.getBoundingClientRect(); - var targetPos = this.contents.locationOf(target, this.settings.ignoreClass); - - return { - "left": targetPos.left, - "top": targetPos.top - }; - } - - onDisplayed(view) { - // Stub, override with a custom functions - } - - onResize(view, e) { - // Stub, override with a custom functions - } - - bounds(force) { - if(force || !this.elementBounds) { - this.elementBounds = bounds(this.element); - } - - return this.elementBounds; - } - - highlight(cfiRange, data={}, cb, className = "epubjs-hl", styles = {}) { - if (!this.contents) { - return; - } - const attributes = Object.assign({"fill": "yellow", "fill-opacity": "0.3", "mix-blend-mode": "multiply"}, styles); - let range = this.contents.range(cfiRange); - - let emitter = () => { - this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); - }; - - data["epubcfi"] = cfiRange; - - if (!this.pane) { - this.pane = new Pane(this.iframe, this.element); - } - - let m = new Highlight(range, className, data, attributes); - let h = this.pane.addMark(m); - - this.highlights[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] }; - - h.element.setAttribute("ref", className); - h.element.addEventListener("click", emitter); - h.element.addEventListener("touchstart", emitter); - - if (cb) { - h.element.addEventListener("click", cb); - h.element.addEventListener("touchstart", cb); - } - return h; - } - - underline(cfiRange, data={}, cb, className = "epubjs-ul", styles = {}) { - if (!this.contents) { - return; - } - const attributes = Object.assign({"stroke": "black", "stroke-opacity": "0.3", "mix-blend-mode": "multiply"}, styles); - let range = this.contents.range(cfiRange); - let emitter = () => { - this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); - }; - - data["epubcfi"] = cfiRange; - - if (!this.pane) { - this.pane = new Pane(this.iframe, this.element); - } - - let m = new Underline(range, className, data, attributes); - let h = this.pane.addMark(m); - - this.underlines[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] }; - - h.element.setAttribute("ref", className); - h.element.addEventListener("click", emitter); - h.element.addEventListener("touchstart", emitter); - - if (cb) { - h.element.addEventListener("click", cb); - h.element.addEventListener("touchstart", cb); - } - return h; - } - - mark(cfiRange, data={}, cb) { - if (!this.contents) { - return; - } - - if (cfiRange in this.marks) { - let item = this.marks[cfiRange]; - return item; - } - - let range = this.contents.range(cfiRange); - if (!range) { - return; - } - let container = range.commonAncestorContainer; - let parent = (container.nodeType === 1) ? container : container.parentNode; - - let emitter = (e) => { - this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); - }; - - if (range.collapsed && container.nodeType === 1) { - range = new Range(); - range.selectNodeContents(container); - } else if (range.collapsed) { // Webkit doesn't like collapsed ranges - range = new Range(); - range.selectNodeContents(parent); - } - - let mark = this.document.createElement("a"); - mark.setAttribute("ref", "epubjs-mk"); - mark.style.position = "absolute"; - - mark.dataset["epubcfi"] = cfiRange; - - if (data) { - Object.keys(data).forEach((key) => { - mark.dataset[key] = data[key]; - }); - } - - if (cb) { - mark.addEventListener("click", cb); - mark.addEventListener("touchstart", cb); - } - - mark.addEventListener("click", emitter); - mark.addEventListener("touchstart", emitter); - - this.placeMark(mark, range); - - this.element.appendChild(mark); - - this.marks[cfiRange] = { "element": mark, "range": range, "listeners": [emitter, cb] }; - - return parent; - } - - placeMark(element, range) { - let top, right, left; - - if(this.layout.name === "pre-paginated" || - this.settings.axis !== "horizontal") { - let pos = range.getBoundingClientRect(); - top = pos.top; - right = pos.right; - } else { - // Element might break columns, so find the left most element - let rects = range.getClientRects(); - - let rect; - for (var i = 0; i != rects.length; i++) { - rect = rects[i]; - if (!left || rect.left < left) { - left = rect.left; - // right = rect.right; - right = Math.ceil(left / this.layout.props.pageWidth) * this.layout.props.pageWidth - (this.layout.gap / 2); - top = rect.top; - } - } - } - - element.style.top = `${top}px`; - element.style.left = `${right}px`; - } - - unhighlight(cfiRange) { - let item; - if (cfiRange in this.highlights) { - item = this.highlights[cfiRange]; - - this.pane.removeMark(item.mark); - item.listeners.forEach((l) => { - if (l) { - item.element.removeEventListener("click", l); - item.element.removeEventListener("touchstart", l); - }; - }); - delete this.highlights[cfiRange]; - } - } - - ununderline(cfiRange) { - let item; - if (cfiRange in this.underlines) { - item = this.underlines[cfiRange]; - this.pane.removeMark(item.mark); - item.listeners.forEach((l) => { - if (l) { - item.element.removeEventListener("click", l); - item.element.removeEventListener("touchstart", l); - }; - }); - delete this.underlines[cfiRange]; - } - } - - unmark(cfiRange) { - let item; - if (cfiRange in this.marks) { - item = this.marks[cfiRange]; - this.element.removeChild(item.element); - item.listeners.forEach((l) => { - if (l) { - item.element.removeEventListener("click", l); - item.element.removeEventListener("touchstart", l); - }; - }); - delete this.marks[cfiRange]; - } - } - - destroy() { - - for (let cfiRange in this.highlights) { - this.unhighlight(cfiRange); - } - - for (let cfiRange in this.underlines) { - this.ununderline(cfiRange); - } - - for (let cfiRange in this.marks) { - this.unmark(cfiRange); - } - - if (this.blobUrl) { - revokeBlobUrl(this.blobUrl); - } - - if(this.displayed){ - this.displayed = false; - - this.removeListeners(); - this.contents.destroy(); - - this.stopExpanding = true; - this.element.removeChild(this.iframe); - - if (this.pane) { - this.pane.element.remove(); - this.pane = undefined; - } - - this.iframe = undefined; - this.contents = undefined; - - this._textWidth = null; - this._textHeight = null; - this._width = null; - this._height = null; - } - - // this.element.style.height = "0px"; - // this.element.style.width = "0px"; - } + constructor(section, options) { + this.settings = extend( + { + ignoreClass: "", + axis: undefined, //options.layout && options.layout.props.flow === "scrolled" ? "vertical" : "horizontal", + direction: undefined, + width: 0, + height: 0, + layout: undefined, + globalLayoutProperties: {}, + method: undefined, + forceRight: false, + allowScriptedContent: false, + allowPopups: false, + }, + options || {} + ); + + this.id = "epubjs-view-" + uuid(); + this.section = section; + this.index = section.index; + + this.element = this.container(this.settings.axis); + + this.added = false; + this.displayed = false; + this.rendered = false; + + // this.width = this.settings.width; + // this.height = this.settings.height; + + this.fixedWidth = 0; + this.fixedHeight = 0; + + // Blank Cfi for Parsing + this.epubcfi = new EpubCFI(); + + this.layout = this.settings.layout; + // Dom events to listen for + // this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"]; + + this.pane = undefined; + this.highlights = {}; + this.underlines = {}; + this.marks = {}; + } + + container(axis) { + var element = document.createElement("div"); + + element.classList.add("epub-view"); + + // this.element.style.minHeight = "100px"; + element.style.height = "0px"; + element.style.width = "0px"; + element.style.overflow = "hidden"; + element.style.position = "relative"; + element.style.display = "block"; + + if (axis && axis == "horizontal") { + element.style.flex = "none"; + } else { + element.style.flex = "initial"; + } + + return element; + } + + create() { + if (this.iframe) { + return this.iframe; + } + + if (!this.element) { + this.element = this.createContainer(); + } + + this.iframe = document.createElement("iframe"); + this.iframe.id = this.id; + this.iframe.scrolling = "no"; // Might need to be removed: breaks ios width calculations + this.iframe.style.overflow = "hidden"; + this.iframe.seamless = "seamless"; + // Back up if seamless isn't supported + this.iframe.style.border = "none"; + + // sandbox + this.iframe.sandbox = "allow-same-origin"; + if (this.settings.allowScriptedContent) { + this.iframe.sandbox += " allow-scripts"; + } + if (this.settings.allowPopups) { + this.iframe.sandbox += " allow-popups"; + } + + this.iframe.setAttribute("enable-annotation", "true"); + this.resizing = true; + this.element.style.visibility = "hidden"; + this.iframe.style.visibility = "hidden"; + this.iframe.style.width = "0"; + this.iframe.style.height = "0"; + this._width = 0; + this._height = 0; + this.element.setAttribute("ref", this.index); + this.added = true; + this.elementBounds = bounds(this.element); + + if ("srcdoc" in this.iframe) { + this.supportsSrcdoc = true; + } else { + this.supportsSrcdoc = false; + } + + if (!this.settings.method) { + this.settings.method = this.supportsSrcdoc ? "srcdoc" : "write"; + } + + return this.iframe; + } + + render(request, show) { + this.create(); + // Fit to size of the container, apply padding + this.size(); + + if (!this.sectionRender) { + this.sectionRender = this.section.render(request); + } + + // Render Chain + return this.sectionRender + .then( + function (contents) { + return this.load(contents); + }.bind(this) + ) + .then( + function () { + // find and report the writingMode axis + let writingMode = this.contents.writingMode(); + + // Set the axis based on the flow and writing mode + let axis; + if (this.settings.flow === "scrolled") { + axis = + writingMode.indexOf("vertical") === 0 ? "horizontal" : "vertical"; + } else { + axis = + writingMode.indexOf("vertical") === 0 ? "vertical" : "horizontal"; + } + + if ( + writingMode.indexOf("vertical") === 0 && + this.settings.flow === "paginated" + ) { + this.layout.delta = this.layout.height; + } + + this.setAxis(axis); + this.emit(EVENTS.VIEWS.AXIS, axis); + + this.setWritingMode(writingMode); + this.emit(EVENTS.VIEWS.WRITING_MODE, writingMode); + + // apply the layout function to the contents + this.layout.format(this.contents, this.section, this.axis); + + // Listen for events that require an expansion of the iframe + this.addListeners(); + + return new Promise((resolve, reject) => { + // Expand the iframe to the full size of the content + this.expand(); + + if (this.settings.forceRight) { + this.element.style.marginLeft = this.width() + "px"; + } + resolve(); + }); + }.bind(this), + function (e) { + this.emit(EVENTS.VIEWS.LOAD_ERROR, e); + return new Promise((resolve, reject) => { + reject(e); + }); + }.bind(this) + ) + .then( + function () { + this.emit(EVENTS.VIEWS.RENDERED, this.section); + }.bind(this) + ); + } + + reset() { + if (this.iframe) { + this.iframe.style.width = "0"; + this.iframe.style.height = "0"; + this._width = 0; + this._height = 0; + this._textWidth = undefined; + this._contentWidth = undefined; + this._textHeight = undefined; + this._contentHeight = undefined; + } + this._needsReframe = true; + } + + // Determine locks base on settings + size(_width, _height) { + var width = _width || this.settings.width; + var height = _height || this.settings.height; + + if (this.layout.name === "pre-paginated") { + this.lock("both", width, height); + } else if (this.settings.axis === "horizontal") { + this.lock("height", width, height); + } else { + this.lock("width", width, height); + } + + this.settings.width = width; + this.settings.height = height; + } + + // Lock an axis to element dimensions, taking borders into account + lock(what, width, height) { + var elBorders = borders(this.element); + var iframeBorders; + + if (this.iframe) { + iframeBorders = borders(this.iframe); + } else { + iframeBorders = { width: 0, height: 0 }; + } + + if (what == "width" && isNumber(width)) { + this.lockedWidth = width - elBorders.width - iframeBorders.width; + } + + if (what == "height" && isNumber(height)) { + this.lockedHeight = height - elBorders.height - iframeBorders.height; + } + + if (what === "both" && isNumber(width) && isNumber(height)) { + this.lockedWidth = width - elBorders.width - iframeBorders.width; + this.lockedHeight = height - elBorders.height - iframeBorders.height; + } + + if (this.displayed && this.iframe) { + this.expand(); + } + } + + // Resize a single axis based on content dimensions + expand(force) { + var width = this.lockedWidth; + var height = this.lockedHeight; + var columns; + + if (!this.iframe || this._expanding) return; + + this._expanding = true; + + if (this.layout.name === "pre-paginated") { + width = this.layout.columnWidth; + height = this.layout.height; + } + // Expand Horizontally + else if (this.settings.axis === "horizontal") { + // Get the width of the text + width = this.contents.textWidth(); + + if (width % this.layout.pageWidth > 0) { + width = + Math.ceil(width / this.layout.pageWidth) * this.layout.pageWidth; + } + + if (this.settings.forceEvenPages) { + columns = width / this.layout.pageWidth; + if ( + this.layout.divisor > 1 && + this.layout.name === "reflowable" && + columns % 2 > 0 + ) { + // add a blank page + width += this.layout.pageWidth; + } + } + } // Expand Vertically + else if (this.settings.axis === "vertical") { + height = this.contents.textHeight(); + if ( + this.settings.flow === "paginated" && + height % this.layout.height > 0 + ) { + height = Math.ceil(height / this.layout.height) * this.layout.height; + } + } + + // Only Resize if dimensions have changed or + // if Frame is still hidden, so needs reframing + if (this._needsReframe || width != this._width || height != this._height) { + this.reframe(width, height); + } + + this._expanding = false; + } + + reframe(width, height) { + var size; + + if (isNumber(width)) { + this.element.style.width = width + "px"; + this.iframe.style.width = width + "px"; + this._width = width; + } + + if (isNumber(height)) { + this.element.style.height = height + "px"; + this.iframe.style.height = height + "px"; + this._height = height; + } + + let widthDelta = this.prevBounds ? width - this.prevBounds.width : width; + let heightDelta = this.prevBounds + ? height - this.prevBounds.height + : height; + + size = { + width: width, + height: height, + widthDelta: widthDelta, + heightDelta: heightDelta, + }; + + this.pane && this.pane.render(); + + requestAnimationFrame(() => { + let mark; + for (let m in this.marks) { + if (this.marks.hasOwnProperty(m)) { + mark = this.marks[m]; + this.placeMark(mark.element, mark.range); + } + } + }); + + this.onResize(this, size); + + this.emit(EVENTS.VIEWS.RESIZED, size); + + this.prevBounds = size; + + this.elementBounds = bounds(this.element); + } + + load(contents) { + var loading = new defer(); + var loaded = loading.promise; + + if (!this.iframe) { + loading.reject(new Error("No Iframe Available")); + return loaded; + } + + this.iframe.onload = function (event) { + this.onLoad(event, loading); + }.bind(this); + + if (this.settings.method === "blobUrl") { + this.blobUrl = createBlobUrl(contents, "application/xhtml+xml"); + this.iframe.src = this.blobUrl; + this.element.appendChild(this.iframe); + } else if (this.settings.method === "srcdoc") { + this.iframe.srcdoc = contents; + this.element.appendChild(this.iframe); + } else { + this.element.appendChild(this.iframe); + + this.document = this.iframe.contentDocument; + + if (!this.document) { + loading.reject(new Error("No Document Available")); + return loaded; + } + + this.iframe.contentDocument.open(); + // For Cordova windows platform + if (window.MSApp && MSApp.execUnsafeLocalFunction) { + var outerThis = this; + MSApp.execUnsafeLocalFunction(function () { + outerThis.iframe.contentDocument.write(contents); + }); + } else { + this.iframe.contentDocument.write(contents); + } + this.iframe.contentDocument.close(); + } + + return loaded; + } + + onLoad(event, promise) { + this.window = this.iframe.contentWindow; + this.document = this.iframe.contentDocument; + + this.contents = new Contents( + this.document, + this.document.body, + this.section.cfiBase, + this.section.index + ); + + this.rendering = false; + + var link = this.document.querySelector("link[rel='canonical']"); + if (link) { + link.setAttribute("href", this.section.canonical); + } else { + link = this.document.createElement("link"); + link.setAttribute("rel", "canonical"); + link.setAttribute("href", this.section.canonical); + this.document.querySelector("head").appendChild(link); + } + + this.contents.on(EVENTS.CONTENTS.EXPAND, () => { + if (this.displayed && this.iframe) { + this.expand(); + if (this.contents) { + this.layout.format(this.contents); + } + } + }); + + this.contents.on(EVENTS.CONTENTS.RESIZE, (e) => { + if (this.displayed && this.iframe) { + this.expand(); + if (this.contents) { + this.layout.format(this.contents); + } + } + }); + + promise.resolve(this.contents); + } + + setLayout(layout) { + this.layout = layout; + + if (this.contents) { + this.layout.format(this.contents); + this.expand(); + } + } + + setAxis(axis) { + this.settings.axis = axis; + + if (axis == "horizontal") { + this.element.style.flex = "none"; + } else { + this.element.style.flex = "initial"; + } + + this.size(); + } + + setWritingMode(mode) { + this.writingMode = mode; + } + + addListeners() { + //TODO: Add content listeners for expanding + } + + removeListeners(layoutFunc) { + //TODO: remove content listeners for expanding + } + + display(request) { + var displayed = new defer(); + + if (!this.displayed) { + this.render(request).then( + function () { + this.emit(EVENTS.VIEWS.DISPLAYED, this); + this.onDisplayed(this); + + this.displayed = true; + displayed.resolve(this); + }.bind(this), + function (err) { + displayed.reject(err, this); + } + ); + } else { + displayed.resolve(this); + } + + return displayed.promise; + } + + show() { + this.element.style.visibility = "visible"; + + if (this.iframe) { + this.iframe.style.visibility = "visible"; + + // Remind Safari to redraw the iframe + this.iframe.style.transform = "translateZ(0)"; + this.iframe.offsetWidth; + this.iframe.style.transform = null; + } + + this.emit(EVENTS.VIEWS.SHOWN, this); + } + + hide() { + this.element.style.visibility = "hidden"; + this.iframe.style.visibility = "hidden"; + + this.stopExpanding = true; + this.emit(EVENTS.VIEWS.HIDDEN, this); + } + + offset() { + return { + top: this.element.offsetTop, + left: this.element.offsetLeft, + }; + } + + width() { + return this._width; + } + + height() { + return this._height; + } + + position() { + return this.element.getBoundingClientRect(); + } + + locationOf(target) { + var targetPos = this.contents.locationOf(target, this.settings.ignoreClass); + + return { + left: targetPos.left, + top: targetPos.top, + }; + } + + onDisplayed(view) { + // Stub, override with a custom functions + } + + onResize(view, e) { + // Stub, override with a custom functions + } + + bounds(force) { + if (force || !this.elementBounds) { + this.elementBounds = bounds(this.element); + } + + return this.elementBounds; + } + + highlight(cfiRange, data = {}, cb, className = "epubjs-hl", styles = {}) { + if (!this.contents) { + return; + } + const attributes = Object.assign( + { fill: "yellow", "fill-opacity": "0.3", "mix-blend-mode": "multiply" }, + styles + ); + let range = this.contents.range(cfiRange); + + let emitter = () => { + this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); + }; + + data["epubcfi"] = cfiRange; + + if (!this.pane) { + this.pane = new Pane(this.iframe, this.element); + } + + let m = new Highlight(range, className, data, attributes); + let h = this.pane.addMark(m); + + this.highlights[cfiRange] = { + mark: h, + element: h.element, + listeners: [emitter, cb], + }; + + h.element.setAttribute("ref", className); + h.element.addEventListener("click", emitter); + h.element.addEventListener("touchstart", emitter); + + if (cb) { + h.element.addEventListener("click", cb); + h.element.addEventListener("touchstart", cb); + } + return h; + } + + underline(cfiRange, data = {}, cb, className = "epubjs-ul", styles = {}) { + if (!this.contents) { + return; + } + const attributes = Object.assign( + { + stroke: "black", + "stroke-opacity": "0.3", + "mix-blend-mode": "multiply", + }, + styles + ); + let range = this.contents.range(cfiRange); + let emitter = () => { + this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); + }; + + data["epubcfi"] = cfiRange; + + if (!this.pane) { + this.pane = new Pane(this.iframe, this.element); + } + + let m = new Underline(range, className, data, attributes); + let h = this.pane.addMark(m); + + this.underlines[cfiRange] = { + mark: h, + element: h.element, + listeners: [emitter, cb], + }; + + h.element.setAttribute("ref", className); + h.element.addEventListener("click", emitter); + h.element.addEventListener("touchstart", emitter); + + if (cb) { + h.element.addEventListener("click", cb); + h.element.addEventListener("touchstart", cb); + } + return h; + } + + mark(cfiRange, data = {}, cb) { + if (!this.contents) { + return; + } + + if (cfiRange in this.marks) { + let item = this.marks[cfiRange]; + return item; + } + + let range = this.contents.range(cfiRange); + if (!range) { + return; + } + let container = range.commonAncestorContainer; + let parent = container.nodeType === 1 ? container : container.parentNode; + + let emitter = (e) => { + this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); + }; + + if (range.collapsed && container.nodeType === 1) { + range = new Range(); + range.selectNodeContents(container); + } else if (range.collapsed) { + // Webkit doesn't like collapsed ranges + range = new Range(); + range.selectNodeContents(parent); + } + + let mark = this.document.createElement("a"); + mark.setAttribute("ref", "epubjs-mk"); + mark.style.position = "absolute"; + + mark.dataset["epubcfi"] = cfiRange; + + if (data) { + Object.keys(data).forEach((key) => { + mark.dataset[key] = data[key]; + }); + } + + if (cb) { + mark.addEventListener("click", cb); + mark.addEventListener("touchstart", cb); + } + + mark.addEventListener("click", emitter); + mark.addEventListener("touchstart", emitter); + + this.placeMark(mark, range); + + this.element.appendChild(mark); + + this.marks[cfiRange] = { + element: mark, + range: range, + listeners: [emitter, cb], + }; + + return parent; + } + + placeMark(element, range) { + let top, right, left; + + if ( + this.layout.name === "pre-paginated" || + this.settings.axis !== "horizontal" + ) { + let pos = range.getBoundingClientRect(); + top = pos.top; + right = pos.right; + } else { + // Element might break columns, so find the left most element + let rects = range.getClientRects(); + + let rect; + for (var i = 0; i != rects.length; i++) { + rect = rects[i]; + if (!left || rect.left < left) { + left = rect.left; + right = + Math.ceil(left / this.layout.props.pageWidth) * + this.layout.props.pageWidth - + this.layout.gap / 2; + top = rect.top; + } + } + } + + element.style.top = `${top}px`; + element.style.left = `${right}px`; + } + + unhighlight(cfiRange) { + let item; + if (cfiRange in this.highlights) { + item = this.highlights[cfiRange]; + + this.pane.removeMark(item.mark); + item.listeners.forEach((l) => { + if (l) { + item.element.removeEventListener("click", l); + item.element.removeEventListener("touchstart", l); + } + }); + delete this.highlights[cfiRange]; + } + } + + ununderline(cfiRange) { + let item; + if (cfiRange in this.underlines) { + item = this.underlines[cfiRange]; + this.pane.removeMark(item.mark); + item.listeners.forEach((l) => { + if (l) { + item.element.removeEventListener("click", l); + item.element.removeEventListener("touchstart", l); + } + }); + delete this.underlines[cfiRange]; + } + } + + unmark(cfiRange) { + let item; + if (cfiRange in this.marks) { + item = this.marks[cfiRange]; + this.element.removeChild(item.element); + item.listeners.forEach((l) => { + if (l) { + item.element.removeEventListener("click", l); + item.element.removeEventListener("touchstart", l); + } + }); + delete this.marks[cfiRange]; + } + } + + destroy() { + for (let cfiRange in this.highlights) { + this.unhighlight(cfiRange); + } + + for (let cfiRange in this.underlines) { + this.ununderline(cfiRange); + } + + for (let cfiRange in this.marks) { + this.unmark(cfiRange); + } + + if (this.blobUrl) { + revokeBlobUrl(this.blobUrl); + } + + if (this.displayed) { + this.displayed = false; + + this.removeListeners(); + this.contents.destroy(); + + this.stopExpanding = true; + this.element.removeChild(this.iframe); + + if (this.pane) { + this.pane.element.remove(); + this.pane = undefined; + } + + this.iframe = undefined; + this.contents = undefined; + + this._textWidth = null; + this._textHeight = null; + this._width = null; + this._height = null; + } + } } EventEmitter(IframeView.prototype); diff --git a/src/managers/views/inline.js b/src/managers/views/inline.js index 072b586..1cf9a80 100644 --- a/src/managers/views/inline.js +++ b/src/managers/views/inline.js @@ -1,430 +1,368 @@ import EventEmitter from "event-emitter"; -import {extend, borders, uuid, isNumber, bounds, defer, qs, parse} from "../../utils/core"; -import EpubCFI from "../../epubcfi"; import Contents from "../../contents"; +import EpubCFI from "../../epubcfi"; import { EVENTS } from "../../utils/constants"; +import { + borders, + bounds, + defer, + extend, + isNumber, + parse, + qs, + uuid, +} from "../../utils/core"; class InlineView { - constructor(section, options) { - this.settings = extend({ - ignoreClass : "", - axis: "vertical", - width: 0, - height: 0, - layout: undefined, - globalLayoutProperties: {}, - }, options || {}); + constructor(section, options) { + this.settings = extend( + { + ignoreClass: "", + axis: "vertical", + width: 0, + height: 0, + layout: undefined, + globalLayoutProperties: {}, + }, + options || {} + ); + + this.id = "epubjs-view:" + uuid(); + this.section = section; + this.index = section.index; + + this.element = this.container(this.settings.axis); + + this.added = false; + this.displayed = false; + this.rendered = false; + + this.width = this.settings.width; + this.height = this.settings.height; + + this.fixedWidth = 0; + this.fixedHeight = 0; + + // Blank Cfi for Parsing + this.epubcfi = new EpubCFI(); + + this.layout = this.settings.layout; + } + + container(axis) { + var element = document.createElement("div"); + element.classList.add("epub-view"); + element.style.overflow = "hidden"; + + if (axis && axis == "horizontal") { + element.style.display = "inline-block"; + } else { + element.style.display = "block"; + } + + return element; + } + + create() { + if (this.frame) { + return this.frame; + } + + if (!this.element) { + this.element = this.createContainer(); + } + + this.frame = document.createElement("div"); + this.frame.id = this.id; + this.frame.style.overflow = "hidden"; + this.frame.style.wordSpacing = "initial"; + this.frame.style.lineHeight = "initial"; + + this.resizing = true; + + this.element.style.visibility = "hidden"; + this.frame.style.visibility = "hidden"; + + if (this.settings.axis === "horizontal") { + this.frame.style.width = "auto"; + this.frame.style.height = "0"; + } else { + this.frame.style.width = "0"; + this.frame.style.height = "auto"; + } + + this._width = 0; + this._height = 0; + + this.element.appendChild(this.frame); + this.added = true; + + this.elementBounds = bounds(this.element); + + return this.frame; + } + + render(request, show) { + this.create(); + // Fit to size of the container, apply padding + this.size(); + + // Render Chain + return this.section + .render(request) + .then( + function (contents) { + return this.load(contents); + }.bind(this) + ) + .then(function () {}.bind(this)) + .then( + function () { + // apply the layout function to the contents + this.settings.layout.format(this.contents); + // Listen for events that require an expansion of the iframe + this.addListeners(); + + if (show !== false) { + this.show(); + } + this.emit(EVENTS.VIEWS.RENDERED, this.section); + }.bind(this) + ) + .catch( + function (e) { + this.emit(EVENTS.VIEWS.LOAD_ERROR, e); + }.bind(this) + ); + } + + // Determine locks base on settings + size(_width, _height) { + var width = _width || this.settings.width; + var height = _height || this.settings.height; + + if (this.layout.name === "pre-paginated") { + // TODO: check if these are different than the size set in chapter + this.lock("both", width, height); + } else if (this.settings.axis === "horizontal") { + this.lock("height", width, height); + } else { + this.lock("width", width, height); + } + } + + // Lock an axis to element dimensions, taking borders into account + lock(what, width, height) { + var elBorders = borders(this.element); + var iframeBorders; + + if (this.frame) { + iframeBorders = borders(this.frame); + } else { + iframeBorders = { width: 0, height: 0 }; + } + + if (what == "width" && isNumber(width)) { + this.lockedWidth = width - elBorders.width - iframeBorders.width; + this.resize(this.lockedWidth, false); // width keeps ratio correct + } + + if (what == "height" && isNumber(height)) { + this.lockedHeight = height - elBorders.height - iframeBorders.height; + this.resize(false, this.lockedHeight); + } + + if (what === "both" && isNumber(width) && isNumber(height)) { + this.lockedWidth = width - elBorders.width - iframeBorders.width; + this.lockedHeight = height - elBorders.height - iframeBorders.height; + + this.resize(this.lockedWidth, this.lockedHeight); + } + } + + // Resize a single axis based on content dimensions + expand(force) { + var width = this.lockedWidth; + var height = this.lockedHeight; + + var textWidth, textHeight; + + if (!this.frame || this._expanding) return; + + this._expanding = true; + + // Expand Horizontally + if (this.settings.axis === "horizontal") { + width = this.contentWidth(textWidth); + } // Expand Vertically + else if (this.settings.axis === "vertical") { + height = this.contentHeight(textHeight); + } + + // Only Resize if dimensions have changed or + // if Frame is still hidden, so needs reframing + if (this._needsReframe || width != this._width || height != this._height) { + this.resize(width, height); + } + + this._expanding = false; + } + + contentWidth(min) { + return this.frame.scrollWidth; + } + + contentHeight(min) { + return this.frame.scrollHeight; + } + + resize(width, height) { + if (!this.frame) return; + + if (isNumber(width)) { + this.frame.style.width = width + "px"; + this._width = width; + } + + if (isNumber(height)) { + this.frame.style.height = height + "px"; + this._height = height; + } + + this.prevBounds = this.elementBounds; + + this.elementBounds = bounds(this.element); + + let size = { + width: this.elementBounds.width, + height: this.elementBounds.height, + widthDelta: this.elementBounds.width - this.prevBounds.width, + heightDelta: this.elementBounds.height - this.prevBounds.height, + }; - this.id = "epubjs-view:" + uuid(); - this.section = section; - this.index = section.index; + this.onResize(this, size); + + this.emit(EVENTS.VIEWS.RESIZED, size); + } + + load(contents) { + var loading = new defer(); + var loaded = loading.promise; + var doc = parse(contents, "text/html"); + var body = qs(doc, "body"); + this.frame.innerHTML = body.innerHTML; - this.element = this.container(this.settings.axis); + this.document = this.frame.ownerDocument; + this.window = this.document.defaultView; - this.added = false; - this.displayed = false; - this.rendered = false; - - this.width = this.settings.width; - this.height = this.settings.height; - - this.fixedWidth = 0; - this.fixedHeight = 0; - - // Blank Cfi for Parsing - this.epubcfi = new EpubCFI(); - - this.layout = this.settings.layout; - // Dom events to listen for - // this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"]; - - } - - container(axis) { - var element = document.createElement("div"); - - element.classList.add("epub-view"); - - // if(this.settings.axis === "horizontal") { - // element.style.width = "auto"; - // element.style.height = "0"; - // } else { - // element.style.width = "0"; - // element.style.height = "auto"; - // } - - element.style.overflow = "hidden"; - - if(axis && axis == "horizontal"){ - element.style.display = "inline-block"; - } else { - element.style.display = "block"; - } - - return element; - } - - create() { - - if(this.frame) { - return this.frame; - } - - if(!this.element) { - this.element = this.createContainer(); - } - - this.frame = document.createElement("div"); - this.frame.id = this.id; - this.frame.style.overflow = "hidden"; - this.frame.style.wordSpacing = "initial"; - this.frame.style.lineHeight = "initial"; - - this.resizing = true; - - // this.frame.style.display = "none"; - this.element.style.visibility = "hidden"; - this.frame.style.visibility = "hidden"; - - if(this.settings.axis === "horizontal") { - this.frame.style.width = "auto"; - this.frame.style.height = "0"; - } else { - this.frame.style.width = "0"; - this.frame.style.height = "auto"; - } - - this._width = 0; - this._height = 0; - - this.element.appendChild(this.frame); - this.added = true; - - this.elementBounds = bounds(this.element); - - return this.frame; - } - - render(request, show) { - - // view.onLayout = this.layout.format.bind(this.layout); - this.create(); - - // Fit to size of the container, apply padding - this.size(); - - // Render Chain - return this.section.render(request) - .then(function(contents){ - return this.load(contents); - }.bind(this)) - // .then(function(doc){ - // return this.hooks.content.trigger(view, this); - // }.bind(this)) - .then(function(){ - // this.settings.layout.format(view.contents); - // return this.hooks.layout.trigger(view, this); - }.bind(this)) - // .then(function(){ - // return this.display(); - // }.bind(this)) - // .then(function(){ - // return this.hooks.render.trigger(view, this); - // }.bind(this)) - .then(function(){ - - // apply the layout function to the contents - this.settings.layout.format(this.contents); - - // Expand the iframe to the full size of the content - // this.expand(); - - // Listen for events that require an expansion of the iframe - this.addListeners(); - - if(show !== false) { - //this.q.enqueue(function(view){ - this.show(); - //}, view); - } - // this.map = new Map(view, this.layout); - //this.hooks.show.trigger(view, this); - this.emit(EVENTS.VIEWS.RENDERED, this.section); - - }.bind(this)) - .catch(function(e){ - this.emit(EVENTS.VIEWS.LOAD_ERROR, e); - }.bind(this)); - - } - - // Determine locks base on settings - size(_width, _height) { - var width = _width || this.settings.width; - var height = _height || this.settings.height; - - if(this.layout.name === "pre-paginated") { - // TODO: check if these are different than the size set in chapter - this.lock("both", width, height); - } else if(this.settings.axis === "horizontal") { - this.lock("height", width, height); - } else { - this.lock("width", width, height); - } - - } - - // Lock an axis to element dimensions, taking borders into account - lock(what, width, height) { - var elBorders = borders(this.element); - var iframeBorders; + this.contents = new Contents(this.document, this.frame); - if(this.frame) { - iframeBorders = borders(this.frame); - } else { - iframeBorders = {width: 0, height: 0}; - } + this.rendering = false; - if(what == "width" && isNumber(width)){ - this.lockedWidth = width - elBorders.width - iframeBorders.width; - this.resize(this.lockedWidth, false); // width keeps ratio correct - } - - if(what == "height" && isNumber(height)){ - this.lockedHeight = height - elBorders.height - iframeBorders.height; - this.resize(false, this.lockedHeight); - } - - if(what === "both" && - isNumber(width) && - isNumber(height)){ - - this.lockedWidth = width - elBorders.width - iframeBorders.width; - this.lockedHeight = height - elBorders.height - iframeBorders.height; - - this.resize(this.lockedWidth, this.lockedHeight); - } - - } - - // Resize a single axis based on content dimensions - expand(force) { - var width = this.lockedWidth; - var height = this.lockedHeight; - - var textWidth, textHeight; - - if(!this.frame || this._expanding) return; - - this._expanding = true; + loading.resolve(this.contents); - // Expand Horizontally - if(this.settings.axis === "horizontal") { - width = this.contentWidth(textWidth); - } // Expand Vertically - else if(this.settings.axis === "vertical") { - height = this.contentHeight(textHeight); - } + return loaded; + } - // Only Resize if dimensions have changed or - // if Frame is still hidden, so needs reframing - if(this._needsReframe || width != this._width || height != this._height){ - this.resize(width, height); - } + setLayout(layout) { + this.layout = layout; + } - this._expanding = false; - } + resizeListenters() {} - contentWidth(min) { - return this.frame.scrollWidth; - } + addListeners() { + //TODO: Add content listeners for expanding + } - contentHeight(min) { - return this.frame.scrollHeight; - } + removeListeners(layoutFunc) { + //TODO: remove content listeners for expanding + } + display(request) { + var displayed = new defer(); - resize(width, height) { + if (!this.displayed) { + this.render(request).then( + function () { + this.emit(EVENTS.VIEWS.DISPLAYED, this); + this.onDisplayed(this); - if(!this.frame) return; + this.displayed = true; - if(isNumber(width)){ - this.frame.style.width = width + "px"; - this._width = width; - } + displayed.resolve(this); + }.bind(this) + ); + } else { + displayed.resolve(this); + } - if(isNumber(height)){ - this.frame.style.height = height + "px"; - this._height = height; - } + return displayed.promise; + } - this.prevBounds = this.elementBounds; + show() { + this.element.style.visibility = "visible"; - this.elementBounds = bounds(this.element); + if (this.frame) { + this.frame.style.visibility = "visible"; + } - let size = { - width: this.elementBounds.width, - height: this.elementBounds.height, - widthDelta: this.elementBounds.width - this.prevBounds.width, - heightDelta: this.elementBounds.height - this.prevBounds.height, - }; + this.emit(EVENTS.VIEWS.SHOWN, this); + } - this.onResize(this, size); + hide() { + this.element.style.visibility = "hidden"; + this.frame.style.visibility = "hidden"; - this.emit(EVENTS.VIEWS.RESIZED, size); + this.stopExpanding = true; + this.emit(EVENTS.VIEWS.HIDDEN, this); + } - } + position() { + return this.element.getBoundingClientRect(); + } + locationOf(target) { + var parentPos = this.frame.getBoundingClientRect(); + var targetPos = this.contents.locationOf(target, this.settings.ignoreClass); - load(contents) { - var loading = new defer(); - var loaded = loading.promise; - var doc = parse(contents, "text/html"); - var body = qs(doc, "body"); + return { + left: window.scrollX + parentPos.left + targetPos.left, + top: window.scrollY + parentPos.top + targetPos.top, + }; + } - /* - var srcs = doc.querySelectorAll("[src]"); + onDisplayed(view) { + // Stub, override with a custom functions + } - Array.prototype.slice.call(srcs) - .forEach(function(item) { - var src = item.getAttribute("src"); - var assetUri = URI(src); - var origin = assetUri.origin(); - var absoluteUri; + onResize(view, e) { + // Stub, override with a custom functions + } - if (!origin) { - absoluteUri = assetUri.absoluteTo(this.section.url); - item.src = absoluteUri; - } - }.bind(this)); - */ - this.frame.innerHTML = body.innerHTML; + bounds() { + if (!this.elementBounds) { + this.elementBounds = bounds(this.element); + } + return this.elementBounds; + } - this.document = this.frame.ownerDocument; - this.window = this.document.defaultView; + destroy() { + if (this.displayed) { + this.displayed = false; - this.contents = new Contents(this.document, this.frame); + this.removeListeners(); - this.rendering = false; + this.stopExpanding = true; + this.element.removeChild(this.frame); + this.displayed = false; + this.frame = null; - loading.resolve(this.contents); - - - return loaded; - } - - setLayout(layout) { - this.layout = layout; - } - - - resizeListenters() { - // Test size again - // clearTimeout(this.expanding); - // this.expanding = setTimeout(this.expand.bind(this), 350); - } - - addListeners() { - //TODO: Add content listeners for expanding - } - - removeListeners(layoutFunc) { - //TODO: remove content listeners for expanding - } - - display(request) { - var displayed = new defer(); - - if (!this.displayed) { - - this.render(request).then(function () { - - this.emit(EVENTS.VIEWS.DISPLAYED, this); - this.onDisplayed(this); - - this.displayed = true; - - displayed.resolve(this); - - }.bind(this)); - - } else { - displayed.resolve(this); - } - - - return displayed.promise; - } - - show() { - - this.element.style.visibility = "visible"; - - if(this.frame){ - this.frame.style.visibility = "visible"; - } - - this.emit(EVENTS.VIEWS.SHOWN, this); - } - - hide() { - // this.frame.style.display = "none"; - this.element.style.visibility = "hidden"; - this.frame.style.visibility = "hidden"; - - this.stopExpanding = true; - this.emit(EVENTS.VIEWS.HIDDEN, this); - } - - position() { - return this.element.getBoundingClientRect(); - } - - locationOf(target) { - var parentPos = this.frame.getBoundingClientRect(); - var targetPos = this.contents.locationOf(target, this.settings.ignoreClass); - - return { - "left": window.scrollX + parentPos.left + targetPos.left, - "top": window.scrollY + parentPos.top + targetPos.top - }; - } - - onDisplayed(view) { - // Stub, override with a custom functions - } - - onResize(view, e) { - // Stub, override with a custom functions - } - - bounds() { - if(!this.elementBounds) { - this.elementBounds = bounds(this.element); - } - return this.elementBounds; - } - - destroy() { - - if(this.displayed){ - this.displayed = false; - - this.removeListeners(); - - this.stopExpanding = true; - this.element.removeChild(this.frame); - this.displayed = false; - this.frame = null; - - this._textWidth = null; - this._textHeight = null; - this._width = null; - this._height = null; - } - // this.element.style.height = "0px"; - // this.element.style.width = "0px"; - } + this._textWidth = null; + this._textHeight = null; + this._width = null; + this._height = null; + } + } } EventEmitter(InlineView.prototype); diff --git a/src/mapping.js b/src/mapping.js index 0075f29..ee0d213 100644 --- a/src/mapping.js +++ b/src/mapping.js @@ -10,502 +10,459 @@ import { nodeBounds } from "./utils/core"; * @param {boolean} [dev] toggle developer highlighting */ class Mapping { - constructor(layout, direction, axis, dev=false) { - this.layout = layout; - this.horizontal = (axis === "horizontal") ? true : false; - this.direction = direction || "ltr"; - this._dev = dev; - } - - /** - * Find CFI pairs for entire section at once - */ - section(view) { - var ranges = this.findRanges(view); - var map = this.rangeListToCfiList(view.section.cfiBase, ranges); - - return map; - } - - /** - * Find CFI pairs for a page - * @param {Contents} contents Contents from view - * @param {string} cfiBase string of the base for a cfi - * @param {number} start position to start at - * @param {number} end position to end at - */ - page(contents, cfiBase, start, end) { - var root = contents && contents.document ? contents.document.body : false; - var result; - - if (!root) { - return; - } - - result = this.rangePairToCfiPair(cfiBase, { - start: this.findStart(root, start, end), - end: this.findEnd(root, start, end) - }); - - if (this._dev === true) { - let doc = contents.document; - let startRange = new EpubCFI(result.start).toRange(doc); - let endRange = new EpubCFI(result.end).toRange(doc); - - let selection = doc.defaultView.getSelection(); - let r = doc.createRange(); - selection.removeAllRanges(); - r.setStart(startRange.startContainer, startRange.startOffset); - r.setEnd(endRange.endContainer, endRange.endOffset); - selection.addRange(r); - } - - return result; - } - - /** - * Walk a node, preforming a function on each node it finds - * @private - * @param {Node} root Node to walkToNode - * @param {function} func walk function - * @return {*} returns the result of the walk function - */ - walk(root, func) { - // IE11 has strange issue, if root is text node IE throws exception on - // calling treeWalker.nextNode(), saying - // Unexpected call to method or property access instead of returning null value - if(root && root.nodeType === Node.TEXT_NODE) { - return; - } - // safeFilter is required so that it can work in IE as filter is a function for IE - // and for other browser filter is an object. - var filter = { - acceptNode: function(node) { - if (node.data.trim().length > 0) { - return NodeFilter.FILTER_ACCEPT; - } else { - return NodeFilter.FILTER_REJECT; - } - } - }; - var safeFilter = filter.acceptNode; - safeFilter.acceptNode = filter.acceptNode; - - var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, safeFilter, false); - var node; - var result; - while ((node = treeWalker.nextNode())) { - result = func(node); - if(result) break; - } - - return result; - } - - findRanges(view){ - var columns = []; - var scrollWidth = view.contents.scrollWidth(); - var spreads = Math.ceil( scrollWidth / this.layout.spreadWidth); - var count = spreads * this.layout.divisor; - var columnWidth = this.layout.columnWidth; - var gap = this.layout.gap; - var start, end; - - for (var i = 0; i < count.pages; i++) { - start = (columnWidth + gap) * i; - end = (columnWidth * (i+1)) + (gap * i); - columns.push({ - start: this.findStart(view.document.body, start, end), - end: this.findEnd(view.document.body, start, end) - }); - } - - return columns; - } - - /** - * Find Start Range - * @private - * @param {Node} root root node - * @param {number} start position to start at - * @param {number} end position to end at - * @return {Range} - */ - findStart(root, start, end){ - var stack = [root]; - var $el; - var found; - var $prev = root; - - while (stack.length) { - - $el = stack.shift(); - - found = this.walk($el, (node) => { - var left, right, top, bottom; - var elPos; - var elRange; - - - elPos = nodeBounds(node); - - if (this.horizontal && this.direction === "ltr") { - - left = this.horizontal ? elPos.left : elPos.top; - right = this.horizontal ? elPos.right : elPos.bottom; - - if( left >= start && left <= end ) { - return node; - } else if (right > start) { - return node; - } else { - $prev = node; - stack.push(node); - } - - } else if (this.horizontal && this.direction === "rtl") { - - left = elPos.left; - right = elPos.right; - - if( right <= end && right >= start ) { - return node; - } else if (left < end) { - return node; - } else { - $prev = node; - stack.push(node); - } - - } else { - - top = elPos.top; - bottom = elPos.bottom; - - if( top >= start && top <= end ) { - return node; - } else if (bottom > start) { - return node; - } else { - $prev = node; - stack.push(node); - } - - } - - - }); - - if(found) { - return this.findTextStartRange(found, start, end); - } - - } - - // Return last element - return this.findTextStartRange($prev, start, end); - } - - /** - * Find End Range - * @private - * @param {Node} root root node - * @param {number} start position to start at - * @param {number} end position to end at - * @return {Range} - */ - findEnd(root, start, end){ - var stack = [root]; - var $el; - var $prev = root; - var found; - - while (stack.length) { - - $el = stack.shift(); - - found = this.walk($el, (node) => { - - var left, right, top, bottom; - var elPos; - var elRange; - - elPos = nodeBounds(node); - - if (this.horizontal && this.direction === "ltr") { - - left = Math.round(elPos.left); - right = Math.round(elPos.right); - - if(left > end && $prev) { - return $prev; - } else if(right > end) { - return node; - } else { - $prev = node; - stack.push(node); - } - - } else if (this.horizontal && this.direction === "rtl") { - - left = Math.round(this.horizontal ? elPos.left : elPos.top); - right = Math.round(this.horizontal ? elPos.right : elPos.bottom); - - if(right < start && $prev) { - return $prev; - } else if(left < start) { - return node; - } else { - $prev = node; - stack.push(node); - } - - } else { - - top = Math.round(elPos.top); - bottom = Math.round(elPos.bottom); - - if(top > end && $prev) { - return $prev; - } else if(bottom > end) { - return node; - } else { - $prev = node; - stack.push(node); - } - - } - - }); - - - if(found){ - return this.findTextEndRange(found, start, end); - } - - } - - // end of chapter - return this.findTextEndRange($prev, start, end); - } - - /** - * Find Text Start Range - * @private - * @param {Node} root root node - * @param {number} start position to start at - * @param {number} end position to end at - * @return {Range} - */ - findTextStartRange(node, start, end){ - var ranges = this.splitTextNodeIntoRanges(node); - var range; - var pos; - var left, top, right; - - for (var i = 0; i < ranges.length; i++) { - range = ranges[i]; - - pos = range.getBoundingClientRect(); - - if (this.horizontal && this.direction === "ltr") { - - left = pos.left; - if( left >= start ) { - return range; - } - - } else if (this.horizontal && this.direction === "rtl") { - - right = pos.right; - if( right <= end ) { - return range; - } - - } else { - - top = pos.top; - if( top >= start ) { - return range; - } - - } - - // prev = range; - - } - - return ranges[0]; - } - - /** - * Find Text End Range - * @private - * @param {Node} root root node - * @param {number} start position to start at - * @param {number} end position to end at - * @return {Range} - */ - findTextEndRange(node, start, end){ - var ranges = this.splitTextNodeIntoRanges(node); - var prev; - var range; - var pos; - var left, right, top, bottom; - - for (var i = 0; i < ranges.length; i++) { - range = ranges[i]; - - pos = range.getBoundingClientRect(); - - if (this.horizontal && this.direction === "ltr") { - - left = pos.left; - right = pos.right; - - if(left > end && prev) { - return prev; - } else if(right > end) { - return range; - } - - } else if (this.horizontal && this.direction === "rtl") { - - left = pos.left - right = pos.right; - - if(right < start && prev) { - return prev; - } else if(left < start) { - return range; - } - - } else { - - top = pos.top; - bottom = pos.bottom; - - if(top > end && prev) { - return prev; - } else if(bottom > end) { - return range; - } - - } - - - prev = range; - - } - - // Ends before limit - return ranges[ranges.length-1]; - - } - - /** - * Split up a text node into ranges for each word - * @private - * @param {Node} root root node - * @param {string} [_splitter] what to split on - * @return {Range[]} - */ - splitTextNodeIntoRanges(node, _splitter){ - var ranges = []; - var textContent = node.textContent || ""; - var text = textContent.trim(); - var range; - var doc = node.ownerDocument; - var splitter = _splitter || " "; - - var pos = text.indexOf(splitter); - - if(pos === -1 || node.nodeType != Node.TEXT_NODE) { - range = doc.createRange(); - range.selectNodeContents(node); - return [range]; - } - - range = doc.createRange(); - range.setStart(node, 0); - range.setEnd(node, pos); - ranges.push(range); - range = false; - - while ( pos != -1 ) { - - pos = text.indexOf(splitter, pos + 1); - if(pos > 0) { - - if(range) { - range.setEnd(node, pos); - ranges.push(range); - } - - range = doc.createRange(); - range.setStart(node, pos+1); - } - } - - if(range) { - range.setEnd(node, text.length); - ranges.push(range); - } - - return ranges; - } - - - /** - * Turn a pair of ranges into a pair of CFIs - * @private - * @param {string} cfiBase base string for an EpubCFI - * @param {object} rangePair { start: Range, end: Range } - * @return {object} { start: "epubcfi(...)", end: "epubcfi(...)" } - */ - rangePairToCfiPair(cfiBase, rangePair){ - - var startRange = rangePair.start; - var endRange = rangePair.end; - - startRange.collapse(true); - endRange.collapse(false); - - let startCfi = new EpubCFI(startRange, cfiBase).toString(); - let endCfi = new EpubCFI(endRange, cfiBase).toString(); - - return { - start: startCfi, - end: endCfi - }; - - } - - rangeListToCfiList(cfiBase, columns){ - var map = []; - var cifPair; - - for (var i = 0; i < columns.length; i++) { - cifPair = this.rangePairToCfiPair(cfiBase, columns[i]); - - map.push(cifPair); - - } - - return map; - } - - /** - * Set the axis for mapping - * @param {string} axis horizontal | vertical - * @return {boolean} is it horizontal? - */ - axis(axis) { - if (axis) { - this.horizontal = (axis === "horizontal") ? true : false; - } - return this.horizontal; - } + constructor(layout, direction, axis, dev = false) { + this.layout = layout; + this.horizontal = axis === "horizontal" ? true : false; + this.direction = direction || "ltr"; + this._dev = dev; + } + + /** + * Find CFI pairs for entire section at once + */ + section(view) { + var ranges = this.findRanges(view); + var map = this.rangeListToCfiList(view.section.cfiBase, ranges); + + return map; + } + + /** + * Find CFI pairs for a page + * @param {Contents} contents Contents from view + * @param {string} cfiBase string of the base for a cfi + * @param {number} start position to start at + * @param {number} end position to end at + */ + page(contents, cfiBase, start, end) { + var root = contents && contents.document ? contents.document.body : false; + var result; + + if (!root) { + return; + } + + result = this.rangePairToCfiPair(cfiBase, { + start: this.findStart(root, start, end), + end: this.findEnd(root, start, end), + }); + + if (this._dev === true) { + let doc = contents.document; + let startRange = new EpubCFI(result.start).toRange(doc); + let endRange = new EpubCFI(result.end).toRange(doc); + + let selection = doc.defaultView.getSelection(); + let r = doc.createRange(); + selection.removeAllRanges(); + r.setStart(startRange.startContainer, startRange.startOffset); + r.setEnd(endRange.endContainer, endRange.endOffset); + selection.addRange(r); + } + + return result; + } + + /** + * Walk a node, preforming a function on each node it finds + * @private + * @param {Node} root Node to walkToNode + * @param {function} func walk function + * @return {*} returns the result of the walk function + */ + walk(root, func) { + // IE11 has strange issue, if root is text node IE throws exception on + // calling treeWalker.nextNode(), saying + // Unexpected call to method or property access instead of returning null value + if (root && root.nodeType === Node.TEXT_NODE) { + return; + } + // safeFilter is required so that it can work in IE as filter is a function for IE + // and for other browser filter is an object. + var filter = { + acceptNode: function (node) { + if (node.data.trim().length > 0) { + return NodeFilter.FILTER_ACCEPT; + } else { + return NodeFilter.FILTER_REJECT; + } + }, + }; + var safeFilter = filter.acceptNode; + safeFilter.acceptNode = filter.acceptNode; + + var treeWalker = document.createTreeWalker( + root, + NodeFilter.SHOW_TEXT, + safeFilter, + false + ); + var node; + var result; + while ((node = treeWalker.nextNode())) { + result = func(node); + if (result) break; + } + + return result; + } + + findRanges(view) { + var columns = []; + var scrollWidth = view.contents.scrollWidth(); + var spreads = Math.ceil(scrollWidth / this.layout.spreadWidth); + var count = spreads * this.layout.divisor; + var columnWidth = this.layout.columnWidth; + var gap = this.layout.gap; + var start, end; + + for (var i = 0; i < count.pages; i++) { + start = (columnWidth + gap) * i; + end = columnWidth * (i + 1) + gap * i; + columns.push({ + start: this.findStart(view.document.body, start, end), + end: this.findEnd(view.document.body, start, end), + }); + } + + return columns; + } + + /** + * Find Start Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + findStart(root, start, end) { + var stack = [root]; + var $el; + var found; + var $prev = root; + + while (stack.length) { + $el = stack.shift(); + + found = this.walk($el, (node) => { + var left, right, top, bottom; + var elPos; + + elPos = nodeBounds(node); + + if (this.horizontal && this.direction === "ltr") { + left = this.horizontal ? elPos.left : elPos.top; + right = this.horizontal ? elPos.right : elPos.bottom; + + if (left >= start && left <= end) { + return node; + } else if (right > start) { + return node; + } else { + $prev = node; + stack.push(node); + } + } else if (this.horizontal && this.direction === "rtl") { + left = elPos.left; + right = elPos.right; + + if (right <= end && right >= start) { + return node; + } else if (left < end) { + return node; + } else { + $prev = node; + stack.push(node); + } + } else { + top = elPos.top; + bottom = elPos.bottom; + + if (top >= start && top <= end) { + return node; + } else if (bottom > start) { + return node; + } else { + $prev = node; + stack.push(node); + } + } + }); + + if (found) { + return this.findTextStartRange(found, start, end); + } + } + + // Return last element + return this.findTextStartRange($prev, start, end); + } + + /** + * Find End Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + findEnd(root, start, end) { + var stack = [root]; + var $el; + var $prev = root; + var found; + + while (stack.length) { + $el = stack.shift(); + + found = this.walk($el, (node) => { + var left, right, top, bottom; + var elPos; + + elPos = nodeBounds(node); + + if (this.horizontal && this.direction === "ltr") { + left = Math.round(elPos.left); + right = Math.round(elPos.right); + + if (left > end && $prev) { + return $prev; + } else if (right > end) { + return node; + } else { + $prev = node; + stack.push(node); + } + } else if (this.horizontal && this.direction === "rtl") { + left = Math.round(this.horizontal ? elPos.left : elPos.top); + right = Math.round(this.horizontal ? elPos.right : elPos.bottom); + + if (right < start && $prev) { + return $prev; + } else if (left < start) { + return node; + } else { + $prev = node; + stack.push(node); + } + } else { + top = Math.round(elPos.top); + bottom = Math.round(elPos.bottom); + + if (top > end && $prev) { + return $prev; + } else if (bottom > end) { + return node; + } else { + $prev = node; + stack.push(node); + } + } + }); + + if (found) { + return this.findTextEndRange(found, start, end); + } + } + + // end of chapter + return this.findTextEndRange($prev, start, end); + } + + /** + * Find Text Start Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + findTextStartRange(node, start, end) { + var ranges = this.splitTextNodeIntoRanges(node); + var range; + var pos; + var left, top, right; + + for (var i = 0; i < ranges.length; i++) { + range = ranges[i]; + + pos = range.getBoundingClientRect(); + + if (this.horizontal && this.direction === "ltr") { + left = pos.left; + if (left >= start) { + return range; + } + } else if (this.horizontal && this.direction === "rtl") { + right = pos.right; + if (right <= end) { + return range; + } + } else { + top = pos.top; + if (top >= start) { + return range; + } + } + } + + return ranges[0]; + } + + /** + * Find Text End Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + findTextEndRange(node, start, end) { + var ranges = this.splitTextNodeIntoRanges(node); + var prev; + var range; + var pos; + var left, right, top, bottom; + + for (var i = 0; i < ranges.length; i++) { + range = ranges[i]; + + pos = range.getBoundingClientRect(); + + if (this.horizontal && this.direction === "ltr") { + left = pos.left; + right = pos.right; + + if (left > end && prev) { + return prev; + } else if (right > end) { + return range; + } + } else if (this.horizontal && this.direction === "rtl") { + left = pos.left; + right = pos.right; + + if (right < start && prev) { + return prev; + } else if (left < start) { + return range; + } + } else { + top = pos.top; + bottom = pos.bottom; + + if (top > end && prev) { + return prev; + } else if (bottom > end) { + return range; + } + } + + prev = range; + } + + // Ends before limit + return ranges[ranges.length - 1]; + } + + /** + * Split up a text node into ranges for each word + * @private + * @param {Node} root root node + * @param {string} [_splitter] what to split on + * @return {Range[]} + */ + splitTextNodeIntoRanges(node, _splitter) { + var ranges = []; + var textContent = node.textContent || ""; + var text = textContent.trim(); + var range; + var doc = node.ownerDocument; + var splitter = _splitter || " "; + + var pos = text.indexOf(splitter); + + if (pos === -1 || node.nodeType != Node.TEXT_NODE) { + range = doc.createRange(); + range.selectNodeContents(node); + return [range]; + } + + range = doc.createRange(); + range.setStart(node, 0); + range.setEnd(node, pos); + ranges.push(range); + range = false; + + while (pos != -1) { + pos = text.indexOf(splitter, pos + 1); + if (pos > 0) { + if (range) { + range.setEnd(node, pos); + ranges.push(range); + } + + range = doc.createRange(); + range.setStart(node, pos + 1); + } + } + + if (range) { + range.setEnd(node, text.length); + ranges.push(range); + } + + return ranges; + } + + /** + * Turn a pair of ranges into a pair of CFIs + * @private + * @param {string} cfiBase base string for an EpubCFI + * @param {object} rangePair { start: Range, end: Range } + * @return {object} { start: "epubcfi(...)", end: "epubcfi(...)" } + */ + rangePairToCfiPair(cfiBase, rangePair) { + var startRange = rangePair.start; + var endRange = rangePair.end; + + startRange.collapse(true); + endRange.collapse(false); + + let startCfi = new EpubCFI(startRange, cfiBase).toString(); + let endCfi = new EpubCFI(endRange, cfiBase).toString(); + + return { + start: startCfi, + end: endCfi, + }; + } + + rangeListToCfiList(cfiBase, columns) { + var map = []; + var cifPair; + + for (var i = 0; i < columns.length; i++) { + cifPair = this.rangePairToCfiPair(cfiBase, columns[i]); + + map.push(cifPair); + } + + return map; + } + + /** + * Set the axis for mapping + * @param {string} axis horizontal | vertical + * @return {boolean} is it horizontal? + */ + axis(axis) { + if (axis) { + this.horizontal = axis === "horizontal" ? true : false; + } + return this.horizontal; + } } export default Mapping; diff --git a/src/navigation.js b/src/navigation.js index 801598b..4bc5517 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -1,357 +1,361 @@ -import {qs, qsa, querySelectorByType, filterChildren, getParentByTagName} from "./utils/core"; +import { filterChildren, qs, qsa, querySelectorByType } from "./utils/core"; /** * Navigation Parser * @param {document} xml navigation html / xhtml / ncx */ class Navigation { - constructor(xml) { - this.toc = []; - this.tocByHref = {}; - this.tocById = {}; + constructor(xml) { + this.toc = []; + this.tocByHref = {}; + this.tocById = {}; - this.landmarks = []; - this.landmarksByType = {}; + this.landmarks = []; + this.landmarksByType = {}; - this.length = 0; - if (xml) { - this.parse(xml); - } - } + this.length = 0; + if (xml) { + this.parse(xml); + } + } - /** - * Parse out the navigation items - * @param {document} xml navigation html / xhtml / ncx - */ - parse(xml) { - let isXml = xml.nodeType; - let html; - let ncx; + /** + * Parse out the navigation items + * @param {document} xml navigation html / xhtml / ncx + */ + parse(xml) { + let isXml = xml.nodeType; + let html; + let ncx; - if (isXml) { - html = qs(xml, "html"); - ncx = qs(xml, "ncx"); - } + if (isXml) { + html = qs(xml, "html"); + ncx = qs(xml, "ncx"); + } - if (!isXml) { - this.toc = this.load(xml); - } else if(html) { - this.toc = this.parseNav(xml); - this.landmarks = this.parseLandmarks(xml); - } else if(ncx){ - this.toc = this.parseNcx(xml); - } + if (!isXml) { + this.toc = this.load(xml); + } else if (html) { + this.toc = this.parseNav(xml); + this.landmarks = this.parseLandmarks(xml); + } else if (ncx) { + this.toc = this.parseNcx(xml); + } - this.length = 0; + this.length = 0; - this.unpack(this.toc); - } + this.unpack(this.toc); + } - /** - * Unpack navigation items - * @private - * @param {array} toc - */ - unpack(toc) { - var item; + /** + * Unpack navigation items + * @private + * @param {array} toc + */ + unpack(toc) { + var item; - for (var i = 0; i < toc.length; i++) { - item = toc[i]; + for (var i = 0; i < toc.length; i++) { + item = toc[i]; - if (item.href) { - this.tocByHref[item.href] = i; - } + if (item.href) { + this.tocByHref[item.href] = i; + } - if (item.id) { - this.tocById[item.id] = i; - } + if (item.id) { + this.tocById[item.id] = i; + } - this.length++; + this.length++; - if (item.subitems.length) { - this.unpack(item.subitems); - } - } + if (item.subitems.length) { + this.unpack(item.subitems); + } + } + } - } + /** + * Get an item from the navigation + * @param {string} target + * @return {object} navItem + */ + get(target) { + var index; - /** - * Get an item from the navigation - * @param {string} target - * @return {object} navItem - */ - get(target) { - var index; + if (!target) { + return this.toc; + } - if(!target) { - return this.toc; - } + if (target.indexOf("#") === 0) { + index = this.tocById[target.substring(1)]; + } else if (target in this.tocByHref) { + index = this.tocByHref[target]; + } - if(target.indexOf("#") === 0) { - index = this.tocById[target.substring(1)]; - } else if(target in this.tocByHref){ - index = this.tocByHref[target]; - } + return this.getByIndex(target, index, this.toc); + } - return this.getByIndex(target, index, this.toc); - } + /** + * Get an item from navigation subitems recursively by index + * @param {string} target + * @param {number} index + * @param {array} navItems + * @return {object} navItem + */ + getByIndex(target, index, navItems) { + if (navItems.length === 0) { + return; + } - /** - * Get an item from navigation subitems recursively by index - * @param {string} target - * @param {number} index - * @param {array} navItems - * @return {object} navItem - */ - getByIndex(target, index, navItems) { - if (navItems.length === 0) { - return; - } + const item = navItems[index]; + if (item && (target === item.id || target === item.href)) { + return item; + } else { + let result; + for (let i = 0; i < navItems.length; ++i) { + result = this.getByIndex(target, index, navItems[i].subitems); + if (result) { + break; + } + } + return result; + } + } - const item = navItems[index]; - if (item && (target === item.id || target === item.href)) { - return item; - } else { - let result; - for (let i = 0; i < navItems.length; ++i) { - result = this.getByIndex(target, index, navItems[i].subitems); - if (result) { - break; - } - } - return result; - } - } + /** + * Get a landmark by type + * List of types: https://idpf.github.io/epub-vocabs/structure/ + * @param {string} type + * @return {object} landmarkItem + */ + landmark(type) { + var index; - /** - * Get a landmark by type - * List of types: https://idpf.github.io/epub-vocabs/structure/ - * @param {string} type - * @return {object} landmarkItem - */ - landmark(type) { - var index; + if (!type) { + return this.landmarks; + } - if(!type) { - return this.landmarks; - } + index = this.landmarksByType[type]; - index = this.landmarksByType[type]; + return this.landmarks[index]; + } - return this.landmarks[index]; - } + /** + * Parse toc from a Epub > 3.0 Nav + * @private + * @param {document} navHtml + * @return {array} navigation list + */ + parseNav(navHtml) { + var navElement = querySelectorByType(navHtml, "nav", "toc"); + var list = []; - /** - * Parse toc from a Epub > 3.0 Nav - * @private - * @param {document} navHtml - * @return {array} navigation list - */ - parseNav(navHtml){ - var navElement = querySelectorByType(navHtml, "nav", "toc"); - var list = []; + if (!navElement) return list; - if (!navElement) return list; + let navList = filterChildren(navElement, "ol", true); + if (!navList) return list; - let navList = filterChildren(navElement, "ol", true); - if (!navList) return list; + list = this.parseNavList(navList); + return list; + } - list = this.parseNavList(navList); - return list; - } + /** + * Parses lists in the toc + * @param {document} navListHtml + * @param {string} parent id + * @return {array} navigation list + */ + parseNavList(navListHtml, parent) { + const result = []; - /** - * Parses lists in the toc - * @param {document} navListHtml - * @param {string} parent id - * @return {array} navigation list - */ - parseNavList(navListHtml, parent) { - const result = []; + if (!navListHtml) return result; + if (!navListHtml.children) return result; - if (!navListHtml) return result; - if (!navListHtml.children) return result; - - for (let i = 0; i < navListHtml.children.length; i++) { - const item = this.navItem(navListHtml.children[i], parent); + for (let i = 0; i < navListHtml.children.length; i++) { + const item = this.navItem(navListHtml.children[i], parent); - if (item) { - result.push(item); - } - } + if (item) { + result.push(item); + } + } - return result; - } + return result; + } - /** - * Create a navItem - * @private - * @param {element} item - * @return {object} navItem - */ - navItem(item, parent) { - let id = item.getAttribute("id") || undefined; - let content = filterChildren(item, "a", true) - || filterChildren(item, "span", true); + /** + * Create a navItem + * @private + * @param {element} item + * @return {object} navItem + */ + navItem(item, parent) { + let id = item.getAttribute("id") || undefined; + let content = + filterChildren(item, "a", true) || filterChildren(item, "span", true); - if (!content) { - return; - } + if (!content) { + return; + } - let src = content.getAttribute("href") || ""; - - if (!id) { - id = src; - } - let text = content.textContent || ""; + let src = content.getAttribute("href") || ""; - let subitems = []; - let nested = filterChildren(item, "ol", true); - if (nested) { - subitems = this.parseNavList(nested, id); - } + if (!id) { + id = src; + } + let text = content.textContent || ""; - return { - "id": id, - "href": src, - "label": text, - "subitems" : subitems, - "parent" : parent - }; - } + let subitems = []; + let nested = filterChildren(item, "ol", true); + if (nested) { + subitems = this.parseNavList(nested, id); + } - /** - * Parse landmarks from a Epub > 3.0 Nav - * @private - * @param {document} navHtml - * @return {array} landmarks list - */ - parseLandmarks(navHtml){ - var navElement = querySelectorByType(navHtml, "nav", "landmarks"); - var navItems = navElement ? qsa(navElement, "li") : []; - var length = navItems.length; - var i; - var list = []; - var item; + return { + id: id, + href: src, + label: text, + subitems: subitems, + parent: parent, + }; + } - if(!navItems || length === 0) return list; + /** + * Parse landmarks from a Epub > 3.0 Nav + * @private + * @param {document} navHtml + * @return {array} landmarks list + */ + parseLandmarks(navHtml) { + var navElement = querySelectorByType(navHtml, "nav", "landmarks"); + var navItems = navElement ? qsa(navElement, "li") : []; + var length = navItems.length; + var i; + var list = []; + var item; - for (i = 0; i < length; ++i) { - item = this.landmarkItem(navItems[i]); - if (item) { - list.push(item); - this.landmarksByType[item.type] = i; - } - } + if (!navItems || length === 0) return list; - return list; - } + for (i = 0; i < length; ++i) { + item = this.landmarkItem(navItems[i]); + if (item) { + list.push(item); + this.landmarksByType[item.type] = i; + } + } - /** - * Create a landmarkItem - * @private - * @param {element} item - * @return {object} landmarkItem - */ - landmarkItem(item){ - let content = filterChildren(item, "a", true); + return list; + } - if (!content) { - return; - } + /** + * Create a landmarkItem + * @private + * @param {element} item + * @return {object} landmarkItem + */ + landmarkItem(item) { + let content = filterChildren(item, "a", true); - let type = content.getAttributeNS("http://www.idpf.org/2007/ops", "type") || undefined; - let href = content.getAttribute("href") || ""; - let text = content.textContent || ""; + if (!content) { + return; + } - return { - "href": href, - "label": text, - "type" : type - }; - } + let type = + content.getAttributeNS("http://www.idpf.org/2007/ops", "type") || + undefined; + let href = content.getAttribute("href") || ""; + let text = content.textContent || ""; - /** - * Parse from a Epub > 3.0 NC - * @private - * @param {document} navHtml - * @return {array} navigation list - */ - parseNcx(tocXml){ - var navPoints = qsa(tocXml, "navPoint"); - var length = navPoints.length; - var i; - var toc = {}; - var list = []; - var item, parent; + return { + href: href, + label: text, + type: type, + }; + } - if(!navPoints || length === 0) return list; + /** + * Parse from a Epub > 3.0 NC + * @private + * @param {document} navHtml + * @return {array} navigation list + */ + parseNcx(tocXml) { + var navPoints = qsa(tocXml, "navPoint"); + var length = navPoints.length; + var i; + var toc = {}; + var list = []; + var item, parent; - for (i = 0; i < length; ++i) { - item = this.ncxItem(navPoints[i]); - toc[item.id] = item; - if(!item.parent) { - list.push(item); - } else { - parent = toc[item.parent]; - parent.subitems.push(item); - } - } + if (!navPoints || length === 0) return list; - return list; - } + for (i = 0; i < length; ++i) { + item = this.ncxItem(navPoints[i]); + toc[item.id] = item; + if (!item.parent) { + list.push(item); + } else { + parent = toc[item.parent]; + parent.subitems.push(item); + } + } - /** - * Create a ncxItem - * @private - * @param {element} item - * @return {object} ncxItem - */ - ncxItem(item){ - var id = item.getAttribute("id") || false, - content = qs(item, "content"), - src = content.getAttribute("src"), - navLabel = qs(item, "navLabel"), - text = navLabel.textContent ? navLabel.textContent : "", - subitems = [], - parentNode = item.parentNode, - parent; + return list; + } - if(parentNode && (parentNode.nodeName === "navPoint" || parentNode.nodeName.split(':').slice(-1)[0] === "navPoint")) { - parent = parentNode.getAttribute("id"); - } + /** + * Create a ncxItem + * @private + * @param {element} item + * @return {object} ncxItem + */ + ncxItem(item) { + var id = item.getAttribute("id") || false, + content = qs(item, "content"), + src = content.getAttribute("src"), + navLabel = qs(item, "navLabel"), + text = navLabel.textContent ? navLabel.textContent : "", + subitems = [], + parentNode = item.parentNode, + parent; + if ( + parentNode && + (parentNode.nodeName === "navPoint" || + parentNode.nodeName.split(":").slice(-1)[0] === "navPoint") + ) { + parent = parentNode.getAttribute("id"); + } - return { - "id": id, - "href": src, - "label": text, - "subitems" : subitems, - "parent" : parent - }; - } + return { + id: id, + href: src, + label: text, + subitems: subitems, + parent: parent, + }; + } - /** - * Load Spine Items - * @param {object} json the items to be loaded - * @return {Array} navItems - */ - load(json) { - return json.map(item => { - item.label = item.title; - item.subitems = item.children ? this.load(item.children) : []; - return item; - }); - } + /** + * Load Spine Items + * @param {object} json the items to be loaded + * @return {Array} navItems + */ + load(json) { + return json.map((item) => { + item.label = item.title; + item.subitems = item.children ? this.load(item.children) : []; + return item; + }); + } - /** - * forEach pass through - * @param {Function} fn function to run on each item - * @return {method} forEach loop - */ - forEach(fn) { - return this.toc.forEach(fn); - } + /** + * forEach pass through + * @param {Function} fn function to run on each item + * @return {method} forEach loop + */ + forEach(fn) { + return this.toc.forEach(fn); + } } export default Navigation; diff --git a/src/packaging.js b/src/packaging.js index e038471..90d34b8 100644 --- a/src/packaging.js +++ b/src/packaging.js @@ -1,4 +1,4 @@ -import {qs, qsa, qsp, indexOfElementNode} from "./utils/core"; +import { indexOfElementNode, qs, qsa, qsp } from "./utils/core"; /** * Open Packaging Format Parser @@ -6,368 +6,375 @@ import {qs, qsa, qsp, indexOfElementNode} from "./utils/core"; * @param {document} packageDocument OPF XML */ class Packaging { - constructor(packageDocument) { - this.manifest = {}; - this.navPath = ''; - this.ncxPath = ''; - this.coverPath = ''; - this.spineNodeIndex = 0; - this.spine = []; - this.metadata = {}; + constructor(packageDocument) { + this.manifest = {}; + this.navPath = ""; + this.ncxPath = ""; + this.coverPath = ""; + this.spineNodeIndex = 0; + this.spine = []; + this.metadata = {}; - if (packageDocument) { - this.parse(packageDocument); - } - } + if (packageDocument) { + this.parse(packageDocument); + } + } - /** - * Parse OPF XML - * @param {document} packageDocument OPF XML - * @return {object} parsed package parts - */ - parse(packageDocument){ - var metadataNode, manifestNode, spineNode; + /** + * Parse OPF XML + * @param {document} packageDocument OPF XML + * @return {object} parsed package parts + */ + parse(packageDocument) { + var metadataNode, manifestNode, spineNode; - if(!packageDocument) { - throw new Error("Package File Not Found"); - } + if (!packageDocument) { + throw new Error("Package File Not Found"); + } - metadataNode = qs(packageDocument, "metadata"); - if(!metadataNode) { - throw new Error("No Metadata Found"); - } + metadataNode = qs(packageDocument, "metadata"); + if (!metadataNode) { + throw new Error("No Metadata Found"); + } - manifestNode = qs(packageDocument, "manifest"); - if(!manifestNode) { - throw new Error("No Manifest Found"); - } + manifestNode = qs(packageDocument, "manifest"); + if (!manifestNode) { + throw new Error("No Manifest Found"); + } - spineNode = qs(packageDocument, "spine"); - if(!spineNode) { - throw new Error("No Spine Found"); - } + spineNode = qs(packageDocument, "spine"); + if (!spineNode) { + throw new Error("No Spine Found"); + } - this.manifest = this.parseManifest(manifestNode); - this.navPath = this.findNavPath(manifestNode); - this.ncxPath = this.findNcxPath(manifestNode, spineNode); - this.coverPath = this.findCoverPath(packageDocument); + this.manifest = this.parseManifest(manifestNode); + this.navPath = this.findNavPath(manifestNode); + this.ncxPath = this.findNcxPath(manifestNode, spineNode); + this.coverPath = this.findCoverPath(packageDocument); - this.spineNodeIndex = indexOfElementNode(spineNode); + this.spineNodeIndex = indexOfElementNode(spineNode); - this.spine = this.parseSpine(spineNode, this.manifest); + this.spine = this.parseSpine(spineNode, this.manifest); - this.uniqueIdentifier = this.findUniqueIdentifier(packageDocument); - this.metadata = this.parseMetadata(metadataNode); + this.uniqueIdentifier = this.findUniqueIdentifier(packageDocument); + this.metadata = this.parseMetadata(metadataNode); - this.metadata.direction = spineNode.getAttribute("page-progression-direction"); + this.metadata.direction = spineNode.getAttribute( + "page-progression-direction" + ); - return { - "metadata" : this.metadata, - "spine" : this.spine, - "manifest" : this.manifest, - "navPath" : this.navPath, - "ncxPath" : this.ncxPath, - "coverPath": this.coverPath, - "spineNodeIndex" : this.spineNodeIndex - }; - } + return { + metadata: this.metadata, + spine: this.spine, + manifest: this.manifest, + navPath: this.navPath, + ncxPath: this.ncxPath, + coverPath: this.coverPath, + spineNodeIndex: this.spineNodeIndex, + }; + } - /** - * Parse Metadata - * @private - * @param {node} xml - * @return {object} metadata - */ - parseMetadata(xml){ - var metadata = {}; + /** + * Parse Metadata + * @private + * @param {node} xml + * @return {object} metadata + */ + parseMetadata(xml) { + var metadata = {}; - metadata.title = this.getElementText(xml, "title"); - metadata.creator = this.getElementText(xml, "creator"); - metadata.description = this.getElementText(xml, "description"); + metadata.title = this.getElementText(xml, "title"); + metadata.creator = this.getElementText(xml, "creator"); + metadata.description = this.getElementText(xml, "description"); - metadata.pubdate = this.getElementText(xml, "date"); + metadata.pubdate = this.getElementText(xml, "date"); - metadata.publisher = this.getElementText(xml, "publisher"); + metadata.publisher = this.getElementText(xml, "publisher"); - metadata.identifier = this.getElementText(xml, "identifier"); - metadata.language = this.getElementText(xml, "language"); - metadata.rights = this.getElementText(xml, "rights"); + metadata.identifier = this.getElementText(xml, "identifier"); + metadata.language = this.getElementText(xml, "language"); + metadata.rights = this.getElementText(xml, "rights"); - metadata.modified_date = this.getPropertyText(xml, "dcterms:modified"); + metadata.modified_date = this.getPropertyText(xml, "dcterms:modified"); - metadata.layout = this.getPropertyText(xml, "rendition:layout"); - metadata.orientation = this.getPropertyText(xml, "rendition:orientation"); - metadata.flow = this.getPropertyText(xml, "rendition:flow"); - metadata.viewport = this.getPropertyText(xml, "rendition:viewport"); - metadata.media_active_class = this.getPropertyText(xml, "media:active-class"); - metadata.spread = this.getPropertyText(xml, "rendition:spread"); - // metadata.page_prog_dir = packageXml.querySelector("spine").getAttribute("page-progression-direction"); + metadata.layout = this.getPropertyText(xml, "rendition:layout"); + metadata.orientation = this.getPropertyText(xml, "rendition:orientation"); + metadata.flow = this.getPropertyText(xml, "rendition:flow"); + metadata.viewport = this.getPropertyText(xml, "rendition:viewport"); + metadata.media_active_class = this.getPropertyText( + xml, + "media:active-class" + ); + metadata.spread = this.getPropertyText(xml, "rendition:spread"); + // metadata.page_prog_dir = packageXml.querySelector("spine").getAttribute("page-progression-direction"); - return metadata; - } + return metadata; + } - /** - * Parse Manifest - * @private - * @param {node} manifestXml - * @return {object} manifest - */ - parseManifest(manifestXml){ - var manifest = {}; + /** + * Parse Manifest + * @private + * @param {node} manifestXml + * @return {object} manifest + */ + parseManifest(manifestXml) { + var manifest = {}; - //-- Turn items into an array - // var selected = manifestXml.querySelectorAll("item"); - var selected = qsa(manifestXml, "item"); - var items = Array.prototype.slice.call(selected); + //-- Turn items into an array + // var selected = manifestXml.querySelectorAll("item"); + var selected = qsa(manifestXml, "item"); + var items = Array.prototype.slice.call(selected); - //-- Create an object with the id as key - items.forEach(function(item){ - var id = item.getAttribute("id"), - href = item.getAttribute("href") || "", - type = item.getAttribute("media-type") || "", - overlay = item.getAttribute("media-overlay") || "", - properties = item.getAttribute("properties") || ""; + //-- Create an object with the id as key + items.forEach(function (item) { + var id = item.getAttribute("id"), + href = item.getAttribute("href") || "", + type = item.getAttribute("media-type") || "", + overlay = item.getAttribute("media-overlay") || "", + properties = item.getAttribute("properties") || ""; - manifest[id] = { - "href" : href, - // "url" : href, - "type" : type, - "overlay" : overlay, - "properties" : properties.length ? properties.split(" ") : [] - }; + manifest[id] = { + href: href, + // "url" : href, + type: type, + overlay: overlay, + properties: properties.length ? properties.split(" ") : [], + }; + }); - }); + return manifest; + } - return manifest; + /** + * Parse Spine + * @private + * @param {node} spineXml + * @param {Packaging.manifest} manifest + * @return {object} spine + */ + parseSpine(spineXml, manifest) { + var spine = []; - } + var selected = qsa(spineXml, "itemref"); + var items = Array.prototype.slice.call(selected); - /** - * Parse Spine - * @private - * @param {node} spineXml - * @param {Packaging.manifest} manifest - * @return {object} spine - */ - parseSpine(spineXml, manifest){ - var spine = []; + // var epubcfi = new EpubCFI(); - var selected = qsa(spineXml, "itemref"); - var items = Array.prototype.slice.call(selected); + //-- Add to array to maintain ordering and cross reference with manifest + items.forEach(function (item, index) { + var idref = item.getAttribute("idref"); + // var cfiBase = epubcfi.generateChapterComponent(spineNodeIndex, index, Id); + var props = item.getAttribute("properties") || ""; + var propArray = props.length ? props.split(" ") : []; + // var manifestProps = manifest[Id].properties; + // var manifestPropArray = manifestProps.length ? manifestProps.split(" ") : []; - // var epubcfi = new EpubCFI(); + var itemref = { + id: item.getAttribute("id"), + idref: idref, + linear: item.getAttribute("linear") || "yes", + properties: propArray, + // "href" : manifest[Id].href, + // "url" : manifest[Id].url, + index: index, + // "cfiBase" : cfiBase + }; + spine.push(itemref); + }); - //-- Add to array to maintain ordering and cross reference with manifest - items.forEach(function(item, index){ - var idref = item.getAttribute("idref"); - // var cfiBase = epubcfi.generateChapterComponent(spineNodeIndex, index, Id); - var props = item.getAttribute("properties") || ""; - var propArray = props.length ? props.split(" ") : []; - // var manifestProps = manifest[Id].properties; - // var manifestPropArray = manifestProps.length ? manifestProps.split(" ") : []; + return spine; + } - var itemref = { - "id" : item.getAttribute("id"), - "idref" : idref, - "linear" : item.getAttribute("linear") || "yes", - "properties" : propArray, - // "href" : manifest[Id].href, - // "url" : manifest[Id].url, - "index" : index - // "cfiBase" : cfiBase - }; - spine.push(itemref); - }); + /** + * Find Unique Identifier + * @private + * @param {node} packageXml + * @return {string} Unique Identifier text + */ + findUniqueIdentifier(packageXml) { + var uniqueIdentifierId = + packageXml.documentElement.getAttribute("unique-identifier"); + if (!uniqueIdentifierId) { + return ""; + } + var identifier = packageXml.getElementById(uniqueIdentifierId); + if (!identifier) { + return ""; + } - return spine; - } + if ( + identifier.localName === "identifier" && + identifier.namespaceURI === "http://purl.org/dc/elements/1.1/" + ) { + return identifier.childNodes.length > 0 + ? identifier.childNodes[0].nodeValue.trim() + : ""; + } - /** - * Find Unique Identifier - * @private - * @param {node} packageXml - * @return {string} Unique Identifier text - */ - findUniqueIdentifier(packageXml){ - var uniqueIdentifierId = packageXml.documentElement.getAttribute("unique-identifier"); - if (! uniqueIdentifierId) { - return ""; - } - var identifier = packageXml.getElementById(uniqueIdentifierId); - if (! identifier) { - return ""; - } + return ""; + } - if (identifier.localName === "identifier" && identifier.namespaceURI === "http://purl.org/dc/elements/1.1/") { - return identifier.childNodes.length > 0 ? identifier.childNodes[0].nodeValue.trim() : ""; - } + /** + * Find TOC NAV + * @private + * @param {element} manifestNode + * @return {string} + */ + findNavPath(manifestNode) { + // Find item with property "nav" + // Should catch nav regardless of order + // var node = manifestNode.querySelector("item[properties$='nav'], item[properties^='nav '], item[properties*=' nav ']"); + var node = qsp(manifestNode, "item", { properties: "nav" }); + return node ? node.getAttribute("href") : false; + } - return ""; - } + /** + * Find TOC NCX + * media-type="application/x-dtbncx+xml" href="toc.ncx" + * @private + * @param {element} manifestNode + * @param {element} spineNode + * @return {string} + */ + findNcxPath(manifestNode, spineNode) { + // var node = manifestNode.querySelector("item[media-type='application/x-dtbncx+xml']"); + var node = qsp(manifestNode, "item", { + "media-type": "application/x-dtbncx+xml", + }); + var tocId; - /** - * Find TOC NAV - * @private - * @param {element} manifestNode - * @return {string} - */ - findNavPath(manifestNode){ - // Find item with property "nav" - // Should catch nav regardless of order - // var node = manifestNode.querySelector("item[properties$='nav'], item[properties^='nav '], item[properties*=' nav ']"); - var node = qsp(manifestNode, "item", {"properties":"nav"}); - return node ? node.getAttribute("href") : false; - } + // If we can't find the toc by media-type then try to look for id of the item in the spine attributes as + // according to http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4.1.2, + // "The item that describes the NCX must be referenced by the spine toc attribute." + if (!node) { + tocId = spineNode.getAttribute("toc"); + if (tocId) { + // node = manifestNode.querySelector("item[id='" + tocId + "']"); + node = manifestNode.querySelector(`#${tocId}`); + } + } - /** - * Find TOC NCX - * media-type="application/x-dtbncx+xml" href="toc.ncx" - * @private - * @param {element} manifestNode - * @param {element} spineNode - * @return {string} - */ - findNcxPath(manifestNode, spineNode){ - // var node = manifestNode.querySelector("item[media-type='application/x-dtbncx+xml']"); - var node = qsp(manifestNode, "item", {"media-type":"application/x-dtbncx+xml"}); - var tocId; + return node ? node.getAttribute("href") : false; + } - // If we can't find the toc by media-type then try to look for id of the item in the spine attributes as - // according to http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4.1.2, - // "The item that describes the NCX must be referenced by the spine toc attribute." - if (!node) { - tocId = spineNode.getAttribute("toc"); - if(tocId) { - // node = manifestNode.querySelector("item[id='" + tocId + "']"); - node = manifestNode.querySelector(`#${tocId}`); - } - } + /** + * Find the Cover Path + * + * Fallback for Epub 2.0 + * @private + * @param {node} packageXml + * @return {string} href + */ + findCoverPath(packageXml) { + // Try parsing cover with epub 3. + var node = qsp(packageXml, "item", { properties: "cover-image" }); + if (node) return node.getAttribute("href"); - return node ? node.getAttribute("href") : false; - } + // Fallback to epub 2. + var metaCover = qsp(packageXml, "meta", { name: "cover" }); - /** - * Find the Cover Path - * - * Fallback for Epub 2.0 - * @private - * @param {node} packageXml - * @return {string} href - */ - findCoverPath(packageXml){ - var pkg = qs(packageXml, "package"); - var epubVersion = pkg.getAttribute("version"); - - // Try parsing cover with epub 3. - // var node = packageXml.querySelector("item[properties='cover-image']"); - var node = qsp(packageXml, "item", {"properties":"cover-image"}); - if (node) return node.getAttribute("href"); - - // Fallback to epub 2. - var metaCover = qsp(packageXml, "meta", {"name":"cover"}); + if (metaCover) { + var coverId = metaCover.getAttribute("content"); + var cover = packageXml.getElementById(coverId); + return cover ? cover.getAttribute("href") : ""; + } else { + return false; + } + } - if (metaCover) { - var coverId = metaCover.getAttribute("content"); - // var cover = packageXml.querySelector("item[id='" + coverId + "']"); - var cover = packageXml.getElementById(coverId); - return cover ? cover.getAttribute("href") : ""; - } - else { - return false; - } - } + /** + * Get text of a namespaced element + * @private + * @param {node} xml + * @param {string} tag + * @return {string} text + */ + getElementText(xml, tag) { + var found = xml.getElementsByTagNameNS( + "http://purl.org/dc/elements/1.1/", + tag + ); + var el; - /** - * Get text of a namespaced element - * @private - * @param {node} xml - * @param {string} tag - * @return {string} text - */ - getElementText(xml, tag){ - var found = xml.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", tag); - var el; + if (!found || found.length === 0) return ""; - if(!found || found.length === 0) return ""; + el = found[0]; - el = found[0]; + if (el.childNodes.length) { + return el.childNodes[0].nodeValue; + } - if(el.childNodes.length){ - return el.childNodes[0].nodeValue; - } + return ""; + } - return ""; + /** + * Get text by property + * @private + * @param {node} xml + * @param {string} property + * @return {string} text + */ + getPropertyText(xml, property) { + var el = qsp(xml, "meta", { property: property }); - } + if (el && el.childNodes.length) { + return el.childNodes[0].nodeValue; + } - /** - * Get text by property - * @private - * @param {node} xml - * @param {string} property - * @return {string} text - */ - getPropertyText(xml, property){ - var el = qsp(xml, "meta", {"property":property}); + return ""; + } - if(el && el.childNodes.length){ - return el.childNodes[0].nodeValue; - } + /** + * Load JSON Manifest + * @param {document} packageDocument OPF XML + * @return {object} parsed package parts + */ + load(json) { + this.metadata = json.metadata; - return ""; - } + let spine = json.readingOrder || json.spine; + this.spine = spine.map((item, index) => { + item.index = index; + item.linear = item.linear || "yes"; + return item; + }); - /** - * Load JSON Manifest - * @param {document} packageDocument OPF XML - * @return {object} parsed package parts - */ - load(json) { - this.metadata = json.metadata; + json.resources.forEach((item, index) => { + this.manifest[index] = item; - let spine = json.readingOrder || json.spine; - this.spine = spine.map((item, index) =>{ - item.index = index; - item.linear = item.linear || "yes"; - return item; - }); + if (item.rel && item.rel[0] === "cover") { + this.coverPath = item.href; + } + }); - json.resources.forEach((item, index) => { - this.manifest[index] = item; + this.spineNodeIndex = 0; - if (item.rel && item.rel[0] === "cover") { - this.coverPath = item.href; - } - }); + this.toc = json.toc.map((item, index) => { + item.label = item.title; + return item; + }); - this.spineNodeIndex = 0; + return { + metadata: this.metadata, + spine: this.spine, + manifest: this.manifest, + navPath: this.navPath, + ncxPath: this.ncxPath, + coverPath: this.coverPath, + spineNodeIndex: this.spineNodeIndex, + toc: this.toc, + }; + } - this.toc = json.toc.map((item, index) =>{ - item.label = item.title; - return item; - }); - - return { - "metadata" : this.metadata, - "spine" : this.spine, - "manifest" : this.manifest, - "navPath" : this.navPath, - "ncxPath" : this.ncxPath, - "coverPath": this.coverPath, - "spineNodeIndex" : this.spineNodeIndex, - "toc" : this.toc - }; - } - - destroy() { - this.manifest = undefined; - this.navPath = undefined; - this.ncxPath = undefined; - this.coverPath = undefined; - this.spineNodeIndex = undefined; - this.spine = undefined; - this.metadata = undefined; - } + destroy() { + this.manifest = undefined; + this.navPath = undefined; + this.ncxPath = undefined; + this.coverPath = undefined; + this.spineNodeIndex = undefined; + this.spine = undefined; + this.metadata = undefined; + } } export default Packaging; diff --git a/src/pagelist.js b/src/pagelist.js index 6de82f6..c2b2e35 100644 --- a/src/pagelist.js +++ b/src/pagelist.js @@ -1,10 +1,10 @@ import EpubCFI from "./epubcfi"; import { - qs, - qsa, - querySelectorByType, - indexOfSorted, - locationOf + indexOfSorted, + locationOf, + qs, + qsa, + querySelectorByType, } from "./utils/core"; /** @@ -12,263 +12,261 @@ import { * @param {document} [xml] */ class PageList { - constructor(xml) { - this.pages = []; - this.locations = []; - this.epubcfi = new EpubCFI(); + constructor(xml) { + this.pages = []; + this.locations = []; + this.epubcfi = new EpubCFI(); - this.firstPage = 0; - this.lastPage = 0; - this.totalPages = 0; + this.firstPage = 0; + this.lastPage = 0; + this.totalPages = 0; - this.toc = undefined; - this.ncx = undefined; + this.toc = undefined; + this.ncx = undefined; - if (xml) { - this.pageList = this.parse(xml); - } + if (xml) { + this.pageList = this.parse(xml); + } - if(this.pageList && this.pageList.length) { - this.process(this.pageList); - } - } + if (this.pageList && this.pageList.length) { + this.process(this.pageList); + } + } - /** - * Parse PageList Xml - * @param {document} xml - */ - parse(xml) { - var html = qs(xml, "html"); - var ncx = qs(xml, "ncx"); + /** + * Parse PageList Xml + * @param {document} xml + */ + parse(xml) { + var html = qs(xml, "html"); + var ncx = qs(xml, "ncx"); - if(html) { - return this.parseNav(xml); - } else if(ncx){ - return this.parseNcx(xml); - } + if (html) { + return this.parseNav(xml); + } else if (ncx) { + return this.parseNcx(xml); + } + } - } + /** + * Parse a Nav PageList + * @private + * @param {node} navHtml + * @return {PageList.item[]} list + */ + parseNav(navHtml) { + var navElement = querySelectorByType(navHtml, "nav", "page-list"); + var navItems = navElement ? qsa(navElement, "li") : []; + var length = navItems.length; + var i; + var list = []; + var item; - /** - * Parse a Nav PageList - * @private - * @param {node} navHtml - * @return {PageList.item[]} list - */ - parseNav(navHtml){ - var navElement = querySelectorByType(navHtml, "nav", "page-list"); - var navItems = navElement ? qsa(navElement, "li") : []; - var length = navItems.length; - var i; - var list = []; - var item; + if (!navItems || length === 0) return list; - if(!navItems || length === 0) return list; + for (i = 0; i < length; ++i) { + item = this.item(navItems[i]); + list.push(item); + } - for (i = 0; i < length; ++i) { - item = this.item(navItems[i]); - list.push(item); - } + return list; + } - return list; - } + parseNcx(navXml) { + var list = []; + var i = 0; + var item; + var pageList; + var pageTargets; + var length = 0; - parseNcx(navXml) { - var list = []; - var i = 0; - var item; - var pageList; - var pageTargets; - var length = 0; + pageList = qs(navXml, "pageList"); + if (!pageList) return list; - pageList = qs(navXml, "pageList"); - if (!pageList) return list; + pageTargets = qsa(pageList, "pageTarget"); + length = pageTargets.length; - pageTargets = qsa(pageList, "pageTarget"); - length = pageTargets.length; + if (!pageTargets || pageTargets.length === 0) { + return list; + } - if (!pageTargets || pageTargets.length === 0) { - return list; - } + for (i = 0; i < length; ++i) { + item = this.ncxItem(pageTargets[i]); + list.push(item); + } - for (i = 0; i < length; ++i) { - item = this.ncxItem(pageTargets[i]); - list.push(item); - } + return list; + } - return list; - } + ncxItem(item) { + var navLabel = qs(item, "navLabel"); + var navLabelText = qs(navLabel, "text"); + var pageText = navLabelText.textContent; + var content = qs(item, "content"); - ncxItem(item) { - var navLabel = qs(item, "navLabel"); - var navLabelText = qs(navLabel, "text"); - var pageText = navLabelText.textContent; - var content = qs(item, "content"); + var href = content.getAttribute("src"); + var page = parseInt(pageText, 10); - var href = content.getAttribute("src"); - var page = parseInt(pageText, 10); + return { + href: href, + page: page, + }; + } - return { - "href": href, - "page": page, - }; - } + /** + * Page List Item + * @private + * @param {node} item + * @return {object} pageListItem + */ + item(item) { + var content = qs(item, "a"), + href = content.getAttribute("href") || "", + text = content.textContent || "", + page = parseInt(text), + isCfi = href.indexOf("epubcfi"), + split, + packageUrl, + cfi; - /** - * Page List Item - * @private - * @param {node} item - * @return {object} pageListItem - */ - item(item){ - var content = qs(item, "a"), - href = content.getAttribute("href") || "", - text = content.textContent || "", - page = parseInt(text), - isCfi = href.indexOf("epubcfi"), - split, - packageUrl, - cfi; + if (isCfi != -1) { + split = href.split("#"); + packageUrl = split[0]; + cfi = split.length > 1 ? split[1] : false; + return { + cfi: cfi, + href: href, + packageUrl: packageUrl, + page: page, + }; + } else { + return { + href: href, + page: page, + }; + } + } - if(isCfi != -1) { - split = href.split("#"); - packageUrl = split[0]; - cfi = split.length > 1 ? split[1] : false; - return { - "cfi" : cfi, - "href" : href, - "packageUrl" : packageUrl, - "page" : page - }; - } else { - return { - "href" : href, - "page" : page - }; - } - } + /** + * Process pageList items + * @private + * @param {array} pageList + */ + process(pageList) { + pageList.forEach(function (item) { + this.pages.push(item.page); + if (item.cfi) { + this.locations.push(item.cfi); + } + }, this); + this.firstPage = parseInt(this.pages[0]); + this.lastPage = parseInt(this.pages[this.pages.length - 1]); + this.totalPages = this.lastPage - this.firstPage; + } - /** - * Process pageList items - * @private - * @param {array} pageList - */ - process(pageList){ - pageList.forEach(function(item){ - this.pages.push(item.page); - if (item.cfi) { - this.locations.push(item.cfi); - } - }, this); - this.firstPage = parseInt(this.pages[0]); - this.lastPage = parseInt(this.pages[this.pages.length-1]); - this.totalPages = this.lastPage - this.firstPage; - } + /** + * Get a PageList result from a EpubCFI + * @param {string} cfi EpubCFI String + * @return {number} page + */ + pageFromCfi(cfi) { + var pg = -1; - /** - * Get a PageList result from a EpubCFI - * @param {string} cfi EpubCFI String - * @return {number} page - */ - pageFromCfi(cfi){ - var pg = -1; + // Check if the pageList has not been set yet + if (this.locations.length === 0) { + return -1; + } - // Check if the pageList has not been set yet - if(this.locations.length === 0) { - return -1; - } + // TODO: check if CFI is valid? - // TODO: check if CFI is valid? + // check if the cfi is in the location list + // var index = this.locations.indexOf(cfi); + var index = indexOfSorted(cfi, this.locations, this.epubcfi.compare); + if (index != -1) { + pg = this.pages[index]; + } else { + // Otherwise add it to the list of locations + // Insert it in the correct position in the locations page + //index = EPUBJS.core.insert(cfi, this.locations, this.epubcfi.compare); + index = locationOf(cfi, this.locations, this.epubcfi.compare); + // Get the page at the location just before the new one, or return the first + pg = index - 1 >= 0 ? this.pages[index - 1] : this.pages[0]; + if (pg !== undefined) { + // Add the new page in so that the locations and page array match up + //this.pages.splice(index, 0, pg); + } else { + pg = -1; + } + } + return pg; + } - // check if the cfi is in the location list - // var index = this.locations.indexOf(cfi); - var index = indexOfSorted(cfi, this.locations, this.epubcfi.compare); - if(index != -1) { - pg = this.pages[index]; - } else { - // Otherwise add it to the list of locations - // Insert it in the correct position in the locations page - //index = EPUBJS.core.insert(cfi, this.locations, this.epubcfi.compare); - index = locationOf(cfi, this.locations, this.epubcfi.compare); - // Get the page at the location just before the new one, or return the first - pg = index-1 >= 0 ? this.pages[index-1] : this.pages[0]; - if(pg !== undefined) { - // Add the new page in so that the locations and page array match up - //this.pages.splice(index, 0, pg); - } else { - pg = -1; - } + /** + * Get an EpubCFI from a Page List Item + * @param {string | number} pg + * @return {string} cfi + */ + cfiFromPage(pg) { + var cfi = -1; + // check that pg is an int + if (typeof pg != "number") { + pg = parseInt(pg); + } - } - return pg; - } + // check if the cfi is in the page list + // Pages could be unsorted. + var index = this.pages.indexOf(pg); + if (index != -1) { + cfi = this.locations[index]; + } + // TODO: handle pages not in the list + return cfi; + } - /** - * Get an EpubCFI from a Page List Item - * @param {string | number} pg - * @return {string} cfi - */ - cfiFromPage(pg){ - var cfi = -1; - // check that pg is an int - if(typeof pg != "number"){ - pg = parseInt(pg); - } + /** + * Get a Page from Book percentage + * @param {number} percent + * @return {number} page + */ + pageFromPercentage(percent) { + var pg = Math.round(this.totalPages * percent); + return pg; + } - // check if the cfi is in the page list - // Pages could be unsorted. - var index = this.pages.indexOf(pg); - if(index != -1) { - cfi = this.locations[index]; - } - // TODO: handle pages not in the list - return cfi; - } + /** + * Returns a value between 0 - 1 corresponding to the location of a page + * @param {number} pg the page + * @return {number} percentage + */ + percentageFromPage(pg) { + var percentage = (pg - this.firstPage) / this.totalPages; + return Math.round(percentage * 1000) / 1000; + } - /** - * Get a Page from Book percentage - * @param {number} percent - * @return {number} page - */ - pageFromPercentage(percent){ - var pg = Math.round(this.totalPages * percent); - return pg; - } + /** + * Returns a value between 0 - 1 corresponding to the location of a cfi + * @param {string} cfi EpubCFI String + * @return {number} percentage + */ + percentageFromCfi(cfi) { + var pg = this.pageFromCfi(cfi); + var percentage = this.percentageFromPage(pg); + return percentage; + } - /** - * Returns a value between 0 - 1 corresponding to the location of a page - * @param {number} pg the page - * @return {number} percentage - */ - percentageFromPage(pg){ - var percentage = (pg - this.firstPage) / this.totalPages; - return Math.round(percentage * 1000) / 1000; - } + /** + * Destroy + */ + destroy() { + this.pages = undefined; + this.locations = undefined; + this.epubcfi = undefined; - /** - * Returns a value between 0 - 1 corresponding to the location of a cfi - * @param {string} cfi EpubCFI String - * @return {number} percentage - */ - percentageFromCfi(cfi){ - var pg = this.pageFromCfi(cfi); - var percentage = this.percentageFromPage(pg); - return percentage; - } + this.pageList = undefined; - /** - * Destroy - */ - destroy() { - this.pages = undefined; - this.locations = undefined; - this.epubcfi = undefined; - - this.pageList = undefined; - - this.toc = undefined; - this.ncx = undefined; - } + this.toc = undefined; + this.ncx = undefined; + } } export default PageList; diff --git a/src/rendition.js b/src/rendition.js index 14a21b4..6699d82 100644 --- a/src/rendition.js +++ b/src/rendition.js @@ -1,21 +1,15 @@ import EventEmitter from "event-emitter"; -import { extend, defer, isFloat } from "./utils/core"; -import Hook from "./utils/hook"; -import EpubCFI from "./epubcfi"; -import Queue from "./utils/queue"; -import Layout from "./layout"; -// import Mapping from "./mapping"; -import Themes from "./themes"; -import Contents from "./contents"; import Annotations from "./annotations"; -import { EVENTS, DOM_EVENTS } from "./utils/constants"; - -// Default Views -import IframeView from "./managers/views/iframe"; - -// Default View Managers -import DefaultViewManager from "./managers/default/index"; +import EpubCFI from "./epubcfi"; +import Layout from "./layout"; import ContinuousViewManager from "./managers/continuous/index"; +import DefaultViewManager from "./managers/default/index"; +import IframeView from "./managers/views/iframe"; +import Themes from "./themes"; +import { DOM_EVENTS, EVENTS } from "./utils/constants"; +import { defer, extend, isFloat } from "./utils/core"; +import Hook from "./utils/hook"; +import Queue from "./utils/queue"; /** * Displays an Epub as a series of Views for each Section. @@ -41,1026 +35,1025 @@ import ContinuousViewManager from "./managers/continuous/index"; * @param {boolean} [options.allowPopups=false] enable opening popup in content */ class Rendition { - constructor(book, options) { - - this.settings = extend(this.settings || {}, { - width: null, - height: null, - ignoreClass: "", - manager: "default", - view: "iframe", - flow: null, - layout: null, - spread: null, - minSpreadWidth: 800, - stylesheet: null, - resizeOnOrientationChange: true, - script: null, - snap: false, - defaultDirection: "ltr", - allowScriptedContent: false, - allowPopups: false - }); - - extend(this.settings, options); - - if (typeof(this.settings.manager) === "object") { - this.manager = this.settings.manager; - } - - this.book = book; - - /** - * Adds Hook methods to the Rendition prototype - * @member {object} hooks - * @property {Hook} hooks.content - * @memberof Rendition - */ - this.hooks = {}; - this.hooks.display = new Hook(this); - this.hooks.serialize = new Hook(this); - this.hooks.content = new Hook(this); - this.hooks.unloaded = new Hook(this); - this.hooks.layout = new Hook(this); - this.hooks.render = new Hook(this); - this.hooks.show = new Hook(this); - - this.hooks.content.register(this.handleLinks.bind(this)); - this.hooks.content.register(this.passEvents.bind(this)); - this.hooks.content.register(this.adjustImages.bind(this)); - - this.book.spine.hooks.content.register(this.injectIdentifier.bind(this)); - - if (this.settings.stylesheet) { - this.book.spine.hooks.content.register(this.injectStylesheet.bind(this)); - } - - if (this.settings.script) { - this.book.spine.hooks.content.register(this.injectScript.bind(this)); - } - - /** - * @member {Themes} themes - * @memberof Rendition - */ - this.themes = new Themes(this); - - /** - * @member {Annotations} annotations - * @memberof Rendition - */ - this.annotations = new Annotations(this); - - this.epubcfi = new EpubCFI(); - - this.q = new Queue(this); - - /** - * A Rendered Location Range - * @typedef location - * @type {Object} - * @property {object} start - * @property {string} start.index - * @property {string} start.href - * @property {object} start.displayed - * @property {EpubCFI} start.cfi - * @property {number} start.location - * @property {number} start.percentage - * @property {number} start.displayed.page - * @property {number} start.displayed.total - * @property {object} end - * @property {string} end.index - * @property {string} end.href - * @property {object} end.displayed - * @property {EpubCFI} end.cfi - * @property {number} end.location - * @property {number} end.percentage - * @property {number} end.displayed.page - * @property {number} end.displayed.total - * @property {boolean} atStart - * @property {boolean} atEnd - * @memberof Rendition - */ - this.location = undefined; - - // Hold queue until book is opened - this.q.enqueue(this.book.opened); - - this.starting = new defer(); - /** - * @member {promise} started returns after the rendition has started - * @memberof Rendition - */ - this.started = this.starting.promise; - - // Block the queue until rendering is started - this.q.enqueue(this.start); - } - - /** - * Set the manager function - * @param {function} manager - */ - setManager(manager) { - this.manager = manager; - } - - /** - * Require the manager from passed string, or as a class function - * @param {string|object} manager [description] - * @return {method} - */ - requireManager(manager) { - var viewManager; - - // If manager is a string, try to load from imported managers - if (typeof manager === "string" && manager === "default") { - viewManager = DefaultViewManager; - } else if (typeof manager === "string" && manager === "continuous") { - viewManager = ContinuousViewManager; - } else { - // otherwise, assume we were passed a class function - viewManager = manager; - } - - return viewManager; - } - - /** - * Require the view from passed string, or as a class function - * @param {string|object} view - * @return {view} - */ - requireView(view) { - var View; - - // If view is a string, try to load from imported views, - if (typeof view == "string" && view === "iframe") { - View = IframeView; - } else { - // otherwise, assume we were passed a class function - View = view; - } - - return View; - } - - /** - * Start the rendering - * @return {Promise} rendering has started - */ - start(){ - if (!this.settings.layout && (this.book.package.metadata.layout === "pre-paginated" || this.book.displayOptions.fixedLayout === "true")) { - this.settings.layout = "pre-paginated"; - } - switch(this.book.package.metadata.spread) { - case 'none': - this.settings.spread = 'none'; - break; - case 'both': - this.settings.spread = true; - break; - } - - if(!this.manager) { - this.ViewManager = this.requireManager(this.settings.manager); - this.View = this.requireView(this.settings.view); - - this.manager = new this.ViewManager({ - view: this.View, - queue: this.q, - request: this.book.load.bind(this.book), - settings: this.settings - }); - } - - this.direction(this.book.package.metadata.direction || this.settings.defaultDirection); - - // Parse metadata to get layout props - this.settings.globalLayoutProperties = this.determineLayoutProperties(this.book.package.metadata); - - this.flow(this.settings.globalLayoutProperties.flow); - - this.layout(this.settings.globalLayoutProperties); - - // Listen for displayed views - this.manager.on(EVENTS.MANAGERS.ADDED, this.afterDisplayed.bind(this)); - this.manager.on(EVENTS.MANAGERS.REMOVED, this.afterRemoved.bind(this)); - - // Listen for resizing - this.manager.on(EVENTS.MANAGERS.RESIZED, this.onResized.bind(this)); - - // Listen for rotation - this.manager.on(EVENTS.MANAGERS.ORIENTATION_CHANGE, this.onOrientationChange.bind(this)); - - // Listen for scroll changes - this.manager.on(EVENTS.MANAGERS.SCROLLED, this.reportLocation.bind(this)); - - /** - * Emit that rendering has started - * @event started - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.STARTED); - - // Start processing queue - this.starting.resolve(); - } - - /** - * Call to attach the container to an element in the dom - * Container must be attached before rendering can begin - * @param {element} element to attach to - * @return {Promise} - */ - attachTo(element){ - - return this.q.enqueue(function () { - - // Start rendering - this.manager.render(element, { - "width" : this.settings.width, - "height" : this.settings.height - }); - - /** - * Emit that rendering has attached to an element - * @event attached - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.ATTACHED); - - }.bind(this)); - - } - - /** - * Display a point in the book - * The request will be added to the rendering Queue, - * so it will wait until book is opened, rendering started - * and all other rendering tasks have finished to be called. - * @param {string} target Url or EpubCFI - * @return {Promise} - */ - display(target){ - if (this.displaying) { - this.displaying.resolve(); - } - return this.q.enqueue(this._display, target); - } - - /** - * Tells the manager what to display immediately - * @private - * @param {string} target Url or EpubCFI - * @return {Promise} - */ - _display(target){ - if (!this.book) { - return; - } - var isCfiString = this.epubcfi.isCfiString(target); - var displaying = new defer(); - var displayed = displaying.promise; - var section; - var moveTo; - - this.displaying = displaying; - - // Check if this is a book percentage - if (this.book.locations.length() && isFloat(target)) { - target = this.book.locations.cfiFromPercentage(parseFloat(target)); - } - - section = this.book.spine.get(target); - - if(!section){ - displaying.reject(new Error("No Section Found")); - return displayed; - } - - this.manager.display(section, target) - .then(() => { - displaying.resolve(section); - this.displaying = undefined; - - /** - * Emit that a section has been displayed - * @event displayed - * @param {Section} section - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.DISPLAYED, section); - this.reportLocation(); - }, (err) => { - /** - * Emit that has been an error displaying - * @event displayError - * @param {Section} section - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.DISPLAY_ERROR, err); - }); - - return displayed; - } - - /* - render(view, show) { - - // view.onLayout = this.layout.format.bind(this.layout); - view.create(); - - // Fit to size of the container, apply padding - this.manager.resizeView(view); - - // Render Chain - return view.section.render(this.book.request) - .then(function(contents){ - return view.load(contents); - }.bind(this)) - .then(function(doc){ - return this.hooks.content.trigger(view, this); - }.bind(this)) - .then(function(){ - this.layout.format(view.contents); - return this.hooks.layout.trigger(view, this); - }.bind(this)) - .then(function(){ - return view.display(); - }.bind(this)) - .then(function(){ - return this.hooks.render.trigger(view, this); - }.bind(this)) - .then(function(){ - if(show !== false) { - this.q.enqueue(function(view){ - view.show(); - }, view); - } - // this.map = new Map(view, this.layout); - this.hooks.show.trigger(view, this); - this.trigger("rendered", view.section); - - }.bind(this)) - .catch(function(e){ - this.trigger("loaderror", e); - }.bind(this)); - - } - */ - - /** - * Report what section has been displayed - * @private - * @param {*} view - */ - afterDisplayed(view){ - - view.on(EVENTS.VIEWS.MARK_CLICKED, (cfiRange, data) => this.triggerMarkEvent(cfiRange, data, view.contents)); - - this.hooks.render.trigger(view, this) - .then(() => { - if (view.contents) { - this.hooks.content.trigger(view.contents, this).then(() => { - /** - * Emit that a section has been rendered - * @event rendered - * @param {Section} section - * @param {View} view - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.RENDERED, view.section, view); - }); - } else { - this.emit(EVENTS.RENDITION.RENDERED, view.section, view); - } - }); - - } - - /** - * Report what has been removed - * @private - * @param {*} view - */ - afterRemoved(view){ - this.hooks.unloaded.trigger(view, this).then(() => { - /** - * Emit that a section has been removed - * @event removed - * @param {Section} section - * @param {View} view - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.REMOVED, view.section, view); - }); - } - - /** - * Report resize events and display the last seen location - * @private - */ - onResized(size, epubcfi){ - - /** - * Emit that the rendition has been resized - * @event resized - * @param {number} width - * @param {height} height - * @param {string} epubcfi (optional) - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.RESIZED, { - width: size.width, - height: size.height - }, epubcfi); - - if (this.location && this.location.start) { - this.display(epubcfi || this.location.start.cfi); - } - - } - - /** - * Report orientation events and display the last seen location - * @private - */ - onOrientationChange(orientation){ - /** - * Emit that the rendition has been rotated - * @event orientationchange - * @param {string} orientation - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.ORIENTATION_CHANGE, orientation); - } - - /** - * Move the Rendition to a specific offset - * Usually you would be better off calling display() - * @param {object} offset - */ - moveTo(offset){ - this.manager.moveTo(offset); - } - - /** - * Trigger a resize of the views - * @param {number} [width] - * @param {number} [height] - * @param {string} [epubcfi] (optional) - */ - resize(width, height, epubcfi){ - if (width) { - this.settings.width = width; - } - if (height) { - this.settings.height = height; - } - this.manager.resize(width, height, epubcfi); - } - - /** - * Clear all rendered views - */ - clear(){ - this.manager.clear(); - } - - /** - * Go to the next "page" in the rendition - * @return {Promise} - */ - next(){ - return this.q.enqueue(this.manager.next.bind(this.manager)) - .then(this.reportLocation.bind(this)); - } - - /** - * Go to the previous "page" in the rendition - * @return {Promise} - */ - prev(){ - return this.q.enqueue(this.manager.prev.bind(this.manager)) - .then(this.reportLocation.bind(this)); - } - - //-- http://www.idpf.org/epub/301/spec/epub-publications.html#meta-properties-rendering - /** - * Determine the Layout properties from metadata and settings - * @private - * @param {object} metadata - * @return {object} properties - */ - determineLayoutProperties(metadata){ - var properties; - var layout = this.settings.layout || metadata.layout || "reflowable"; - var spread = this.settings.spread || metadata.spread || "auto"; - var orientation = this.settings.orientation || metadata.orientation || "auto"; - var flow = this.settings.flow || metadata.flow || "auto"; - var viewport = metadata.viewport || ""; - var minSpreadWidth = this.settings.minSpreadWidth || metadata.minSpreadWidth || 800; - var direction = this.settings.direction || metadata.direction || "ltr"; - - if ((this.settings.width === 0 || this.settings.width > 0) && - (this.settings.height === 0 || this.settings.height > 0)) { - // viewport = "width="+this.settings.width+", height="+this.settings.height+""; - } - - properties = { - layout : layout, - spread : spread, - orientation : orientation, - flow : flow, - viewport : viewport, - minSpreadWidth : minSpreadWidth, - direction: direction - }; - - return properties; - } - - /** - * Adjust the flow of the rendition to paginated or scrolled - * (scrolled-continuous vs scrolled-doc are handled by different view managers) - * @param {string} flow - */ - flow(flow){ - var _flow = flow; - if (flow === "scrolled" || - flow === "scrolled-doc" || - flow === "scrolled-continuous") { - _flow = "scrolled"; - } - - if (flow === "auto" || flow === "paginated") { - _flow = "paginated"; - } - - this.settings.flow = flow; - - if (this._layout) { - this._layout.flow(_flow); - } - - if (this.manager && this._layout) { - this.manager.applyLayout(this._layout); - } - - if (this.manager) { - this.manager.updateFlow(_flow); - } - - if (this.manager && this.manager.isRendered() && this.location) { - this.manager.clear(); - this.display(this.location.start.cfi); - } - } - - /** - * Adjust the layout of the rendition to reflowable or pre-paginated - * @param {object} settings - */ - layout(settings){ - if (settings) { - this._layout = new Layout(settings); - this._layout.spread(settings.spread, this.settings.minSpreadWidth); - - // this.mapping = new Mapping(this._layout.props); - - this._layout.on(EVENTS.LAYOUT.UPDATED, (props, changed) => { - this.emit(EVENTS.RENDITION.LAYOUT, props, changed); - }) - } - - if (this.manager && this._layout) { - this.manager.applyLayout(this._layout); - } - - return this._layout; - } - - /** - * Adjust if the rendition uses spreads - * @param {string} spread none | auto (TODO: implement landscape, portrait, both) - * @param {int} [min] min width to use spreads at - */ - spread(spread, min){ - - this.settings.spread = spread; - - if (min) { - this.settings.minSpreadWidth = min; - } - - if (this._layout) { - this._layout.spread(spread, min); - } - - if (this.manager && this.manager.isRendered()) { - this.manager.updateLayout(); - } - } - - /** - * Adjust the direction of the rendition - * @param {string} dir - */ - direction(dir){ - - this.settings.direction = dir || "ltr"; - - if (this.manager) { - this.manager.direction(this.settings.direction); - } - - if (this.manager && this.manager.isRendered() && this.location) { - this.manager.clear(); - this.display(this.location.start.cfi); - } - } - - /** - * Report the current location - * @fires relocated - * @fires locationChanged - */ - reportLocation(){ - return this.q.enqueue(function reportedLocation(){ - requestAnimationFrame(function reportedLocationAfterRAF() { - var location = this.manager.currentLocation(); - if (location && location.then && typeof location.then === "function") { - location.then(function(result) { - let located = this.located(result); - - if (!located || !located.start || !located.end) { - return; - } - - this.location = located; - - this.emit(EVENTS.RENDITION.LOCATION_CHANGED, { - index: this.location.start.index, - href: this.location.start.href, - start: this.location.start.cfi, - end: this.location.end.cfi, - percentage: this.location.start.percentage - }); - - this.emit(EVENTS.RENDITION.RELOCATED, this.location); - }.bind(this)); - } else if (location) { - let located = this.located(location); - - if (!located || !located.start || !located.end) { - return; - } - - this.location = located; - - /** - * @event locationChanged - * @deprecated - * @type {object} - * @property {number} index - * @property {string} href - * @property {EpubCFI} start - * @property {EpubCFI} end - * @property {number} percentage - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.LOCATION_CHANGED, { - index: this.location.start.index, - href: this.location.start.href, - start: this.location.start.cfi, - end: this.location.end.cfi, - percentage: this.location.start.percentage - }); - - /** - * @event relocated - * @type {displayedLocation} - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.RELOCATED, this.location); - } - }.bind(this)); - }.bind(this)); - } - - /** - * Get the Current Location object - * @return {displayedLocation | promise} location (may be a promise) - */ - currentLocation(){ - var location = this.manager.currentLocation(); - if (location && location.then && typeof location.then === "function") { - location.then(function(result) { - let located = this.located(result); - return located; - }.bind(this)); - } else if (location) { - let located = this.located(location); - return located; - } - } - - /** - * Creates a Rendition#locationRange from location - * passed by the Manager - * @returns {displayedLocation} - * @private - */ - located(location){ - if (!location.length) { - return {}; - } - let start = location[0]; - let end = location[location.length-1]; - - let located = { - start: { - index: start.index, - href: start.href, - cfi: start.mapping.start, - displayed: { - page: start.pages[0] || 1, - total: start.totalPages - } - }, - end: { - index: end.index, - href: end.href, - cfi: end.mapping.end, - displayed: { - page: end.pages[end.pages.length-1] || 1, - total: end.totalPages - } - } - }; - - let locationStart = this.book.locations.locationFromCfi(start.mapping.start); - let locationEnd = this.book.locations.locationFromCfi(end.mapping.end); - - if (locationStart != null) { - located.start.location = locationStart; - located.start.percentage = this.book.locations.percentageFromLocation(locationStart); - } - if (locationEnd != null) { - located.end.location = locationEnd; - located.end.percentage = this.book.locations.percentageFromLocation(locationEnd); - } - - let pageStart = this.book.pageList.pageFromCfi(start.mapping.start); - let pageEnd = this.book.pageList.pageFromCfi(end.mapping.end); - - if (pageStart != -1) { - located.start.page = pageStart; - } - if (pageEnd != -1) { - located.end.page = pageEnd; - } - - if (end.index === this.book.spine.last().index && - located.end.displayed.page >= located.end.displayed.total) { - located.atEnd = true; - } - - if (start.index === this.book.spine.first().index && - located.start.displayed.page === 1) { - located.atStart = true; - } - - return located; - } - - /** - * Remove and Clean Up the Rendition - */ - destroy(){ - // Clear the queue - // this.q.clear(); - // this.q = undefined; - - this.manager && this.manager.destroy(); - - this.book = undefined; - - // this.views = null; - - // this.hooks.display.clear(); - // this.hooks.serialize.clear(); - // this.hooks.content.clear(); - // this.hooks.layout.clear(); - // this.hooks.render.clear(); - // this.hooks.show.clear(); - // this.hooks = {}; - - // this.themes.destroy(); - // this.themes = undefined; - - // this.epubcfi = undefined; - - // this.starting = undefined; - // this.started = undefined; - - - } - - /** - * Pass the events from a view's Contents - * @private - * @param {Contents} view contents - */ - passEvents(contents){ - DOM_EVENTS.forEach((e) => { - contents.on(e, (ev) => this.triggerViewEvent(ev, contents)); - }); - - contents.on(EVENTS.CONTENTS.SELECTED, (e) => this.triggerSelectedEvent(e, contents)); - } - - /** - * Emit events passed by a view - * @private - * @param {event} e - */ - triggerViewEvent(e, contents){ - this.emit(e.type, e, contents); - } - - /** - * Emit a selection event's CFI Range passed from a a view - * @private - * @param {string} cfirange - */ - triggerSelectedEvent(cfirange, contents){ - /** - * Emit that a text selection has occurred - * @event selected - * @param {string} cfirange - * @param {Contents} contents - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.SELECTED, cfirange, contents); - } - - /** - * Emit a markClicked event with the cfiRange and data from a mark - * @private - * @param {EpubCFI} cfirange - */ - triggerMarkEvent(cfiRange, data, contents){ - /** - * Emit that a mark was clicked - * @event markClicked - * @param {EpubCFI} cfirange - * @param {object} data - * @param {Contents} contents - * @memberof Rendition - */ - this.emit(EVENTS.RENDITION.MARK_CLICKED, cfiRange, data, contents); - } - - /** - * Get a Range from a Visible CFI - * @param {string} cfi EpubCfi String - * @param {string} ignoreClass - * @return {range} - */ - getRange(cfi, ignoreClass){ - var _cfi = new EpubCFI(cfi); - var found = this.manager.visible().filter(function (view) { - if(_cfi.spinePos === view.index) return true; - }); - - // Should only every return 1 item - if (found.length) { - return found[0].contents.range(_cfi, ignoreClass); - } - } - - /** - * Hook to adjust images to fit in columns - * @param {Contents} contents - * @private - */ - adjustImages(contents) { - - if (this._layout.name === "pre-paginated") { - return new Promise(function(resolve){ - resolve(); - }); - } - - let computed = contents.window.getComputedStyle(contents.content, null); - let height = (contents.content.offsetHeight - (parseFloat(computed.paddingTop) + parseFloat(computed.paddingBottom))) * .95; - let horizontalPadding = parseFloat(computed.paddingLeft) + parseFloat(computed.paddingRight); - - contents.addStylesheetRules({ - "img" : { - "max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important", - "max-height": height + "px" + "!important", - "object-fit": "contain", - "page-break-inside": "avoid", - "break-inside": "avoid", - "box-sizing": "border-box" - }, - "svg" : { - "max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important", - "max-height": height + "px" + "!important", - "page-break-inside": "avoid", - "break-inside": "avoid" - } - }); - - return new Promise(function(resolve, reject){ - // Wait to apply - setTimeout(function() { - resolve(); - }, 1); - }); - } - - /** - * Get the Contents object of each rendered view - * @returns {Contents[]} - */ - getContents () { - return this.manager ? this.manager.getContents() : []; - } - - /** - * Get the views member from the manager - * @returns {Views} - */ - views () { - let views = this.manager ? this.manager.views : undefined; - return views || []; - } - - /** - * Hook to handle link clicks in rendered content - * @param {Contents} contents - * @private - */ - handleLinks(contents) { - if (contents) { - contents.on(EVENTS.CONTENTS.LINK_CLICKED, (href) => { - let relative = this.book.path.relative(href); - this.display(relative); - }); - } - } - - /** - * Hook to handle injecting stylesheet before - * a Section is serialized - * @param {document} doc - * @param {Section} section - * @private - */ - injectStylesheet(doc, section) { - let style = doc.createElement("link"); - style.setAttribute("type", "text/css"); - style.setAttribute("rel", "stylesheet"); - style.setAttribute("href", this.settings.stylesheet); - doc.getElementsByTagName("head")[0].appendChild(style); - } - - /** - * Hook to handle injecting scripts before - * a Section is serialized - * @param {document} doc - * @param {Section} section - * @private - */ - injectScript(doc, section) { - let script = doc.createElement("script"); - script.setAttribute("type", "text/javascript"); - script.setAttribute("src", this.settings.script); - script.textContent = " "; // Needed to prevent self closing tag - doc.getElementsByTagName("head")[0].appendChild(script); - } - - /** - * Hook to handle the document identifier before - * a Section is serialized - * @param {document} doc - * @param {Section} section - * @private - */ - injectIdentifier(doc, section) { - let ident = this.book.packaging.metadata.identifier; - let meta = doc.createElement("meta"); - meta.setAttribute("name", "dc.relation.ispartof"); - if (ident) { - meta.setAttribute("content", ident); - } - doc.getElementsByTagName("head")[0].appendChild(meta); - } - + constructor(book, options) { + this.settings = extend(this.settings || {}, { + width: null, + height: null, + ignoreClass: "", + manager: "default", + view: "iframe", + flow: null, + layout: null, + spread: null, + minSpreadWidth: 800, + stylesheet: null, + resizeOnOrientationChange: true, + script: null, + snap: false, + defaultDirection: "ltr", + allowScriptedContent: false, + allowPopups: false, + }); + + extend(this.settings, options); + + if (typeof this.settings.manager === "object") { + this.manager = this.settings.manager; + } + + this.book = book; + + /** + * Adds Hook methods to the Rendition prototype + * @member {object} hooks + * @property {Hook} hooks.content + * @memberof Rendition + */ + this.hooks = {}; + this.hooks.display = new Hook(this); + this.hooks.serialize = new Hook(this); + this.hooks.content = new Hook(this); + this.hooks.unloaded = new Hook(this); + this.hooks.layout = new Hook(this); + this.hooks.render = new Hook(this); + this.hooks.show = new Hook(this); + + this.hooks.content.register(this.handleLinks.bind(this)); + this.hooks.content.register(this.passEvents.bind(this)); + this.hooks.content.register(this.adjustImages.bind(this)); + + this.book.spine.hooks.content.register(this.injectIdentifier.bind(this)); + + if (this.settings.stylesheet) { + this.book.spine.hooks.content.register(this.injectStylesheet.bind(this)); + } + + if (this.settings.script) { + this.book.spine.hooks.content.register(this.injectScript.bind(this)); + } + + /** + * @member {Themes} themes + * @memberof Rendition + */ + this.themes = new Themes(this); + + /** + * @member {Annotations} annotations + * @memberof Rendition + */ + this.annotations = new Annotations(this); + + this.epubcfi = new EpubCFI(); + + this.q = new Queue(this); + + /** + * A Rendered Location Range + * @typedef location + * @type {Object} + * @property {object} start + * @property {string} start.index + * @property {string} start.href + * @property {object} start.displayed + * @property {EpubCFI} start.cfi + * @property {number} start.location + * @property {number} start.percentage + * @property {number} start.displayed.page + * @property {number} start.displayed.total + * @property {object} end + * @property {string} end.index + * @property {string} end.href + * @property {object} end.displayed + * @property {EpubCFI} end.cfi + * @property {number} end.location + * @property {number} end.percentage + * @property {number} end.displayed.page + * @property {number} end.displayed.total + * @property {boolean} atStart + * @property {boolean} atEnd + * @memberof Rendition + */ + this.location = undefined; + + // Hold queue until book is opened + this.q.enqueue(this.book.opened); + + this.starting = new defer(); + /** + * @member {promise} started returns after the rendition has started + * @memberof Rendition + */ + this.started = this.starting.promise; + + // Block the queue until rendering is started + this.q.enqueue(this.start); + } + + /** + * Set the manager function + * @param {function} manager + */ + setManager(manager) { + this.manager = manager; + } + + /** + * Require the manager from passed string, or as a class function + * @param {string|object} manager [description] + * @return {method} + */ + requireManager(manager) { + var viewManager; + + // If manager is a string, try to load from imported managers + if (typeof manager === "string" && manager === "default") { + viewManager = DefaultViewManager; + } else if (typeof manager === "string" && manager === "continuous") { + viewManager = ContinuousViewManager; + } else { + // otherwise, assume we were passed a class function + viewManager = manager; + } + + return viewManager; + } + + /** + * Require the view from passed string, or as a class function + * @param {string|object} view + * @return {view} + */ + requireView(view) { + var View; + + // If view is a string, try to load from imported views, + if (typeof view == "string" && view === "iframe") { + View = IframeView; + } else { + // otherwise, assume we were passed a class function + View = view; + } + + return View; + } + + /** + * Start the rendering + * @return {Promise} rendering has started + */ + start() { + if ( + !this.settings.layout && + (this.book.package.metadata.layout === "pre-paginated" || + this.book.displayOptions.fixedLayout === "true") + ) { + this.settings.layout = "pre-paginated"; + } + switch (this.book.package.metadata.spread) { + case "none": + this.settings.spread = "none"; + break; + case "both": + this.settings.spread = true; + break; + } + + if (!this.manager) { + this.ViewManager = this.requireManager(this.settings.manager); + this.View = this.requireView(this.settings.view); + + this.manager = new this.ViewManager({ + view: this.View, + queue: this.q, + request: this.book.load.bind(this.book), + settings: this.settings, + }); + } + + this.direction( + this.book.package.metadata.direction || this.settings.defaultDirection + ); + + // Parse metadata to get layout props + this.settings.globalLayoutProperties = this.determineLayoutProperties( + this.book.package.metadata + ); + + this.flow(this.settings.globalLayoutProperties.flow); + + this.layout(this.settings.globalLayoutProperties); + + // Listen for displayed views + this.manager.on(EVENTS.MANAGERS.ADDED, this.afterDisplayed.bind(this)); + this.manager.on(EVENTS.MANAGERS.REMOVED, this.afterRemoved.bind(this)); + + // Listen for resizing + this.manager.on(EVENTS.MANAGERS.RESIZED, this.onResized.bind(this)); + + // Listen for rotation + this.manager.on( + EVENTS.MANAGERS.ORIENTATION_CHANGE, + this.onOrientationChange.bind(this) + ); + + // Listen for scroll changes + this.manager.on(EVENTS.MANAGERS.SCROLLED, this.reportLocation.bind(this)); + + /** + * Emit that rendering has started + * @event started + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.STARTED); + + // Start processing queue + this.starting.resolve(); + } + + /** + * Call to attach the container to an element in the dom + * Container must be attached before rendering can begin + * @param {element} element to attach to + * @return {Promise} + */ + attachTo(element) { + return this.q.enqueue( + function () { + // Start rendering + this.manager.render(element, { + width: this.settings.width, + height: this.settings.height, + }); + + /** + * Emit that rendering has attached to an element + * @event attached + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.ATTACHED); + }.bind(this) + ); + } + + /** + * Display a point in the book + * The request will be added to the rendering Queue, + * so it will wait until book is opened, rendering started + * and all other rendering tasks have finished to be called. + * @param {string} target Url or EpubCFI + * @return {Promise} + */ + display(target) { + if (this.displaying) { + this.displaying.resolve(); + } + return this.q.enqueue(this._display, target); + } + + /** + * Tells the manager what to display immediately + * @private + * @param {string} target Url or EpubCFI + * @return {Promise} + */ + _display(target) { + if (!this.book) { + return; + } + var displaying = new defer(); + var displayed = displaying.promise; + var section; + + this.displaying = displaying; + + // Check if this is a book percentage + if (this.book.locations.length() && isFloat(target)) { + target = this.book.locations.cfiFromPercentage(parseFloat(target)); + } + + section = this.book.spine.get(target); + + if (!section) { + displaying.reject(new Error("No Section Found")); + return displayed; + } + + this.manager.display(section, target).then( + () => { + displaying.resolve(section); + this.displaying = undefined; + + /** + * Emit that a section has been displayed + * @event displayed + * @param {Section} section + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.DISPLAYED, section); + this.reportLocation(); + }, + (err) => { + /** + * Emit that has been an error displaying + * @event displayError + * @param {Section} section + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.DISPLAY_ERROR, err); + } + ); + + return displayed; + } + + /** + * Report what section has been displayed + * @private + * @param {*} view + */ + afterDisplayed(view) { + view.on(EVENTS.VIEWS.MARK_CLICKED, (cfiRange, data) => + this.triggerMarkEvent(cfiRange, data, view.contents) + ); + + this.hooks.render.trigger(view, this).then(() => { + if (view.contents) { + this.hooks.content.trigger(view.contents, this).then(() => { + /** + * Emit that a section has been rendered + * @event rendered + * @param {Section} section + * @param {View} view + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.RENDERED, view.section, view); + }); + } else { + this.emit(EVENTS.RENDITION.RENDERED, view.section, view); + } + }); + } + + /** + * Report what has been removed + * @private + * @param {*} view + */ + afterRemoved(view) { + this.hooks.unloaded.trigger(view, this).then(() => { + /** + * Emit that a section has been removed + * @event removed + * @param {Section} section + * @param {View} view + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.REMOVED, view.section, view); + }); + } + + /** + * Report resize events and display the last seen location + * @private + */ + onResized(size, epubcfi) { + /** + * Emit that the rendition has been resized + * @event resized + * @param {number} width + * @param {height} height + * @param {string} epubcfi (optional) + * @memberof Rendition + */ + this.emit( + EVENTS.RENDITION.RESIZED, + { + width: size.width, + height: size.height, + }, + epubcfi + ); + + if (this.location && this.location.start) { + this.display(epubcfi || this.location.start.cfi); + } + } + + /** + * Report orientation events and display the last seen location + * @private + */ + onOrientationChange(orientation) { + /** + * Emit that the rendition has been rotated + * @event orientationchange + * @param {string} orientation + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.ORIENTATION_CHANGE, orientation); + } + + /** + * Move the Rendition to a specific offset + * Usually you would be better off calling display() + * @param {object} offset + */ + moveTo(offset) { + this.manager.moveTo(offset); + } + + /** + * Trigger a resize of the views + * @param {number} [width] + * @param {number} [height] + * @param {string} [epubcfi] (optional) + */ + resize(width, height, epubcfi) { + if (width) { + this.settings.width = width; + } + if (height) { + this.settings.height = height; + } + this.manager.resize(width, height, epubcfi); + } + + /** + * Clear all rendered views + */ + clear() { + this.manager.clear(); + } + + /** + * Go to the next "page" in the rendition + * @return {Promise} + */ + next() { + return this.q + .enqueue(this.manager.next.bind(this.manager)) + .then(this.reportLocation.bind(this)); + } + + /** + * Go to the previous "page" in the rendition + * @return {Promise} + */ + prev() { + return this.q + .enqueue(this.manager.prev.bind(this.manager)) + .then(this.reportLocation.bind(this)); + } + + //-- http://www.idpf.org/epub/301/spec/epub-publications.html#meta-properties-rendering + /** + * Determine the Layout properties from metadata and settings + * @private + * @param {object} metadata + * @return {object} properties + */ + determineLayoutProperties(metadata) { + var properties; + var layout = this.settings.layout || metadata.layout || "reflowable"; + var spread = this.settings.spread || metadata.spread || "auto"; + var orientation = + this.settings.orientation || metadata.orientation || "auto"; + var flow = this.settings.flow || metadata.flow || "auto"; + var viewport = metadata.viewport || ""; + var minSpreadWidth = + this.settings.minSpreadWidth || metadata.minSpreadWidth || 800; + var direction = this.settings.direction || metadata.direction || "ltr"; + + if ( + (this.settings.width === 0 || this.settings.width > 0) && + (this.settings.height === 0 || this.settings.height > 0) + ) { + // viewport = "width="+this.settings.width+", height="+this.settings.height+""; + } + + properties = { + layout: layout, + spread: spread, + orientation: orientation, + flow: flow, + viewport: viewport, + minSpreadWidth: minSpreadWidth, + direction: direction, + }; + + return properties; + } + + /** + * Adjust the flow of the rendition to paginated or scrolled + * (scrolled-continuous vs scrolled-doc are handled by different view managers) + * @param {string} flow + */ + flow(flow) { + var _flow = flow; + if ( + flow === "scrolled" || + flow === "scrolled-doc" || + flow === "scrolled-continuous" + ) { + _flow = "scrolled"; + } + + if (flow === "auto" || flow === "paginated") { + _flow = "paginated"; + } + + this.settings.flow = flow; + + if (this._layout) { + this._layout.flow(_flow); + } + + if (this.manager && this._layout) { + this.manager.applyLayout(this._layout); + } + + if (this.manager) { + this.manager.updateFlow(_flow); + } + + if (this.manager && this.manager.isRendered() && this.location) { + this.manager.clear(); + this.display(this.location.start.cfi); + } + } + + /** + * Adjust the layout of the rendition to reflowable or pre-paginated + * @param {object} settings + */ + layout(settings) { + if (settings) { + this._layout = new Layout(settings); + this._layout.spread(settings.spread, this.settings.minSpreadWidth); + + // this.mapping = new Mapping(this._layout.props); + + this._layout.on(EVENTS.LAYOUT.UPDATED, (props, changed) => { + this.emit(EVENTS.RENDITION.LAYOUT, props, changed); + }); + } + + if (this.manager && this._layout) { + this.manager.applyLayout(this._layout); + } + + return this._layout; + } + + /** + * Adjust if the rendition uses spreads + * @param {string} spread none | auto (TODO: implement landscape, portrait, both) + * @param {int} [min] min width to use spreads at + */ + spread(spread, min) { + this.settings.spread = spread; + + if (min) { + this.settings.minSpreadWidth = min; + } + + if (this._layout) { + this._layout.spread(spread, min); + } + + if (this.manager && this.manager.isRendered()) { + this.manager.updateLayout(); + } + } + + /** + * Adjust the direction of the rendition + * @param {string} dir + */ + direction(dir) { + this.settings.direction = dir || "ltr"; + + if (this.manager) { + this.manager.direction(this.settings.direction); + } + + if (this.manager && this.manager.isRendered() && this.location) { + this.manager.clear(); + this.display(this.location.start.cfi); + } + } + + /** + * Report the current location + * @fires relocated + * @fires locationChanged + */ + reportLocation() { + return this.q.enqueue( + function reportedLocation() { + requestAnimationFrame( + function reportedLocationAfterRAF() { + var location = this.manager.currentLocation(); + if ( + location && + location.then && + typeof location.then === "function" + ) { + location.then( + function (result) { + let located = this.located(result); + + if (!located || !located.start || !located.end) { + return; + } + + this.location = located; + + this.emit(EVENTS.RENDITION.LOCATION_CHANGED, { + index: this.location.start.index, + href: this.location.start.href, + start: this.location.start.cfi, + end: this.location.end.cfi, + percentage: this.location.start.percentage, + }); + + this.emit(EVENTS.RENDITION.RELOCATED, this.location); + }.bind(this) + ); + } else if (location) { + let located = this.located(location); + + if (!located || !located.start || !located.end) { + return; + } + + this.location = located; + + /** + * @event locationChanged + * @deprecated + * @type {object} + * @property {number} index + * @property {string} href + * @property {EpubCFI} start + * @property {EpubCFI} end + * @property {number} percentage + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.LOCATION_CHANGED, { + index: this.location.start.index, + href: this.location.start.href, + start: this.location.start.cfi, + end: this.location.end.cfi, + percentage: this.location.start.percentage, + }); + + /** + * @event relocated + * @type {displayedLocation} + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.RELOCATED, this.location); + } + }.bind(this) + ); + }.bind(this) + ); + } + + /** + * Get the Current Location object + * @return {displayedLocation | promise} location (may be a promise) + */ + currentLocation() { + var location = this.manager.currentLocation(); + if (location && location.then && typeof location.then === "function") { + location.then( + function (result) { + let located = this.located(result); + return located; + }.bind(this) + ); + } else if (location) { + let located = this.located(location); + return located; + } + } + + /** + * Creates a Rendition#locationRange from location + * passed by the Manager + * @returns {displayedLocation} + * @private + */ + located(location) { + if (!location.length) { + return {}; + } + let start = location[0]; + let end = location[location.length - 1]; + + let located = { + start: { + index: start.index, + href: start.href, + cfi: start.mapping.start, + displayed: { + page: start.pages[0] || 1, + total: start.totalPages, + }, + }, + end: { + index: end.index, + href: end.href, + cfi: end.mapping.end, + displayed: { + page: end.pages[end.pages.length - 1] || 1, + total: end.totalPages, + }, + }, + }; + + let locationStart = this.book.locations.locationFromCfi( + start.mapping.start + ); + let locationEnd = this.book.locations.locationFromCfi(end.mapping.end); + + if (locationStart != null) { + located.start.location = locationStart; + located.start.percentage = + this.book.locations.percentageFromLocation(locationStart); + } + if (locationEnd != null) { + located.end.location = locationEnd; + located.end.percentage = + this.book.locations.percentageFromLocation(locationEnd); + } + + let pageStart = this.book.pageList.pageFromCfi(start.mapping.start); + let pageEnd = this.book.pageList.pageFromCfi(end.mapping.end); + + if (pageStart != -1) { + located.start.page = pageStart; + } + if (pageEnd != -1) { + located.end.page = pageEnd; + } + + if ( + end.index === this.book.spine.last().index && + located.end.displayed.page >= located.end.displayed.total + ) { + located.atEnd = true; + } + + if ( + start.index === this.book.spine.first().index && + located.start.displayed.page === 1 + ) { + located.atStart = true; + } + + return located; + } + + /** + * Remove and Clean Up the Rendition + */ + destroy() { + // Clear the queue + // this.q.clear(); + // this.q = undefined; + + this.manager && this.manager.destroy(); + + this.book = undefined; + + // this.views = null; + + // this.hooks.display.clear(); + // this.hooks.serialize.clear(); + // this.hooks.content.clear(); + // this.hooks.layout.clear(); + // this.hooks.render.clear(); + // this.hooks.show.clear(); + // this.hooks = {}; + + // this.themes.destroy(); + // this.themes = undefined; + + // this.epubcfi = undefined; + + // this.starting = undefined; + // this.started = undefined; + } + + /** + * Pass the events from a view's Contents + * @private + * @param {Contents} view contents + */ + passEvents(contents) { + DOM_EVENTS.forEach((e) => { + contents.on(e, (ev) => this.triggerViewEvent(ev, contents)); + }); + + contents.on(EVENTS.CONTENTS.SELECTED, (e) => + this.triggerSelectedEvent(e, contents) + ); + } + + /** + * Emit events passed by a view + * @private + * @param {event} e + */ + triggerViewEvent(e, contents) { + this.emit(e.type, e, contents); + } + + /** + * Emit a selection event's CFI Range passed from a a view + * @private + * @param {string} cfirange + */ + triggerSelectedEvent(cfirange, contents) { + /** + * Emit that a text selection has occurred + * @event selected + * @param {string} cfirange + * @param {Contents} contents + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.SELECTED, cfirange, contents); + } + + /** + * Emit a markClicked event with the cfiRange and data from a mark + * @private + * @param {EpubCFI} cfirange + */ + triggerMarkEvent(cfiRange, data, contents) { + /** + * Emit that a mark was clicked + * @event markClicked + * @param {EpubCFI} cfirange + * @param {object} data + * @param {Contents} contents + * @memberof Rendition + */ + this.emit(EVENTS.RENDITION.MARK_CLICKED, cfiRange, data, contents); + } + + /** + * Get a Range from a Visible CFI + * @param {string} cfi EpubCfi String + * @param {string} ignoreClass + * @return {range} + */ + getRange(cfi, ignoreClass) { + var _cfi = new EpubCFI(cfi); + var found = this.manager.visible().filter(function (view) { + if (_cfi.spinePos === view.index) return true; + }); + + // Should only every return 1 item + if (found.length) { + return found[0].contents.range(_cfi, ignoreClass); + } + } + + /** + * Hook to adjust images to fit in columns + * @param {Contents} contents + * @private + */ + adjustImages(contents) { + if (this._layout.name === "pre-paginated") { + return new Promise(function (resolve) { + resolve(); + }); + } + + let computed = contents.window.getComputedStyle(contents.content, null); + let height = + (contents.content.offsetHeight - + (parseFloat(computed.paddingTop) + + parseFloat(computed.paddingBottom))) * + 0.95; + let horizontalPadding = + parseFloat(computed.paddingLeft) + parseFloat(computed.paddingRight); + + contents.addStylesheetRules({ + img: { + "max-width": + (this._layout.columnWidth + ? this._layout.columnWidth - horizontalPadding + "px" + : "100%") + "!important", + "max-height": height + "px" + "!important", + "object-fit": "contain", + "page-break-inside": "avoid", + "break-inside": "avoid", + "box-sizing": "border-box", + }, + svg: { + "max-width": + (this._layout.columnWidth + ? this._layout.columnWidth - horizontalPadding + "px" + : "100%") + "!important", + "max-height": height + "px" + "!important", + "page-break-inside": "avoid", + "break-inside": "avoid", + }, + }); + + return new Promise(function (resolve, reject) { + // Wait to apply + setTimeout(function () { + resolve(); + }, 1); + }); + } + + /** + * Get the Contents object of each rendered view + * @returns {Contents[]} + */ + getContents() { + return this.manager ? this.manager.getContents() : []; + } + + /** + * Get the views member from the manager + * @returns {Views} + */ + views() { + let views = this.manager ? this.manager.views : undefined; + return views || []; + } + + /** + * Hook to handle link clicks in rendered content + * @param {Contents} contents + * @private + */ + handleLinks(contents) { + if (contents) { + contents.on(EVENTS.CONTENTS.LINK_CLICKED, (href) => { + let relative = this.book.path.relative(href); + this.display(relative); + }); + } + } + + /** + * Hook to handle injecting stylesheet before + * a Section is serialized + * @param {document} doc + * @param {Section} section + * @private + */ + injectStylesheet(doc, section) { + let style = doc.createElement("link"); + style.setAttribute("type", "text/css"); + style.setAttribute("rel", "stylesheet"); + style.setAttribute("href", this.settings.stylesheet); + doc.getElementsByTagName("head")[0].appendChild(style); + } + + /** + * Hook to handle injecting scripts before + * a Section is serialized + * @param {document} doc + * @param {Section} section + * @private + */ + injectScript(doc, section) { + let script = doc.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("src", this.settings.script); + script.textContent = " "; // Needed to prevent self closing tag + doc.getElementsByTagName("head")[0].appendChild(script); + } + + /** + * Hook to handle the document identifier before + * a Section is serialized + * @param {document} doc + * @param {Section} section + * @private + */ + injectIdentifier(doc, section) { + let ident = this.book.packaging.metadata.identifier; + let meta = doc.createElement("meta"); + meta.setAttribute("name", "dc.relation.ispartof"); + if (ident) { + meta.setAttribute("content", ident); + } + doc.getElementsByTagName("head")[0].appendChild(meta); + } } //-- Enable binding events to Renderer diff --git a/src/resources.js b/src/resources.js index b5d77f8..b13eb18 100644 --- a/src/resources.js +++ b/src/resources.js @@ -1,9 +1,9 @@ -import {substitute} from "./utils/replacements"; -import {createBase64Url, createBlobUrl, blob2base64} from "./utils/core"; -import Url from "./utils/url"; +import path from "path-webpack"; +import { blob2base64, createBase64Url, createBlobUrl } from "./utils/core"; import mime from "./utils/mime"; import Path from "./utils/path"; -import path from "path-webpack"; +import { substitute } from "./utils/replacements"; +import Url from "./utils/url"; /** * Handle Package Resources @@ -15,306 +15,307 @@ import path from "path-webpack"; * @param {method} [options.resolver] */ class Resources { - constructor(manifest, options) { - this.settings = { - replacements: (options && options.replacements) || "base64", - archive: (options && options.archive), - resolver: (options && options.resolver), - request: (options && options.request) - }; + constructor(manifest, options) { + this.settings = { + replacements: (options && options.replacements) || "base64", + archive: options && options.archive, + resolver: options && options.resolver, + request: options && options.request, + }; - this.process(manifest); - } + this.process(manifest); + } - /** - * Process resources - * @param {Manifest} manifest - */ - process(manifest){ - this.manifest = manifest; - this.resources = Object.keys(manifest). - map(function (key){ - return manifest[key]; - }); + /** + * Process resources + * @param {Manifest} manifest + */ + process(manifest) { + this.manifest = manifest; + this.resources = Object.keys(manifest).map(function (key) { + return manifest[key]; + }); - this.replacementUrls = []; + this.replacementUrls = []; - this.html = []; - this.assets = []; - this.css = []; + this.html = []; + this.assets = []; + this.css = []; - this.urls = []; - this.cssUrls = []; + this.urls = []; + this.cssUrls = []; - this.split(); - this.splitUrls(); - } + this.split(); + this.splitUrls(); + } - /** - * Split resources by type - * @private - */ - split(){ + /** + * Split resources by type + * @private + */ + split() { + // HTML + this.html = this.resources.filter(function (item) { + if (item.type === "application/xhtml+xml" || item.type === "text/html") { + return true; + } + }); - // HTML - this.html = this.resources. - filter(function (item){ - if (item.type === "application/xhtml+xml" || - item.type === "text/html") { - return true; - } - }); + // Exclude HTML + this.assets = this.resources.filter(function (item) { + if (item.type !== "application/xhtml+xml" && item.type !== "text/html") { + return true; + } + }); - // Exclude HTML - this.assets = this.resources. - filter(function (item){ - if (item.type !== "application/xhtml+xml" && - item.type !== "text/html") { - return true; - } - }); + // Only CSS + this.css = this.resources.filter(function (item) { + if (item.type === "text/css") { + return true; + } + }); + } - // Only CSS - this.css = this.resources. - filter(function (item){ - if (item.type === "text/css") { - return true; - } - }); - } + /** + * Convert split resources into Urls + * @private + */ + splitUrls() { + // All Assets Urls + this.urls = this.assets.map( + function (item) { + return item.href; + }.bind(this) + ); - /** - * Convert split resources into Urls - * @private - */ - splitUrls(){ + // Css Urls + this.cssUrls = this.css.map(function (item) { + return item.href; + }); + } - // All Assets Urls - this.urls = this.assets. - map(function(item) { - return item.href; - }.bind(this)); + /** + * Create a url to a resource + * @param {string} url + * @return {Promise} Promise resolves with url string + */ + createUrl(url) { + var parsedUrl = new Url(url); + var mimeType = mime.lookup(parsedUrl.filename); - // Css Urls - this.cssUrls = this.css.map(function(item) { - return item.href; - }); + if (this.settings.archive) { + return this.settings.archive.createUrl(url, { + base64: this.settings.replacements === "base64", + }); + } else { + if (this.settings.replacements === "base64") { + return this.settings + .request(url, "blob") + .then((blob) => { + return blob2base64(blob); + }) + .then((blob) => { + return createBase64Url(blob, mimeType); + }); + } else { + return this.settings.request(url, "blob").then((blob) => { + return createBlobUrl(blob, mimeType); + }); + } + } + } - } + /** + * Create blob urls for all the assets + * @return {Promise} returns replacement urls + */ + replacements() { + if (this.settings.replacements === "none") { + return new Promise( + function (resolve) { + resolve(this.urls); + }.bind(this) + ); + } - /** - * Create a url to a resource - * @param {string} url - * @return {Promise} Promise resolves with url string - */ - createUrl (url) { - var parsedUrl = new Url(url); - var mimeType = mime.lookup(parsedUrl.filename); + var replacements = this.urls.map((url) => { + var absolute = this.settings.resolver(url); - if (this.settings.archive) { - return this.settings.archive.createUrl(url, {"base64": (this.settings.replacements === "base64")}); - } else { - if (this.settings.replacements === "base64") { - return this.settings.request(url, 'blob') - .then((blob) => { - return blob2base64(blob); - }) - .then((blob) => { - return createBase64Url(blob, mimeType); - }); - } else { - return this.settings.request(url, 'blob').then((blob) => { - return createBlobUrl(blob, mimeType); - }) - } - } - } + return this.createUrl(absolute).catch(() => { + return null; + }); + }); - /** - * Create blob urls for all the assets - * @return {Promise} returns replacement urls - */ - replacements(){ - if (this.settings.replacements === "none") { - return new Promise(function(resolve) { - resolve(this.urls); - }.bind(this)); - } + return Promise.all(replacements).then((replacementUrls) => { + this.replacementUrls = replacementUrls.filter((url) => { + return typeof url === "string"; + }); + return replacementUrls; + }); + } - var replacements = this.urls.map( (url) => { - var absolute = this.settings.resolver(url); + /** + * Replace URLs in CSS resources + * @private + * @param {Archive} [archive] + * @param {method} [resolver] + * @return {Promise} + */ + replaceCss(archive, resolver) { + var replaced = []; + archive = archive || this.settings.archive; + resolver = resolver || this.settings.resolver; + this.cssUrls.forEach( + function (href) { + var replacement = this.createCssFile(href, archive, resolver).then( + function (replacementUrl) { + // switch the url in the replacementUrls + var indexInUrls = this.urls.indexOf(href); + if (indexInUrls > -1) { + this.replacementUrls[indexInUrls] = replacementUrl; + } + }.bind(this) + ); - return this.createUrl(absolute). - catch((err) => { - console.error(err); - return null; - }); - }); + replaced.push(replacement); + }.bind(this) + ); + return Promise.all(replaced); + } - return Promise.all(replacements) - .then( (replacementUrls) => { - this.replacementUrls = replacementUrls.filter((url) => { - return (typeof(url) === "string"); - }); - return replacementUrls; - }); - } + /** + * Create a new CSS file with the replaced URLs + * @private + * @param {string} href the original css file + * @return {Promise} returns a BlobUrl to the new CSS file or a data url + */ + createCssFile(href) { + var newUrl; - /** - * Replace URLs in CSS resources - * @private - * @param {Archive} [archive] - * @param {method} [resolver] - * @return {Promise} - */ - replaceCss(archive, resolver){ - var replaced = []; - archive = archive || this.settings.archive; - resolver = resolver || this.settings.resolver; - this.cssUrls.forEach(function(href) { - var replacement = this.createCssFile(href, archive, resolver) - .then(function (replacementUrl) { - // switch the url in the replacementUrls - var indexInUrls = this.urls.indexOf(href); - if (indexInUrls > -1) { - this.replacementUrls[indexInUrls] = replacementUrl; - } - }.bind(this)) + if (path.isAbsolute(href)) { + return new Promise(function (resolve) { + resolve(); + }); + } + var absolute = this.settings.resolver(href); - replaced.push(replacement); - }.bind(this)); - return Promise.all(replaced); - } + // Get the text of the css file from the archive + var textResponse; - /** - * Create a new CSS file with the replaced URLs - * @private - * @param {string} href the original css file - * @return {Promise} returns a BlobUrl to the new CSS file or a data url - */ - createCssFile(href){ - var newUrl; + if (this.settings.archive) { + textResponse = this.settings.archive.getText(absolute); + } else { + textResponse = this.settings.request(absolute, "text"); + } - if (path.isAbsolute(href)) { - return new Promise(function(resolve){ - resolve(); - }); - } + // Get asset links relative to css file + var relUrls = this.urls.map((assetHref) => { + var resolved = this.settings.resolver(assetHref); + var relative = new Path(absolute).relative(resolved); - var absolute = this.settings.resolver(href); + return relative; + }); - // Get the text of the css file from the archive - var textResponse; + if (!textResponse) { + // file not found, don't replace + return new Promise(function (resolve) { + resolve(); + }); + } - if (this.settings.archive) { - textResponse = this.settings.archive.getText(absolute); - } else { - textResponse = this.settings.request(absolute, "text"); - } + return textResponse.then( + (text) => { + // Replacements in the css text + text = substitute(text, relUrls, this.replacementUrls); - // Get asset links relative to css file - var relUrls = this.urls.map( (assetHref) => { - var resolved = this.settings.resolver(assetHref); - var relative = new Path(absolute).relative(resolved); + // Get the new url + if (this.settings.replacements === "base64") { + newUrl = createBase64Url(text, "text/css"); + } else { + newUrl = createBlobUrl(text, "text/css"); + } - return relative; - }); + return newUrl; + }, + (err) => { + // handle response errors + return new Promise(function (resolve) { + resolve(); + }); + } + ); + } - if (!textResponse) { - // file not found, don't replace - return new Promise(function(resolve){ - resolve(); - }); - } + /** + * Resolve all resources URLs relative to an absolute URL + * @param {string} absolute to be resolved to + * @param {resolver} [resolver] + * @return {string[]} array with relative Urls + */ + relativeTo(absolute, resolver) { + resolver = resolver || this.settings.resolver; - return textResponse.then( (text) => { - // Replacements in the css text - text = substitute(text, relUrls, this.replacementUrls); + // Get Urls relative to current sections + return this.urls.map( + function (href) { + var resolved = resolver(href); + var relative = new Path(absolute).relative(resolved); + return relative; + }.bind(this) + ); + } - // Get the new url - if (this.settings.replacements === "base64") { - newUrl = createBase64Url(text, "text/css"); - } else { - newUrl = createBlobUrl(text, "text/css"); - } + /** + * Get a URL for a resource + * @param {string} path + * @return {string} url + */ + get(path) { + var indexInUrls = this.urls.indexOf(path); + if (indexInUrls === -1) { + return; + } + if (this.replacementUrls.length) { + return new Promise( + function (resolve, reject) { + resolve(this.replacementUrls[indexInUrls]); + }.bind(this) + ); + } else { + return this.createUrl(path); + } + } - return newUrl; - }, (err) => { - // handle response errors - return new Promise(function(resolve){ - resolve(); - }); - }); + /** + * Substitute urls in content, with replacements, + * relative to a url if provided + * @param {string} content + * @param {string} [url] url to resolve to + * @return {string} content with urls substituted + */ + substitute(content, url) { + var relUrls; + if (url) { + relUrls = this.relativeTo(url); + } else { + relUrls = this.urls; + } + return substitute(content, relUrls, this.replacementUrls); + } - } + destroy() { + this.settings = undefined; + this.manifest = undefined; + this.resources = undefined; + this.replacementUrls = undefined; + this.html = undefined; + this.assets = undefined; + this.css = undefined; - /** - * Resolve all resources URLs relative to an absolute URL - * @param {string} absolute to be resolved to - * @param {resolver} [resolver] - * @return {string[]} array with relative Urls - */ - relativeTo(absolute, resolver){ - resolver = resolver || this.settings.resolver; - - // Get Urls relative to current sections - return this.urls. - map(function(href) { - var resolved = resolver(href); - var relative = new Path(absolute).relative(resolved); - return relative; - }.bind(this)); - } - - /** - * Get a URL for a resource - * @param {string} path - * @return {string} url - */ - get(path) { - var indexInUrls = this.urls.indexOf(path); - if (indexInUrls === -1) { - return; - } - if (this.replacementUrls.length) { - return new Promise(function(resolve, reject) { - resolve(this.replacementUrls[indexInUrls]); - }.bind(this)); - } else { - return this.createUrl(path); - } - } - - /** - * Substitute urls in content, with replacements, - * relative to a url if provided - * @param {string} content - * @param {string} [url] url to resolve to - * @return {string} content with urls substituted - */ - substitute(content, url) { - var relUrls; - if (url) { - relUrls = this.relativeTo(url); - } else { - relUrls = this.urls; - } - return substitute(content, relUrls, this.replacementUrls); - } - - destroy() { - this.settings = undefined; - this.manifest = undefined; - this.resources = undefined; - this.replacementUrls = undefined; - this.html = undefined; - this.assets = undefined; - this.css = undefined; - - this.urls = undefined; - this.cssUrls = undefined; - } + this.urls = undefined; + this.cssUrls = undefined; + } } export default Resources; diff --git a/src/section.js b/src/section.js index aa61310..2b57940 100644 --- a/src/section.js +++ b/src/section.js @@ -1,10 +1,9 @@ -import { defer } from "./utils/core"; +import { DOMParser as XMLDOMSerializer } from "@xmldom/xmldom"; import EpubCFI from "./epubcfi"; +import { defer, sprint } from "./utils/core"; import Hook from "./utils/hook"; -import { sprint } from "./utils/core"; import { replaceBase } from "./utils/replacements"; import Request from "./utils/request"; -import { DOMParser as XMLDOMSerializer } from "@xmldom/xmldom"; /** * Represents a Section of the Book @@ -14,310 +13,347 @@ import { DOMParser as XMLDOMSerializer } from "@xmldom/xmldom"; * @param {object} hooks hooks for serialize and content */ class Section { - constructor(item, hooks){ - this.idref = item.idref; - this.linear = item.linear === "yes"; - this.properties = item.properties; - this.index = item.index; - this.href = item.href; - this.url = item.url; - this.canonical = item.canonical; - this.next = item.next; - this.prev = item.prev; + constructor(item, hooks) { + this.idref = item.idref; + this.linear = item.linear === "yes"; + this.properties = item.properties; + this.index = item.index; + this.href = item.href; + this.url = item.url; + this.canonical = item.canonical; + this.next = item.next; + this.prev = item.prev; - this.cfiBase = item.cfiBase; + this.cfiBase = item.cfiBase; - if (hooks) { - this.hooks = hooks; - } else { - this.hooks = {}; - this.hooks.serialize = new Hook(this); - this.hooks.content = new Hook(this); - } + if (hooks) { + this.hooks = hooks; + } else { + this.hooks = {}; + this.hooks.serialize = new Hook(this); + this.hooks.content = new Hook(this); + } - this.document = undefined; - this.contents = undefined; - this.output = undefined; - } + this.document = undefined; + this.contents = undefined; + this.output = undefined; + } - /** - * Load the section from its url - * @param {method} [_request] a request method to use for loading - * @return {document} a promise with the xml document - */ - load(_request){ - var request = _request || this.request || Request; - var loading = new defer(); - var loaded = loading.promise; + /** + * Load the section from its url + * @param {method} [_request] a request method to use for loading + * @return {document} a promise with the xml document + */ + load(_request) { + var request = _request || this.request || Request; + var loading = new defer(); + var loaded = loading.promise; - if(this.contents) { - loading.resolve(this.contents); - } else { - request(this.url) - .then(function(xml){ - // var directory = new Url(this.url).directory; + if (this.contents) { + loading.resolve(this.contents); + } else { + request(this.url) + .then( + function (xml) { + // var directory = new Url(this.url).directory; - this.document = xml; - this.contents = xml.documentElement; + this.document = xml; + this.contents = xml.documentElement; - return this.hooks.content.trigger(this.document, this); - }.bind(this)) - .then(function(){ - loading.resolve(this.contents); - }.bind(this)) - .catch(function(error){ - loading.reject(error); - }); - } + return this.hooks.content.trigger(this.document, this); + }.bind(this) + ) + .then( + function () { + loading.resolve(this.contents); + }.bind(this) + ) + .catch(function (error) { + loading.reject(error); + }); + } - return loaded; - } + return loaded; + } - /** - * Adds a base tag for resolving urls in the section - * @private - */ - base(){ - return replaceBase(this.document, this); - } + /** + * Adds a base tag for resolving urls in the section + * @private + */ + base() { + return replaceBase(this.document, this); + } - /** - * Render the contents of a section - * @param {method} [_request] a request method to use for loading - * @return {string} output a serialized XML Document - */ - render(_request){ - var rendering = new defer(); - var rendered = rendering.promise; - this.output; // TODO: better way to return this from hooks? + /** + * Render the contents of a section + * @param {method} [_request] a request method to use for loading + * @return {string} output a serialized XML Document + */ + render(_request) { + var rendering = new defer(); + var rendered = rendering.promise; + this.output; // TODO: better way to return this from hooks? - this.load(_request). - then(function(contents){ - var userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''; - var isIE = userAgent.indexOf('Trident') >= 0; - var Serializer; - if (typeof XMLSerializer === "undefined" || isIE) { - Serializer = XMLDOMSerializer; - } else { - Serializer = XMLSerializer; - } - var serializer = new Serializer(); - this.output = serializer.serializeToString(contents); - return this.output; - }.bind(this)). - then(function(){ - return this.hooks.serialize.trigger(this.output, this); - }.bind(this)). - then(function(){ - rendering.resolve(this.output); - }.bind(this)) - .catch(function(error){ - rendering.reject(error); - }); + this.load(_request) + .then( + function (contents) { + var userAgent = + (typeof navigator !== "undefined" && navigator.userAgent) || ""; + var isIE = userAgent.indexOf("Trident") >= 0; + var Serializer; + if (typeof XMLSerializer === "undefined" || isIE) { + Serializer = XMLDOMSerializer; + } else { + Serializer = XMLSerializer; + } + var serializer = new Serializer(); + this.output = serializer.serializeToString(contents); + return this.output; + }.bind(this) + ) + .then( + function () { + return this.hooks.serialize.trigger(this.output, this); + }.bind(this) + ) + .then( + function () { + rendering.resolve(this.output); + }.bind(this) + ) + .catch(function (error) { + rendering.reject(error); + }); - return rendered; - } + return rendered; + } - /** - * Find a string in a section - * @param {string} _query The query string to find - * @return {object[]} A list of matches, with form {cfi, excerpt} - */ - find(_query){ - var section = this; - var matches = []; - var query = _query.toLowerCase(); - var find = function(node){ - var text = node.textContent.toLowerCase(); - var range = section.document.createRange(); - var cfi; - var pos; - var last = -1; - var excerpt; - var limit = 150; + /** + * Find a string in a section + * @param {string} _query The query string to find + * @return {object[]} A list of matches, with form {cfi, excerpt} + */ + find(_query) { + var section = this; + var matches = []; + var query = _query.toLowerCase(); + var find = function (node) { + var text = node.textContent.toLowerCase(); + var range = section.document.createRange(); + var cfi; + var pos; + var last = -1; + var excerpt; + var limit = 150; - while (pos != -1) { - // Search for the query - pos = text.indexOf(query, last + 1); + while (pos != -1) { + // Search for the query + pos = text.indexOf(query, last + 1); - if (pos != -1) { - // We found it! Generate a CFI - range = section.document.createRange(); - range.setStart(node, pos); - range.setEnd(node, pos + query.length); + if (pos != -1) { + // We found it! Generate a CFI + range = section.document.createRange(); + range.setStart(node, pos); + range.setEnd(node, pos + query.length); - cfi = section.cfiFromRange(range); + cfi = section.cfiFromRange(range); - // Generate the excerpt - if (node.textContent.length < limit) { - excerpt = node.textContent; - } - else { - excerpt = node.textContent.substring(pos - limit/2, pos + limit/2); - excerpt = "..." + excerpt + "..."; - } + // Generate the excerpt + if (node.textContent.length < limit) { + excerpt = node.textContent; + } else { + excerpt = node.textContent.substring( + pos - limit / 2, + pos + limit / 2 + ); + excerpt = "..." + excerpt + "..."; + } - // Add the CFI to the matches list - matches.push({ - cfi: cfi, - excerpt: excerpt - }); - } + // Add the CFI to the matches list + matches.push({ + cfi: cfi, + excerpt: excerpt, + }); + } - last = pos; - } - }; + last = pos; + } + }; - sprint(section.document, function(node) { - find(node); - }); + sprint(section.document, function (node) { + find(node); + }); - return matches; - }; + return matches; + } + /** + * Search a string in multiple sequential Element of the section. If the document.createTreeWalker api is missed(eg: IE8), use `find` as a fallback. + * @param {string} _query The query string to search + * @param {int} maxSeqEle The maximum number of Element that are combined for search, default value is 5. + * @return {object[]} A list of matches, with form {cfi, excerpt} + */ + search(_query, maxSeqEle = 5) { + if (typeof document.createTreeWalker == "undefined") { + return this.find(_query); + } + let matches = []; + const excerptLimit = 150; + const section = this; + const query = _query.toLowerCase(); + const search = function (nodeList) { + const textWithCase = nodeList.reduce((acc, current) => { + return acc + current.textContent; + }, ""); + const text = textWithCase.toLowerCase(); + const pos = text.indexOf(query); + if (pos != -1) { + const startNodeIndex = 0, + endPos = pos + query.length; + let endNodeIndex = 0, + l = 0; + if (pos < nodeList[startNodeIndex].length) { + let cfi; + while (endNodeIndex < nodeList.length - 1) { + l += nodeList[endNodeIndex].length; + if (endPos <= l) { + break; + } + endNodeIndex += 1; + } - /** - * Search a string in multiple sequential Element of the section. If the document.createTreeWalker api is missed(eg: IE8), use `find` as a fallback. - * @param {string} _query The query string to search - * @param {int} maxSeqEle The maximum number of Element that are combined for search, default value is 5. - * @return {object[]} A list of matches, with form {cfi, excerpt} - */ - search(_query , maxSeqEle = 5){ - if (typeof(document.createTreeWalker) == "undefined") { - return this.find(_query); - } - let matches = []; - const excerptLimit = 150; - const section = this; - const query = _query.toLowerCase(); - const search = function(nodeList){ - const textWithCase = nodeList.reduce((acc ,current)=>{ - return acc + current.textContent; - },""); - const text = textWithCase.toLowerCase(); - const pos = text.indexOf(query); - if (pos != -1){ - const startNodeIndex = 0 , endPos = pos + query.length; - let endNodeIndex = 0 , l = 0; - if (pos < nodeList[startNodeIndex].length){ - let cfi; - while( endNodeIndex < nodeList.length - 1 ){ - l += nodeList[endNodeIndex].length; - if ( endPos <= l){ - break; - } - endNodeIndex += 1; - } + let startNode = nodeList[startNodeIndex], + endNode = nodeList[endNodeIndex]; + let range = section.document.createRange(); + range.setStart(startNode, pos); + let beforeEndLengthCount = nodeList + .slice(0, endNodeIndex) + .reduce((acc, current) => { + return acc + current.textContent.length; + }, 0); + range.setEnd( + endNode, + beforeEndLengthCount > endPos + ? endPos + : endPos - beforeEndLengthCount + ); + cfi = section.cfiFromRange(range); - let startNode = nodeList[startNodeIndex] , endNode = nodeList[endNodeIndex]; - let range = section.document.createRange(); - range.setStart(startNode,pos); - let beforeEndLengthCount = nodeList.slice(0, endNodeIndex).reduce((acc,current)=>{return acc+current.textContent.length;},0) ; - range.setEnd(endNode, beforeEndLengthCount > endPos ? endPos : endPos - beforeEndLengthCount ); - cfi = section.cfiFromRange(range); + let excerpt = nodeList + .slice(0, endNodeIndex + 1) + .reduce((acc, current) => { + return acc + current.textContent; + }, ""); + if (excerpt.length > excerptLimit) { + excerpt = excerpt.substring( + pos - excerptLimit / 2, + pos + excerptLimit / 2 + ); + excerpt = "..." + excerpt + "..."; + } + matches.push({ + cfi: cfi, + excerpt: excerpt, + }); + } + } + }; - let excerpt = nodeList.slice(0, endNodeIndex+1).reduce((acc,current)=>{return acc+current.textContent ;},""); - if (excerpt.length > excerptLimit){ - excerpt = excerpt.substring(pos - excerptLimit/2, pos + excerptLimit/2); - excerpt = "..." + excerpt + "..."; - } - matches.push({ - cfi: cfi, - excerpt: excerpt - }); - } - } - } + const treeWalker = document.createTreeWalker( + section.document, + NodeFilter.SHOW_TEXT, + null, + false + ); + let node, + nodeList = []; + while ((node = treeWalker.nextNode())) { + nodeList.push(node); + if (nodeList.length == maxSeqEle) { + search(nodeList.slice(0, maxSeqEle)); + nodeList = nodeList.slice(1, maxSeqEle); + } + } + if (nodeList.length > 0) { + search(nodeList); + } + return matches; + } - const treeWalker = document.createTreeWalker(section.document, NodeFilter.SHOW_TEXT, null, false); - let node , nodeList = []; - while (node = treeWalker.nextNode()) { - nodeList.push(node); - if (nodeList.length == maxSeqEle){ - search(nodeList.slice(0 , maxSeqEle)); - nodeList = nodeList.slice(1, maxSeqEle); - } - } - if (nodeList.length > 0){ - search(nodeList); - } - return matches; - } + /** + * Reconciles the current chapters layout properties with + * the global layout properties. + * @param {object} globalLayout The global layout settings object, chapter properties string + * @return {object} layoutProperties Object with layout properties + */ + reconcileLayoutSettings(globalLayout) { + //-- Get the global defaults + var settings = { + layout: globalLayout.layout, + spread: globalLayout.spread, + orientation: globalLayout.orientation, + }; - /** - * Reconciles the current chapters layout properties with - * the global layout properties. - * @param {object} globalLayout The global layout settings object, chapter properties string - * @return {object} layoutProperties Object with layout properties - */ - reconcileLayoutSettings(globalLayout){ - //-- Get the global defaults - var settings = { - layout : globalLayout.layout, - spread : globalLayout.spread, - orientation : globalLayout.orientation - }; + //-- Get the chapter's display type + this.properties.forEach(function (prop) { + var rendition = prop.replace("rendition:", ""); + var split = rendition.indexOf("-"); + var property, value; - //-- Get the chapter's display type - this.properties.forEach(function(prop){ - var rendition = prop.replace("rendition:", ""); - var split = rendition.indexOf("-"); - var property, value; + if (split != -1) { + property = rendition.slice(0, split); + value = rendition.slice(split + 1); - if(split != -1){ - property = rendition.slice(0, split); - value = rendition.slice(split+1); + settings[property] = value; + } + }); + return settings; + } - settings[property] = value; - } - }); - return settings; - } + /** + * Get a CFI from a Range in the Section + * @param {range} _range + * @return {string} cfi an EpubCFI string + */ + cfiFromRange(_range) { + return new EpubCFI(_range, this.cfiBase).toString(); + } - /** - * Get a CFI from a Range in the Section - * @param {range} _range - * @return {string} cfi an EpubCFI string - */ - cfiFromRange(_range) { - return new EpubCFI(_range, this.cfiBase).toString(); - } + /** + * Get a CFI from an Element in the Section + * @param {element} el + * @return {string} cfi an EpubCFI string + */ + cfiFromElement(el) { + return new EpubCFI(el, this.cfiBase).toString(); + } - /** - * Get a CFI from an Element in the Section - * @param {element} el - * @return {string} cfi an EpubCFI string - */ - cfiFromElement(el) { - return new EpubCFI(el, this.cfiBase).toString(); - } + /** + * Unload the section document + */ + unload() { + this.document = undefined; + this.contents = undefined; + this.output = undefined; + } - /** - * Unload the section document - */ - unload() { - this.document = undefined; - this.contents = undefined; - this.output = undefined; - } + destroy() { + this.unload(); + this.hooks.serialize.clear(); + this.hooks.content.clear(); - destroy() { - this.unload(); - this.hooks.serialize.clear(); - this.hooks.content.clear(); + this.hooks = undefined; + this.idref = undefined; + this.linear = undefined; + this.properties = undefined; + this.index = undefined; + this.href = undefined; + this.url = undefined; + this.next = undefined; + this.prev = undefined; - this.hooks = undefined; - this.idref = undefined; - this.linear = undefined; - this.properties = undefined; - this.index = undefined; - this.href = undefined; - this.url = undefined; - this.next = undefined; - this.prev = undefined; - - this.cfiBase = undefined; - } + this.cfiBase = undefined; + } } export default Section; diff --git a/src/spine.js b/src/spine.js index ffe51e1..d127cbe 100644 --- a/src/spine.js +++ b/src/spine.js @@ -1,274 +1,276 @@ import EpubCFI from "./epubcfi"; -import Hook from "./utils/hook"; import Section from "./section"; -import {replaceBase, replaceCanonical, replaceMeta} from "./utils/replacements"; +import Hook from "./utils/hook"; +import { + replaceBase, + replaceCanonical, + replaceMeta, +} from "./utils/replacements"; /** * A collection of Spine Items */ class Spine { - constructor() { - this.spineItems = []; - this.spineByHref = {}; - this.spineById = {}; + constructor() { + this.spineItems = []; + this.spineByHref = {}; + this.spineById = {}; - this.hooks = {}; - this.hooks.serialize = new Hook(); - this.hooks.content = new Hook(); + this.hooks = {}; + this.hooks.serialize = new Hook(); + this.hooks.content = new Hook(); - // Register replacements - this.hooks.content.register(replaceBase); - this.hooks.content.register(replaceCanonical); - this.hooks.content.register(replaceMeta); + // Register replacements + this.hooks.content.register(replaceBase); + this.hooks.content.register(replaceCanonical); + this.hooks.content.register(replaceMeta); - this.epubcfi = new EpubCFI(); + this.epubcfi = new EpubCFI(); + this.loaded = false; + this.items = undefined; + this.manifest = undefined; + this.spineNodeIndex = undefined; + this.baseUrl = undefined; + this.length = undefined; + } - this.loaded = false; + /** + * Unpack items from a opf into spine items + * @param {Packaging} _package + * @param {method} resolver URL resolver + * @param {method} canonical Resolve canonical url + */ + unpack(_package, resolver, canonical) { + this.items = _package.spine; + this.manifest = _package.manifest; + this.spineNodeIndex = _package.spineNodeIndex; + this.baseUrl = _package.baseUrl || _package.basePath || ""; + this.length = this.items.length; - this.items = undefined; - this.manifest = undefined; - this.spineNodeIndex = undefined; - this.baseUrl = undefined; - this.length = undefined; - } + this.items.forEach((item, index) => { + var manifestItem = this.manifest[item.idref]; + var spineItem; - /** - * Unpack items from a opf into spine items - * @param {Packaging} _package - * @param {method} resolver URL resolver - * @param {method} canonical Resolve canonical url - */ - unpack(_package, resolver, canonical) { + item.index = index; + item.cfiBase = this.epubcfi.generateChapterComponent( + this.spineNodeIndex, + item.index, + item.id + ); - this.items = _package.spine; - this.manifest = _package.manifest; - this.spineNodeIndex = _package.spineNodeIndex; - this.baseUrl = _package.baseUrl || _package.basePath || ""; - this.length = this.items.length; + if (item.href) { + item.url = resolver(item.href, true); + item.canonical = canonical(item.href); + } - this.items.forEach( (item, index) => { - var manifestItem = this.manifest[item.idref]; - var spineItem; + if (manifestItem) { + item.href = manifestItem.href; + item.url = resolver(item.href, true); + item.canonical = canonical(item.href); - item.index = index; - item.cfiBase = this.epubcfi.generateChapterComponent(this.spineNodeIndex, item.index, item.id); + if (manifestItem.properties.length) { + item.properties.push.apply(item.properties, manifestItem.properties); + } + } - if (item.href) { - item.url = resolver(item.href, true); - item.canonical = canonical(item.href); - } + if (item.linear === "yes") { + item.prev = function () { + let prevIndex = item.index; + while (prevIndex > 0) { + let prev = this.get(prevIndex - 1); + if (prev && prev.linear) { + return prev; + } + prevIndex -= 1; + } + return; + }.bind(this); + item.next = function () { + let nextIndex = item.index; + while (nextIndex < this.spineItems.length - 1) { + let next = this.get(nextIndex + 1); + if (next && next.linear) { + return next; + } + nextIndex += 1; + } + return; + }.bind(this); + } else { + item.prev = function () { + return; + }; + item.next = function () { + return; + }; + } - if(manifestItem) { - item.href = manifestItem.href; - item.url = resolver(item.href, true); - item.canonical = canonical(item.href); + spineItem = new Section(item, this.hooks); - if(manifestItem.properties.length){ - item.properties.push.apply(item.properties, manifestItem.properties); - } - } + this.append(spineItem); + }); - if (item.linear === "yes") { - item.prev = function() { - let prevIndex = item.index; - while (prevIndex > 0) { - let prev = this.get(prevIndex-1); - if (prev && prev.linear) { - return prev; - } - prevIndex -= 1; - } - return; - }.bind(this); - item.next = function() { - let nextIndex = item.index; - while (nextIndex < this.spineItems.length-1) { - let next = this.get(nextIndex+1); - if (next && next.linear) { - return next; - } - nextIndex += 1; - } - return; - }.bind(this); - } else { - item.prev = function() { - return; - } - item.next = function() { - return; - } - } + this.loaded = true; + } + /** + * Get an item from the spine + * @param {string|number} [target] + * @return {Section} section + * @example spine.get(); + * @example spine.get(1); + * @example spine.get("chap1.html"); + * @example spine.get("#id1234"); + */ + get(target) { + var index = 0; - spineItem = new Section(item, this.hooks); + if (typeof target === "undefined") { + while (index < this.spineItems.length) { + let next = this.spineItems[index]; + if (next && next.linear) { + break; + } + index += 1; + } + } else if (this.epubcfi.isCfiString(target)) { + let cfi = new EpubCFI(target); + index = cfi.spinePos; + } else if (typeof target === "number" || isNaN(target) === false) { + index = target; + } else if (typeof target === "string" && target.indexOf("#") === 0) { + index = this.spineById[target.substring(1)]; + } else if (typeof target === "string") { + // Remove fragments + target = target.split("#")[0]; + index = this.spineByHref[target] || this.spineByHref[encodeURI(target)]; + } - this.append(spineItem); + return this.spineItems[index] || null; + } + /** + * Append a Section to the Spine + * @private + * @param {Section} section + */ + append(section) { + var index = this.spineItems.length; + section.index = index; - }); + this.spineItems.push(section); - this.loaded = true; - } + // Encode and Decode href lookups + // see pr for details: https://github.com/futurepress/epub.js/pull/358 + this.spineByHref[decodeURI(section.href)] = index; + this.spineByHref[encodeURI(section.href)] = index; + this.spineByHref[section.href] = index; - /** - * Get an item from the spine - * @param {string|number} [target] - * @return {Section} section - * @example spine.get(); - * @example spine.get(1); - * @example spine.get("chap1.html"); - * @example spine.get("#id1234"); - */ - get(target) { - var index = 0; + this.spineById[section.idref] = index; - if (typeof target === "undefined") { - while (index < this.spineItems.length) { - let next = this.spineItems[index]; - if (next && next.linear) { - break; - } - index += 1; - } - } else if(this.epubcfi.isCfiString(target)) { - let cfi = new EpubCFI(target); - index = cfi.spinePos; - } else if(typeof target === "number" || isNaN(target) === false){ - index = target; - } else if(typeof target === "string" && target.indexOf("#") === 0) { - index = this.spineById[target.substring(1)]; - } else if(typeof target === "string") { - // Remove fragments - target = target.split("#")[0]; - index = this.spineByHref[target] || this.spineByHref[encodeURI(target)]; - } + return index; + } - return this.spineItems[index] || null; - } + /** + * Prepend a Section to the Spine + * @private + * @param {Section} section + */ + prepend(section) { + // var index = this.spineItems.unshift(section); + this.spineByHref[section.href] = 0; + this.spineById[section.idref] = 0; - /** - * Append a Section to the Spine - * @private - * @param {Section} section - */ - append(section) { - var index = this.spineItems.length; - section.index = index; + // Re-index + this.spineItems.forEach(function (item, index) { + item.index = index; + }); - this.spineItems.push(section); + return 0; + } - // Encode and Decode href lookups - // see pr for details: https://github.com/futurepress/epub.js/pull/358 - this.spineByHref[decodeURI(section.href)] = index; - this.spineByHref[encodeURI(section.href)] = index; - this.spineByHref[section.href] = index; + // insert(section, index) { + // + // }; - this.spineById[section.idref] = index; + /** + * Remove a Section from the Spine + * @private + * @param {Section} section + */ + remove(section) { + var index = this.spineItems.indexOf(section); - return index; - } + if (index > -1) { + delete this.spineByHref[section.href]; + delete this.spineById[section.idref]; - /** - * Prepend a Section to the Spine - * @private - * @param {Section} section - */ - prepend(section) { - // var index = this.spineItems.unshift(section); - this.spineByHref[section.href] = 0; - this.spineById[section.idref] = 0; + return this.spineItems.splice(index, 1); + } + } - // Re-index - this.spineItems.forEach(function(item, index){ - item.index = index; - }); + /** + * Loop over the Sections in the Spine + * @return {method} forEach + */ + each() { + return this.spineItems.forEach.apply(this.spineItems, arguments); + } - return 0; - } + /** + * Find the first Section in the Spine + * @return {Section} first section + */ + first() { + let index = 0; - // insert(section, index) { - // - // }; + do { + let next = this.get(index); - /** - * Remove a Section from the Spine - * @private - * @param {Section} section - */ - remove(section) { - var index = this.spineItems.indexOf(section); + if (next && next.linear) { + return next; + } + index += 1; + } while (index < this.spineItems.length); + } - if(index > -1) { - delete this.spineByHref[section.href]; - delete this.spineById[section.idref]; + /** + * Find the last Section in the Spine + * @return {Section} last section + */ + last() { + let index = this.spineItems.length - 1; - return this.spineItems.splice(index, 1); - } - } + do { + let prev = this.get(index); + if (prev && prev.linear) { + return prev; + } + index -= 1; + } while (index >= 0); + } - /** - * Loop over the Sections in the Spine - * @return {method} forEach - */ - each() { - return this.spineItems.forEach.apply(this.spineItems, arguments); - } + destroy() { + this.each((section) => section.destroy()); - /** - * Find the first Section in the Spine - * @return {Section} first section - */ - first() { - let index = 0; + this.spineItems = undefined; + this.spineByHref = undefined; + this.spineById = undefined; - do { - let next = this.get(index); + this.hooks.serialize.clear(); + this.hooks.content.clear(); + this.hooks = undefined; - if (next && next.linear) { - return next; - } - index += 1; - } while (index < this.spineItems.length) ; - } + this.epubcfi = undefined; - /** - * Find the last Section in the Spine - * @return {Section} last section - */ - last() { - let index = this.spineItems.length-1; + this.loaded = false; - do { - let prev = this.get(index); - if (prev && prev.linear) { - return prev; - } - index -= 1; - } while (index >= 0); - } - - destroy() { - this.each((section) => section.destroy()); - - this.spineItems = undefined - this.spineByHref = undefined - this.spineById = undefined - - this.hooks.serialize.clear(); - this.hooks.content.clear(); - this.hooks = undefined; - - this.epubcfi = undefined; - - this.loaded = false; - - this.items = undefined; - this.manifest = undefined; - this.spineNodeIndex = undefined; - this.baseUrl = undefined; - this.length = undefined; - } + this.items = undefined; + this.manifest = undefined; + this.spineNodeIndex = undefined; + this.baseUrl = undefined; + this.length = undefined; + } } export default Spine; diff --git a/src/store.js b/src/store.js index 4775275..acda0c9 100644 --- a/src/store.js +++ b/src/store.js @@ -1,9 +1,9 @@ -import {defer, isXml, parse} from "./utils/core"; -import httpRequest from "./utils/request"; -import mime from "./utils/mime"; -import Path from "./utils/path"; import EventEmitter from "event-emitter"; import localforage from "localforage"; +import { defer, isXml, parse } from "./utils/core"; +import mime from "./utils/mime"; +import Path from "./utils/path"; +import httpRequest from "./utils/request"; /** * Handles saving and requesting files from local storage @@ -13,370 +13,352 @@ import localforage from "localforage"; * @param {function} [resolver] */ class Store { + constructor(name, requester, resolver) { + this.urlCache = {}; + this.storage = undefined; + this.name = name; + this.requester = requester || httpRequest; + this.resolver = resolver; + this.online = true; - constructor(name, requester, resolver) { - this.urlCache = {}; + this.checkRequirements(); + this.addListeners(); + } - this.storage = undefined; + /** + * Checks to see if localForage exists in global namspace, + * Requires localForage if it isn't there + * @private + */ + checkRequirements() { + try { + let store; + if (typeof localforage === "undefined") { + store = localforage; + } + this.storage = store.createInstance({ + name: this.name, + }); + } catch (e) { + throw new Error("localForage lib not loaded"); + } + } - this.name = name; - this.requester = requester || httpRequest; - this.resolver = resolver; + /** + * Add online and offline event listeners + * @private + */ + addListeners() { + this._status = this.status.bind(this); + window.addEventListener("online", this._status); + window.addEventListener("offline", this._status); + } - this.online = true; + /** + * Remove online and offline event listeners + * @private + */ + removeListeners() { + window.removeEventListener("online", this._status); + window.removeEventListener("offline", this._status); + this._status = undefined; + } - this.checkRequirements(); + /** + * Update the online / offline status + * @private + */ + status(event) { + let online = navigator.onLine; + this.online = online; + if (online) { + this.emit("online", this); + } else { + this.emit("offline", this); + } + } - this.addListeners(); - } + /** + * Add all of a book resources to the store + * @param {Resources} resources book resources + * @param {boolean} [force] force resaving resources + * @return {Promise} store objects + */ + add(resources, force) { + let mapped = resources.resources.map((item) => { + let { href } = item; + let url = this.resolver(href); + let encodedUrl = window.encodeURIComponent(url); - /** - * Checks to see if localForage exists in global namspace, - * Requires localForage if it isn't there - * @private - */ - checkRequirements(){ - try { - let store; - if (typeof localforage === "undefined") { - store = localforage; - } - this.storage = store.createInstance({ - name: this.name - }); - } catch (e) { - throw new Error("localForage lib not loaded"); - } - } + return this.storage.getItem(encodedUrl).then((item) => { + if (!item || force) { + return this.requester(url, "binary").then((data) => { + return this.storage.setItem(encodedUrl, data); + }); + } else { + return item; + } + }); + }); + return Promise.all(mapped); + } - /** - * Add online and offline event listeners - * @private - */ - addListeners() { - this._status = this.status.bind(this); - window.addEventListener('online', this._status); - window.addEventListener('offline', this._status); - } + /** + * Put binary data from a url to storage + * @param {string} url a url to request from storage + * @param {boolean} [withCredentials] + * @param {object} [headers] + * @return {Promise} + */ + put(url, withCredentials, headers) { + let encodedUrl = window.encodeURIComponent(url); - /** - * Remove online and offline event listeners - * @private - */ - removeListeners() { - window.removeEventListener('online', this._status); - window.removeEventListener('offline', this._status); - this._status = undefined; - } + return this.storage.getItem(encodedUrl).then((result) => { + if (!result) { + return this.requester(url, "binary", withCredentials, headers).then( + (data) => { + return this.storage.setItem(encodedUrl, data); + } + ); + } + return result; + }); + } - /** - * Update the online / offline status - * @private - */ - status(event) { - let online = navigator.onLine; - this.online = online; - if (online) { - this.emit("online", this); - } else { - this.emit("offline", this); - } - } + /** + * Request a url + * @param {string} url a url to request from storage + * @param {string} [type] specify the type of the returned result + * @param {boolean} [withCredentials] + * @param {object} [headers] + * @return {Promise} + */ + request(url, type, withCredentials, headers) { + if (this.online) { + // From network + return this.requester(url, type, withCredentials, headers).then( + (data) => { + // save to store if not present + this.put(url); + return data; + } + ); + } else { + // From store + return this.retrieve(url, type); + } + } - /** - * Add all of a book resources to the store - * @param {Resources} resources book resources - * @param {boolean} [force] force resaving resources - * @return {Promise} store objects - */ - add(resources, force) { - let mapped = resources.resources.map((item) => { - let { href } = item; - let url = this.resolver(href); - let encodedUrl = window.encodeURIComponent(url); + /** + * Request a url from storage + * @param {string} url a url to request from storage + * @param {string} [type] specify the type of the returned result + * @return {Promise} + */ + retrieve(url, type) { + var response; + var path = new Path(url); - return this.storage.getItem(encodedUrl).then((item) => { - if (!item || force) { - return this.requester(url, "binary") - .then((data) => { - return this.storage.setItem(encodedUrl, data); - }); - } else { - return item; - } - }); + // If type isn't set, determine it from the file extension + if (!type) { + type = path.extension; + } - }); - return Promise.all(mapped); - } + if (type == "blob") { + response = this.getBlob(url); + } else { + response = this.getText(url); + } - /** - * Put binary data from a url to storage - * @param {string} url a url to request from storage - * @param {boolean} [withCredentials] - * @param {object} [headers] - * @return {Promise} - */ - put(url, withCredentials, headers) { - let encodedUrl = window.encodeURIComponent(url); + return response.then((r) => { + var deferred = new defer(); + var result; + if (r) { + result = this.handleResponse(r, type); + deferred.resolve(result); + } else { + deferred.reject({ + message: "File not found in storage: " + url, + stack: new Error().stack, + }); + } + return deferred.promise; + }); + } - return this.storage.getItem(encodedUrl).then((result) => { - if (!result) { - return this.requester(url, "binary", withCredentials, headers).then((data) => { - return this.storage.setItem(encodedUrl, data); - }); - } - return result; - }); - } + /** + * Handle the response from request + * @private + * @param {any} response + * @param {string} [type] + * @return {any} the parsed result + */ + handleResponse(response, type) { + var r; - /** - * Request a url - * @param {string} url a url to request from storage - * @param {string} [type] specify the type of the returned result - * @param {boolean} [withCredentials] - * @param {object} [headers] - * @return {Promise} - */ - request(url, type, withCredentials, headers){ - if (this.online) { - // From network - return this.requester(url, type, withCredentials, headers).then((data) => { - // save to store if not present - this.put(url); - return data; - }) - } else { - // From store - return this.retrieve(url, type); - } + if (type == "json") { + r = JSON.parse(response); + } else if (isXml(type)) { + r = parse(response, "text/xml"); + } else if (type == "xhtml") { + r = parse(response, "application/xhtml+xml"); + } else if (type == "html" || type == "htm") { + r = parse(response, "text/html"); + } else { + r = response; + } - } + return r; + } - /** - * Request a url from storage - * @param {string} url a url to request from storage - * @param {string} [type] specify the type of the returned result - * @return {Promise} - */ - retrieve(url, type) { - var deferred = new defer(); - var response; - var path = new Path(url); + /** + * Get a Blob from Storage by Url + * @param {string} url + * @param {string} [mimeType] + * @return {Blob} + */ + getBlob(url, mimeType) { + let encodedUrl = window.encodeURIComponent(url); - // If type isn't set, determine it from the file extension - if(!type) { - type = path.extension; - } + return this.storage.getItem(encodedUrl).then(function (uint8array) { + if (!uint8array) return; - if(type == "blob"){ - response = this.getBlob(url); - } else { - response = this.getText(url); - } + mimeType = mimeType || mime.lookup(url); + return new Blob([uint8array], { type: mimeType }); + }); + } - return response.then((r) => { - var deferred = new defer(); - var result; - if (r) { - result = this.handleResponse(r, type); - deferred.resolve(result); - } else { - deferred.reject({ - message : "File not found in storage: " + url, - stack : new Error().stack - }); - } - return deferred.promise; - }); - } + /** + * Get Text from Storage by Url + * @param {string} url + * @param {string} [mimeType] + * @return {string} + */ + getText(url, mimeType) { + let encodedUrl = window.encodeURIComponent(url); - /** - * Handle the response from request - * @private - * @param {any} response - * @param {string} [type] - * @return {any} the parsed result - */ - handleResponse(response, type){ - var r; + mimeType = mimeType || mime.lookup(url); - if(type == "json") { - r = JSON.parse(response); - } - else - if(isXml(type)) { - r = parse(response, "text/xml"); - } - else - if(type == "xhtml") { - r = parse(response, "application/xhtml+xml"); - } - else - if(type == "html" || type == "htm") { - r = parse(response, "text/html"); - } else { - r = response; - } + return this.storage.getItem(encodedUrl).then(function (uint8array) { + var deferred = new defer(); + var reader = new FileReader(); + var blob; - return r; - } + if (!uint8array) return; - /** - * Get a Blob from Storage by Url - * @param {string} url - * @param {string} [mimeType] - * @return {Blob} - */ - getBlob(url, mimeType){ - let encodedUrl = window.encodeURIComponent(url); + blob = new Blob([uint8array], { type: mimeType }); - return this.storage.getItem(encodedUrl).then(function(uint8array) { - if(!uint8array) return; + reader.addEventListener("loadend", () => { + deferred.resolve(reader.result); + }); - mimeType = mimeType || mime.lookup(url); + reader.readAsText(blob, mimeType); - return new Blob([uint8array], {type : mimeType}); - }); + return deferred.promise; + }); + } - } + /** + * Get a base64 encoded result from Storage by Url + * @param {string} url + * @param {string} [mimeType] + * @return {string} base64 encoded + */ + getBase64(url, mimeType) { + let encodedUrl = window.encodeURIComponent(url); - /** - * Get Text from Storage by Url - * @param {string} url - * @param {string} [mimeType] - * @return {string} - */ - getText(url, mimeType){ - let encodedUrl = window.encodeURIComponent(url); + mimeType = mimeType || mime.lookup(url); - mimeType = mimeType || mime.lookup(url); + return this.storage.getItem(encodedUrl).then((uint8array) => { + var deferred = new defer(); + var reader = new FileReader(); + var blob; - return this.storage.getItem(encodedUrl).then(function(uint8array) { - var deferred = new defer(); - var reader = new FileReader(); - var blob; + if (!uint8array) return; - if(!uint8array) return; + blob = new Blob([uint8array], { type: mimeType }); - blob = new Blob([uint8array], {type : mimeType}); + reader.addEventListener("loadend", () => { + deferred.resolve(reader.result); + }); + reader.readAsDataURL(blob, mimeType); - reader.addEventListener("loadend", () => { - deferred.resolve(reader.result); - }); + return deferred.promise; + }); + } - reader.readAsText(blob, mimeType); + /** + * Create a Url from a stored item + * @param {string} url + * @param {object} [options.base64] use base64 encoding or blob url + * @return {Promise} url promise with Url string + */ + createUrl(url, options) { + var deferred = new defer(); + var _URL = window.URL || window.webkitURL || window.mozURL; + var tempUrl; + var response; + var useBase64 = options && options.base64; - return deferred.promise; - }); - } + if (url in this.urlCache) { + deferred.resolve(this.urlCache[url]); + return deferred.promise; + } - /** - * Get a base64 encoded result from Storage by Url - * @param {string} url - * @param {string} [mimeType] - * @return {string} base64 encoded - */ - getBase64(url, mimeType){ - let encodedUrl = window.encodeURIComponent(url); + if (useBase64) { + response = this.getBase64(url); - mimeType = mimeType || mime.lookup(url); + if (response) { + response.then( + function (tempUrl) { + this.urlCache[url] = tempUrl; + deferred.resolve(tempUrl); + }.bind(this) + ); + } + } else { + response = this.getBlob(url); - return this.storage.getItem(encodedUrl).then((uint8array) => { - var deferred = new defer(); - var reader = new FileReader(); - var blob; + if (response) { + response.then( + function (blob) { + tempUrl = _URL.createObjectURL(blob); + this.urlCache[url] = tempUrl; + deferred.resolve(tempUrl); + }.bind(this) + ); + } + } - if(!uint8array) return; + if (!response) { + deferred.reject({ + message: "File not found in storage: " + url, + stack: new Error().stack, + }); + } - blob = new Blob([uint8array], {type : mimeType}); + return deferred.promise; + } - reader.addEventListener("loadend", () => { - deferred.resolve(reader.result); - }); - reader.readAsDataURL(blob, mimeType); + /** + * Revoke Temp Url for a archive item + * @param {string} url url of the item in the store + */ + revokeUrl(url) { + var _URL = window.URL || window.webkitURL || window.mozURL; + var fromCache = this.urlCache[url]; + if (fromCache) _URL.revokeObjectURL(fromCache); + } - return deferred.promise; - }); - } - - /** - * Create a Url from a stored item - * @param {string} url - * @param {object} [options.base64] use base64 encoding or blob url - * @return {Promise} url promise with Url string - */ - createUrl(url, options){ - var deferred = new defer(); - var _URL = window.URL || window.webkitURL || window.mozURL; - var tempUrl; - var response; - var useBase64 = options && options.base64; - - if(url in this.urlCache) { - deferred.resolve(this.urlCache[url]); - return deferred.promise; - } - - if (useBase64) { - response = this.getBase64(url); - - if (response) { - response.then(function(tempUrl) { - - this.urlCache[url] = tempUrl; - deferred.resolve(tempUrl); - - }.bind(this)); - - } - - } else { - - response = this.getBlob(url); - - if (response) { - response.then(function(blob) { - - tempUrl = _URL.createObjectURL(blob); - this.urlCache[url] = tempUrl; - deferred.resolve(tempUrl); - - }.bind(this)); - - } - } - - - if (!response) { - deferred.reject({ - message : "File not found in storage: " + url, - stack : new Error().stack - }); - } - - return deferred.promise; - } - - /** - * Revoke Temp Url for a archive item - * @param {string} url url of the item in the store - */ - revokeUrl(url){ - var _URL = window.URL || window.webkitURL || window.mozURL; - var fromCache = this.urlCache[url]; - if(fromCache) _URL.revokeObjectURL(fromCache); - } - - destroy() { - var _URL = window.URL || window.webkitURL || window.mozURL; - for (let fromCache in this.urlCache) { - _URL.revokeObjectURL(fromCache); - } - this.urlCache = {}; - this.removeListeners(); - } + destroy() { + var _URL = window.URL || window.webkitURL || window.mozURL; + for (let fromCache in this.urlCache) { + _URL.revokeObjectURL(fromCache); + } + this.urlCache = {}; + this.removeListeners(); + } } EventEmitter(Store.prototype); diff --git a/src/themes.js b/src/themes.js index 6e37f1d..64cce65 100644 --- a/src/themes.js +++ b/src/themes.js @@ -6,263 +6,271 @@ import Url from "./utils/url"; * @param {Rendition} rendition */ class Themes { - constructor(rendition) { - this.rendition = rendition; - this._themes = { - "default" : { - "rules" : {}, - "url" : "", - "serialized" : "" - } - }; - this._overrides = {}; - this._current = "default"; - this._injected = []; - this.rendition.hooks.content.register(this.inject.bind(this)); - this.rendition.hooks.content.register(this.overrides.bind(this)); + constructor(rendition) { + this.rendition = rendition; + this._themes = { + default: { + rules: {}, + url: "", + serialized: "", + }, + }; + this._overrides = {}; + this._current = "default"; + this._injected = []; + this.rendition.hooks.content.register(this.inject.bind(this)); + this.rendition.hooks.content.register(this.overrides.bind(this)); + } - } + /** + * Add themes to be used by a rendition + * @param {object | Array | string} + * @example themes.register("light", "http://example.com/light.css") + * @example themes.register("light", { "body": { "color": "purple"}}) + * @example themes.register({ "light" : {...}, "dark" : {...}}) + */ + register() { + if (arguments.length === 0) { + return; + } + if (arguments.length === 1 && typeof arguments[0] === "object") { + return this.registerThemes(arguments[0]); + } + if (arguments.length === 1 && typeof arguments[0] === "string") { + return this.default(arguments[0]); + } + if (arguments.length === 2 && typeof arguments[1] === "string") { + return this.registerUrl(arguments[0], arguments[1]); + } + if (arguments.length === 2 && typeof arguments[1] === "object") { + return this.registerRules(arguments[0], arguments[1]); + } + } - /** - * Add themes to be used by a rendition - * @param {object | Array | string} - * @example themes.register("light", "http://example.com/light.css") - * @example themes.register("light", { "body": { "color": "purple"}}) - * @example themes.register({ "light" : {...}, "dark" : {...}}) - */ - register () { - if (arguments.length === 0) { - return; - } - if (arguments.length === 1 && typeof(arguments[0]) === "object") { - return this.registerThemes(arguments[0]); - } - if (arguments.length === 1 && typeof(arguments[0]) === "string") { - return this.default(arguments[0]); - } - if (arguments.length === 2 && typeof(arguments[1]) === "string") { - return this.registerUrl(arguments[0], arguments[1]); - } - if (arguments.length === 2 && typeof(arguments[1]) === "object") { - return this.registerRules(arguments[0], arguments[1]); - } - } + /** + * Add a default theme to be used by a rendition + * @param {object | string} theme + * @example themes.register("http://example.com/default.css") + * @example themes.register({ "body": { "color": "purple"}}) + */ + default(theme) { + if (!theme) { + return; + } + if (typeof theme === "string") { + return this.registerUrl("default", theme); + } + if (typeof theme === "object") { + return this.registerRules("default", theme); + } + } - /** - * Add a default theme to be used by a rendition - * @param {object | string} theme - * @example themes.register("http://example.com/default.css") - * @example themes.register({ "body": { "color": "purple"}}) - */ - default (theme) { - if (!theme) { - return; - } - if (typeof(theme) === "string") { - return this.registerUrl("default", theme); - } - if (typeof(theme) === "object") { - return this.registerRules("default", theme); - } - } + /** + * Register themes object + * @param {object} themes + */ + registerThemes(themes) { + for (var theme in themes) { + if (themes.hasOwnProperty(theme)) { + if (typeof themes[theme] === "string") { + this.registerUrl(theme, themes[theme]); + } else { + this.registerRules(theme, themes[theme]); + } + } + } + } - /** - * Register themes object - * @param {object} themes - */ - registerThemes (themes) { - for (var theme in themes) { - if (themes.hasOwnProperty(theme)) { - if (typeof(themes[theme]) === "string") { - this.registerUrl(theme, themes[theme]); - } else { - this.registerRules(theme, themes[theme]); - } - } - } - } + /** + * Register a theme by passing its css as string + * @param {string} name + * @param {string} css + */ + registerCss(name, css) { + this._themes[name] = { serialized: css }; + if (this._injected[name] || name == "default") { + this.update(name); + } + } - /** - * Register a theme by passing its css as string - * @param {string} name - * @param {string} css - */ - registerCss (name, css) { - this._themes[name] = { "serialized" : css }; - if (this._injected[name] || name == 'default') { - this.update(name); - } - } + /** + * Register a url + * @param {string} name + * @param {string} input + */ + registerUrl(name, input) { + var url = new Url(input); + this._themes[name] = { url: url.toString() }; + if (this._injected[name] || name == "default") { + this.update(name); + } + } - /** - * Register a url - * @param {string} name - * @param {string} input - */ - registerUrl (name, input) { - var url = new Url(input); - this._themes[name] = { "url": url.toString() }; - if (this._injected[name] || name == 'default') { - this.update(name); - } - } + /** + * Register rule + * @param {string} name + * @param {object} rules + */ + registerRules(name, rules) { + this._themes[name] = { rules: rules }; + // TODO: serialize css rules + if (this._injected[name] || name == "default") { + this.update(name); + } + } - /** - * Register rule - * @param {string} name - * @param {object} rules - */ - registerRules (name, rules) { - this._themes[name] = { "rules": rules }; - // TODO: serialize css rules - if (this._injected[name] || name == 'default') { - this.update(name); - } - } + /** + * Select a theme + * @param {string} name + */ + select(name) { + var prev = this._current; + var contents; - /** - * Select a theme - * @param {string} name - */ - select (name) { - var prev = this._current; - var contents; + this._current = name; + this.update(name); - this._current = name; - this.update(name); + contents = this.rendition.getContents(); + contents.forEach((content) => { + content.removeClass(prev); + content.addClass(name); + }); + } - contents = this.rendition.getContents(); - contents.forEach( (content) => { - content.removeClass(prev); - content.addClass(name); - }); - } + /** + * Update a theme + * @param {string} name + */ + update(name) { + var contents = this.rendition.getContents(); + contents.forEach((content) => { + this.add(name, content); + }); + } - /** - * Update a theme - * @param {string} name - */ - update (name) { - var contents = this.rendition.getContents(); - contents.forEach( (content) => { - this.add(name, content); - }); - } + /** + * Inject all themes into contents + * @param {Contents} contents + */ + inject(contents) { + var links = []; + var themes = this._themes; + var theme; - /** - * Inject all themes into contents - * @param {Contents} contents - */ - inject (contents) { - var links = []; - var themes = this._themes; - var theme; + for (var name in themes) { + if ( + themes.hasOwnProperty(name) && + (name === this._current || name === "default") + ) { + theme = themes[name]; + if ( + (theme.rules && Object.keys(theme.rules).length > 0) || + (theme.url && links.indexOf(theme.url) === -1) + ) { + this.add(name, contents); + } + this._injected.push(name); + } + } - for (var name in themes) { - if (themes.hasOwnProperty(name) && (name === this._current || name === "default")) { - theme = themes[name]; - if((theme.rules && Object.keys(theme.rules).length > 0) || (theme.url && links.indexOf(theme.url) === -1)) { - this.add(name, contents); - } - this._injected.push(name); - } - } + if (this._current != "default") { + contents.addClass(this._current); + } + } - if(this._current != "default") { - contents.addClass(this._current); - } - } + /** + * Add Theme to contents + * @param {string} name + * @param {Contents} contents + */ + add(name, contents) { + var theme = this._themes[name]; - /** - * Add Theme to contents - * @param {string} name - * @param {Contents} contents - */ - add (name, contents) { - var theme = this._themes[name]; + if (!theme || !contents) { + return; + } - if (!theme || !contents) { - return; - } + if (theme.url) { + contents.addStylesheet(theme.url); + } else if (theme.serialized) { + contents.addStylesheetCss(theme.serialized, name); + theme.injected = true; + } else if (theme.rules) { + contents.addStylesheetRules(theme.rules, name); + theme.injected = true; + } + } - if (theme.url) { - contents.addStylesheet(theme.url); - } else if (theme.serialized) { - contents.addStylesheetCss(theme.serialized, name); - theme.injected = true; - } else if (theme.rules) { - contents.addStylesheetRules(theme.rules, name); - theme.injected = true; - } - } + /** + * Add override + * @param {string} name + * @param {string} value + * @param {boolean} priority + */ + override(name, value, priority) { + var contents = this.rendition.getContents(); - /** - * Add override - * @param {string} name - * @param {string} value - * @param {boolean} priority - */ - override (name, value, priority) { - var contents = this.rendition.getContents(); + this._overrides[name] = { + value: value, + priority: priority === true, + }; - this._overrides[name] = { - value: value, - priority: priority === true - }; + contents.forEach((content) => { + content.css( + name, + this._overrides[name].value, + this._overrides[name].priority + ); + }); + } - contents.forEach( (content) => { - content.css(name, this._overrides[name].value, this._overrides[name].priority); - }); - } + removeOverride(name) { + var contents = this.rendition.getContents(); - removeOverride (name) { - var contents = this.rendition.getContents(); + delete this._overrides[name]; - delete this._overrides[name]; + contents.forEach((content) => { + content.css(name); + }); + } - contents.forEach( (content) => { - content.css(name); - }); - } + /** + * Add all overrides + * @param {Content} content + */ + overrides(contents) { + var overrides = this._overrides; - /** - * Add all overrides - * @param {Content} content - */ - overrides (contents) { - var overrides = this._overrides; + for (var rule in overrides) { + if (overrides.hasOwnProperty(rule)) { + contents.css(rule, overrides[rule].value, overrides[rule].priority); + } + } + } - for (var rule in overrides) { - if (overrides.hasOwnProperty(rule)) { - contents.css(rule, overrides[rule].value, overrides[rule].priority); - } - } - } + /** + * Adjust the font size of a rendition + * @param {number} size + */ + fontSize(size) { + this.override("font-size", size); + } - /** - * Adjust the font size of a rendition - * @param {number} size - */ - fontSize (size) { - this.override("font-size", size); - } - - /** - * Adjust the font-family of a rendition - * @param {string} f - */ - font (f) { - this.override("font-family", f, true); - } - - destroy() { - this.rendition = undefined; - this._themes = undefined; - this._overrides = undefined; - this._current = undefined; - this._injected = undefined; - } + /** + * Adjust the font-family of a rendition + * @param {string} f + */ + font(f) { + this.override("font-family", f, true); + } + destroy() { + this.rendition = undefined; + this._themes = undefined; + this._overrides = undefined; + this._current = undefined; + this._injected = undefined; + } } export default Themes; diff --git a/src/utils/constants.js b/src/utils/constants.js index ac0a268..66fce45 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,62 +1,73 @@ export const EPUBJS_VERSION = "0.3"; // Dom events to listen for -export const DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "mousemove", "click", "touchend", "touchstart", "touchmove"]; +export const DOM_EVENTS = [ + "keydown", + "keyup", + "keypressed", + "mouseup", + "mousedown", + "mousemove", + "click", + "touchend", + "touchstart", + "touchmove", +]; export const EVENTS = { - BOOK : { - OPEN_FAILED : "openFailed" + BOOK: { + OPEN_FAILED: "openFailed", }, - CONTENTS : { - EXPAND : "expand", - RESIZE : "resize", - SELECTED : "selected", - SELECTED_RANGE : "selectedRange", - LINK_CLICKED : "linkClicked" + CONTENTS: { + EXPAND: "expand", + RESIZE: "resize", + SELECTED: "selected", + SELECTED_RANGE: "selectedRange", + LINK_CLICKED: "linkClicked", }, - LOCATIONS : { - CHANGED : "changed" + LOCATIONS: { + CHANGED: "changed", }, - MANAGERS : { - RESIZE : "resize", - RESIZED : "resized", - ORIENTATION_CHANGE : "orientationchange", - ADDED : "added", - SCROLL : "scroll", - SCROLLED : "scrolled", - REMOVED : "removed", + MANAGERS: { + RESIZE: "resize", + RESIZED: "resized", + ORIENTATION_CHANGE: "orientationchange", + ADDED: "added", + SCROLL: "scroll", + SCROLLED: "scrolled", + REMOVED: "removed", }, - VIEWS : { + VIEWS: { AXIS: "axis", WRITING_MODE: "writingMode", - LOAD_ERROR : "loaderror", - RENDERED : "rendered", - RESIZED : "resized", - DISPLAYED : "displayed", - SHOWN : "shown", - HIDDEN : "hidden", - MARK_CLICKED : "markClicked" + LOAD_ERROR: "loaderror", + RENDERED: "rendered", + RESIZED: "resized", + DISPLAYED: "displayed", + SHOWN: "shown", + HIDDEN: "hidden", + MARK_CLICKED: "markClicked", }, - RENDITION : { - STARTED : "started", - ATTACHED : "attached", - DISPLAYED : "displayed", - DISPLAY_ERROR : "displayerror", - RENDERED : "rendered", - REMOVED : "removed", - RESIZED : "resized", - ORIENTATION_CHANGE : "orientationchange", - LOCATION_CHANGED : "locationChanged", - RELOCATED : "relocated", - MARK_CLICKED : "markClicked", - SELECTED : "selected", - LAYOUT: "layout" + RENDITION: { + STARTED: "started", + ATTACHED: "attached", + DISPLAYED: "displayed", + DISPLAY_ERROR: "displayerror", + RENDERED: "rendered", + REMOVED: "removed", + RESIZED: "resized", + ORIENTATION_CHANGE: "orientationchange", + LOCATION_CHANGED: "locationChanged", + RELOCATED: "relocated", + MARK_CLICKED: "markClicked", + SELECTED: "selected", + LAYOUT: "layout", }, - LAYOUT : { - UPDATED : "updated" + LAYOUT: { + UPDATED: "updated", }, - ANNOTATION : { - ATTACH : "attach", - DETACH : "detach" - } -} + ANNOTATION: { + ATTACH: "attach", + DETACH: "detach", + }, +}; diff --git a/src/utils/core.js b/src/utils/core.js index 9490207..d07edc8 100644 --- a/src/utils/core.js +++ b/src/utils/core.js @@ -1,7 +1,7 @@ /** * Core Utilities and Helpers * @module Core -*/ + */ import { DOMParser as XMLDOMParser } from "@xmldom/xmldom"; /** @@ -9,12 +9,21 @@ import { DOMParser as XMLDOMParser } from "@xmldom/xmldom"; * @returns {function} requestAnimationFrame * @memberof Core */ -export const requestAnimationFrame = (typeof window != "undefined") ? (window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame) : false; +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); +const _URL = + typeof URL != "undefined" + ? URL + : typeof window != "undefined" + ? window.URL || window.webkitURL || window.mozURL + : undefined; /** * Generates a UUID @@ -23,13 +32,16 @@ const _URL = typeof URL != "undefined" ? URL : (typeof window != "undefined" ? ( * @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; + 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; } /** @@ -38,13 +50,13 @@ export function uuid() { * @memberof Core */ export function documentHeight() { - return Math.max( - document.documentElement.clientHeight, - document.body.scrollHeight, - document.documentElement.scrollHeight, - document.body.offsetHeight, - document.documentElement.offsetHeight - ); + return Math.max( + document.documentElement.clientHeight, + document.body.scrollHeight, + document.documentElement.scrollHeight, + document.body.offsetHeight, + document.documentElement.offsetHeight + ); } /** @@ -54,7 +66,7 @@ export function documentHeight() { * @memberof Core */ export function isElement(obj) { - return !!(obj && obj.nodeType == 1); + return !!(obj && obj.nodeType == 1); } /** @@ -63,7 +75,7 @@ export function isElement(obj) { * @memberof Core */ export function isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); + return !isNaN(parseFloat(n)) && isFinite(n); } /** @@ -72,17 +84,17 @@ export function isNumber(n) { * @memberof Core */ export function isFloat(n) { - let f = parseFloat(n); + let f = parseFloat(n); - if (isNumber(n) === false) { - return false; - } + if (isNumber(n) === false) { + return false; + } - if (typeof n === "string" && n.indexOf(".") > -1) { - return true; - } + if (typeof n === "string" && n.indexOf(".") > -1) { + return true; + } - return Math.floor(f) !== f; + return Math.floor(f) !== f; } /** @@ -92,22 +104,25 @@ export function isFloat(n) { * @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; + 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; - } + 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; - } - } + for (var i = 0; i < length; i++) { + if (typeof document.body.style[prefixes[i] + lower] != "undefined") { + return prefixes[i] + lower; + } + } - return unprefixed; + return unprefixed; } /** @@ -117,13 +132,13 @@ export function prefixed(unprefixed) { * @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; + 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; } /** @@ -133,14 +148,18 @@ export function defaults(obj) { * @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; + 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; } /** @@ -153,10 +172,10 @@ export function extend(target) { * @memberof Core */ export function insert(item, array, compareFunction) { - var location = locationOf(item, array, compareFunction); - array.splice(location, 0, item); + var location = locationOf(item, array, compareFunction); + array.splice(location, 0, item); - return location; + return location; } /** @@ -170,33 +189,33 @@ export function insert(item, array, compareFunction) { * @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; - } + 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); - } + 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); + } } /** @@ -211,33 +230,33 @@ export function locationOf(item, array, compareFunction, _start, _end) { * @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 - } + 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); - } + 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 @@ -247,27 +266,41 @@ export function indexOfSorted(item, array, compareFunction, _start, _end) { * @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 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; - var width = 0; - var height = 0; + widthProps.forEach(function (prop) { + width += parseFloat(style[prop]) || 0; + }); - widthProps.forEach(function(prop){ - width += parseFloat(style[prop]) || 0; - }); - - heightProps.forEach(function(prop){ - height += parseFloat(style[prop]) || 0; - }); - - return { - height: height, - width: width - }; + heightProps.forEach(function (prop) { + height += parseFloat(style[prop]) || 0; + }); + return { + height: height, + width: width, + }; } /** @@ -278,27 +311,39 @@ export function bounds(el) { * @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 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; - var width = 0; - var height = 0; + widthProps.forEach(function (prop) { + width += parseFloat(style[prop]) || 0; + }); - widthProps.forEach(function(prop){ - width += parseFloat(style[prop]) || 0; - }); - - heightProps.forEach(function(prop){ - height += parseFloat(style[prop]) || 0; - }); - - return { - height: height, - width: width - }; + heightProps.forEach(function (prop) { + height += parseFloat(style[prop]) || 0; + }); + return { + height: height, + width: width, + }; } /** @@ -309,16 +354,16 @@ export function borders(el) { * @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; + 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; } /** @@ -327,19 +372,17 @@ export function nodeBounds(node) { * @memberof Core */ export function windowBounds() { + var width = window.innerWidth; + var height = window.innerHeight; - var width = window.innerWidth; - var height = window.innerHeight; - - return { - top: 0, - left: 0, - right: width, - bottom: height, - width: width, - height: height - }; - + return { + top: 0, + left: 0, + right: width, + bottom: height, + width: width, + height: height, + }; } /** @@ -350,19 +393,19 @@ export function windowBounds() { * @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; - } + 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; + return index; } /** @@ -372,7 +415,7 @@ export function indexOfNode(node, typeId) { * @memberof Core */ export function indexOfTextNode(textNode) { - return indexOfNode(textNode, TEXT_NODE); + return indexOfNode(textNode, TEXT_NODE); } /** @@ -382,7 +425,7 @@ export function indexOfTextNode(textNode) { * @memberof Core */ export function indexOfElementNode(elementNode) { - return indexOfNode(elementNode, ELEMENT_NODE); + return indexOfNode(elementNode, ELEMENT_NODE); } /** @@ -392,7 +435,7 @@ export function indexOfElementNode(elementNode) { * @memberof Core */ export function isXml(ext) { - return ["xml", "opf", "ncx"].indexOf(ext) > -1; + return ["xml", "opf", "ncx"].indexOf(ext) > -1; } /** @@ -402,8 +445,8 @@ export function isXml(ext) { * @returns {Blob} * @memberof Core */ -export function createBlob(content, mime){ - return new Blob([content], {type : mime }); +export function createBlob(content, mime) { + return new Blob([content], { type: mime }); } /** @@ -413,13 +456,13 @@ export function createBlob(content, mime){ * @returns {string} url * @memberof Core */ -export function createBlobUrl(content, mime){ - var tempUrl; - var blob = createBlob(content, mime); +export function createBlobUrl(content, mime) { + var tempUrl; + var blob = createBlob(content, mime); - tempUrl = _URL.createObjectURL(blob); + tempUrl = _URL.createObjectURL(blob); - return tempUrl; + return tempUrl; } /** @@ -427,8 +470,8 @@ export function createBlobUrl(content, mime){ * @param {string} url * @memberof Core */ -export function revokeBlobUrl(url){ - return _URL.revokeObjectURL(url); +export function revokeBlobUrl(url) { + return _URL.revokeObjectURL(url); } /** @@ -438,20 +481,20 @@ export function revokeBlobUrl(url){ * @returns {string} url * @memberof Core */ -export function createBase64Url(content, mime){ - var data; - var datauri; +export function createBase64Url(content, mime) { + var data; + var datauri; - if (typeof(content) !== "string") { - // Only handles strings - return; - } + if (typeof content !== "string") { + // Only handles strings + return; + } - data = btoa(content); + data = btoa(content); - datauri = "data:" + mime + ";base64," + data; + datauri = "data:" + mime + ";base64," + data; - return datauri; + return datauri; } /** @@ -460,8 +503,8 @@ export function createBase64Url(content, mime){ * @returns {string} type * @memberof Core */ -export function type(obj){ - return Object.prototype.toString.call(obj).slice(8, -1); +export function type(obj) { + return Object.prototype.toString.call(obj).slice(8, -1); } /** @@ -473,24 +516,24 @@ export function type(obj){ * @memberof Core */ export function parse(markup, mime, forceXMLDom) { - var doc; - var Parser; + var doc; + var Parser; - if (typeof DOMParser === "undefined" || forceXMLDom) { - Parser = XMLDOMParser; - } else { - Parser = DOMParser; - } + 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); - } + // 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); + doc = new Parser().parseFromString(markup, mime); - return doc; + return doc; } /** @@ -501,19 +544,19 @@ export function parse(markup, mime, forceXMLDom) { * @memberof Core */ export function qs(el, sel) { - var elements; - if (!el) { - throw new Error("No Element Provided"); - } + 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]; - } - } + if (typeof el.querySelector != "undefined") { + return el.querySelector(sel); + } else { + elements = el.getElementsByTagName(sel); + if (elements.length) { + return elements[0]; + } + } } /** @@ -524,12 +567,11 @@ export function qs(el, sel) { * @memberof Core */ export function qsa(el, sel) { - - if (typeof el.querySelector != "undefined") { - return el.querySelectorAll(sel); - } else { - return el.getElementsByTagName(sel); - } + if (typeof el.querySelector != "undefined") { + return el.querySelectorAll(sel); + } else { + return el.getElementsByTagName(sel); + } } /** @@ -541,29 +583,29 @@ export function qsa(el, sel) { * @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; - }); + 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]; - } - } + if (filtered) { + return filtered[0]; + } + } } /** @@ -573,16 +615,21 @@ export function qsp(el, sel, props) { * @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); - } + 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 + ); + } } /** @@ -593,11 +640,11 @@ export function sprint(root, func) { * @param {function | object} filter function 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); - } + var treeWalker = document.createTreeWalker(root, filter, null, false); + let node; + while ((node = treeWalker.nextNode())) { + func(node); + } } /** @@ -605,20 +652,20 @@ export function treeWalker(root, func, filter) { * @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); - } +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); + } } /** @@ -628,50 +675,49 @@ export function walk(node,callback){ * @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); - }; - }); + 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 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 associated 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; + /* A method to reject the associated 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(); + 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); + /* A newly created Pomise object. + * Initially in pending state. + */ + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + Object.freeze(this); } /** @@ -682,23 +728,26 @@ export function defer() { * @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; - } +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; + } } /** @@ -708,15 +757,15 @@ export function querySelectorByType(html, element, type){ * @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; + 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; } /** @@ -726,11 +775,11 @@ export function findChildren(el) { * @memberof Core */ export function parents(node) { - var nodes = [node]; - for (; node; node = node.parentNode) { - nodes.unshift(node); - } - return nodes + var nodes = [node]; + for (; node; node = node.parentNode) { + nodes.unshift(node); + } + return nodes; } /** @@ -742,21 +791,21 @@ export function parents(node) { * @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; - } + 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; + } } /** @@ -767,15 +816,15 @@ export function filterChildren(el, nodeName, single) { * @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; - } + let parent; + if (node === null || tagname === "") return; + parent = node.parentNode; + while (parent.nodeType === 1) { + if (parent.tagName.toLowerCase() === tagname) { + return parent; + } + parent = parent.parentNode; + } } /** @@ -784,93 +833,96 @@ export function getParentByTagName(node, tagname) { * @memberof Core */ export class RangeObject { - constructor() { - this.collapsed = false; - this.commonAncestorContainer = undefined; - this.endContainer = undefined; - this.endOffset = undefined; - this.startContainer = undefined; - this.startOffset = undefined; - } + 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; + setStart(startNode, startOffset) { + this.startContainer = startNode; + this.startOffset = startOffset; - if (!this.endContainer) { - this.collapse(true); - } else { - this.commonAncestorContainer = this._commonAncestorContainer(); - } + if (!this.endContainer) { + this.collapse(true); + } else { + this.commonAncestorContainer = this._commonAncestorContainer(); + } - this._checkCollapsed(); - } + this._checkCollapsed(); + } - setEnd(endNode, endOffset) { - this.endContainer = endNode; - this.endOffset = endOffset; + setEnd(endNode, endOffset) { + this.endContainer = endNode; + this.endOffset = endOffset; - if (!this.startContainer) { - this.collapse(false); - } else { - this.collapsed = false; - this.commonAncestorContainer = this._commonAncestorContainer(); - } + if (!this.startContainer) { + this.collapse(false); + } else { + this.collapsed = false; + this.commonAncestorContainer = this._commonAncestorContainer(); + } - this._checkCollapsed(); - } + 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; - } - } + 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); - } + 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); - } + selectNodeContents(referenceNode) { + 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); + _commonAncestorContainer(startContainer, endContainer) { + var startParents = parents(startContainer || this.startContainer); + var endParents = parents(endContainer || this.endContainer); - if (startParents[0] != endParents[0]) return undefined; + if (startParents[0] != endParents[0]) return undefined; - for (var i = 0; i < startParents.length; i++) { - if (startParents[i] != endParents[i]) { - return startParents[i - 1]; - } - } - } + 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; - } - } + _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 - } + toString() { + // TODO: implement walking between start and end to find text + } } diff --git a/src/utils/hook.js b/src/utils/hook.js index 85f4463..6337d59 100644 --- a/src/utils/hook.js +++ b/src/utils/hook.js @@ -6,77 +6,76 @@ * @example this.content = new EPUBJS.Hook(this); */ class Hook { - constructor(context){ - this.context = context || this; - this.hooks = []; - } + constructor(context) { + this.context = context || this; + this.hooks = []; + } - /** - * Adds a function to be run before a hook completes - * @example this.content.register(function(){...}); - */ - register(){ - for(var i = 0; i < arguments.length; ++i) { - if (typeof arguments[i] === "function") { - this.hooks.push(arguments[i]); - } else { - // unpack array - for(var j = 0; j < arguments[i].length; ++j) { - this.hooks.push(arguments[i][j]); - } - } - } - } + /** + * Adds a function to be run before a hook completes + * @example this.content.register(function(){...}); + */ + register() { + for (var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") { + this.hooks.push(arguments[i]); + } else { + // unpack array + for (var j = 0; j < arguments[i].length; ++j) { + this.hooks.push(arguments[i][j]); + } + } + } + } - /** - * Removes a function - * @example this.content.deregister(function(){...}); - */ - deregister(func){ - let hook; - for (let i = 0; i < this.hooks.length; i++) { - hook = this.hooks[i]; - if (hook === func) { - this.hooks.splice(i, 1); - break; - } - } - } + /** + * Removes a function + * @example this.content.deregister(function(){...}); + */ + deregister(func) { + let hook; + for (let i = 0; i < this.hooks.length; i++) { + hook = this.hooks[i]; + if (hook === func) { + this.hooks.splice(i, 1); + break; + } + } + } - /** - * Triggers a hook to run all functions - * @example this.content.trigger(args).then(function(){...}); - */ - trigger(){ - var args = arguments; - var context = this.context; - var promises = []; + /** + * Triggers a hook to run all functions + * @example this.content.trigger(args).then(function(){...}); + */ + trigger() { + var args = arguments; + var context = this.context; + var promises = []; - this.hooks.forEach(function(task) { - try { - var executing = task.apply(context, args); - } catch (err) { - console.log(err); - } + this.hooks.forEach(function (task) { + try { + var executing = task.apply(context, args); + } catch (err) { + console.error(err); + } - if(executing && typeof executing["then"] === "function") { - // Task is a function that returns a promise - promises.push(executing); - } - // Otherwise Task resolves immediately, continue - }); + if (executing && typeof executing["then"] === "function") { + // Task is a function that returns a promise + promises.push(executing); + } + // Otherwise Task resolves immediately, continue + }); + return Promise.all(promises); + } - return Promise.all(promises); - } + // Adds a function to be run before a hook completes + list() { + return this.hooks; + } - // Adds a function to be run before a hook completes - list(){ - return this.hooks; - } - - clear(){ - return this.hooks = []; - } + clear() { + return (this.hooks = []); + } } export default Hook; diff --git a/src/utils/mime.js b/src/utils/mime.js index 8f4cca3..06634c0 100755 --- a/src/utils/mime.js +++ b/src/utils/mime.js @@ -4,166 +4,173 @@ edited down */ var table = { - "application" : { - "ecmascript" : [ "es", "ecma" ], - "javascript" : "js", - "ogg" : "ogx", - "pdf" : "pdf", - "postscript" : [ "ps", "ai", "eps", "epsi", "epsf", "eps2", "eps3" ], - "rdf+xml" : "rdf", - "smil" : [ "smi", "smil" ], - "xhtml+xml" : [ "xhtml", "xht" ], - "xml" : [ "xml", "xsl", "xsd", "opf", "ncx" ], - "zip" : "zip", - "x-httpd-eruby" : "rhtml", - "x-latex" : "latex", - "x-maker" : [ "frm", "maker", "frame", "fm", "fb", "book", "fbdoc" ], - "x-object" : "o", - "x-shockwave-flash" : [ "swf", "swfl" ], - "x-silverlight" : "scr", - "epub+zip" : "epub", - "font-tdpfr" : "pfr", - "inkml+xml" : [ "ink", "inkml" ], - "json" : "json", - "jsonml+json" : "jsonml", - "mathml+xml" : "mathml", - "metalink+xml" : "metalink", - "mp4" : "mp4s", - // "oebps-package+xml" : "opf", - "omdoc+xml" : "omdoc", - "oxps" : "oxps", - "vnd.amazon.ebook" : "azw", - "widget" : "wgt", - // "x-dtbncx+xml" : "ncx", - "x-dtbook+xml" : "dtb", - "x-dtbresource+xml" : "res", - "x-font-bdf" : "bdf", - "x-font-ghostscript" : "gsf", - "x-font-linux-psf" : "psf", - "x-font-otf" : "otf", - "x-font-pcf" : "pcf", - "x-font-snf" : "snf", - "x-font-ttf" : [ "ttf", "ttc" ], - "x-font-type1" : [ "pfa", "pfb", "pfm", "afm" ], - "x-font-woff" : "woff", - "x-mobipocket-ebook" : [ "prc", "mobi" ], - "x-mspublisher" : "pub", - "x-nzb" : "nzb", - "x-tgif" : "obj", - "xaml+xml" : "xaml", - "xml-dtd" : "dtd", - "xproc+xml" : "xpl", - "xslt+xml" : "xslt", - "internet-property-stream" : "acx", - "x-compress" : "z", - "x-compressed" : "tgz", - "x-gzip" : "gz", - }, - "audio" : { - "flac" : "flac", - "midi" : [ "mid", "midi", "kar", "rmi" ], - "mpeg" : [ "mpga", "mpega", "mp2", "mp3", "m4a", "mp2a", "m2a", "m3a" ], - "mpegurl" : "m3u", - "ogg" : [ "oga", "ogg", "spx" ], - "x-aiff" : [ "aif", "aiff", "aifc" ], - "x-ms-wma" : "wma", - "x-wav" : "wav", - "adpcm" : "adp", - "mp4" : "mp4a", - "webm" : "weba", - "x-aac" : "aac", - "x-caf" : "caf", - "x-matroska" : "mka", - "x-pn-realaudio-plugin" : "rmp", - "xm" : "xm", - "mid" : [ "mid", "rmi" ] - }, - "image" : { - "gif" : "gif", - "ief" : "ief", - "jpeg" : [ "jpeg", "jpg", "jpe" ], - "pcx" : "pcx", - "png" : "png", - "svg+xml" : [ "svg", "svgz" ], - "tiff" : [ "tiff", "tif" ], - "x-icon" : "ico", - "bmp" : "bmp", - "webp" : "webp", - "x-pict" : [ "pic", "pct" ], - "x-tga" : "tga", - "cis-cod" : "cod" - }, - "text" : { - "cache-manifest" : [ "manifest", "appcache" ], - "css" : "css", - "csv" : "csv", - "html" : [ "html", "htm", "shtml", "stm" ], - "mathml" : "mml", - "plain" : [ "txt", "text", "brf", "conf", "def", "list", "log", "in", "bas" ], - "richtext" : "rtx", - "tab-separated-values" : "tsv", - "x-bibtex" : "bib" - }, - "video" : { - "mpeg" : [ "mpeg", "mpg", "mpe", "m1v", "m2v", "mp2", "mpa", "mpv2" ], - "mp4" : [ "mp4", "mp4v", "mpg4" ], - "quicktime" : [ "qt", "mov" ], - "ogg" : "ogv", - "vnd.mpegurl" : [ "mxu", "m4u" ], - "x-flv" : "flv", - "x-la-asf" : [ "lsf", "lsx" ], - "x-mng" : "mng", - "x-ms-asf" : [ "asf", "asx", "asr" ], - "x-ms-wm" : "wm", - "x-ms-wmv" : "wmv", - "x-ms-wmx" : "wmx", - "x-ms-wvx" : "wvx", - "x-msvideo" : "avi", - "x-sgi-movie" : "movie", - "x-matroska" : [ "mpv", "mkv", "mk3d", "mks" ], - "3gpp2" : "3g2", - "h261" : "h261", - "h263" : "h263", - "h264" : "h264", - "jpeg" : "jpgv", - "jpm" : [ "jpm", "jpgm" ], - "mj2" : [ "mj2", "mjp2" ], - "vnd.ms-playready.media.pyv" : "pyv", - "vnd.uvvu.mp4" : [ "uvu", "uvvu" ], - "vnd.vivo" : "viv", - "webm" : "webm", - "x-f4v" : "f4v", - "x-m4v" : "m4v", - "x-ms-vob" : "vob", - "x-smv" : "smv" - } + application: { + ecmascript: ["es", "ecma"], + javascript: "js", + ogg: "ogx", + pdf: "pdf", + postscript: ["ps", "ai", "eps", "epsi", "epsf", "eps2", "eps3"], + "rdf+xml": "rdf", + smil: ["smi", "smil"], + "xhtml+xml": ["xhtml", "xht"], + xml: ["xml", "xsl", "xsd", "opf", "ncx"], + zip: "zip", + "x-httpd-eruby": "rhtml", + "x-latex": "latex", + "x-maker": ["frm", "maker", "frame", "fm", "fb", "book", "fbdoc"], + "x-object": "o", + "x-shockwave-flash": ["swf", "swfl"], + "x-silverlight": "scr", + "epub+zip": "epub", + "font-tdpfr": "pfr", + "inkml+xml": ["ink", "inkml"], + json: "json", + "jsonml+json": "jsonml", + "mathml+xml": "mathml", + "metalink+xml": "metalink", + mp4: "mp4s", + // "oebps-package+xml" : "opf", + "omdoc+xml": "omdoc", + oxps: "oxps", + "vnd.amazon.ebook": "azw", + widget: "wgt", + // "x-dtbncx+xml" : "ncx", + "x-dtbook+xml": "dtb", + "x-dtbresource+xml": "res", + "x-font-bdf": "bdf", + "x-font-ghostscript": "gsf", + "x-font-linux-psf": "psf", + "x-font-otf": "otf", + "x-font-pcf": "pcf", + "x-font-snf": "snf", + "x-font-ttf": ["ttf", "ttc"], + "x-font-type1": ["pfa", "pfb", "pfm", "afm"], + "x-font-woff": "woff", + "x-mobipocket-ebook": ["prc", "mobi"], + "x-mspublisher": "pub", + "x-nzb": "nzb", + "x-tgif": "obj", + "xaml+xml": "xaml", + "xml-dtd": "dtd", + "xproc+xml": "xpl", + "xslt+xml": "xslt", + "internet-property-stream": "acx", + "x-compress": "z", + "x-compressed": "tgz", + "x-gzip": "gz", + }, + audio: { + flac: "flac", + midi: ["mid", "midi", "kar", "rmi"], + mpeg: ["mpga", "mpega", "mp2", "mp3", "m4a", "mp2a", "m2a", "m3a"], + mpegurl: "m3u", + ogg: ["oga", "ogg", "spx"], + "x-aiff": ["aif", "aiff", "aifc"], + "x-ms-wma": "wma", + "x-wav": "wav", + adpcm: "adp", + mp4: "mp4a", + webm: "weba", + "x-aac": "aac", + "x-caf": "caf", + "x-matroska": "mka", + "x-pn-realaudio-plugin": "rmp", + xm: "xm", + mid: ["mid", "rmi"], + }, + image: { + gif: "gif", + ief: "ief", + jpeg: ["jpeg", "jpg", "jpe"], + pcx: "pcx", + png: "png", + "svg+xml": ["svg", "svgz"], + tiff: ["tiff", "tif"], + "x-icon": "ico", + bmp: "bmp", + webp: "webp", + "x-pict": ["pic", "pct"], + "x-tga": "tga", + "cis-cod": "cod", + }, + text: { + "cache-manifest": ["manifest", "appcache"], + css: "css", + csv: "csv", + html: ["html", "htm", "shtml", "stm"], + mathml: "mml", + plain: ["txt", "text", "brf", "conf", "def", "list", "log", "in", "bas"], + richtext: "rtx", + "tab-separated-values": "tsv", + "x-bibtex": "bib", + }, + video: { + mpeg: ["mpeg", "mpg", "mpe", "m1v", "m2v", "mp2", "mpa", "mpv2"], + mp4: ["mp4", "mp4v", "mpg4"], + quicktime: ["qt", "mov"], + ogg: "ogv", + "vnd.mpegurl": ["mxu", "m4u"], + "x-flv": "flv", + "x-la-asf": ["lsf", "lsx"], + "x-mng": "mng", + "x-ms-asf": ["asf", "asx", "asr"], + "x-ms-wm": "wm", + "x-ms-wmv": "wmv", + "x-ms-wmx": "wmx", + "x-ms-wvx": "wvx", + "x-msvideo": "avi", + "x-sgi-movie": "movie", + "x-matroska": ["mpv", "mkv", "mk3d", "mks"], + "3gpp2": "3g2", + h261: "h261", + h263: "h263", + h264: "h264", + jpeg: "jpgv", + jpm: ["jpm", "jpgm"], + mj2: ["mj2", "mjp2"], + "vnd.ms-playready.media.pyv": "pyv", + "vnd.uvvu.mp4": ["uvu", "uvvu"], + "vnd.vivo": "viv", + webm: "webm", + "x-f4v": "f4v", + "x-m4v": "m4v", + "x-ms-vob": "vob", + "x-smv": "smv", + }, }; -var mimeTypes = (function() { - var type, subtype, val, index, mimeTypes = {}; - for (type in table) { - if (table.hasOwnProperty(type)) { - for (subtype in table[type]) { - if (table[type].hasOwnProperty(subtype)) { - val = table[type][subtype]; - if (typeof val == "string") { - mimeTypes[val] = type + "/" + subtype; - } else { - for (index = 0; index < val.length; index++) { - mimeTypes[val[index]] = type + "/" + subtype; - } - } - } - } - } - } - return mimeTypes; +var mimeTypes = (function () { + var type, + subtype, + val, + index, + mimeTypes = {}; + for (type in table) { + if (table.hasOwnProperty(type)) { + for (subtype in table[type]) { + if (table[type].hasOwnProperty(subtype)) { + val = table[type][subtype]; + if (typeof val == "string") { + mimeTypes[val] = type + "/" + subtype; + } else { + for (index = 0; index < val.length; index++) { + mimeTypes[val[index]] = type + "/" + subtype; + } + } + } + } + } + } + return mimeTypes; })(); -var defaultValue = "text/plain";//"application/octet-stream"; +var defaultValue = "text/plain"; //"application/octet-stream"; function lookup(filename) { - return filename && mimeTypes[filename.split(".").pop().toLowerCase()] || defaultValue; -}; + return ( + (filename && mimeTypes[filename.split(".").pop().toLowerCase()]) || + defaultValue + ); +} export default { lookup }; diff --git a/src/utils/path.js b/src/utils/path.js index 6a060cb..d6403f5 100644 --- a/src/utils/path.js +++ b/src/utils/path.js @@ -8,95 +8,94 @@ import path from "path-webpack"; * @class */ class Path { - constructor(pathString) { - var protocol; - var parsed; + constructor(pathString) { + var protocol; + var parsed; - protocol = pathString.indexOf("://"); - if (protocol > -1) { - pathString = new URL(pathString).pathname; - } + protocol = pathString.indexOf("://"); + if (protocol > -1) { + pathString = new URL(pathString).pathname; + } - parsed = this.parse(pathString); + parsed = this.parse(pathString); - this.path = pathString; + this.path = pathString; - if (this.isDirectory(pathString)) { - this.directory = pathString; - } else { - this.directory = parsed.dir + "/"; - } + if (this.isDirectory(pathString)) { + this.directory = pathString; + } else { + this.directory = parsed.dir + "/"; + } - this.filename = parsed.base; - this.extension = parsed.ext.slice(1); + this.filename = parsed.base; + this.extension = parsed.ext.slice(1); + } - } + /** + * Parse the path: https://nodejs.org/api/path.html#path_path_parse_path + * @param {string} what + * @returns {object} + */ + parse(what) { + return path.parse(what); + } - /** - * Parse the path: https://nodejs.org/api/path.html#path_path_parse_path - * @param {string} what - * @returns {object} - */ - parse (what) { - return path.parse(what); - } + /** + * @param {string} what + * @returns {boolean} + */ + isAbsolute(what) { + return path.isAbsolute(what || this.path); + } - /** - * @param {string} what - * @returns {boolean} - */ - isAbsolute (what) { - return path.isAbsolute(what || this.path); - } + /** + * Check if path ends with a directory + * @param {string} what + * @returns {boolean} + */ + isDirectory(what) { + return what.charAt(what.length - 1) === "/"; + } - /** - * Check if path ends with a directory - * @param {string} what - * @returns {boolean} - */ - isDirectory (what) { - return (what.charAt(what.length-1) === "/"); - } + /** + * Resolve a path against the directory of the Path + * + * https://nodejs.org/api/path.html#path_path_resolve_paths + * @param {string} what + * @returns {string} resolved + */ + resolve(what) { + return path.resolve(this.directory, what); + } - /** - * Resolve a path against the directory of the Path - * - * https://nodejs.org/api/path.html#path_path_resolve_paths - * @param {string} what - * @returns {string} resolved - */ - resolve (what) { - return path.resolve(this.directory, what); - } + /** + * Resolve a path relative to the directory of the Path + * + * https://nodejs.org/api/path.html#path_path_relative_from_to + * @param {string} what + * @returns {string} relative + */ + relative(what) { + var isAbsolute = what && what.indexOf("://") > -1; - /** - * Resolve a path relative to the directory of the Path - * - * https://nodejs.org/api/path.html#path_path_relative_from_to - * @param {string} what - * @returns {string} relative - */ - relative (what) { - var isAbsolute = what && (what.indexOf("://") > -1); + if (isAbsolute) { + return what; + } - if (isAbsolute) { - return what; - } + return path.relative(this.directory, what); + } - return path.relative(this.directory, what); - } + splitPath(filename) { + return this.splitPathRe.exec(filename).slice(1); + } - splitPath(filename) { - return this.splitPathRe.exec(filename).slice(1); - } - - /** - * Return the path string - * @returns {string} path - */ - toString () { - return this.path; - } + /** + * Return the path string + * @returns {string} path + */ + toString() { + return this.path; + } } export default Path; diff --git a/src/utils/queue.js b/src/utils/queue.js index 1f8a18a..fa448eb 100644 --- a/src/utils/queue.js +++ b/src/utils/queue.js @@ -1,4 +1,4 @@ -import {defer, requestAnimationFrame} from "./core"; +import { defer, requestAnimationFrame } from "./core"; /** * Queue for handling tasks one at a time @@ -6,205 +6,194 @@ import {defer, requestAnimationFrame} from "./core"; * @param {scope} context what this will resolve to in the tasks */ class Queue { - constructor(context){ - this._q = []; - this.context = context; - this.tick = requestAnimationFrame; - this.running = false; - this.paused = false; - } + constructor(context) { + this._q = []; + this.context = context; + this.tick = requestAnimationFrame; + this.running = false; + this.paused = false; + } - /** - * Add an item to the queue - * @return {Promise} - */ - enqueue() { - var deferred, promise; - var queued; - var task = [].shift.call(arguments); - var args = arguments; + /** + * Add an item to the queue + * @return {Promise} + */ + enqueue() { + var deferred, promise; + var queued; + var task = [].shift.call(arguments); + var args = arguments; - // Handle single args without context - // if(args && !Array.isArray(args)) { - // args = [args]; - // } - if(!task) { - throw new Error("No Task Provided"); - } + // Handle single args without context + // if(args && !Array.isArray(args)) { + // args = [args]; + // } + if (!task) { + throw new Error("No Task Provided"); + } - if(typeof task === "function"){ + if (typeof task === "function") { + deferred = new defer(); + promise = deferred.promise; - deferred = new defer(); - promise = deferred.promise; + queued = { + task: task, + args: args, + //"context" : context, + deferred: deferred, + promise: promise, + }; + } else { + // Task is a promise + queued = { + promise: task, + }; + } - queued = { - "task" : task, - "args" : args, - //"context" : context, - "deferred" : deferred, - "promise" : promise - }; + this._q.push(queued); - } else { - // Task is a promise - queued = { - "promise" : task - }; + // Wait to start queue flush + if (this.paused == false && !this.running) { + // setTimeout(this.flush.bind(this), 0); + // this.tick.call(window, this.run.bind(this)); + this.run(); + } - } + return queued.promise; + } - this._q.push(queued); + /** + * Run one item + * @return {Promise} + */ + dequeue() { + var inwait, task, result; - // Wait to start queue flush - if (this.paused == false && !this.running) { - // setTimeout(this.flush.bind(this), 0); - // this.tick.call(window, this.run.bind(this)); - this.run(); - } + if (this._q.length && !this.paused) { + inwait = this._q.shift(); + task = inwait.task; + if (task) { + // console.log(task) - return queued.promise; - } + result = task.apply(this.context, inwait.args); - /** - * Run one item - * @return {Promise} - */ - dequeue(){ - var inwait, task, result; + if (result && typeof result["then"] === "function") { + // Task is a function that returns a promise + return result.then( + function () { + inwait.deferred.resolve.apply(this.context, arguments); + }.bind(this), + function () { + inwait.deferred.reject.apply(this.context, arguments); + }.bind(this) + ); + } else { + // Task resolves immediately + inwait.deferred.resolve.apply(this.context, result); + return inwait.promise; + } + } else if (inwait.promise) { + // Task is a promise + return inwait.promise; + } + } else { + inwait = new defer(); + inwait.deferred.resolve(); + return inwait.promise; + } + } - if(this._q.length && !this.paused) { - inwait = this._q.shift(); - task = inwait.task; - if(task){ - // console.log(task) + // Run All Immediately + dump() { + while (this._q.length) { + this.dequeue(); + } + } - result = task.apply(this.context, inwait.args); + /** + * Run all tasks sequentially, at convince + * @return {Promise} + */ + run() { + if (!this.running) { + this.running = true; + this.defered = new defer(); + } - if(result && typeof result["then"] === "function") { - // Task is a function that returns a promise - return result.then(function(){ - inwait.deferred.resolve.apply(this.context, arguments); - }.bind(this), function() { - inwait.deferred.reject.apply(this.context, arguments); - }.bind(this)); - } else { - // Task resolves immediately - inwait.deferred.resolve.apply(this.context, result); - return inwait.promise; - } + this.tick.call(window, () => { + if (this._q.length) { + this.dequeue().then( + function () { + this.run(); + }.bind(this) + ); + } else { + this.defered.resolve(); + this.running = undefined; + } + }); + // Unpause + if (this.paused == true) { + this.paused = false; + } + return this.defered.promise; + } - } else if(inwait.promise) { - // Task is a promise - return inwait.promise; - } + /** + * Flush all, as quickly as possible + * @return {Promise} + */ + flush() { + if (this.running) { + return this.running; + } - } else { - inwait = new defer(); - inwait.deferred.resolve(); - return inwait.promise; - } + if (this._q.length) { + this.running = this.dequeue().then( + function () { + this.running = undefined; + return this.flush(); + }.bind(this) + ); - } + return this.running; + } + } - // Run All Immediately - dump(){ - while(this._q.length) { - this.dequeue(); - } - } + /** + * Clear all items in wait + */ + clear() { + this._q = []; + } - /** - * Run all tasks sequentially, at convince - * @return {Promise} - */ - run(){ + /** + * Get the number of tasks in the queue + * @return {number} tasks + */ + length() { + return this._q.length; + } - if(!this.running){ - this.running = true; - this.defered = new defer(); - } + /** + * Pause a running queue + */ + pause() { + this.paused = true; + } - this.tick.call(window, () => { - - if(this._q.length) { - - this.dequeue() - .then(function(){ - this.run(); - }.bind(this)); - - } else { - this.defered.resolve(); - this.running = undefined; - } - - }); - - // Unpause - if(this.paused == true) { - this.paused = false; - } - - return this.defered.promise; - } - - /** - * Flush all, as quickly as possible - * @return {Promise} - */ - flush(){ - - if(this.running){ - return this.running; - } - - if(this._q.length) { - this.running = this.dequeue() - .then(function(){ - this.running = undefined; - return this.flush(); - }.bind(this)); - - return this.running; - } - - } - - /** - * Clear all items in wait - */ - clear(){ - this._q = []; - } - - /** - * Get the number of tasks in the queue - * @return {number} tasks - */ - length(){ - return this._q.length; - } - - /** - * Pause a running queue - */ - pause(){ - this.paused = true; - } - - /** - * End the queue - */ - stop(){ - this._q = []; - this.running = false; - this.paused = true; - } + /** + * End the queue + */ + stop() { + this._q = []; + this.running = false; + this.paused = true; + } } - /** * Create a new task from a callback * @class @@ -215,32 +204,27 @@ class Queue { * @return {function} task */ class Task { - constructor(task, args, context){ + constructor(task, args, context) { + return function () { + var toApply = arguments || []; - return function(){ - var toApply = arguments || []; + return new Promise((resolve, reject) => { + var callback = function (value, err) { + if (!value && err) { + reject(err); + } else { + resolve(value); + } + }; + // Add the callback to the arguments list + toApply.push(callback); - return new Promise( (resolve, reject) => { - var callback = function(value, err){ - if (!value && err) { - reject(err); - } else { - resolve(value); - } - }; - // Add the callback to the arguments list - toApply.push(callback); - - // Apply all arguments to the functions - task.apply(context || this, toApply); - - }); - - }; - - } + // Apply all arguments to the functions + task.apply(context || this, toApply); + }); + }; + } } - export default Queue; export { Task }; diff --git a/src/utils/replacements.js b/src/utils/replacements.js index a271088..61b3432 100644 --- a/src/utils/replacements.js +++ b/src/utils/replacements.js @@ -1,138 +1,131 @@ -import { qs, qsa } from "./core"; +import { qs } from "./core"; import Url from "./url"; -import Path from "./path"; -export function replaceBase(doc, section){ - var base; - var head; - var url = section.url; - var absolute = (url.indexOf("://") > -1); +export function replaceBase(doc, section) { + var base; + var head; + var url = section.url; + var absolute = url.indexOf("://") > -1; - if(!doc){ - return; - } + if (!doc) { + return; + } - head = qs(doc, "head"); - base = qs(head, "base"); + head = qs(doc, "head"); + base = qs(head, "base"); - if(!base) { - base = doc.createElement("base"); - head.insertBefore(base, head.firstChild); - } + if (!base) { + base = doc.createElement("base"); + head.insertBefore(base, head.firstChild); + } - // Fix for Safari crashing if the url doesn't have an origin - if (!absolute && window && window.location) { - url = window.location.origin + url; - } + // Fix for Safari crashing if the url doesn't have an origin + if (!absolute && window && window.location) { + url = window.location.origin + url; + } - base.setAttribute("href", url); + base.setAttribute("href", url); } -export function replaceCanonical(doc, section){ - var head; - var link; - var url = section.canonical; +export function replaceCanonical(doc, section) { + var head; + var link; + var url = section.canonical; - if(!doc){ - return; - } + if (!doc) { + return; + } - head = qs(doc, "head"); - link = qs(head, "link[rel='canonical']"); + head = qs(doc, "head"); + link = qs(head, "link[rel='canonical']"); - if (link) { - link.setAttribute("href", url); - } else { - link = doc.createElement("link"); - link.setAttribute("rel", "canonical"); - link.setAttribute("href", url); - head.appendChild(link); - } + if (link) { + link.setAttribute("href", url); + } else { + link = doc.createElement("link"); + link.setAttribute("rel", "canonical"); + link.setAttribute("href", url); + head.appendChild(link); + } } -export function replaceMeta(doc, section){ - var head; - var meta; - var id = section.idref; - if(!doc){ - return; - } +export function replaceMeta(doc, section) { + var head; + var meta; + var id = section.idref; + if (!doc) { + return; + } - head = qs(doc, "head"); - meta = qs(head, "link[property='dc.identifier']"); + head = qs(doc, "head"); + meta = qs(head, "link[property='dc.identifier']"); - if (meta) { - meta.setAttribute("content", id); - } else { - meta = doc.createElement("meta"); - meta.setAttribute("name", "dc.identifier"); - meta.setAttribute("content", id); - head.appendChild(meta); - } + if (meta) { + meta.setAttribute("content", id); + } else { + meta = doc.createElement("meta"); + meta.setAttribute("name", "dc.identifier"); + meta.setAttribute("content", id); + head.appendChild(meta); + } } // TODO: move me to Contents export function replaceLinks(contents, fn) { + var links = contents.querySelectorAll("a[href]"); - var links = contents.querySelectorAll("a[href]"); + if (!links.length) { + return; + } - if (!links.length) { - return; - } + var base = qs(contents.ownerDocument, "base"); + var location = base ? base.getAttribute("href") : undefined; + var replaceLink = function (link) { + var href = link.getAttribute("href"); - var base = qs(contents.ownerDocument, "base"); - var location = base ? base.getAttribute("href") : undefined; - var replaceLink = function(link){ - var href = link.getAttribute("href"); + if (href.indexOf("mailto:") === 0) { + return; + } - if(href.indexOf("mailto:") === 0){ - return; - } + var absolute = href.indexOf("://") > -1; - var absolute = (href.indexOf("://") > -1); + if (absolute) { + link.setAttribute("target", "_blank"); + } else { + var linkUrl; + try { + linkUrl = new Url(href, location); + } catch (error) { + // NOOP + } - if(absolute){ - - link.setAttribute("target", "_blank"); - - }else{ - var linkUrl; - try { - linkUrl = new Url(href, location); - } catch(error) { - // NOOP - } - - link.onclick = function(){ - - if(linkUrl && linkUrl.hash) { - fn(linkUrl.Path.path + linkUrl.hash); - } else if(linkUrl){ - fn(linkUrl.Path.path); - } else { - fn(href); - } - - return false; - }; - } - }.bind(this); - - for (var i = 0; i < links.length; i++) { - replaceLink(links[i]); - } + link.onclick = function () { + if (linkUrl && linkUrl.hash) { + fn(linkUrl.Path.path + linkUrl.hash); + } else if (linkUrl) { + fn(linkUrl.Path.path); + } else { + fn(href); + } + return false; + }; + } + }.bind(this); + for (var i = 0; i < links.length; i++) { + replaceLink(links[i]); + } } export function substitute(content, urls, replacements) { - urls.forEach(function(url, i){ - if (url && replacements[i]) { - // Account for special characters in the file name. - // See https://stackoverflow.com/a/6318729. - url = url.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - content = content.replace(new RegExp(url, "g"), replacements[i]); - } - }); - return content; + urls.forEach(function (url, i) { + if (url && replacements[i]) { + // Account for special characters in the file name. + // See https://stackoverflow.com/a/6318729. + url = url.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + content = content.replace(new RegExp(url, "g"), replacements[i]); + } + }); + return content; } diff --git a/src/utils/request.js b/src/utils/request.js index 0de3b86..586834e 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -1,150 +1,132 @@ -import {defer, isXml, parse} from "./core"; +import { defer, isXml, parse } from "./core"; import Path from "./path"; function request(url, type, withCredentials, headers) { - var supportsURL = (typeof window != "undefined") ? window.URL : false; // TODO: fallback for url if window isn't defined - var BLOB_RESPONSE = supportsURL ? "blob" : "arraybuffer"; + var supportsURL = typeof window != "undefined" ? window.URL : false; // TODO: fallback for url if window isn't defined + var BLOB_RESPONSE = supportsURL ? "blob" : "arraybuffer"; - var deferred = new defer(); + var deferred = new defer(); - var xhr = new XMLHttpRequest(); + var xhr = new XMLHttpRequest(); - //-- Check from PDF.js: - // https://github.com/mozilla/pdf.js/blob/master/web/compatibility.js - var xhrPrototype = XMLHttpRequest.prototype; + //-- Check from PDF.js: + // https://github.com/mozilla/pdf.js/blob/master/web/compatibility.js + var xhrPrototype = XMLHttpRequest.prototype; - var header; + var header; - if (!("overrideMimeType" in xhrPrototype)) { - // IE10 might have response, but not overrideMimeType - Object.defineProperty(xhrPrototype, "overrideMimeType", { - value: function xmlHttpRequestOverrideMimeType() {} - }); - } + if (!("overrideMimeType" in xhrPrototype)) { + // IE10 might have response, but not overrideMimeType + Object.defineProperty(xhrPrototype, "overrideMimeType", { + value: function xmlHttpRequestOverrideMimeType() {}, + }); + } - if(withCredentials) { - xhr.withCredentials = true; - } + if (withCredentials) { + xhr.withCredentials = true; + } - xhr.onreadystatechange = handler; - xhr.onerror = err; + xhr.onreadystatechange = handler; + xhr.onerror = err; - xhr.open("GET", url, true); + xhr.open("GET", url, true); - for(header in headers) { - xhr.setRequestHeader(header, headers[header]); - } + for (header in headers) { + xhr.setRequestHeader(header, headers[header]); + } - if(type == "json") { - xhr.setRequestHeader("Accept", "application/json"); - } + if (type == "json") { + xhr.setRequestHeader("Accept", "application/json"); + } - // If type isn"t set, determine it from the file extension - if(!type) { - type = new Path(url).extension; - } + // If type isn"t set, determine it from the file extension + if (!type) { + type = new Path(url).extension; + } - if(type == "blob"){ - xhr.responseType = BLOB_RESPONSE; - } + if (type == "blob") { + xhr.responseType = BLOB_RESPONSE; + } + if (isXml(type)) { + xhr.overrideMimeType("text/xml"); // for OPF parsing + } - if(isXml(type)) { - // xhr.responseType = "document"; - xhr.overrideMimeType("text/xml"); // for OPF parsing - } + if (type == "binary") { + xhr.responseType = "arraybuffer"; + } - if(type == "xhtml") { - // xhr.responseType = "document"; - } + xhr.send(); - if(type == "html" || type == "htm") { - // xhr.responseType = "document"; - } + function err(e) { + deferred.reject(e); + } - if(type == "binary") { - xhr.responseType = "arraybuffer"; - } + function handler() { + if (this.readyState === XMLHttpRequest.DONE) { + var responseXML = false; - xhr.send(); + if (this.responseType === "" || this.responseType === "document") { + responseXML = this.responseXML; + } - function err(e) { - deferred.reject(e); - } + if (this.status === 200 || this.status === 0 || responseXML) { + //-- Firefox is reporting 0 for blob urls + var r; - function handler() { - if (this.readyState === XMLHttpRequest.DONE) { - var responseXML = false; + if (!this.response && !responseXML) { + deferred.reject({ + status: this.status, + message: "Empty Response", + stack: new Error().stack, + }); + return deferred.promise; + } - if(this.responseType === "" || this.responseType === "document") { - responseXML = this.responseXML; - } + if (this.status === 403) { + deferred.reject({ + status: this.status, + response: this.response, + message: "Forbidden", + stack: new Error().stack, + }); + return deferred.promise; + } + if (responseXML) { + r = this.responseXML; + } else if (isXml(type)) { + // xhr.overrideMimeType("text/xml"); // for OPF parsing + // If this.responseXML wasn't set, try to parse using a DOMParser from text + r = parse(this.response, "text/xml"); + } else if (type == "xhtml") { + r = parse(this.response, "application/xhtml+xml"); + } else if (type == "html" || type == "htm") { + r = parse(this.response, "text/html"); + } else if (type == "json") { + r = JSON.parse(this.response); + } else if (type == "blob") { + if (supportsURL) { + r = this.response; + } else { + //-- Safari doesn't support responseType blob, so create a blob from arraybuffer + r = new Blob([this.response]); + } + } else { + r = this.response; + } - if (this.status === 200 || this.status === 0 || responseXML) { //-- Firefox is reporting 0 for blob urls - var r; + deferred.resolve(r); + } else { + deferred.reject({ + status: this.status, + message: this.response, + stack: new Error().stack, + }); + } + } + } - if (!this.response && !responseXML) { - deferred.reject({ - status: this.status, - message : "Empty Response", - stack : new Error().stack - }); - return deferred.promise; - } - - if (this.status === 403) { - deferred.reject({ - status: this.status, - response: this.response, - message : "Forbidden", - stack : new Error().stack - }); - return deferred.promise; - } - if(responseXML){ - r = this.responseXML; - } else - if(isXml(type)){ - // xhr.overrideMimeType("text/xml"); // for OPF parsing - // If this.responseXML wasn't set, try to parse using a DOMParser from text - r = parse(this.response, "text/xml"); - }else - if(type == "xhtml"){ - r = parse(this.response, "application/xhtml+xml"); - }else - if(type == "html" || type == "htm"){ - r = parse(this.response, "text/html"); - }else - if(type == "json"){ - r = JSON.parse(this.response); - }else - if(type == "blob"){ - - if(supportsURL) { - r = this.response; - } else { - //-- Safari doesn't support responseType blob, so create a blob from arraybuffer - r = new Blob([this.response]); - } - - }else{ - r = this.response; - } - - deferred.resolve(r); - } else { - - deferred.reject({ - status: this.status, - message : this.response, - stack : new Error().stack - }); - - } - } - } - - return deferred.promise; + return deferred.promise; } export default request; diff --git a/src/utils/scrolltype.js b/src/utils/scrolltype.js index 7d2e47b..815a1c6 100644 --- a/src/utils/scrolltype.js +++ b/src/utils/scrolltype.js @@ -1,55 +1,55 @@ // Detect RTL scroll type // Based on https://github.com/othree/jquery.rtl-scroll-type/blob/master/src/jquery.rtl-scroll.js export default function scrollType() { - var type = "reverse"; - var definer = createDefiner(); - document.body.appendChild(definer); + var type = "reverse"; + var definer = createDefiner(); + document.body.appendChild(definer); - if (definer.scrollLeft > 0) { - type = "default"; - } else { - if (typeof Element !== 'undefined' && Element.prototype.scrollIntoView) { - definer.children[0].children[1].scrollIntoView(); - if (definer.scrollLeft < 0) { - type = "negative"; - } - } else { - definer.scrollLeft = 1; - if (definer.scrollLeft === 0) { - type = "negative"; - } - } - } + if (definer.scrollLeft > 0) { + type = "default"; + } else { + if (typeof Element !== "undefined" && Element.prototype.scrollIntoView) { + definer.children[0].children[1].scrollIntoView(); + if (definer.scrollLeft < 0) { + type = "negative"; + } + } else { + definer.scrollLeft = 1; + if (definer.scrollLeft === 0) { + type = "negative"; + } + } + } - document.body.removeChild(definer); - return type; + document.body.removeChild(definer); + return type; } export function createDefiner() { - var definer = document.createElement('div'); - definer.dir="rtl"; + var definer = document.createElement("div"); + definer.dir = "rtl"; - definer.style.position = "fixed"; - definer.style.width = "1px"; - definer.style.height = "1px"; - definer.style.top = "0px"; - definer.style.left = "0px"; - definer.style.overflow = "hidden"; + definer.style.position = "fixed"; + definer.style.width = "1px"; + definer.style.height = "1px"; + definer.style.top = "0px"; + definer.style.left = "0px"; + definer.style.overflow = "hidden"; - var innerDiv = document.createElement('div'); - innerDiv.style.width = "2px"; + var innerDiv = document.createElement("div"); + innerDiv.style.width = "2px"; - var spanA = document.createElement('span'); - spanA.style.width = "1px"; - spanA.style.display = "inline-block"; + var spanA = document.createElement("span"); + spanA.style.width = "1px"; + spanA.style.display = "inline-block"; - var spanB = document.createElement('span'); - spanB.style.width = "1px"; - spanB.style.display = "inline-block"; + var spanB = document.createElement("span"); + spanB.style.width = "1px"; + spanB.style.display = "inline-block"; - innerDiv.appendChild(spanA); - innerDiv.appendChild(spanB); - definer.appendChild(innerDiv); + innerDiv.appendChild(spanA); + innerDiv.appendChild(spanB); + definer.appendChild(innerDiv); - return definer; + return definer; } diff --git a/src/utils/url.js b/src/utils/url.js index 3cc8c04..525fd2a 100644 --- a/src/utils/url.js +++ b/src/utils/url.js @@ -1,5 +1,5 @@ -import Path from "./path"; import path from "path-webpack"; +import Path from "./path"; /** * creates a Url object for parsing and manipulation of a url string @@ -8,101 +8,104 @@ import path from "path-webpack"; * default to window.location.href */ class Url { - constructor(urlString, baseString) { - var absolute = (urlString.indexOf("://") > -1); - var pathname = urlString; - var basePath; + constructor(urlString, baseString) { + var absolute = urlString.indexOf("://") > -1; + var pathname = urlString; + var basePath; - this.Url = undefined; - this.href = urlString; - this.protocol = ""; - this.origin = ""; - this.hash = ""; - this.hash = ""; - this.search = ""; - this.base = baseString; + this.Url = undefined; + this.href = urlString; + this.protocol = ""; + this.origin = ""; + this.hash = ""; + this.hash = ""; + this.search = ""; + this.base = baseString; - if (!absolute && - baseString !== false && - typeof(baseString) !== "string" && - window && window.location) { - this.base = window.location.href; - } + if ( + !absolute && + baseString !== false && + typeof baseString !== "string" && + window && + window.location + ) { + this.base = window.location.href; + } - // URL Polyfill doesn't throw an error if base is empty - if (absolute || this.base) { - try { - if (this.base) { // Safari doesn't like an undefined base - this.Url = new URL(urlString, this.base); - } else { - this.Url = new URL(urlString); - } - this.href = this.Url.href; + // URL Polyfill doesn't throw an error if base is empty + if (absolute || this.base) { + try { + if (this.base) { + // Safari doesn't like an undefined base + this.Url = new URL(urlString, this.base); + } else { + this.Url = new URL(urlString); + } + this.href = this.Url.href; - this.protocol = this.Url.protocol; - this.origin = this.Url.origin; - this.hash = this.Url.hash; - this.search = this.Url.search; + this.protocol = this.Url.protocol; + this.origin = this.Url.origin; + this.hash = this.Url.hash; + this.search = this.Url.search; - pathname = this.Url.pathname + (this.Url.search ? this.Url.search : ''); - } catch (e) { - // Skip URL parsing - this.Url = undefined; - // resolve the pathname from the base - if (this.base) { - basePath = new Path(this.base); - pathname = basePath.resolve(pathname); - } - } - } + pathname = this.Url.pathname + (this.Url.search ? this.Url.search : ""); + } catch (e) { + // Skip URL parsing + this.Url = undefined; + // resolve the pathname from the base + if (this.base) { + basePath = new Path(this.base); + pathname = basePath.resolve(pathname); + } + } + } - this.Path = new Path(pathname); + this.Path = new Path(pathname); - this.directory = this.Path.directory; - this.filename = this.Path.filename; - this.extension = this.Path.extension; + this.directory = this.Path.directory; + this.filename = this.Path.filename; + this.extension = this.Path.extension; + } - } + /** + * @returns {Path} + */ + path() { + return this.Path; + } - /** - * @returns {Path} - */ - path () { - return this.Path; - } + /** + * Resolves a relative path to a absolute url + * @param {string} what + * @returns {string} url + */ + resolve(what) { + var isAbsolute = what.indexOf("://") > -1; + var fullpath; - /** - * Resolves a relative path to a absolute url - * @param {string} what - * @returns {string} url - */ - resolve (what) { - var isAbsolute = (what.indexOf("://") > -1); - var fullpath; + if (isAbsolute) { + return what; + } - if (isAbsolute) { - return what; - } + fullpath = path.resolve(this.directory, what); + return this.origin + fullpath; + } - fullpath = path.resolve(this.directory, what); - return this.origin + fullpath; - } + /** + * Resolve a path relative to the url + * @param {string} what + * @returns {string} path + */ + relative(what) { + return path.relative(what, this.directory); + } - /** - * Resolve a path relative to the url - * @param {string} what - * @returns {string} path - */ - relative (what) { - return path.relative(what, this.directory); - } - - /** - * @returns {string} - */ - toString () { - return this.href; - } + /** + * @returns {string} + */ + toString() { + return this.href; + } } export default Url; diff --git a/test/old/epub.js b/test/old/epub.js deleted file mode 100644 index 8723114..0000000 --- a/test/old/epub.js +++ /dev/null @@ -1,371 +0,0 @@ -var domain = window.location.origin; - -module('Core'); - -test("EPUBJS.core.resolveUrl", 1, function() { - var a = "http://example.com/fred/chasen/"; - var b = "/chasen/derf.html"; - - var resolved = EPUBJS.core.resolveUrl(a, b); - - equal( resolved, "http://example.com/fred/chasen/derf.html", "resolved" ); - -}); - -test("EPUBJS.core.resolveUrl ../", 1, function() { - var a = "http://example.com/fred/chasen/"; - var b = "../derf.html"; - - var resolved = EPUBJS.core.resolveUrl(a, b); - - equal( resolved, "http://example.com/fred/derf.html", "resolved" ); -}); - - -test("EPUBJS.core.resolveUrl folders", 1, function() { - var a = "/fred/chasen/"; - var b = "/fred/chasen/derf.html"; - - var resolved = EPUBJS.core.resolveUrl(a, b); - - equal( resolved, "/fred/chasen/derf.html", "resolved" ); -}); - -test("EPUBJS.core.resolveUrl ../folders", 1, function() { - var a = "/fred/chasen/"; - var b = "../../derf.html"; - - var resolved = EPUBJS.core.resolveUrl(a, b); - - equal( resolved, "/derf.html", "resolved" ); -}); - -module('Create'); - -asyncTest("Create new ePub(/path/to/epub/)", 1, function() { - - var book = ePub("../books/moby-dick/"); - book.opened.then(function(){ - equal( book.url, "../books/moby-dick/OPS/", "book url is passed to new EPUBJS.Book" ); - start(); - }); - -}); - -asyncTest("Create new ePub(/path/to/epub/package.opf)", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( book.url, domain + "/books/moby-dick/OPS/", "bookPath is passed to new EPUBJS.Book" ); - start(); - }); - -}); - -asyncTest("Open using ePub(/path/to/epub/package.opf)", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( book.packageUrl, "../books/moby-dick/OPS/package.opf", "packageUrl is set" ); - start(); - }); - -}); - -asyncTest("Open Remote ePub", 1, function() { - - var book = ePub("https://s3.amazonaws.com/moby-dick/"); - book.opened.then(function(){ - equal( book.packageUrl, "https://s3.amazonaws.com/moby-dick/OPS/package.opf", "packageUrl is set" ); - start(); - }); - -}); - -asyncTest("Open Remote ePub from Package", 1, function() { - - var book = ePub("https://s3.amazonaws.com/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( book.packageUrl, "https://s3.amazonaws.com/moby-dick/OPS/package.opf", "packageUrl is set" ); - start(); - }); - -}); - -asyncTest("Find Epub Package", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( book.packageUrl, "../books/moby-dick/OPS/package.opf", "packageUrl is set" ); - start(); - }); - -}); - -module('Parse'); - -//TODO: add mocked tests for parser - -asyncTest("Manifest", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( Object.keys(book.package.manifest).length, 152, "Manifest is parsed" ); - start(); - }); - -}); - -asyncTest("Metadata", 3, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( book.package.metadata.creator, "Herman Melville", "Creator metadata is parsed" ); - equal( book.package.metadata.title, "Moby-Dick", "Title metadata is parsed" ); - equal( book.package.metadata.identifier, "code.google.com.epub-samples.moby-dick-basic", "Identifier metadata is parsed" ); - start(); - }); - -}); - -asyncTest("Spine", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( book.package.spine.length, 144, "Spine is parsed" ); - start(); - }); - -}); - -asyncTest("Cover", 1, function() { - - var book = ePub("../books/moby-dick/"); - book.opened.then(function(){ - equal( book.cover, "../books/moby-dick/OPS/images/9780316000000.jpg", "Cover is set" ); - start(); - }); - -}); - -module('Spine'); - -asyncTest("Length", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( book.spine.length, 144, "All spine items present" ); - start(); - }); - -}); - -asyncTest("Items", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - equal( book.spine.spineItems.length, 144, "All spine items added" ); - start(); - }); - -}); - -asyncTest("First Item", 2, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - var section = book.spine.get(0); - equal( section.href, "cover.xhtml", "First spine item href found" ); - equal( section.cfiBase, "/6/2[cover]", "First spine item cfi found" ); - - start(); - }); - -}); - -asyncTest("Find Item by Href", 2, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - var section = book.spine.get("chapter_001.xhtml"); - equal( section.href, "chapter_001.xhtml", "chap 1 spine item href found" ); - equal( section.cfiBase, "/6/14[xchapter_001]", "chap 1 spine item cfi found" ); - - start(); - }); - -}); - -asyncTest("Find Item by ID", 2, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - var section = book.spine.get("#xchapter_050"); - equal( section.href, "chapter_050.xhtml", "chap 50 spine item href found" ); - equal( section.cfiBase, "/6/112[xchapter_050]", "chap 50 spine item cfi found" ); - - start(); - }); - -}); - -asyncTest("Render Spine Item", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - var section = book.spine.get("#xchapter_050"); - section.render().then(function(content){ - equal( content.substring(377, 429), "

Chapter 50. Ahab’s Boat and Crew. Fedallah.

", "Chapter text rendered as string" ); - }); - - start(); - }); - -}); - -module('Navigation'); - -asyncTest("NCX & Nav", 2, function() { - - var book = ePub("../books/moby-dick/"); - book.opened.then(function(){ - equal( book.navigation.navUrl, "../books/moby-dick/OPS/toc.xhtml", "Nav URL found" ); - equal( book.navigation.ncxUrl, "../books/moby-dick/OPS/toc.ncx", "NCX URL found" ); - - start(); - }); - -}); - - -asyncTest("Load TOC Auto Pick", 1, function() { - - var book = ePub("../books/moby-dick/"); - book.opened.then(function(){ - book.navigation.load().then(function(toc){ - equal( toc.length, 141, "Full Nav toc parsed" ); - start(); - }); - }); - -}); - -asyncTest("Load TOC from Nav", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - var nav = book.navigation.nav.load(); - nav.then(function(toc){ - equal( toc.length, 141, "Full Nav toc parsed" ); - start(); - }); - }); - -}); - -asyncTest("Load TOC from NCX", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.opened.then(function(){ - var ncx = book.navigation.ncx.load(); - ncx.then(function(toc){ - equal( toc.length, 14, "Full NCX toc parsed" ); - start(); - }); - }); - -}); - -asyncTest("Get all TOC", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.loaded.navigation.then(function(){ - equal( book.navigation.get().length, 141, "Full Nav toc parsed" ); - start(); - }); - -}); - -asyncTest("Get TOC item by href", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.loaded.navigation.then(function(){ - var item = book.navigation.get("chapter_001.xhtml"); - equal( item.id, "toc-chapter_001", "Found TOC item" ); - start(); - }); - -}); - -asyncTest("Get TOC item by ID", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - book.loaded.navigation.then(function(){ - var item = book.navigation.get("#toc-chapter_001"); - equal( item.href, "chapter_001.xhtml", "Found TOC item" ); - start(); - }); - -}); - -module('Hooks'); - -asyncTest("Register a new hook", 1, function() { - - var beforeDisplay = new EPUBJS.Hook(); - beforeDisplay.register(function(args){ - var defer = new RSVP.defer(); - console.log("ran", 1); - defer.resolve(); - return defer.promise; - }); - equal( beforeDisplay.hooks.length, 1, "Registered a hook" ); - start(); - -// this.beforeDisplay.trigger(args).then(function(){}); - -}); - -asyncTest("Trigger all new hook", 4, function() { - - var beforeDisplay = new EPUBJS.Hook(this); - this.testerObject = {tester: 1}; - - beforeDisplay.register(function(testerObject){ - var defer = new RSVP.defer(); - - start(); - equal( testerObject.tester, 1, "tester is 1" ); - stop(); - - testerObject.tester += 1; - - defer.resolve(); - return defer.promise; - }); - - beforeDisplay.register(function(testerObject){ - var defer = new RSVP.defer(); - - start(); - equal(testerObject.tester, 2, "tester is 2" ); - stop(); - - testerObject.tester += 1; - - defer.resolve(); - return defer.promise; - }); - - start(); - equal( beforeDisplay.hooks.length, 2, "Added two hooks" ); - stop(); - - beforeDisplay.trigger(this.testerObject).then(function(){ - - start(); - equal( this.testerObject.tester, 3, "tester is 3" ); - - }.bind(this)); - -}); diff --git a/test/old/rendering.js b/test/old/rendering.js deleted file mode 100644 index 5cf1357..0000000 --- a/test/old/rendering.js +++ /dev/null @@ -1,16 +0,0 @@ -module('Rendering'); -/* -asyncTest("Render To", 1, function() { - - var book = ePub("../books/moby-dick/OPS/package.opf"); - var rendition = book.renderTo("qunit-fixture", {width:400, height:600}); - var displayed = rendition.display(0); - - displayed.then(function(){ - equal( $( "iframe", "#qunit-fixture" ).length, 1, "iframe added successfully" ); - start(); - }); - - -}); -*/ diff --git a/types/annotations.d.ts b/types/annotations.d.ts index 718319d..f9da7eb 100644 --- a/types/annotations.d.ts +++ b/types/annotations.d.ts @@ -1,53 +1,60 @@ -import Rendition from "./rendition"; import View from "./managers/view"; +import Rendition from "./rendition"; export default class Annotations { constructor(rendition: Rendition); - add(type: string, cfiRange: string, data?: object, cb?: Function, className?: string, styles?: object): Annotation; - + add( + type: string, + cfiRange: string, + data?: object, + cb?: Function, + className?: string, + styles?: object + ): Annotation; remove(cfiRange: string, type: string): void; + highlight( + cfiRange: string, + data?: object, + cb?: Function, + className?: string, + styles?: object + ): void; + underline( + cfiRange: string, + data?: object, + cb?: Function, + className?: string, + styles?: object + ): void; + mark(cfiRange: string, data?: object, cb?: Function): void; + each(): Array; - highlight(cfiRange: string, data?: object, cb?: Function, className?: string, styles?: object): void; - - underline(cfiRange: string, data?: object, cb?: Function, className?: string, styles?: object): void; - - mark(cfiRange: string, data?: object, cb?: Function): void; - - each(): Array - - private _removeFromAnnotationBySectionIndex(sectionIndex: number, hash: string): void; - + private _removeFromAnnotationBySectionIndex( + sectionIndex: number, + hash: string + ): void; private _annotationsAt(index: number): void; - private inject(view: View): void; - private clear(view: View): void; } declare class Annotation { constructor(options: { - type: string, - cfiRange: string, - data?: object, - sectionIndex?: number, - cb?: Function, - className?: string, - styles?: object - }); - + type: string; + cfiRange: string; + data?: object; + sectionIndex?: number; + cb?: Function; + className?: string; + styles?: object; + }); update(data: object): void; - attach(view: View): any; - detach(view: View): any; - // Event emitters emit(type: any, ...args: any[]): void; - off(type: any, listener: any): any; - on(type: any, listener: any): any; - once(type: any, listener: any, ...args: any[]): any; } diff --git a/types/archive.d.ts b/types/archive.d.ts index c705aa4..763a9f1 100644 --- a/types/archive.d.ts +++ b/types/archive.d.ts @@ -1,27 +1,25 @@ -import JSZip = require('jszip'); +import JSZip = require("jszip"); export default class Archive { constructor(); open(input: BinaryType, isBase64?: boolean): Promise; - openUrl(zipUrl: string, isBase64?: boolean): Promise; - request(url: string, type?: string): Promise; - + request( + url: string, + type?: string + ): Promise; getBlob(url: string, mimeType?: string): Promise; - getText(url: string): Promise; - getBase64(url: string, mimeType?: string): Promise; - createUrl(url: string, options: { base64: boolean }): Promise; - revokeUrl(url: string): void; - destroy(): void; private checkRequirements(): void; - - private handleResponse(response: any, type?: string): Blob | string | JSON | Document | XMLDocument; + private handleResponse( + response: any, + type?: string + ): Blob | string | JSON | Document | XMLDocument; } diff --git a/types/book.d.ts b/types/book.d.ts index 7835053..fe38beb 100644 --- a/types/book.d.ts +++ b/types/book.d.ts @@ -1,122 +1,98 @@ -import { +import Archive from "./archive"; +import Container from "./container"; +import Locations from "./locations"; +import Navigation from "./navigation"; +import Packaging, { PackagingManifestObject, PackagingMetadataObject, - PackagingSpineItem, - PackagingObject } from "./packaging"; +import PageList, { PageListItem } from "./pagelist"; import Rendition, { RenditionOptions } from "./rendition"; -import Section, { SpineItem } from "./section"; -import Archive from "./archive"; -import Navigation from "./navigation"; -import PageList, {PageListItem} from "./pagelist"; -import Spine from "./spine"; -import Locations from "./locations"; -import Url from "./utils/url"; -import Path from "./utils/path"; import Resources from "./resources"; -import Container from "./container"; -import Packaging from "./packaging"; +import Section, { SpineItem } from "./section"; +import Spine from "./spine"; import Store from "./store"; +import Path from "./utils/path"; +import Url from "./utils/url"; export interface BookOptions { - requestMethod?: (url: string, type: string, withCredentials: object, headers: object) => Promise; - requestCredentials?: object, - requestHeaders?: object, - encoding?: string, - replacements?: string, - canonical?: (path: string) => string, - openAs?: string, - store?: string + requestMethod?: ( + url: string, + type: string, + withCredentials: object, + headers: object + ) => Promise; + requestCredentials?: object; + requestHeaders?: object; + encoding?: string; + replacements?: string; + canonical?: (path: string) => string; + openAs?: string; + store?: string; } export default class Book { - constructor(url: string, options?: BookOptions); - constructor(options?: BookOptions); + constructor(url: string, options?: BookOptions); + constructor(options?: BookOptions); - settings: BookOptions; - opening: any; // should be core.defer - opened: Promise; - isOpen: boolean; - loaded: { - metadata: Promise, - spine: Promise, - manifest: Promise, - cover: Promise, - navigation: Promise, - pageList: Promise, - resources: Promise, - } - ready: Promise; - request: Function; - spine: Spine; - locations: Locations; - navigation: Navigation; - pageList: PageList; - url: Url; - path: Path; - archived: boolean; - archive: Archive; - resources: Resources; - rendition: Rendition - container: Container; - packaging: Packaging; - storage: Store; + settings: BookOptions; + opening: any; // should be core.defer + opened: Promise; + isOpen: boolean; + loaded: { + metadata: Promise; + spine: Promise; + manifest: Promise; + cover: Promise; + navigation: Promise; + pageList: Promise; + resources: Promise; + }; + ready: Promise; + request: Function; + spine: Spine; + locations: Locations; + navigation: Navigation; + pageList: PageList; + url: Url; + path: Path; + archived: boolean; + archive: Archive; + resources: Resources; + rendition: Rendition; + container: Container; + packaging: Packaging; + storage: Store; + canonical(path: string): string; + coverUrl(): Promise; + destroy(): void; + determineType(input: string): string; + getRange(cfiRange: string): Promise; + key(identifier?: string): string; + load(path: string): Promise; + loadNavigation(opf: XMLDocument): Promise; + open(input: string, what?: string): Promise; + open(input: ArrayBuffer, what?: string): Promise; + openContainer(url: string): Promise; + openEpub(data: BinaryType, encoding?: string): Promise; + openManifest(url: string): Promise; + openPackaging(url: string): Promise; + renderTo(element: Element, options?: RenditionOptions): Rendition; + renderTo(element: string, options?: RenditionOptions): Rendition; + resolve(path: string, absolute?: boolean): string; + section(target: string): Section; + section(target: number): Section; + setRequestCredentials(credentials: object): void; + setRequestHeaders(headers: object): void; + unarchive(input: BinaryType, encoding?: string): Promise; + store(name: string): Store; + unpack(opf: XMLDocument): Promise; - canonical(path: string): string; - - coverUrl(): Promise; - - destroy(): void; - - determineType(input: string): string; - - getRange(cfiRange: string): Promise; - - key(identifier?: string): string; - - load(path: string): Promise; - - loadNavigation(opf: XMLDocument): Promise; - - open(input: string, what?: string): Promise; - open(input: ArrayBuffer, what?: string): Promise; - - openContainer(url: string): Promise; - - openEpub(data: BinaryType, encoding?: string): Promise; - - openManifest(url: string): Promise; - - openPackaging(url: string): Promise; - - renderTo(element: Element, options?: RenditionOptions): Rendition; - renderTo(element: string, options?: RenditionOptions): Rendition; - - private replacements(): Promise; - - resolve(path: string, absolute?: boolean): string; - - section(target: string): Section; - section(target: number): Section; - - setRequestCredentials(credentials: object): void; - - setRequestHeaders(headers: object): void; - - unarchive(input: BinaryType, encoding?: string): Promise; - - store(name: string): Store; - - unpack(opf: XMLDocument): Promise; - - // Event emitters - emit(type: any, ...args: any[]): void; - - off(type: any, listener: any): any; - - on(type: any, listener: any): any; - - once(type: any, listener: any, ...args: any[]): any; - + private replacements(): Promise; + // Event emitters + emit(type: any, ...args: any[]): void; + off(type: any, listener: any): any; + on(type: any, listener: any): any; + once(type: any, listener: any, ...args: any[]): any; } diff --git a/types/container.d.ts b/types/container.d.ts index 139c941..01146ca 100644 --- a/types/container.d.ts +++ b/types/container.d.ts @@ -2,6 +2,5 @@ export default class Container { constructor(containerDocument: Document); parse(containerDocument: Document): void; - destroy(): void; } diff --git a/types/contents.d.ts b/types/contents.d.ts index a0bb432..b6f4104 100644 --- a/types/contents.d.ts +++ b/types/contents.d.ts @@ -1,139 +1,107 @@ import EpubCFI from "./epubcfi"; export interface ViewportSettings { - width: string, - height: string, - scale: string, - scalable: string, - minimum: string, - maximum: string + width: string; + height: string; + scale: string; + scalable: string; + minimum: string; + maximum: string; } export default class Contents { - constructor(doc: Document, content: Element, cfiBase: string, sectionIndex: number); + constructor( + doc: Document, + content: Element, + cfiBase: string, + sectionIndex: number + ); - epubcfi: EpubCFI; - document: Document; - documentElement: Element; - content: Element; - window: Window; - sectionIndex: number; - cfiBase: string; + epubcfi: EpubCFI; + document: Document; + documentElement: Element; + content: Element; + window: Window; + sectionIndex: number; + cfiBase: string; - static listenedEvents: string[]; + static listenedEvents: string[]; - addClass(className: string): void; + addClass(className: string): void; + addScript(src: string): Promise; + addStylesheet(src: string): Promise; + addStylesheetRules( + rules: Array | object, + key: string + ): Promise; + addStylesheetCss(serializedCss: string, key: string): Promise; + cfiFromNode(node: Node, ignoreClass?: string): string; + cfiFromRange(range: Range, ignoreClass?: string): string; + columns( + width: number, + height: number, + columnWidth: number, + gap: number, + dir: string + ): void; + contentHeight(h: number): number; + contentWidth(w: number): number; + css(property: string, value: string, priority?: boolean): string; + destroy(): void; + direction(dir: string): void; + fit(width: number, height: number): void; + height(h: number): number; + locationOf( + target: string | EpubCFI, + ignoreClass?: string + ): Promise<{ top: number; left: number }>; + map(layout: any): any; + mapPage( + cfiBase: string, + layout: object, + start: number, + end: number, + dev: boolean + ): any; + overflow(overflow: string): string; + overflowX(overflow: string): string; + overflowY(overflow: string): string; + range(cfi: string, ignoreClass?: string): Range; + removeClass(className: any): void; + root(): Element; + scaler(scale: number, offsetX: number, offsetY: number): void; + scrollHeight(): number; + scrollWidth(): number; + size(width: number, height: number): void; + textHeight(): number; + textWidth(): number; + viewport(options: ViewportSettings): ViewportSettings; + width(w: number): number; + writingMode(mode: string): string; + // Event emitters + emit(type: any, ...args: any[]): void; + off(type: any, listener: any): any; + on(type: any, listener: any): any; + once(type: any, listener: any, ...args: any[]): any; - addScript(src: string): Promise; - - addStylesheet(src: string): Promise; - - addStylesheetRules(rules: Array | object, key: string): Promise; - - addStylesheetCss(serializedCss: string, key: string): Promise; - - cfiFromNode(node: Node, ignoreClass?: string): string; - - cfiFromRange(range: Range, ignoreClass?: string): string; - - columns(width: number, height: number, columnWidth: number, gap: number, dir: string): void; - - contentHeight(h: number): number; - - contentWidth(w: number): number; - - css(property: string, value: string, priority?: boolean): string; - - destroy(): void; - - direction(dir: string): void; - - fit(width: number, height: number): void; - - height(h: number): number; - - locationOf(target: string | EpubCFI, ignoreClass?: string): Promise<{ top: number, left: number }>; - - map(layout: any): any; - - mapPage(cfiBase: string, layout: object, start: number, end: number, dev: boolean): any; - - overflow(overflow: string): string; - - overflowX(overflow: string): string; - - overflowY(overflow: string): string; - - range(cfi: string, ignoreClass?: string): Range; - - removeClass(className: any): void; - - root(): Element; - - scaler(scale: number, offsetX: number, offsetY: number): void; - - scrollHeight(): number; - - scrollWidth(): number; - - size(width: number, height: number): void; - - textHeight(): number; - - textWidth(): number; - - viewport(options: ViewportSettings): ViewportSettings; - - width(w: number): number; - - writingMode(mode: string): string; - - // Event emitters - emit(type: any, ...args: any[]): void; - - off(type: any, listener: any): any; - - on(type: any, listener: any): any; - - once(type: any, listener: any, ...args: any[]): any; - - private addEventListeners(): void; - - private addSelectionListeners(): void; - - private epubReadingSystem(name: string, version: string): object; - - private expand(): void; - - private fontLoadListeners(): void; - - private imageLoadListeners(): void; - - private layoutStyle(style: string): string; - - private linksHandler(): void; - - private listeners(): void; - - private mediaQueryListeners(): void; - - private onSelectionChange(e: Event): void; - - private removeEventListeners(): void; - - private removeListeners(): void; - - private removeSelectionListeners(): void; - - private resizeCheck(): void; - - private resizeListeners(): void; - - private resizeObservers(): void; - - private transitionListeners(): void; - - private triggerEvent(e: Event): void; - - private triggerSelectedEvent(selection: Selection): void; + private addEventListeners(): void; + private addSelectionListeners(): void; + private epubReadingSystem(name: string, version: string): object; + private expand(): void; + private fontLoadListeners(): void; + private imageLoadListeners(): void; + private layoutStyle(style: string): string; + private linksHandler(): void; + private listeners(): void; + private mediaQueryListeners(): void; + private onSelectionChange(e: Event): void; + private removeEventListeners(): void; + private removeListeners(): void; + private removeSelectionListeners(): void; + private resizeCheck(): void; + private resizeListeners(): void; + private resizeObservers(): void; + private transitionListeners(): void; + private triggerEvent(e: Event): void; + private triggerSelectedEvent(selection: Selection): void; } diff --git a/types/core.d.ts b/types/core.d.ts index 1858cd4..bcfc7aa 100644 --- a/types/core.d.ts +++ b/types/core.d.ts @@ -1,83 +1,87 @@ export module Core { - export function uuid(): string; - export function documentHeight(): number; - export function isElement(obj: object): boolean; - export function isNumber(n: any): boolean; - export function isFloat(n: any): boolean; - export function prefixed(unprefixed: string): string; - export function defaults(obj: object): object; - export function extend(target: object): object; - - export function insert(item: any, array: Array, compareFunction: Function): number; - - export function locationOf(item: any, array: Array, compareFunction: Function, _start: Function, _end: Function): number; - - export function indexOfSorted(item: any, array: Array, compareFunction: Function, _start: Function, _end: Function): number; - - export function bounds(el: Element): { width: Number, height: Number}; - - export function borders(el: Element): { width: Number, height: Number}; - + export function insert( + item: any, + array: Array, + compareFunction: Function + ): number; + export function locationOf( + item: any, + array: Array, + compareFunction: Function, + _start: Function, + _end: Function + ): number; + export function indexOfSorted( + item: any, + array: Array, + compareFunction: Function, + _start: Function, + _end: Function + ): number; + export function bounds(el: Element): { width: Number; height: Number }; + export function borders(el: Element): { width: Number; height: Number }; export function nodeBounds(node: Node): object; - - export function windowBounds(): { width: Number, height: Number, top: Number, left: Number, right: Number, bottom: Number }; - + export function windowBounds(): { + width: Number; + height: Number; + top: Number; + left: Number; + right: Number; + bottom: Number; + }; export function indexOfNode(node: Node, typeId: string): number; - export function indexOfTextNode(textNode: Node): number; - export function indexOfElementNode(elementNode: Element): number; - export function isXml(ext: string): boolean; - export function createBlob(content: any, mime: string): Blob; - export function createBlobUrl(content: any, mime: string): string; - export function revokeBlobUrl(url: string): void; - - export function createBase64Url(content: any, mime: string): string - + export function createBase64Url(content: any, mime: string): string; export function type(obj: object): string; - - export function parse(markup: string, mime: string, forceXMLDom: boolean): Document; - + export function parse( + markup: string, + mime: string, + forceXMLDom: boolean + ): Document; export function qs(el: Element, sel: string): Element; - export function qsa(el: Element, sel: string): ArrayLike; - - export function qsp(el: Element, sel: string, props: Array): ArrayLike; - + export function qsp( + el: Element, + sel: string, + props: Array + ): ArrayLike; export function sprint(root: Node, func: Function): void; - - export function treeWalker(root: Node, func: Function, filter: object | Function): void; - + export function treeWalker( + root: Node, + func: Function, + filter: object | Function + ): void; export function walk(node: Node, callback: Function): void; - export function blob2base64(blob: Blob): string; - export function defer(): Promise; - - export function querySelectorByType(html: Element, element: string, type: string): Array; - + export function querySelectorByType( + html: Element, + element: string, + type: string + ): Array; export function findChildren(el: Element): Array; - export function parents(node: Element): Array; - - export function filterChildren(el: Element, nodeName: string, single: boolean): Array; - - export function getParentByTagName(node: Element, tagname: string): Array; - - export class RangeObject extends Range { - - } - + export function filterChildren( + el: Element, + nodeName: string, + single: boolean + ): Array; + export function getParentByTagName( + node: Element, + tagname: string + ): Array; + export class RangeObject extends Range {} } diff --git a/types/epub.d.ts b/types/epub.d.ts index c87c9de..45ae4bb 100644 --- a/types/epub.d.ts +++ b/types/epub.d.ts @@ -2,5 +2,8 @@ import Book, { BookOptions } from "./book"; export default Epub; -declare function Epub(urlOrData: string | ArrayBuffer, options?: BookOptions) : Book; -declare function Epub(options?: BookOptions) : Book; +declare function Epub( + urlOrData: string | ArrayBuffer, + options?: BookOptions +): Book; +declare function Epub(options?: BookOptions): Book; diff --git a/types/epubcfi.d.ts b/types/epubcfi.d.ts index f9748bb..4a91f49 100644 --- a/types/epubcfi.d.ts +++ b/types/epubcfi.d.ts @@ -1,97 +1,98 @@ interface EpubCFISegment { - steps: Array, + steps: Array; terminal: { - offset: number, - assertion: string - } + offset: number; + assertion: string; + }; } interface EpubCFIStep { - id: string, - tagName: string, - type: number, - index: number + id: string; + tagName: string; + type: number; + index: number; } interface EpubCFIComponent { - steps: Array, + steps: Array; terminal: { - offset: number, - assertion: string - } + offset: number; + assertion: string; + }; } export default class EpubCFI { - constructor(cfiFrom?: string | Range | Node, base?: string | object, ignoreClass?: string); + constructor( + cfiFrom?: string | Range | Node, + base?: string | object, + ignoreClass?: string + ); - base: EpubCFIComponent; - spinePos: number; - range: boolean; + base: EpubCFIComponent; + spinePos: number; + range: boolean; - isCfiString(str: string): boolean; - - fromNode(anchor: Node, base: string | object, ignoreClass?: string): EpubCFI; - - fromRange(range: Range, base: string | object, ignoreClass?: string): EpubCFI; - - parse(cfiStr: string): EpubCFI; - - collapse(toStart?: boolean): void; - - compare(cfiOne: string | EpubCFI, cfiTwo: string | EpubCFI): number; - - equalStep(stepA: object, stepB: object): boolean; - - filter(anchor: Element, ignoreClass?: string): Element | false; - - toRange(_doc?: Document, ignoreClass?: string): Range; - - toString(): string; - - private filteredStep(node: Node, ignoreClass?: string): any; - - private findNode(steps: Array, _doc?: Document, ignoreClass?: string): Node; - - private fixMiss(steps: Array, offset: number, _doc?: Document, ignoreClass?: string): any; - - private checkType(cfi: string | Range | Node): string | false; - - private generateChapterComponent(_spineNodeIndex: number, _pos: number, id: string): string; - - private getChapterComponent(cfiStr: string): string; - - private getCharecterOffsetComponent(cfiStr: string): string; - - private getPathComponent(cfiStr: string): string; - - private getRange(cfiStr: string): string; - - private joinSteps(steps: Array): Array; - - private normalizedMap(children: Array, nodeType: number, ignoreClass?: string): object; - - private parseComponent(componentStr: string): object; - - private parseStep(stepStr: string): object; - - private parseTerminal(termialStr: string): object; - - private patchOffset(anchor: Node, offset: number, ignoreClass?: string): number; - - private pathTo(node: Node, offset: number, ignoreClass?: string): EpubCFISegment; - - private position(anchor: Node): number; - - private segmentString(segment: EpubCFISegment): string; - - private step(node: Node): EpubCFIStep; - - private stepsToQuerySelector(steps: Array): string; - - private stepsToXpath(steps: Array): string; - - private textNodes(container: Node, ignoreClass?: string): Array; - - private walkToNode(steps: Array, _doc?: Document, ignoreClass?: string): Node; + isCfiString(str: string): boolean; + fromNode(anchor: Node, base: string | object, ignoreClass?: string): EpubCFI; + fromRange(range: Range, base: string | object, ignoreClass?: string): EpubCFI; + parse(cfiStr: string): EpubCFI; + collapse(toStart?: boolean): void; + compare(cfiOne: string | EpubCFI, cfiTwo: string | EpubCFI): number; + equalStep(stepA: object, stepB: object): boolean; + filter(anchor: Element, ignoreClass?: string): Element | false; + toRange(_doc?: Document, ignoreClass?: string): Range; + toString(): string; + private filteredStep(node: Node, ignoreClass?: string): any; + private findNode( + steps: Array, + _doc?: Document, + ignoreClass?: string + ): Node; + private fixMiss( + steps: Array, + offset: number, + _doc?: Document, + ignoreClass?: string + ): any; + private checkType(cfi: string | Range | Node): string | false; + private generateChapterComponent( + _spineNodeIndex: number, + _pos: number, + id: string + ): string; + private getChapterComponent(cfiStr: string): string; + private getCharecterOffsetComponent(cfiStr: string): string; + private getPathComponent(cfiStr: string): string; + private getRange(cfiStr: string): string; + private joinSteps(steps: Array): Array; + private normalizedMap( + children: Array, + nodeType: number, + ignoreClass?: string + ): object; + private parseComponent(componentStr: string): object; + private parseStep(stepStr: string): object; + private parseTerminal(termialStr: string): object; + private patchOffset( + anchor: Node, + offset: number, + ignoreClass?: string + ): number; + private pathTo( + node: Node, + offset: number, + ignoreClass?: string + ): EpubCFISegment; + private position(anchor: Node): number; + private segmentString(segment: EpubCFISegment): string; + private step(node: Node): EpubCFIStep; + private stepsToQuerySelector(steps: Array): string; + private stepsToXpath(steps: Array): string; + private textNodes(container: Node, ignoreClass?: string): Array; + private walkToNode( + steps: Array, + _doc?: Document, + ignoreClass?: string + ): Node; } diff --git a/types/epubjs-tests.ts b/types/epubjs-tests.ts deleted file mode 100644 index e64bb87..0000000 --- a/types/epubjs-tests.ts +++ /dev/null @@ -1,9 +0,0 @@ -import ePub, { Book } from '../'; - -function testEpub() { - const epub = ePub("https://s3.amazonaws.com/moby-dick/moby-dick.epub"); - - const book = new Book("https://s3.amazonaws.com/moby-dick/moby-dick.epub", {}); -} - -testEpub(); diff --git a/types/index.d.ts b/types/index.d.ts index 6a8ec9b..92c2782 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -8,13 +8,9 @@ export as namespace ePub; export default Epub; -export { default as Book } from './book'; -export { default as EpubCFI } from './epubcfi'; -export { default as Rendition, Location } from './rendition'; -export { default as Contents } from './contents'; -export { default as Layout } from './layout'; -export { NavItem } from './navigation'; - -declare namespace ePub { - -} +export { default as Book } from "./book"; +export { default as Contents } from "./contents"; +export { default as EpubCFI } from "./epubcfi"; +export { default as Layout } from "./layout"; +export { NavItem } from "./navigation"; +export { Location, default as Rendition } from "./rendition"; diff --git a/types/layout.d.ts b/types/layout.d.ts index 04d373d..bcfe467 100644 --- a/types/layout.d.ts +++ b/types/layout.d.ts @@ -1,10 +1,10 @@ import Contents from "./contents"; interface LayoutSettings { - layout: string, - spread: string, - minSpreadWidth: number, - evenSpreads: boolean + layout: string; + spread: string; + minSpreadWidth: number; + evenSpreads: boolean; } export default class Layout { @@ -13,35 +13,30 @@ export default class Layout { settings: LayoutSettings; name: string; props: { - name: string, - spread: string, - flow: string, - width: number, - height: number, - spreadWidth: number, - delta: number, - columnWidth: number, - gap: number, - divisor: number + name: string; + spread: string; + flow: string; + width: number; + height: number; + spreadWidth: number; + delta: number; + columnWidth: number; + gap: number; + divisor: number; }; flow(flow: string): string; - spread(spread: string, min: number): boolean; - - calculate(_width:number, _height:number, _gap?:number): void; - + calculate(_width: number, _height: number, _gap?: number): void; format(contents: Contents): void | Promise; - - count(totalLength: number, pageLength: number): {spreads: Number, pages: Number}; - + count( + totalLength: number, + pageLength: number + ): { spreads: Number; pages: Number }; // Event emitters emit(type: any, ...args: any[]): void; - off(type: any, listener: any): any; - on(type: any, listener: any): any; - once(type: any, listener: any, ...args: any[]): any; private update(props: object): void; diff --git a/types/locations.d.ts b/types/locations.d.ts index 1f0de6a..1c935ec 100644 --- a/types/locations.d.ts +++ b/types/locations.d.ts @@ -1,41 +1,29 @@ -import Spine from "./spine"; -import Section from "./section"; import EpubCFI from "./epubcfi"; +import Section from "./section"; +import Spine from "./spine"; export default class Locations { constructor(spine: Spine, request?: Function, pause?: number); generate(chars: number): Promise>; - process(section: Section): Promise>; - locationFromCfi(cfi: string | EpubCFI): Location; - percentageFromCfi(cfi: string | EpubCFI): number; - percentageFromLocation(loc: number): number; - cfiFromLocation(loc: number): string; - cfiFromPercentage(percentage: number): string; - load(locations: string): Array; - save(): string; - currentLocation(): Location; currentLocation(curr: string | number): void; - length(): number; - destroy(): void; private createRange(): { - startContainer: Element, - startOffset: number, - endContainer: Element, - endOffset: number + startContainer: Element; + startOffset: number; + endContainer: Element; + endOffset: number; }; - - private parse(contents: Node, cfiBase: string, chars: number) : Array; + private parse(contents: Node, cfiBase: string, chars: number): Array; } diff --git a/types/managers/manager.d.ts b/types/managers/manager.d.ts index 5486fb1..884b5fe 100644 --- a/types/managers/manager.d.ts +++ b/types/managers/manager.d.ts @@ -1,90 +1,59 @@ -import Section from "../section"; -import Layout from "../layout"; import Contents from "../contents"; -import View, { ViewSettings } from "./view"; +import Layout from "../layout"; import { EpubCFIPair } from "../mapping"; +import Section from "../section"; +import View, { ViewSettings } from "./view"; export interface ViewLocation { - index: number, - href: string, - pages: number[], - totalPages: number, - mapping: EpubCFIPair + index: number; + href: string; + pages: number[]; + totalPages: number; + mapping: EpubCFIPair; } export interface ManagerOptions extends ViewSettings { - infinite?: boolean, - overflow?: string, - [key: string]: any + infinite?: boolean; + overflow?: string; + [key: string]: any; } export default class Manager { constructor(options: object); - render(element: Element, size?: { width: Number, height: Number }): void; - - resize(width: Number, height: Number): void; - - onOrientationChange(e: Event): void; - - private createView(section: Section): View; - - display(section: Section, target: string | number): Promise; - - private afterDisplayed(view: View): void; - - private afterResized(view: View): void; - - private moveTo(offset: {top: Number, left: Number}): void; - - private append(section: Section): Promise; - - private prepend(section: Section): Promise; - - next(): Promise; - - prev(): Promise; - - current(): View; - + applyLayout(layout: Layout): void; + bounds(): object; clear(): void; - + current(): View; currentLocation(): ViewLocation[]; - + destroy(): void; + direction(dir: string): void; + display(section: Section, target: string | number): Promise; + getContents(): Contents[]; + isRendered(): boolean; + next(): Promise; + onOrientationChange(e: Event): void; + prev(): Promise; + render(element: Element, size?: { width: Number; height: Number }): void; + resize(width: Number, height: Number): void; + setLayout(layout: Layout): void; + updateAxis(axis: string, forceUpdate: boolean): void; + updateFlow(flow: string): void; + updateLayout(): void; visible(): View[]; + private createView(section: Section): View; + private afterDisplayed(view: View): void; + private afterResized(view: View): void; + private moveTo(offset: { top: Number; left: Number }): void; + private append(section: Section): Promise; + private prepend(section: Section): Promise; private scrollBy(x: number, y: number, silent: boolean): void; - private scrollTo(x: number, y: number, silent: boolean): void; - private onScroll(): void; - - bounds(): object; - - applyLayout(layout: Layout): void; - - updateLayout(): void; - - setLayout(layout: Layout): void; - - updateAxis(axis: string, forceUpdate: boolean): void; - - updateFlow(flow: string): void; - - getContents(): Contents[]; - - direction(dir: string): void; - - isRendered(): boolean; - - destroy(): void; - // Event emitters emit(type: any, ...args: any[]): void; - off(type: any, listener: any): any; - on(type: any, listener: any): any; - once(type: any, listener: any, ...args: any[]): any; } diff --git a/types/managers/view.d.ts b/types/managers/view.d.ts index b8f297f..ef20252 100644 --- a/types/managers/view.d.ts +++ b/types/managers/view.d.ts @@ -1,80 +1,65 @@ -import Section from "../section"; import Contents from "../contents"; import Layout from "../layout"; +import Section from "../section"; export interface ViewSettings { - ignoreClass?: string, - axis?: string, - flow?: string, - layout?: Layout, - method?: string, - width?: number, - height?: number, - forceEvenPages?: boolean, - allowScriptedContent?: boolean + ignoreClass?: string; + axis?: string; + flow?: string; + layout?: Layout; + method?: string; + width?: number; + height?: number; + forceEvenPages?: boolean; + allowScriptedContent?: boolean; } export default class View { constructor(section: Section, options: ViewSettings); create(): any; - render(request?: Function, show?: boolean): Promise; - reset(): void; - size(_width: Number, _height: Number): void; - load(content: Contents): Promise; - setLayout(layout: Layout): void; - setAxis(axis: string): void; - display(request?: Function): Promise; - show(): void; - hide(): void; - - offset(): { top: Number, left: Number }; - + offset(): { top: Number; left: Number }; width(): Number; - height(): Number; - position(): object; - - locationOf(target: string): { top: Number, left: Number }; - + locationOf(target: string): { top: Number; left: Number }; onDisplayed(view: View): void; - onResize(view: View): void; - bounds(force?: boolean): object; + highlight( + cfiRange: string, + data?: object, + cb?: Function, + className?: string, + styles?: object + ): void; - highlight(cfiRange: string, data?: object, cb?: Function, className?: string, styles?: object): void; - - underline(cfiRange: string, data?: object, cb?: Function, className?: string, styles?: object): void; - - mark(cfiRange: string, data?: object, cb?: Function): void; + underline( + cfiRange: string, + data?: object, + cb?: Function, + className?: string, + styles?: object + ): void; + mark(cfiRange: string, data?: object, cb?: Function): void; unhighlight(cfiRange: string): void; - ununderline(cfiRange: string): void; - unmark(cfiRange: string): void; - destroy(): void; - private onLoad(event: Event, promise: Promise): void; - // Event emitters emit(type: any, ...args: any[]): void; - off(type: any, listener: any): any; - on(type: any, listener: any): any; - once(type: any, listener: any, ...args: any[]): any; } diff --git a/types/mapping.d.ts b/types/mapping.d.ts index 00f6bb4..9168e43 100644 --- a/types/mapping.d.ts +++ b/types/mapping.d.ts @@ -1,34 +1,35 @@ -import Layout from "./layout"; import Contents from "./contents"; +import Layout from "./layout"; export interface EpubCFIPair { - start: string, - end: string + start: string; + end: string; } export interface RangePair { - start: Range, - end: Range + start: Range; + end: Range; } export default class Mapping { constructor(layout: Layout, direction?: string, axis?: string, dev?: boolean); - page(contents: Contents, cfiBase: string, start: number, end: number): EpubCFIPair; - + page( + contents: Contents, + cfiBase: string, + start: number, + end: number + ): EpubCFIPair; axis(axis: string): boolean; private walk(root: Node, func: Function); - private findStart(root: Node, start: number, end: number): Range; - private findEnd(root: Node, start: number, end: number): Range; - private findTextStartRange(node: Node, start: number, end: number): Range; - private findTextEndRange(node: Node, start: number, end: number): Range; - private splitTextNodeIntoRanges(node: Node, _splitter?: string): Array; - - private rangePairToCfiPair(cfiBase: string, rangePair: RangePair): EpubCFIPair; + private rangePairToCfiPair( + cfiBase: string, + rangePair: RangePair + ): EpubCFIPair; } diff --git a/types/navigation.d.ts b/types/navigation.d.ts index 8b4f8a0..744afc7 100644 --- a/types/navigation.d.ts +++ b/types/navigation.d.ts @@ -1,15 +1,15 @@ export interface NavItem { - id: string, - href: string, - label: string, - subitems?: Array, - parent?: string + id: string; + href: string; + label: string; + subitems?: Array; + parent?: string; } export interface LandmarkItem { - href?: string, - label?: string, - type?: string + href?: string; + label?: string; + type?: string; } export default class Navigation { @@ -19,28 +19,21 @@ export default class Navigation { landmarks: Array; parse(xml: XMLDocument): void; - - get(target: string) : NavItem; - - landmark(type: string) : LandmarkItem; - + get(target: string): NavItem; + landmark(type: string): LandmarkItem; load(json: string): Array; - forEach(fn: (item: NavItem) => {}): any; private unpack(toc: Array): void; - private parseNav(navHtml: XMLDocument): Array; - private navItem(item: Element): NavItem; - private parseLandmarks(navHtml: XMLDocument): Array; - private landmarkItem(item: Element): LandmarkItem; - private parseNcx(navHtml: XMLDocument): Array; - private ncxItem(item: Element): NavItem; - - private getByIndex(target: string, index: number, navItems: NavItem[]): NavItem; + private getByIndex( + target: string, + index: number, + navItems: NavItem[] + ): NavItem; } diff --git a/types/packaging.d.ts b/types/packaging.d.ts index 630f93e..a3f3bb7 100644 --- a/types/packaging.d.ts +++ b/types/packaging.d.ts @@ -1,47 +1,47 @@ import { SpineItem } from "./section"; export interface PackagingObject { - metadata: PackagingMetadataObject, - spine: Array, - manifest: PackagingManifestObject, - navPath: string, - ncxPath: string, - coverPath: string, - spineNodeIndex: number + metadata: PackagingMetadataObject; + spine: Array; + manifest: PackagingManifestObject; + navPath: string; + ncxPath: string; + coverPath: string; + spineNodeIndex: number; } export interface PackagingMetadataObject { - title: string, - creator: string, - description: string, - pubdate: string, - publisher: string, - identifier: string, - language: string, - rights: string, - modified_date: string, - layout: string, - orientation: string, - flow: string, - viewport: string, - spread: string, - direction: string, + title: string; + creator: string; + description: string; + pubdate: string; + publisher: string; + identifier: string; + language: string; + rights: string; + modified_date: string; + layout: string; + orientation: string; + flow: string; + viewport: string; + spread: string; + direction: string; } export interface PackagingSpineItem { - idref: string, - properties: Array, - index: number + idref: string; + properties: Array; + index: number; } export interface PackagingManifestItem { - href: string, - type: string, - properties: Array + href: string; + type: string; + properties: Array; } export interface PackagingManifestObject { - [key: string]: PackagingManifestItem + [key: string]: PackagingManifestItem; } export default class Packaging { @@ -56,24 +56,18 @@ export default class Packaging { metadata: PackagingMetadataObject; parse(packageDocument: XMLDocument): PackagingObject; - load(json: string): PackagingObject; - destroy(): void; private parseMetadata(xml: Node): PackagingMetadataObject; - private parseManifest(xml: Node): PackagingManifestObject; - - private parseSpine(xml: Node, manifest: PackagingManifestObject): Array; - + private parseSpine( + xml: Node, + manifest: PackagingManifestObject + ): Array; private findNavPath(manifestNode: Node): string | false; - private findNcxPath(manifestNode: Node, spineNode: Node): string | false; - private findCoverPath(packageXml: Node): string; - - private getElementText(xml: Node, tag: string): string - - private getPropertyText(xml: Node, property: string): string + private getElementText(xml: Node, tag: string): string; + private getPropertyText(xml: Node, property: string): string; } diff --git a/types/pagelist.d.ts b/types/pagelist.d.ts index 5104098..e977c56 100644 --- a/types/pagelist.d.ts +++ b/types/pagelist.d.ts @@ -1,29 +1,21 @@ export interface PageListItem { - href: string, - page: string, - cfi?: string, - packageUrl?: string + href: string; + page: string; + cfi?: string; + packageUrl?: string; } export default class Pagelist { constructor(xml: XMLDocument); parse(xml: XMLDocument): Array; - pageFromCfi(cfi: string): number; - cfiFromPage(pg: string | number): string; - pageFromPercentage(percent: number): number; - percentageFromPage(pg: number): number; - destroy(): void; private parseNav(navHtml: Node): Array; - private item(item: Node): PageListItem; - private process(pageList: Array): void; - } diff --git a/types/rendition.d.ts b/types/rendition.d.ts index 69adb70..7d3bc09 100644 --- a/types/rendition.d.ts +++ b/types/rendition.d.ts @@ -1,155 +1,119 @@ +import Annotations from "./annotations"; import Book from "./book"; import Contents from "./contents"; -import Section from "./section"; -import View from "./managers/view"; -import Hook from "./utils/hook"; -import Themes from "./themes"; import EpubCFI from "./epubcfi"; -import Annotations from "./annotations"; +import View from "./managers/view"; +import Section from "./section"; +import Themes from "./themes"; +import Hook from "./utils/hook"; import Queue from "./utils/queue"; export interface RenditionOptions { - width?: number | string, - height?: number | string, - ignoreClass?: string, - manager?: string | Function | object, - view?: string | Function | object, - flow?: string, - layout?: string, - spread?: string, - minSpreadWidth?: number, - stylesheet?: string, - resizeOnOrientationChange?: boolean, - script?: string, - infinite?: boolean, - overflow?: string, - snap?: boolean | object, - defaultDirection?: string, - allowScriptedContent?: boolean, - allowPopups?: boolean + width?: number | string; + height?: number | string; + ignoreClass?: string; + manager?: string | Function | object; + view?: string | Function | object; + flow?: string; + layout?: string; + spread?: string; + minSpreadWidth?: number; + stylesheet?: string; + resizeOnOrientationChange?: boolean; + script?: string; + infinite?: boolean; + overflow?: string; + snap?: boolean | object; + defaultDirection?: string; + allowScriptedContent?: boolean; + allowPopups?: boolean; } export interface DisplayedLocation { - index: number, - href: string, - cfi: string, - location: number, - percentage: number, + index: number; + href: string; + cfi: string; + location: number; + percentage: number; displayed: { - page: number, - total: number - } + page: number; + total: number; + }; } export interface Location { - start: DisplayedLocation, - end: DisplayedLocation, - atStart: boolean, - atEnd: boolean + start: DisplayedLocation; + end: DisplayedLocation; + atStart: boolean; + atEnd: boolean; } export default class Rendition { - constructor(book: Book, options: RenditionOptions); + constructor(book: Book, options: RenditionOptions); + settings: RenditionOptions; + book: Book; + hooks: { + display: Hook; + serialize: Hook; + content: Hook; + unloaded: Hook; + layout: Hook; + render: Hook; + show: Hook; + }; + themes: Themes; + annotations: Annotations; + epubcfi: EpubCFI; + q: Queue; + location: Location; + started: Promise; - settings: RenditionOptions; - book: Book; - hooks: { - display: Hook, - serialize: Hook, - content: Hook, - unloaded: Hook, - layout: Hook, - render: Hook, - show: Hook - } - themes: Themes; - annotations: Annotations; - epubcfi: EpubCFI; - q: Queue; - location: Location; - started: Promise; - - adjustImages(contents: Contents): Promise; - - attachTo(element: Element): Promise; - - clear(): void; - - currentLocation(): DisplayedLocation; - currentLocation(): Promise; - - destroy(): void; - - determineLayoutProperties(metadata: object): object; - - direction(dir: string): void; - - display(target?: string): Promise; - display(target?: number): Promise; - - flow(flow: string): void; - - getContents(): Contents; - - getRange(cfi: string, ignoreClass?: string): Range; - - handleLinks(contents: Contents): void; - - injectIdentifier(doc: Document, section: Section): void; - - injectScript(doc: Document, section: Section): void; - - injectStylesheet(doc: Document, section: Section): void; - - layout(settings: any): any; - - located(location: Location): DisplayedLocation; - - moveTo(offset: number): void; - - next(): Promise; - - onOrientationChange(orientation: string): void; - - passEvents(contents: Contents): void; - - prev(): Promise; - - reportLocation(): Promise; - - requireManager(manager: string | Function | object): any; - - requireView(view: string | Function | object): any; - - resize(width: number, height: number): void; - - setManager(manager: Function): void; - - spread(spread: string, min?: number): void; - - start(): void; - - views(): Array; - - // Event emitters - emit(type: any, ...args: any[]): void; - - off(type: any, listener: any): any; - - on(type: any, listener: any): any; - - once(type: any, listener: any, ...args: any[]): any; - - private triggerMarkEvent(cfiRange: string, data: object, contents: Contents): void; - - private triggerSelectedEvent(cfirange: string, contents: Contents): void; - - private triggerViewEvent(e: Event, contents: Contents): void; - - private onResized(size: { width: number, height: number }): void; - - private afterDisplayed(view: any): void; - - private afterRemoved(view: any): void; + adjustImages(contents: Contents): Promise; + attachTo(element: Element): Promise; + clear(): void; + currentLocation(): DisplayedLocation; + currentLocation(): Promise; + destroy(): void; + determineLayoutProperties(metadata: object): object; + direction(dir: string): void; + display(target?: string): Promise; + display(target?: number): Promise; + flow(flow: string): void; + getContents(): Contents; + getRange(cfi: string, ignoreClass?: string): Range; + handleLinks(contents: Contents): void; + injectIdentifier(doc: Document, section: Section): void; + injectScript(doc: Document, section: Section): void; + injectStylesheet(doc: Document, section: Section): void; + layout(settings: any): any; + located(location: Location): DisplayedLocation; + moveTo(offset: number): void; + next(): Promise; + onOrientationChange(orientation: string): void; + passEvents(contents: Contents): void; + prev(): Promise; + reportLocation(): Promise; + requireManager(manager: string | Function | object): any; + requireView(view: string | Function | object): any; + resize(width: number, height: number): void; + setManager(manager: Function): void; + spread(spread: string, min?: number): void; + start(): void; + views(): Array; + // Event emitters + emit(type: any, ...args: any[]): void; + off(type: any, listener: any): any; + on(type: any, listener: any): any; + once(type: any, listener: any, ...args: any[]): any; + private triggerMarkEvent( + cfiRange: string, + data: object, + contents: Contents + ): void; + private triggerSelectedEvent(cfirange: string, contents: Contents): void; + private triggerViewEvent(e: Event, contents: Contents): void; + private onResized(size: { width: number; height: number }): void; + private afterDisplayed(view: any): void; + private afterRemoved(view: any): void; } diff --git a/types/resources.d.ts b/types/resources.d.ts index f4bfb37..dfb3c8e 100644 --- a/types/resources.d.ts +++ b/types/resources.d.ts @@ -1,33 +1,29 @@ -import { PackagingManifestObject } from "./packaging"; import Archive from "./archive"; +import { PackagingManifestObject } from "./packaging"; export default class Resources { - constructor(manifest: PackagingManifestObject, options: { - replacements?: string, - archive?: Archive, - resolver?: Function, - request?: Function - }); - + constructor( + manifest: PackagingManifestObject, + options: { + replacements?: string; + archive?: Archive; + resolver?: Function; + request?: Function; + } + ); process(manifest: PackagingManifestObject): void; - createUrl(url: string): Promise; - replacements(): Promise>; - relativeTo(absolute: boolean, resolver?: Function): Array; - get(path: string): string; - substitute(content: string, url?: string): string; - destroy(): void; private split(): void; - private splitUrls(): void; - - private replaceCss(archive: Archive, resolver?: Function): Promise>; - + private replaceCss( + archive: Archive, + resolver?: Function + ): Promise>; private createCssFile(href: string): Promise; } diff --git a/types/section.d.ts b/types/section.d.ts index 075bef5..7ee302f 100644 --- a/types/section.d.ts +++ b/types/section.d.ts @@ -1,32 +1,31 @@ import { HooksObject } from "./utils/hook"; export interface GlobalLayout { - layout: string, - spread: string, - orientation: string + layout: string; + spread: string; + orientation: string; } export interface LayoutSettings { - layout: string, - spread: string, - orientation: string + layout: string; + spread: string; + orientation: string; } export interface SpineItem { - index: number, - cfiBase: string, - href?: string, - url?: string, - canonical?: string, - properties?: Array, - linear?: string, - next: () => SpineItem, - prev: () => SpineItem, + index: number; + cfiBase: string; + href?: string; + url?: string; + canonical?: string; + properties?: Array; + linear?: string; + next: () => SpineItem; + prev: () => SpineItem; } export default class Section { constructor(item: SpineItem, hooks: HooksObject); - idref: string; linear: boolean; properties: Array; @@ -37,28 +36,17 @@ export default class Section { next: () => SpineItem; prev: () => SpineItem; cfiBase: string; - document: Document; contents: Element; output: string; - hooks: HooksObject; - load(_request?: Function): Document; - render(_request?: Function): string; - find(_query: string): Array; - reconcileLayoutSettings(globalLayout: GlobalLayout): LayoutSettings; - cfiFromRange(_range: Range): string; - cfiFromElement(el: Element): string; - unload(): void; - destroy(): void; - private base(): void; } diff --git a/types/spine.d.ts b/types/spine.d.ts index 6cd59ce..2d96a7b 100644 --- a/types/spine.d.ts +++ b/types/spine.d.ts @@ -4,27 +4,17 @@ import Hook from "./utils/hook"; export default class Spine { constructor(); - hooks: { - serialize: Hook, - content: Hook + serialize: Hook; + content: Hook; }; - unpack(_package: Packaging, resolver: Function, canonical: Function): void; - get(target?: string | number): Section; - each(...args: any[]): any; - first(): Section; - last(): Section; - destroy(): void; - private append(section: Section): number; - private prepend(section: Section): number; - private remove(section: Section): number; } diff --git a/types/store.d.ts b/types/store.d.ts index 655a17d..add3da7 100644 --- a/types/store.d.ts +++ b/types/store.d.ts @@ -1,30 +1,29 @@ -import localForage = require('localforage'); +import localForage = require("localforage"); import Resources from "./resources"; export default class Store { constructor(name: string, request?: Function, resolver?: Function); - add(resources: Resources, force?: boolean): Promise>; - put(url: string, withCredentials?: boolean, headers?: object): Promise; - - request(url: string, type?: string, withCredentials?: boolean, headers?: object): Promise; - - retrieve(url: string, type?: string): Promise; - + request( + url: string, + type?: string, + withCredentials?: boolean, + headers?: object + ): Promise; + retrieve( + url: string, + type?: string + ): Promise; getBlob(url: string, mimeType?: string): Promise; - getText(url: string): Promise; - getBase64(url: string, mimeType?: string): Promise; - createUrl(url: string, options: { base64: boolean }): Promise; - revokeUrl(url: string): void; - destroy(): void; - private checkRequirements(): void; - - private handleResponse(response: any, type?: string): Blob | string | JSON | Document | XMLDocument; + private handleResponse( + response: any, + type?: string + ): Blob | string | JSON | Document | XMLDocument; } diff --git a/types/themes.d.ts b/types/themes.d.ts index 465a127..b6055a0 100644 --- a/types/themes.d.ts +++ b/types/themes.d.ts @@ -1,40 +1,23 @@ -import Rendition from "./rendition"; import Contents from "./contents"; +import Rendition from "./rendition"; export default class Themes { constructor(rendition: Rendition); - - register( themeObject: object ): void; - - register( theme: string, url: string ): void; - - register( theme: string, themeObject: object ): void; - - default( theme: object | string ): void; - - registerThemes( themes: object ): void; - - registerCss( name: string, css: string ): void; - - registerUrl( name: string, input: string ): void; - - registerRules( name: string, rules: object ): void; - - select( name: string ): void; - - update( name: string ): void; - - inject( content: Contents ): void; - - add( name: string, contents: Contents ): void; - + register(themeObject: object): void; + register(theme: string, url: string): void; + register(theme: string, themeObject: object): void; + default(theme: object | string): void; + registerThemes(themes: object): void; + registerCss(name: string, css: string): void; + registerUrl(name: string, input: string): void; + registerRules(name: string, rules: object): void; + select(name: string): void; + update(name: string): void; + inject(content: Contents): void; + add(name: string, contents: Contents): void; override(name: string, value: string, priority?: boolean): void; - overrides(contents: Contents): void; - fontSize(size: string): void; - font(f: string): void; - destroy(): void; } diff --git a/types/tsconfig.json b/types/tsconfig.json index 546c5a3..d636bab 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -1,24 +1,16 @@ { - "compilerOptions": { - "module": "commonjs", - "lib": [ - "es6", - "dom" - ], - "noImplicitAny": true, - "noImplicitThis": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "baseUrl": "../", - "typeRoots": [ - "../" - ], - "types": [], - "noEmit": true, - "forceConsistentCasingInFileNames": true - }, - "files": [ - "index.d.ts", - "epubjs-tests.ts" - ] + "compilerOptions": { + "module": "commonjs", + "lib": ["es6", "dom"], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "baseUrl": "../", + "typeRoots": ["../"], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": ["index.d.ts"] } diff --git a/types/utils/constants.d.ts b/types/utils/constants.d.ts index 1c5c5c5..82f55e4 100644 --- a/types/utils/constants.d.ts +++ b/types/utils/constants.d.ts @@ -4,6 +4,6 @@ export const DOM_EVENTS: Array; export const EVENTS: { [key: string]: { - [key: string]: string - } -} + [key: string]: string; + }; +}; diff --git a/types/utils/core.d.ts b/types/utils/core.d.ts index 6469a66..0dc186f 100644 --- a/types/utils/core.d.ts +++ b/types/utils/core.d.ts @@ -1,79 +1,85 @@ export function uuid(): string; - export function documentHeight(): number; - export function isElement(obj: object): boolean; - export function isNumber(n: any): boolean; - export function isFloat(n: any): boolean; - export function prefixed(unprefixed: string): string; - export function defaults(obj: object): object; - export function extend(target: object): object; - -export function insert(item: any, array: Array, compareFunction: Function): number; - -export function locationOf(item: any, array: Array, compareFunction: Function, _start: Function, _end: Function): number; - -export function indexOfSorted(item: any, array: Array, compareFunction: Function, _start: Function, _end: Function): number; - -export function bounds(el: Element): { width: Number, height: Number}; - -export function borders(el: Element): { width: Number, height: Number}; - +export function insert( + item: any, + array: Array, + compareFunction: Function +): number; +export function locationOf( + item: any, + array: Array, + compareFunction: Function, + _start: Function, + _end: Function +): number; +export function indexOfSorted( + item: any, + array: Array, + compareFunction: Function, + _start: Function, + _end: Function +): number; +export function bounds(el: Element): { width: Number; height: Number }; +export function borders(el: Element): { width: Number; height: Number }; export function nodeBounds(node: Node): object; - -export function windowBounds(): { width: Number, height: Number, top: Number, left: Number, right: Number, bottom: Number }; - +export function windowBounds(): { + width: Number; + height: Number; + top: Number; + left: Number; + right: Number; + bottom: Number; +}; export function indexOfNode(node: Node, typeId: string): number; - export function indexOfTextNode(textNode: Node): number; - export function indexOfElementNode(elementNode: Element): number; - export function isXml(ext: string): boolean; - export function createBlob(content: any, mime: string): Blob; - export function createBlobUrl(content: any, mime: string): string; - export function revokeBlobUrl(url: string): void; - -export function createBase64Url(content: any, mime: string): string - +export function createBase64Url(content: any, mime: string): string; export function type(obj: object): string; - -export function parse(markup: string, mime: string, forceXMLDom: boolean): Document; - +export function parse( + markup: string, + mime: string, + forceXMLDom: boolean +): Document; export function qs(el: Element, sel: string): Element; - export function qsa(el: Element, sel: string): ArrayLike; - -export function qsp(el: Element, sel: string, props: Array): ArrayLike; - +export function qsp( + el: Element, + sel: string, + props: Array +): ArrayLike; export function sprint(root: Node, func: Function): void; - -export function treeWalker(root: Node, func: Function, filter: object | Function): void; - +export function treeWalker( + root: Node, + func: Function, + filter: object | Function +): void; export function walk(node: Node, callback: Function): void; - export function blob2base64(blob: Blob): string; - export function defer(): Promise; - -export function querySelectorByType(html: Element, element: string, type: string): Array; - +export function querySelectorByType( + html: Element, + element: string, + type: string +): Array; export function findChildren(el: Element): Array; - export function parents(node: Element): Array; - -export function filterChildren(el: Element, nodeName: string, single: boolean): Array; - -export function getParentByTagName(node: Element, tagname: string): Array; - -export class RangeObject extends Range { - -} +export function filterChildren( + el: Element, + nodeName: string, + single: boolean +): Array; +export function getParentByTagName( + node: Element, + tagname: string +): Array; +export class RangeObject extends Range {} diff --git a/types/utils/hook.d.ts b/types/utils/hook.d.ts index 9db1491..5f14514 100644 --- a/types/utils/hook.d.ts +++ b/types/utils/hook.d.ts @@ -1,5 +1,5 @@ interface HooksObject { - [key: string]: Hook + [key: string]: Hook; } export default class Hook { @@ -7,12 +7,8 @@ export default class Hook { register(func: Function): void; register(arr: Array): void; - deregister(func: Function): void; - trigger(...args: any[]): Promise; - list(): Array; - clear(): void; } diff --git a/types/utils/path.d.ts b/types/utils/path.d.ts index b91e88b..378d631 100644 --- a/types/utils/path.d.ts +++ b/types/utils/path.d.ts @@ -2,16 +2,10 @@ export default class Path { constructor(pathString: string); parse(what: string): object; - isAbsolute(what: string): boolean; - isDirectory(what: string): boolean; - resolve(what: string): string; - relative(what: string): string; - splitPath(filename: string): string; - toString(): string; } diff --git a/types/utils/queue.d.ts b/types/utils/queue.d.ts index 4813ef6..6fe1db2 100644 --- a/types/utils/queue.d.ts +++ b/types/utils/queue.d.ts @@ -1,31 +1,21 @@ -import { defer } from "./core"; - export interface QueuedTask { - task: any | Task, - args: any[], - deferred: any, // should be defer, but not working - promise: Promise + task: any | Task; + args: any[]; + deferred: any; // should be defer, but not working + promise: Promise; } export default class Queue { constructor(context: any); enqueue(func: Promise | Function, ...args: any[]): Promise; - dequeue(): Promise; - dump(): void; - run(): Promise; - flush(): Promise; - clear(): void; - length(): number; - pause(): void; - stop(): void; } diff --git a/types/utils/replacements.d.ts b/types/utils/replacements.d.ts index 9728ee4..687b522 100644 --- a/types/utils/replacements.d.ts +++ b/types/utils/replacements.d.ts @@ -1,12 +1,12 @@ -import Section from "../section"; import Contents from "../contents"; +import Section from "../section"; export function replaceBase(doc: Document, section: Section): void; - export function replaceCanonical(doc: Document, section: Section): void; - export function replaceMeta(doc: Document, section: Section): void; - export function replaceLinks(contents: Contents, fn: Function): void; - -export function substitute(contents: Contents, urls: string[], replacements: string[]): void; +export function substitute( + contents: Contents, + urls: string[], + replacements: string[] +): void; diff --git a/types/utils/request.d.ts b/types/utils/request.d.ts index e6c9653..a154f9e 100644 --- a/types/utils/request.d.ts +++ b/types/utils/request.d.ts @@ -1 +1,6 @@ -export default function request(url: string, type?: string, withCredentials?: boolean, headers?: object): Promise; +export default function request( + url: string, + type?: string, + withCredentials?: boolean, + headers?: object +): Promise; diff --git a/types/utils/scrolltype.d.ts b/types/utils/scrolltype.d.ts index f1dd74f..3d41622 100644 --- a/types/utils/scrolltype.d.ts +++ b/types/utils/scrolltype.d.ts @@ -1,3 +1,2 @@ export default function scrollType(): string; - export function createDefiner(): Node; diff --git a/types/utils/url.d.ts b/types/utils/url.d.ts index bf30dfb..2c1969e 100644 --- a/types/utils/url.d.ts +++ b/types/utils/url.d.ts @@ -4,10 +4,7 @@ export default class Url { constructor(urlString: string, baseString: string); path(): Path; - resolve(what: string): string; - relative(what: string): string; - toString(): string; } diff --git a/webpack.config.js b/webpack.config.js index c5a5ef0..4c09bc5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,83 +1,88 @@ -var webpack = require("webpack"); var path = require("path"); -var PROD = (process.env.NODE_ENV === "production") -var LEGACY = (process.env.LEGACY) -var MINIMIZE = (process.env.MINIMIZE === "true") +var LEGACY = process.env.LEGACY; +var MINIMIZE = process.env.MINIMIZE === "true"; var hostname = "localhost"; var port = 8080; var filename = "[name]"; var sourceMapFilename = "[name]"; if (LEGACY) { - filename += ".legacy"; + filename += ".legacy"; } if (MINIMIZE) { - filename += ".min.js"; - sourceMapFilename += ".min.js.map"; + filename += ".min.js"; + sourceMapFilename += ".min.js.map"; } else { - filename += ".js"; - sourceMapFilename += ".js.map"; + filename += ".js"; + sourceMapFilename += ".js.map"; } module.exports = { - mode: process.env.NODE_ENV, - entry: { - "epub": "./src/epub.js", - }, - devtool: MINIMIZE ? false : 'source-map', - output: { - path: path.resolve("./dist"), - filename: filename, - sourceMapFilename: sourceMapFilename, - library: "ePub", - libraryTarget: "umd", - libraryExport: "default", - publicPath: "/dist/" - }, - optimization: { - minimize: MINIMIZE - }, - externals: { - "jszip/dist/jszip": "JSZip", - "xmldom": "xmldom" - }, - plugins: [], - resolve: { - alias: { - path: "path-webpack" - } - }, - devServer: { - host: hostname, - port: port, - inline: true, - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE", - "Access-Control-Allow-Headers": "Content-Type" - } - }, - module: { - rules: [ - { - test: /\.js$/, - exclude: /node_modules/, - use: { - loader: "babel-loader", - options: { - presets: [["@babel/preset-env", { - targets: LEGACY ? "defaults" : "last 2 Chrome versions, last 2 Safari versions, last 2 ChromeAndroid versions, last 2 iOS versions, last 2 Firefox versions, last 2 Edge versions", - corejs: 3, - useBuiltIns: "usage", - bugfixes: true, - modules: false - }]] - } - } - } - ] - }, - performance: { - hints: false - } -} + mode: process.env.NODE_ENV, + entry: { + epub: "./src/epub.js", + }, + devtool: MINIMIZE ? false : "source-map", + output: { + path: path.resolve("./dist"), + filename: filename, + sourceMapFilename: sourceMapFilename, + library: "ePub", + libraryTarget: "umd", + libraryExport: "default", + publicPath: "/dist/", + }, + optimization: { + minimize: MINIMIZE, + }, + externals: { + "jszip/dist/jszip": "JSZip", + xmldom: "xmldom", + }, + plugins: [], + resolve: { + alias: { + path: "path-webpack", + }, + }, + devServer: { + host: hostname, + port: port, + inline: true, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE", + "Access-Control-Allow-Headers": "Content-Type", + }, + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: [ + [ + "@babel/preset-env", + { + targets: LEGACY + ? "defaults" + : "last 2 Chrome versions, last 2 Safari versions, last 2 ChromeAndroid versions, last 2 iOS versions, last 2 Firefox versions, last 2 Edge versions", + corejs: 3, + useBuiltIns: "usage", + bugfixes: true, + modules: false, + }, + ], + ], + }, + }, + }, + ], + }, + performance: { + hints: false, + }, +};