epubjs
Version:
Parse and Render Epubs
1,258 lines (1,050 loc) • 31.1 kB
JavaScript
import _Promise from "babel-runtime/core-js/promise";
import _typeof from "babel-runtime/helpers/typeof";
import _classCallCheck from "babel-runtime/helpers/classCallCheck";
import _createClass from "babel-runtime/helpers/createClass";
import EventEmitter from "event-emitter";
import { extend, defer, isFloat } from "../utils/core";
import Hook from "../utils/hook";
import EpubCFI from "../utils/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, EPUBJS_VERSION } from "../utils/constants";
import Book from "../book/book";
import Spine from "../book/spine";
import Locations from "../epub/locations";
import PageList from "../epub/pagelist";
import Epub from "../epub/epub";
// import Navigation from "../epub/navigation";
import { replaceBase, replaceCanonical, replaceMeta } from "../utils/replacements";
import Url from "../utils/url";
var DEV = false;
// Default Views
import IframeView from "../managers/views/iframe";
// Default View Managers
import DefaultViewManager from "../managers/default/index";
import ContinuousViewManager from "../managers/continuous/index";
/**
* Displays an Epub as a series of Views for each Section.
* Requires Manager and View class to handle specifics of rendering
* the section contetn.
* @class
* @param {Book} book
* @param {object} [options]
* @param {number} [options.width]
* @param {number} [options.height]
* @param {string} [options.ignoreClass] class for the cfi parser to ignore
* @param {string | function | object} [options.manager='default']
* @param {string | function} [options.view='iframe']
* @param {string} [options.layout] layout to force
* @param {string} [options.spread] force spread value
* @param {number} [options.minSpreadWidth] overridden by spread: none (never) / both (always)
* @param {string} [options.stylesheet] url of stylesheet to be injected
* @param {string} [options.script] url of script to be injected
*/
var Rendition = function () {
function Rendition(manifest, options) {
var _this = this;
_classCallCheck(this, Rendition);
this.settings = extend(this.settings || {}, {
width: null,
height: null,
ignoreClass: "",
manager: "default",
view: "iframe",
flow: null,
layout: null,
spread: null,
minSpreadWidth: 800,
stylesheet: null,
script: null,
worker: undefined,
workerScope: undefined
});
extend(this.settings, options);
if (_typeof(this.settings.manager) === "object") {
this.manager = this.settings.manager;
}
/**
* 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.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.hooks.content.register(this.addIdentifier.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);
/**
* @private
*/
this.spineByHref = undefined;
this.spineBySource = undefined;
this.spineById = undefined;
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.started);
if (manifest) {
this.unpack(manifest);
}
// If a service workers is used, block queue till it is ready
if (this.settings.worker && navigator && 'serviceWorker' in navigator) {
this.q.enqueue(function () {
return _this.worker(_this.settings.worker).catch(function () {
// worker failed, will need replacements
_this.starting = new defer();
_this.started = _this.starting.promise;
// Block the queue again
return _this.q.enqueue(_this.started);
});
});
}
}
/**
* Load Book object or JSON manifest
*/
_createClass(Rendition, [{
key: "unpack",
value: function unpack(manifest) {
var _this2 = this;
if (!manifest) {
throw new Error("No manifest provided");
}
if (typeof manifest === "string") {
this.manifest = JSON.parse(manifest);
} else {
this.manifest = manifest;
}
var spine = this.manifest.spine.map(function (item, index) {
item.index = index;
return item;
});
this.spineByHref = {};
this.spineBySource = {};
this.spineById = {};
this.manifest.spine.forEach(function (section, index) {
_this2.spineByHref[decodeURI(section.href)] = index;
_this2.spineByHref[encodeURI(section.href)] = index;
_this2.spineByHref[section.href] = index;
if (section.source) {
_this2.spineBySource[decodeURI(section.source)] = index;
_this2.spineBySource[encodeURI(section.source)] = index;
_this2.spineBySource[section.source] = index;
}
_this2.spineById[section.idref] = index;
});
this.book = new Book(manifest);
this.start();
}
/**
* Set the manager function
* @param {function} manager
*/
}, {
key: "setManager",
value: function setManager(manager) {
this.manager = manager;
}
/**
* Require the manager from passed string, or as a class function
* @param {string|object} manager [description]
* @return {method}
*/
}, {
key: "requireManager",
value: function 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}
*/
}, {
key: "requireView",
value: function 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
*/
}, {
key: "start",
value: function start() {
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,
spine: this.manifest.spine,
hooks: this.hooks,
// request: this.book.load.bind(this.book),
settings: this.settings
});
}
this.direction(this.manifest.metadata.direction);
// Parse metadata to get layout props
this.settings.globalLayoutProperties = this.determineLayoutProperties(this.manifest.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}
*/
}, {
key: "attachTo",
value: function 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}
*/
}, {
key: "display",
value: function 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}
*/
}, {
key: "_display",
value: function _display(target) {
var _this3 = this;
// 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.locations && this.locations.length() && (isFloat(target) || target === "1.0")) {
// Handle 1.0
target = this.locations.cfiFromPercentage(parseFloat(target));
}
section = this.findInSpine(target);
if (!section) {
displaying.reject(new Error("No Section Found"));
return displayed;
}
this.manager.display(section, target).then(function () {
displaying.resolve(section);
_this3.displaying = undefined;
/**
* Emit that a section has been displayed
* @event displayed
* @param {Section} section
* @memberof Rendition
*/
_this3.emit(EVENTS.RENDITION.DISPLAYED, section);
_this3.reportLocation();
}, function (err) {
/**
* Emit that has been an error displaying
* @event displayError
* @param {Section} section
* @memberof Rendition
*/
_this3.emit(EVENTS.RENDITION.DISPLAY_ERROR, err);
});
return displayed;
}
/**
* Report what section has been displayed
* @private
* @param {*} view
*/
}, {
key: "afterDisplayed",
value: function afterDisplayed(view) {
var _this4 = this;
view.on(EVENTS.VIEWS.MARK_CLICKED, function (cfiRange, data) {
return _this4.triggerMarkEvent(cfiRange, data, view);
});
this.hooks.render.trigger(view, this).then(function () {
if (view.contents) {
_this4.hooks.content.trigger(view.contents, _this4).then(function () {
/**
* Emit that a section has been rendered
* @event rendered
* @param {Section} section
* @param {View} view
* @memberof Rendition
*/
_this4.emit(EVENTS.RENDITION.RENDERED, view.section, view);
});
} else {
_this4.emit(EVENTS.RENDITION.RENDERED, view.section, view);
}
});
}
/**
* Report what has been removed
* @private
* @param {*} view
*/
}, {
key: "afterRemoved",
value: function afterRemoved(view) {
var _this5 = this;
this.hooks.unloaded.trigger(view, this).then(function () {
/**
* Emit that a section has been removed
* @event removed
* @param {Section} section
* @param {View} view
* @memberof Rendition
*/
_this5.emit(EVENTS.RENDITION.REMOVED, view.section, view);
});
}
/**
* Report resize events and display the last seen location
* @private
*/
}, {
key: "onResized",
value: function onResized(size) {
/**
* Emit that the rendition has been resized
* @event resized
* @param {number} width
* @param {height} height
* @memberof Rendition
*/
this.emit(EVENTS.RENDITION.RESIZED, {
width: size.width,
height: size.height
});
if (this.location && this.location.start) {
this.display(this.location.start.cfi);
}
}
/**
* Report orientation events and display the last seen location
* @private
*/
}, {
key: "onOrientationChange",
value: function 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
*/
}, {
key: "moveTo",
value: function moveTo(offset) {
this.manager.moveTo(offset);
}
/**
* Trigger a resize of the views
* @param {number} [width]
* @param {number} [height]
*/
}, {
key: "resize",
value: function resize(width, height) {
if (width) {
this.settings.width = width;
}
if (height) {
this.settings.height = height;
}
this.manager.resize(width, height);
}
/**
* Clear all rendered views
*/
}, {
key: "clear",
value: function clear() {
this.manager.clear();
}
/**
* Go to the next "page" in the rendition
* @return {Promise}
*/
}, {
key: "next",
value: function 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}
*/
}, {
key: "prev",
value: function 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
*/
}, {
key: "determineLayoutProperties",
value: function 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";
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
*/
}, {
key: "flow",
value: function flow(_flow2) {
var _flow = _flow2;
if (_flow2 === "scrolled" || _flow2 === "scrolled-doc" || _flow2 === "scrolled-continuous") {
_flow = "scrolled";
}
if (_flow2 === "auto" || _flow2 === "paginated") {
_flow = "paginated";
}
this.settings.flow = _flow2;
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
*/
}, {
key: "layout",
value: function layout(settings) {
var _this6 = this;
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, function (props, changed) {
_this6.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
*/
}, {
key: "spread",
value: function spread(_spread, min) {
this._layout.spread(_spread, min);
if (this.manager.isRendered()) {
this.manager.updateLayout();
}
}
/**
* Adjust the direction of the rendition
* @param {string} dir
*/
}, {
key: "direction",
value: function 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
*/
}, {
key: "reportLocation",
value: function 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) {
var 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) {
var 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)
*/
}, {
key: "currentLocation",
value: function currentLocation() {
var location = this.manager.currentLocation();
if (location && location.then && typeof location.then === "function") {
location.then(function (result) {
var located = this.located(result);
return located;
}.bind(this));
} else if (location) {
var located = this.located(location);
return located;
}
}
/**
* Creates a Rendition#locationRange from location
* passed by the Manager
* @returns {displayedLocation}
* @private
*/
}, {
key: "located",
value: function located(location) {
if (!location.length) {
return {};
}
var start = location[0];
var end = location[location.length - 1];
var 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
}
}
};
if (this.locations) {
var locationStart = this.locations.locationFromCfi(start.mapping.start);
var locationEnd = this.locations.locationFromCfi(end.mapping.end);
if (locationStart != null) {
located.start.location = locationStart;
located.start.percentage = this.locations.percentageFromLocation(locationStart);
}
if (locationEnd != null) {
located.end.location = locationEnd;
located.end.percentage = this.locations.percentageFromLocation(locationEnd);
}
}
if (this.pageList) {
var pageStart = this.pageList.pageFromCfi(start.mapping.start);
var pageEnd = this.pageList.pageFromCfi(end.mapping.end);
if (pageStart != -1) {
located.start.page = pageStart;
}
if (pageEnd != -1) {
located.end.page = pageEnd;
}
}
if (end.index === this.manifest.spine[this.manifest.spine.length - 1].index && located.end.displayed.page >= located.end.displayed.total) {
located.atEnd = true;
}
if (start.index === this.manifest.spine[0].index && located.start.displayed.page === 1) {
located.atStart = true;
}
return located;
}
/**
* Remove and Clean Up the Rendition
*/
}, {
key: "destroy",
value: function destroy() {
// Clear the queue
this.q.clear();
this.q = undefined;
this.manager && this.manager.destroy();
this.manifest = undefined;
this.spineByHref = undefined;
this.spineBySource = undefined;
this.spineById = undefined;
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 {View} view
*/
}, {
key: "passEvents",
value: function passEvents(contents) {
var _this7 = this;
var listenedEvents = Contents.listenedEvents;
listenedEvents.forEach(function (e) {
contents.on(e, function (ev) {
return _this7.triggerViewEvent(ev, contents);
});
});
contents.on(EVENTS.CONTENTS.SELECTED, function (e) {
return _this7.triggerSelectedEvent(e, contents);
});
}
/**
* Emit events passed by a view
* @private
* @param {event} e
*/
}, {
key: "triggerViewEvent",
value: function triggerViewEvent(e, contents) {
this.emit(e.type, e, contents);
}
/**
* Emit a selection event's CFI Range passed from a a view
* @private
* @param {EpubCFI} cfirange
*/
}, {
key: "triggerSelectedEvent",
value: function triggerSelectedEvent(cfirange, contents) {
/**
* Emit that a text selection has occured
* @event selected
* @param {EpubCFI} 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
*/
}, {
key: "triggerMarkEvent",
value: function 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}
*/
}, {
key: "getRange",
value: function 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
*/
}, {
key: "adjustImages",
value: function adjustImages(contents) {
if (this._layout.name === "pre-paginated") {
return new _Promise(function (resolve) {
resolve();
});
}
contents.addStylesheetRules({
"img": {
"max-width": (this._layout.columnWidth ? this._layout.columnWidth + "px" : "100%") + "!important",
"max-height": (this._layout.height ? this._layout.height * 0.6 + "px" : "60%") + "!important",
"object-fit": "contain",
"page-break-inside": "avoid"
},
"svg": {
"max-width": (this._layout.columnWidth ? this._layout.columnWidth + "px" : "100%") + "!important",
"max-height": (this._layout.height ? this._layout.height * 0.6 + "px" : "60%") + "!important",
"page-break-inside": "avoid"
}
});
return new _Promise(function (resolve, reject) {
// Wait to apply
setTimeout(function () {
resolve();
}, 1);
});
}
/**
* Hook to add the book identifier
* @param {Contents} contents
* @private
*/
}, {
key: "addIdentifier",
value: function addIdentifier(contents) {
var ident = this.book.metadata.identifier;
contents.addIdentifier(ident);
}
/**
* Get the Contents object of each rendered view
* @returns {Contents[]}
*/
}, {
key: "getContents",
value: function getContents() {
return this.manager ? this.manager.getContents() : [];
}
/**
* Get the views member from the manager
* @returns {Views}
*/
}, {
key: "views",
value: function views() {
var views = this.manager ? this.manager.views : undefined;
return views || [];
}
/**
* Hook to handle link clicks in rendered content
* @param {Contents} contents
* @private
*/
}, {
key: "handleLinks",
value: function handleLinks(contents) {
var _this8 = this;
if (contents) {
contents.on(EVENTS.CONTENTS.LINK_CLICKED, function (href) {
var relative = _this8.book.path.relative(href);
_this8.display(relative);
});
}
}
/**
* @return {object} spineItem
*/
}, {
key: "findInSpine",
value: function findInSpine(target) {
var index = 0;
if (this.epubcfi.isCfiString(target)) {
var 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.spineBySource[target] || this.spineBySource[encodeURI(target)];
}
return this.manifest.spine[index] || null;
}
/**
* Generates the Book Key using the identifer in the manifest or other string provided
* @param {string} [identifier] to use instead of metadata identifier
* @return {string} key
*/
}, {
key: "key",
value: function key(identifier) {
var ident = identifier || this.manifest.metadata.identifier;
return "epubjs-" + EPUBJS_VERSION + "-" + ident;
}
}, {
key: "worker",
value: function worker(workerUrl) {
var _this9 = this;
var deferred = new defer();
var key = this.key();
// Resolve early if book is not archived and not cross domain
var url = new Url(this.book.url);
var source = this.book.source ? this.book.source.type : '';
if (source !== "application/epub+zip" && url.origin === window.location.origin) {
deferred.resolve();
}
if ('serviceWorker' in navigator) {
var worker = navigator.serviceWorker.controller;
// Worker is already running
if (worker) {
deferred.resolve();
}
navigator.serviceWorker.register(workerUrl, { scope: this.settings.workerScope }).then(function (reg) {
worker = navigator.serviceWorker.controller;
if (reg.active && !worker) {
_this9.emit(EVENTS.RENDITION.WORKER_INACTIVE);
deferred.resolve();
}
if (worker) {
deferred.resolve();
}
}, function (error) {
// registration failed
console.error(error);
_this9.emit(EVENTS.RENDITION.WORKER_FAILED);
deferred.reject('Worker registration failed', error);
});
navigator.serviceWorker.addEventListener('message', function (event) {
DEV && console.log("[sw msg]", event.data);
if (event.data.msg === "active") {
deferred.resolve();
}
});
navigator.serviceWorker.addEventListener("controllerchange", function (event) {
worker = navigator.serviceWorker.controller;
if (worker) {
deferred.resolve();
}
});
} else {
deferred.resolve();
}
return deferred.promise;
}
}, {
key: "cache",
value: function cache(worker) {
if (!worker) {
worker = navigator.serviceWorker.controller;
}
worker.postMessage({ method: "add", key: key, resources: this.manifest.resources });
}
}, {
key: "scale",
value: function scale(s) {
return this.manager && this.manager.scale(s);
}
}]);
return Rendition;
}();
//-- Enable binding events to Renderer
EventEmitter(Rendition.prototype);
export default Rendition;