diff --git a/examples/highlights.html b/examples/highlights.html index 0596cbd..26e0b31 100644 --- a/examples/highlights.html +++ b/examples/highlights.html @@ -106,7 +106,7 @@ // Apply a class to selected text rendition.on("selected", function(cfiRange, contents) { - contents.highlight(cfiRange, {}, (e) => { + rendition.annotations.highlight(cfiRange, {}, (e) => { console.log("highlight clicked", e.target); }); contents.window.getSelection().removeAllRanges(); diff --git a/src/annotations.js b/src/annotations.js new file mode 100644 index 0000000..a718210 --- /dev/null +++ b/src/annotations.js @@ -0,0 +1,205 @@ +// Manage annotations for a book? + +/* +let a = rendition.annotations.highlight(cfiRange, data) + +a.on("added", () => console.log("added")) +a.on("removed", () => console.log("removed")) +a.on("clicked", () => console.log("clicked")) + +a.update(data) +a.remove(); +a.text(); + +rendition.annotations.show() +rendition.annotations.hide() + +rendition.annotations.highlights.show() +rendition.annotations.highlights.hide() +*/ + +import EventEmitter from "event-emitter"; +import EpubCFI from "./epubcfi"; + +/** + * Handles managing adding & removing Annotations + * @class + */ +class Annotations { + + constructor (rendition) { + this.rendition = rendition; + this.highlights = []; + this.underlines = []; + this.marks = []; + this._annotations = {}; + this._annotationsBySectionIndex = {}; + + this.rendition.hooks.content.register(this.inject.bind(this)); + this.rendition.hooks.unloaded.register(this.clear.bind(this)); + } + + add (type, cfiRange, data, cb) { + let hash = encodeURI(cfiRange); + let cfi = new EpubCFI(cfiRange); + let sectionIndex = cfi.spinePos; + let annotation = new Annotation({ + type, + cfiRange, + data, + sectionIndex, + cb + }); + + this._annotations[hash] = annotation; + + if (sectionIndex in this._annotationsBySectionIndex) { + this._annotationsBySectionIndex[sectionIndex].push(hash); + } else { + this._annotationsBySectionIndex[sectionIndex] = [hash]; + } + + let contents = this.rendition.getContents(); + contents.forEach( (content) => { + if (annotation.sectionIndex === content.sectionIndex) { + annotation.attach(content); + } + }); + + return annotation; + } + + remove (cfiRange) { + let hash = decodeURI(cfiRange); + let result; + if (hash in this._annotations) { + let annotation = this._annotations[hash]; + + let contents = this.rendition.getContents(); + contents.forEach( (content) => { + if (annotation.sectionIndex === content.sectionIndex) { + annotation.detach(content); + } + }); + + delete this._annotations[hash]; + } + return result; + } + + highlight (cfiRange, data, cb) { + this.add("highlight", cfiRange, data, cb); + } + + underline (cfiRange, data, cb) { + this.add("underline", cfiRange, data, cb); + } + + mark (cfiRange, data, cb) { + this.add("mark", cfiRange, data, cb); + } + + each () { + return this._annotations.forEach.apply(this._annotations, arguments); + } + + inject (contents) { + let sectionIndex = contents.index; + if (sectionIndex in this._annotationsBySectionIndex) { + let annotations = this._annotationsBySectionIndex[sectionIndex]; + annotations.forEach((hash) => { + let annotation = this._annotations[hash]; + annotation.attach(contents); + }); + } + } + + clear (contents) { + let sectionIndex = contents.index; + if (sectionIndex in this._annotationsBySectionIndex) { + let annotations = this._annotationsBySectionIndex[sectionIndex]; + annotations.forEach((hash) => { + let annotation = this._annotations[hash]; + annotation.detach(contents); + }); + } + } + + show () { + + } + + hide () { + + } + +} + +class Annotation { + + constructor ({ + type, + cfiRange, + data, + sectionIndex + }) { + this.type = type; + this.cfiRange = cfiRange; + this.data = data; + this.sectionIndex = sectionIndex; + this.mark = undefined; + } + + update (data) { + this.data = data; + } + + attach (contents) { + let {cfiRange, data, type, mark, cb} = this; + let result; + /* + if (mark) { + return; // already added + } + */ + if (type === "highlight") { + result = contents.highlight(cfiRange, data, cb); + } else if (type === "underline") { + result = contents.underline(cfiRange, data, cb); + } else if (type === "mark") { + result = contents.mark(cfiRange, data, cb); + } + + this.mark = result; + + return result; + } + + detach (contents) { + let {cfiRange, type} = this; + + if (contents) { + if (type === "highlight") { + result = contents.unhighlight(cfiRange); + } else if (type === "underline") { + result = contents.ununderline(cfiRange); + } else if (type === "mark") { + result = contents.unmark(cfiRange); + } + } + + this.mark = undefined; + + return result; + } + + text () { + // TODO: needs implementation in contents + } + +} + +EventEmitter(Annotation.prototype); + + +export default Annotations diff --git a/src/contents.js b/src/contents.js index 1ef6f9c..98fcd51 100644 --- a/src/contents.js +++ b/src/contents.js @@ -9,7 +9,7 @@ import { Pane, Highlight, Underline } from "marks-pane"; const EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"]; class Contents { - constructor(doc, content, cfiBase) { + constructor(doc, content, cfiBase, sectionIndex) { // Blank Cfi for Parsing this.epubcfi = new EpubCFI(); @@ -23,6 +23,7 @@ class Contents { height: 0 }; + this.sectionIndex = sectionIndex || 0; this.cfiBase = cfiBase || ""; this.pane = undefined; diff --git a/src/managers/views/iframe.js b/src/managers/views/iframe.js index 4ca3a9b..3280a78 100644 --- a/src/managers/views/iframe.js +++ b/src/managers/views/iframe.js @@ -418,7 +418,7 @@ class IframeView { this.window = this.iframe.contentWindow; this.document = this.iframe.contentDocument; - this.contents = new Contents(this.document, this.document.body, this.section.cfiBase); + this.contents = new Contents(this.document, this.document.body, this.section.cfiBase, this.section.index); this.rendering = false; diff --git a/src/rendition.js b/src/rendition.js index 4f198dc..8c74e0f 100644 --- a/src/rendition.js +++ b/src/rendition.js @@ -7,6 +7,7 @@ import Layout from "./layout"; import Mapping from "./mapping"; import Themes from "./themes"; import Contents from "./contents"; +import Annotations from "./annotations"; /** * [Rendition description] @@ -66,6 +67,7 @@ class Rendition { * @type {Hook} */ 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); @@ -85,6 +87,8 @@ class Rendition { // this.hooks.display.register(this.afterDisplay.bind(this)); this.themes = new Themes(this); + this.annotations = new Annotations(this); + this.epubcfi = new EpubCFI(); this.q = new Queue(this); @@ -171,6 +175,7 @@ class Rendition { // Listen for displayed views this.manager.on("added", this.afterDisplayed.bind(this)); + this.manager.on("removed", this.afterRemoved.bind(this)); // Listen for resizing this.manager.on("resized", this.onResized.bind(this)); @@ -309,11 +314,23 @@ class Rendition { * @param {*} view */ afterDisplayed(view){ - this.hooks.content.trigger(view.contents, this); - this.emit("rendered", view.section, view); + this.hooks.content.trigger(view.contents, this).then(() => { + this.emit("rendered", view.section, view); + }); // this.reportLocation(); } + /** + * Report what has been removed + * @private + * @param {*} view + */ + afterRemoved(view){ + this.hooks.unloaded.trigger(view, this).then(() => { + this.emit("removed", view.section, view); + }); + } + /** * Report resize events and display the last seen location * @private