UNPKG

epubjs

Version:
608 lines (505 loc) 14.2 kB
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"; /** * [Rendition description] * @class * @param {Book} book * @param {object} options * @param {int} options.width * @param {int} options.height * @param {string} options.ignoreClass * @param {string} options.manager * @param {string} options.view * @param {string} options.layout * @param {string} options.spread * @param {int} options.minSpreadWidth overridden by spread: none (never) / both (always) */ 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 }); extend(this.settings, options); if (typeof(this.settings.manager) === "object") { this.manager = this.settings.manager; } this.viewSettings = { ignoreClass: this.settings.ignoreClass }; this.book = book; this.views = null; /** * Adds Hook methods to the Rendition prototype * @property {Hook} hooks */ this.hooks = {}; this.hooks.display = new Hook(this); this.hooks.serialize = new Hook(this); /** * @property {method} hooks.content * @type {Hook} */ this.hooks.content = 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.display.register(this.afterDisplay.bind(this)); this.themes = new Themes(this); this.epubcfi = new EpubCFI(); this.q = new Queue(this); this.q.enqueue(this.book.opened); // Block the queue until rendering is started this.starting = new defer(); this.started = this.starting.promise; 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 function * @param {string|function} manager [description] * @return {method} */ requireManager(manager) { var viewManager; // If manager is a string, try to load from register managers, // or require included managers directly if (typeof manager === "string") { // Use global or require viewManager = typeof ePub != "undefined" ? ePub.ViewManagers[manager] : undefined; //require("./managers/"+manager); } else { // otherwise, assume we were passed a function viewManager = manager; } return viewManager; } /** * Require the view from passed string, or as a function * @param {string|function} view * @return {view} */ requireView(view) { var View; if (typeof view == "string") { View = typeof ePub != "undefined" ? ePub.Views[view] : undefined; //require("./views/"+view); } else { // otherwise, assume we were passed a function View = view; } return View; } /** * Start the rendering * @return {Promise} rendering has started */ 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, request: this.book.load.bind(this.book), settings: this.settings }); } // 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("added", this.afterDisplayed.bind(this)); // Listen for resizing this.manager.on("resized", this.onResized.bind(this)); // Listen for scroll changes this.manager.on("scroll", this.reportLocation.bind(this)); this.on("displayed", this.reportLocation.bind(this)); // Trigger that rendering has started this.emit("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 }); // Trigger Attached this.emit("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){ 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){ var isCfiString = this.epubcfi.isCfiString(target); var displaying = new defer(); var displayed = displaying.promise; var section; var moveTo; // Check if this is a book percentage if (this.book.locations.length && isFloat(target)) { target = this.book.locations.cfiFromPercentage(target); } section = this.book.spine.get(target); if(!section){ displaying.reject(new Error("No Section Found")); return displayed; } // Trim the target fragment // removing the chapter if(!isCfiString && typeof target === "string" && target.indexOf("#") > -1) { moveTo = target.substring(target.indexOf("#")+1); } if (isCfiString) { moveTo = target; } return this.manager.display(section, moveTo) .then(function(){ // this.emit("displayed", section); }.bind(this)); } /* 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 has been displayed * @private * @param {*} view */ afterDisplayed(view){ this.hooks.content.trigger(view.contents, this); this.emit("rendered", view.section); this.reportLocation(); } /** * Report resize events and display the last seen location * @private */ onResized(size){ if(this.location) { this.display(this.location.start); } this.emit("resized", { width: size.width, height: size.height }); } /** * Move the Rendition to a specific offset * Usually you would be better off calling display() * @param {object} offset */ moveTo(offset){ this.manager.moveTo(offset); } /** * 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; if (this.settings.width >= 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 }; return properties; } // applyLayoutProperties(){ // var settings = this.determineLayoutProperties(this.book.package.metadata); // // this.flow(settings.flow); // // this.layout(settings); // }; /** * 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-doc" || flow === "scrolled-continuous") { _flow = "scrolled"; } if (flow === "auto" || flow === "paginated") { _flow = "paginated"; } if (this._layout) { this._layout.flow(_flow); } if (this.manager) { this.manager.updateFlow(_flow); } } /** * 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); } 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._layout.spread(spread, min); if (this.manager.isRendered()) { this.manager.updateLayout(); } } /** * Report the current location * @private */ reportLocation(){ return this.q.enqueue(function(){ var location = this.manager.currentLocation(); if (location && location.then && typeof location.then === "function") { location.then(function(result) { this.location = result; this.percentage = this.book.locations.percentageFromCfi(result); if (this.percentage != null) { this.location.percentage = this.percentage; } this.emit("locationChanged", this.location); }.bind(this)); } else if (location) { this.location = location; this.percentage = this.book.locations.percentageFromCfi(location); if (this.percentage != null) { this.location.percentage = this.percentage; } this.emit("locationChanged", this.location); } }.bind(this)); } /** * Get the Current Location CFI * @return {EpubCFI} location (may be a promise) */ currentLocation(){ var location = this.manager.currentLocation(); if (location && location.then && typeof location.then === "function") { location.then(function(result) { var percentage = this.book.locations.percentageFromCfi(result); if (percentage != null) { result.percentage = percentage; } return result; }.bind(this)); } else if (location) { var percentage = this.book.locations.percentageFromCfi(location); if (percentage != null) { location.percentage = percentage; } return location; } } /** * Remove and Clean Up the Rendition */ destroy(){ // Clear the queue this.q.clear(); this.manager.destroy(); } /** * Pass the events from a view * @private * @param {View} view */ passEvents(contents){ var listenedEvents = Contents.listenedEvents; listenedEvents.forEach(function(e){ contents.on(e, this.triggerViewEvent.bind(this)); }.bind(this)); contents.on("selected", this.triggerSelectedEvent.bind(this)); } /** * Emit events passed by a view * @private * @param {event} e */ triggerViewEvent(e){ this.emit(e.type, e); } /** * Emit a selection event's CFI Range passed from a a view * @private * @param {EpubCFI} cfirange */ triggerSelectedEvent(cfirange){ this.emit("selected", cfirange); } /** * Get a Range from a Visible CFI * @param {string} cfi EpubCfi String * @param {string} ignoreClass * @return {range} */ range(cfi, ignoreClass){ var _cfi = new EpubCFI(cfi); var found = this.visible().filter(function (view) { if(_cfi.spinePos === view.index) return true; }); // Should only every return 1 item if (found.length) { return found[0].range(_cfi, ignoreClass); } } /** * Hook to adjust images to fit in columns * @param {View} view */ adjustImages(contents) { contents.addStylesheetRules([ ["img", ["max-width", (this._layout.spreadWidth) + "px"], ["max-height", (this._layout.height) + "px"] ] ]); return new Promise(function(resolve, reject){ // Wait to apply setTimeout(function() { resolve(); }, 1); }); } getContents () { return this.manager ? this.manager.getContents() : []; } handleLinks(contents) { contents.on("link", (href) => { let relative = this.book.path.relative(href); this.display(relative); }); } } //-- Enable binding events to Renderer EventEmitter(Rendition.prototype); export default Rendition;