diff --git a/examples/offline.html b/examples/offline.html
new file mode 100644
index 0000000..ae5043b
--- /dev/null
+++ b/examples/offline.html
@@ -0,0 +1,97 @@
+
+
+
+
+
+ EPUB.js Storage Example
+
+
+
+
+
+
+
+
+
+
+
+ You are offline. Loading from Storage.
+
+ ‹
+ ›
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 1e3883e..a89789e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "epubjs",
- "version": "0.3.74",
+ "version": "0.3.75",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -49,6 +49,14 @@
"@types/node": "*"
}
},
+ "@types/localforage": {
+ "version": "0.0.34",
+ "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
+ "integrity": "sha1-XjHDLdh5HsS5/z70fJy1Wy0NlDg=",
+ "requires": {
+ "localforage": "*"
+ }
+ },
"@types/node": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.0.tgz",
@@ -10804,6 +10812,14 @@
"json5": "^0.5.0"
}
},
+ "localforage": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.2.tgz",
+ "integrity": "sha1-+kRCYC+Abt0rympUq05lbwMfEhw=",
+ "requires": {
+ "lie": "3.1.1"
+ }
+ },
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
diff --git a/package.json b/package.json
index 3083892..979523e 100644
--- a/package.json
+++ b/package.json
@@ -75,13 +75,15 @@
"webpack-dev-server": "^2.11.2"
},
"dependencies": {
+ "@types/jszip": "^3.1.4",
+ "@types/localforage": "0.0.34",
"event-emitter": "^0.3.5",
"jszip": "^3.1.5",
+ "localforage": "^1.7.2",
"lodash": "^4.17.10",
"marks-pane": "^1.0.9",
"path-webpack": "0.0.3",
"stream-browserify": "^2.0.1",
- "xmldom": "^0.1.27",
- "@types/jszip": "^3.1.4"
+ "xmldom": "^0.1.27"
}
}
diff --git a/src/book.js b/src/book.js
index eb0c827..9635a2a 100644
--- a/src/book.js
+++ b/src/book.js
@@ -13,6 +13,7 @@ import Rendition from "./rendition";
import Archive from "./archive";
import request from "./utils/request";
import EpubCFI from "./epubcfi";
+import Store from "./store";
import { EPUBJS_VERSION, EVENTS } from "./utils/constants";
const CONTAINER_PATH = "META-INF/container.xml";
@@ -39,6 +40,7 @@ const INPUT_TYPE = {
* @param {string} [options.replacements=none] use base64, blobUrl, or none for replacing assets in archived Epubs
* @param {method} [options.canonical] optional function to determine canonical urls for a path
* @param {string} [options.openAs] optional string to determine the input type
+ * @param {string} [options.store=false] cache the contents in local storage, value should be the name of the reader
* @returns {Book}
* @example new Book("/path/to/book.epub", {})
* @example new Book({ replacements: "blobUrl" })
@@ -60,7 +62,8 @@ class Book {
encoding: undefined,
replacements: undefined,
canonical: undefined,
- openAs: undefined
+ openAs: undefined,
+ store: undefined
});
extend(this.settings, options);
@@ -173,6 +176,13 @@ class Book {
*/
this.archive = undefined;
+ /**
+ * @member {Store} storage
+ * @memberof Book
+ * @private
+ */
+ this.storage = undefined;
+
/**
* @member {Resources} resources
* @memberof Book
@@ -202,6 +212,9 @@ class Book {
this.packaging = undefined;
// this.toc = undefined;
+ if (this.settings.store) {
+ this.store();
+ }
if(url) {
this.open(url, this.settings.openAs).catch((error) => {
@@ -233,7 +246,7 @@ class Book {
} else if (type === INPUT_TYPE.EPUB) {
this.archived = true;
this.url = new Url("/", "");
- opening = this.request(input, "binary",this.settings.requestCredentials)
+ opening = this.request(input, "binary", this.settings.requestCredentials)
.then(this.openEpub.bind(this));
} else if(type == INPUT_TYPE.OPF) {
this.url = new Url(input);
@@ -318,13 +331,10 @@ class Book {
* @return {Promise} returns a promise with the requested resource
*/
load(path) {
- var resolved;
-
+ var resolved = this.resolve(path);
if(this.archived) {
- resolved = this.resolve(path);
return this.archive.request(resolved);
} else {
- resolved = this.resolve(path);
return this.request(resolved, null, this.settings.requestCredentials, this.settings.requestHeaders);
}
}
@@ -558,6 +568,61 @@ class Book {
return this.archive.open(input, encoding);
}
+ /**
+ * Store the epubs contents
+ * @private
+ * @param {binary} input epub data
+ * @param {string} [encoding]
+ * @return {Store}
+ */
+ store() {
+ // 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(this.settings.store, 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 {string} coverUrl
diff --git a/src/managers/default/index.js b/src/managers/default/index.js
index 8deeeca..2095465 100644
--- a/src/managers/default/index.js
+++ b/src/managers/default/index.js
@@ -457,7 +457,7 @@ class DefaultViewManager {
}
}
}.bind(this), (err) => {
- displaying.reject(err);
+ return err;
})
.then(function(){
this.views.show();
@@ -529,7 +529,7 @@ class DefaultViewManager {
}
}
}.bind(this), (err) => {
- displaying.reject(err);
+ return err;
})
.then(function(){
if(this.isPaginated && this.settings.axis === "horizontal") {
diff --git a/src/store.js b/src/store.js
new file mode 100644
index 0000000..522c977
--- /dev/null
+++ b/src/store.js
@@ -0,0 +1,349 @@
+import {defer, isXml, parse} from "./utils/core";
+import httpRequest from "./utils/request";
+import mime from "../libs/mime/mime";
+import Path from "./utils/path";
+import EventEmitter from "event-emitter";
+
+/**
+ * Handles saving and requesting files from local storage
+ * @class
+ */
+class Store {
+
+ constructor(name, requester, resolver) {
+ this.urlCache = {};
+
+ this.requester = requester || httpRequest;
+ this.resolver = resolver;
+
+ this.online = true;
+
+ this.checkRequirements();
+
+ // This should be the name of the application for modals
+ localforage.config({
+ name: name || 'epubjs'
+ });
+
+ this.addListeners();
+ }
+
+ /**
+ * Checks to see if localForage exists in global namspace,
+ * Requires localForage if it isn't there
+ * @private
+ */
+ checkRequirements(){
+ try {
+ if (typeof localForage === "undefined") {
+ let localForage = require("localforage");
+ }
+ } catch (e) {
+ throw new Error("localForage lib not loaded");
+ }
+ }
+
+ /**
+ * Add online and offline event listeners
+ * @private
+ */
+ addListeners() {
+ window.addEventListener('online', this.status.bind(this));
+ window.addEventListener('offline', this.status.bind(this));
+ }
+
+ /**
+ * Remove online and offline event listeners
+ * @private
+ */
+ removeListeners() {
+ window.removeEventListener('online', this.status.bind(this));
+ window.removeEventListener('offline', this.status.bind(this));
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * Add all of a book resources to the store
+ * @param {Resources} resources book resources
+ * @param {boolean} [force] force resaving resources
+ * @return {Promise