diff --git a/examples/highlights.html b/examples/highlights.html
index cf03dd8..e5c3fe7 100644
--- a/examples/highlights.html
+++ b/examples/highlights.html
@@ -112,17 +112,25 @@
// console.log(location);
});
-
// Apply a class to selected text
rendition.on("selected", function(cfiRange, contents) {
- rendition.annotations.highlight(cfiRange, {},
- (e) => {
- console.log("highlight clicked", e.target);
- },
- (e) => {
- console.log("highlight mouseover", e.target);
+ rendition.annotations.highlight(
+ cfiRange,
+ {},
+ null,
+ "epubjs-hl",
+ {},
+ {
+ onClick : (e) => {
+ console.log("highlight-click", e.target);
+ },
+ onMouseOver : (e) => {
+ console.log("highlight-mouseover", e.target);
+ },
+ onMouseOut : (e) => {
+ console.log("highlight-mouseout", e.target);
+ }
}
-
);
contents.window.getSelection().removeAllRanges();
diff --git a/src/annotations.js b/src/annotations.js
index ecbcb56..4158db0 100644
--- a/src/annotations.js
+++ b/src/annotations.js
@@ -31,7 +31,7 @@ class Annotations {
* @param {object} styles CSS styles to assign to annotation
* @returns {Annotation} annotation
*/
- add (type, cfiRange, data, cb, className, styles) {
+ add (type, cfiRange, data, cb, className, styles, cbOptions) {
let hash = encodeURI(cfiRange + type);
let cfi = new EpubCFI(cfiRange);
let sectionIndex = cfi.spinePos;
@@ -42,7 +42,8 @@ class Annotations {
sectionIndex,
cb,
className,
- styles
+ styles,
+ cbOptions,
});
this._annotations[hash] = annotation;
@@ -116,8 +117,8 @@ class Annotations {
* @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);
+ highlight (cfiRange, data, cb, className, styles, cbOptions) {
+ return this.add("highlight", cfiRange, data, cb, className, styles, cbOptions);
}
/**
@@ -221,7 +222,8 @@ class Annotation {
sectionIndex,
cb,
className,
- styles
+ styles,
+ cbOptions,
}) {
this.type = type;
this.cfiRange = cfiRange;
@@ -231,6 +233,7 @@ class Annotation {
this.cb = cb;
this.className = className;
this.styles = styles;
+ this.cbOptions = cbOptions;
}
/**
@@ -246,11 +249,11 @@ class Annotation {
* @param {View} view
*/
attach (view) {
- let {cfiRange, data, type, mark, cb, className, styles} = this;
+ let {cfiRange, data, type, mark, cb, cbOptions, className, styles} = this;
let result;
if (type === "highlight") {
- result = view.highlight(cfiRange, data, cb, className, styles);
+ result = view.highlight(cfiRange, data, cb, className, styles, cbOptions);
} else if (type === "underline") {
result = view.underline(cfiRange, data, cb, className, styles);
} else if (type === "mark") {
diff --git a/src/libs/marks/events.js b/src/libs/marks/events.js
new file mode 100644
index 0000000..9f5c4c0
--- /dev/null
+++ b/src/libs/marks/events.js
@@ -0,0 +1,124 @@
+// import 'babelify/polyfill'; // needed for Object.assign
+
+export default {
+ proxyMouse: proxyMouse
+};
+
+
+/**
+* Start proxying all mouse events that occur on the target node to each node in
+* a set of tracked nodes.
+*
+* The items in tracked do not strictly have to be DOM Nodes, but they do have
+* to have dispatchEvent, getBoundingClientRect, and getClientRects methods.
+*
+* @param target {Node} The node on which to listen for mouse events.
+* @param tracked {Node[]} A (possibly mutable) array of nodes to which to proxy
+* events.
+*/
+export function proxyMouse(target, tracked) {
+ function dispatch(e) {
+ // We walk through the set of tracked elements in reverse order so that
+ // events are sent to those most recently added first.
+ //
+ // This is the least surprising behaviour as it simulates the way the
+ // browser would work if items added later were drawn "on top of"
+ // earlier ones.
+ for (var i = tracked.length - 1; i >= 0; i--) {
+ var t = tracked[i];
+ var x = e.clientX
+ var y = e.clientY;
+
+ if (e.touches && e.touches.length) {
+ x = e.touches[0].clientX;
+ y = e.touches[0].clientY;
+ }
+
+ if (!contains(t, target, x, y)) {
+ continue;
+ }
+
+ // The event targets this mark, so dispatch a cloned event:
+ t.dispatchEvent(clone(e));
+ // We only dispatch the cloned event to the first matching mark.
+ break;
+ }
+ }
+
+ if (target.nodeName === "iframe" || target.nodeName === "IFRAME") {
+
+ try {
+ // Try to get the contents if same domain
+ this.target = target.contentDocument;
+ } catch(err){
+ this.target = target;
+ }
+
+ } else {
+ this.target = target;
+ }
+
+ for (var ev of ['mouseup', 'mousedown', 'click', 'touchstart']) {
+ this.target.addEventListener(ev, (e) => dispatch(e), false);
+ }
+
+}
+
+
+/**
+* Clone a mouse event object.
+*
+* @param e {MouseEvent} A mouse event object to clone.
+* @returns {MouseEvent}
+*/
+export function clone(e) {
+ var opts = Object.assign({}, e, {bubbles: false});
+ try {
+ return new MouseEvent(e.type, opts);
+ } catch(err) { // compat: webkit
+ var copy = document.createEvent('MouseEvents');
+ copy.initMouseEvent(e.type, false, opts.cancelable, opts.view,
+ opts.detail, opts.screenX, opts.screenY,
+ opts.clientX, opts.clientY, opts.ctrlKey,
+ opts.altKey, opts.shiftKey, opts.metaKey,
+ opts.button, opts.relatedTarget);
+ return copy;
+ }
+}
+
+
+/**
+* Check if the item contains the point denoted by the passed coordinates
+* @param item {Object} An object with getBoundingClientRect and getClientRects
+* methods.
+* @param x {Number}
+* @param y {Number}
+* @returns {Boolean}
+*/
+function contains(item, target, x, y) {
+ // offset
+ var offset = target.getBoundingClientRect();
+
+ function rectContains(r, x, y) {
+ var top = r.top - offset.top;
+ var left = r.left - offset.left;
+ var bottom = top + r.height;
+ var right = left + r.width;
+ return (top <= y && left <= x && bottom > y && right > x);
+ }
+
+ // Check overall bounding box first
+ var rect = item.getBoundingClientRect();
+ if (!rectContains(rect, x, y)) {
+ return false;
+ }
+
+ // Then continue to check each child rect
+ var rects = item.getClientRects();
+ for (var i = 0, len = rects.length; i < len; i++) {
+ if (rectContains(rects[i], x, y)) {
+ return true;
+ }
+ }
+ return false;
+}
\ No newline at end of file
diff --git a/src/libs/marks/marks.js b/src/libs/marks/marks.js
new file mode 100644
index 0000000..62e633e
--- /dev/null
+++ b/src/libs/marks/marks.js
@@ -0,0 +1,237 @@
+import svg from './svg';
+import events from './events';
+
+export class Pane {
+ constructor(target, container = document.body) {
+ this.target = target;
+ this.element = svg.createElement('svg');
+ this.marks = [];
+
+ // Match the coordinates of the target element
+ this.element.style.position = 'absolute';
+ // Disable pointer events
+ this.element.setAttribute('pointer-events', 'none');
+
+ // Set up mouse event proxying between the target element and the marks
+ events.proxyMouse(this.target, this.marks);
+
+ this.container = container;
+ this.container.appendChild(this.element);
+ this.render();
+ }
+
+ addMark(mark) {
+ var g = svg.createElement('g');
+ // enable pointer events for highlight so that we can capture mouse events
+ g.setAttribute('pointer-events', 'all');
+
+ this.element.appendChild(g);
+ mark.bind(g, this.container);
+
+ this.marks.push(mark);
+
+ mark.render();
+ return mark;
+ }
+
+ removeMark(mark) {
+ var idx = this.marks.indexOf(mark);
+ if (idx === -1) {
+ return;
+ }
+ var el = mark.unbind();
+ this.element.removeChild(el);
+ this.marks.splice(idx, 1);
+ }
+
+ render() {
+ setCoords(this.element, coords(this.target, this.container));
+ for (var m of this.marks) {
+ m.render();
+ }
+ }
+}
+
+
+export class Mark {
+ constructor() {
+ this.element = null;
+ }
+
+ bind(element, container) {
+ this.element = element;
+ this.container = container;
+ }
+
+ unbind() {
+ var el = this.element;
+ this.element = null;
+ return el;
+ }
+
+ render() {}
+
+ dispatchEvent(e) {
+ if (!this.element) return;
+ this.element.dispatchEvent(e);
+ }
+
+ getBoundingClientRect() {
+ return this.element.getBoundingClientRect();
+ }
+
+ getClientRects() {
+ var rects = [];
+ var el = this.element.firstChild;
+ while (el) {
+ rects.push(el.getBoundingClientRect());
+ el = el.nextSibling;
+ }
+ return rects;
+ }
+
+ filteredRanges() {
+ if (!this.range) {
+ return [];
+ }
+
+ // De-duplicate the boxes
+ const rects = Array.from(this.range.getClientRects());
+ const stringRects = rects.map((r) => JSON.stringify(r));
+ const setRects = new Set(stringRects);
+ return Array.from(setRects).map((sr) => JSON.parse(sr));
+ }
+
+}
+
+export class Highlight extends Mark {
+ constructor(range, className, data, attributes) {
+ super();
+ this.range = range;
+ this.className = className;
+ this.data = data || {};
+ this.attributes = attributes || {};
+ }
+
+ bind(element, container) {
+ super.bind(element, container);
+
+ for (var attr in this.data) {
+ if (this.data.hasOwnProperty(attr)) {
+ this.element.dataset[attr] = this.data[attr];
+ }
+ }
+
+ for (var attr in this.attributes) {
+ if (this.attributes.hasOwnProperty(attr)) {
+ this.element.setAttribute(attr, this.attributes[attr]);
+ }
+ }
+
+ if (this.className) {
+ this.element.classList.add(this.className);
+ }
+ }
+
+ render() {
+ // Empty element
+ while (this.element.firstChild) {
+ this.element.removeChild(this.element.firstChild);
+ }
+
+ var docFrag = this.element.ownerDocument.createDocumentFragment();
+ var filtered = this.filteredRanges();
+ var offset = this.element.getBoundingClientRect();
+ var container = this.container.getBoundingClientRect();
+
+ for (var i = 0, len = filtered.length; i < len; i++) {
+ var r = filtered[i];
+ var el = svg.createElement('rect');
+ el.setAttribute('x', r.left - offset.left + container.left);
+ el.setAttribute('y', r.top - offset.top + container.top);
+ el.setAttribute('height', r.height);
+ el.setAttribute('width', r.width);
+ docFrag.appendChild(el);
+ }
+
+ this.element.appendChild(docFrag);
+
+ }
+}
+
+export class Underline extends Highlight {
+ constructor(range, className, data, attributes) {
+ super(range, className, data, attributes);
+ }
+
+ render() {
+ // Empty element
+ while (this.element.firstChild) {
+ this.element.removeChild(this.element.firstChild);
+ }
+
+ var docFrag = this.element.ownerDocument.createDocumentFragment();
+ var filtered = this.filteredRanges();
+ var offset = this.element.getBoundingClientRect();
+ var container = this.container.getBoundingClientRect();
+
+ for (var i = 0, len = filtered.length; i < len; i++) {
+ var r = filtered[i];
+
+ var rect = svg.createElement('rect');
+ rect.setAttribute('x', r.left - offset.left + container.left);
+ rect.setAttribute('y', r.top - offset.top + container.top);
+ rect.setAttribute('height', r.height);
+ rect.setAttribute('width', r.width);
+ rect.setAttribute('fill', 'none');
+
+
+ var line = svg.createElement('line');
+ line.setAttribute('x1', r.left - offset.left + container.left);
+ line.setAttribute('x2', r.left - offset.left + container.left + r.width);
+ line.setAttribute('y1', r.top - offset.top + container.top + r.height - 1);
+ line.setAttribute('y2', r.top - offset.top + container.top + r.height - 1);
+
+ line.setAttribute('stroke-width', 1);
+ line.setAttribute('stroke', 'black'); //TODO: match text color?
+ line.setAttribute('stroke-linecap', 'square');
+
+ docFrag.appendChild(rect);
+
+ docFrag.appendChild(line);
+ }
+
+ this.element.appendChild(docFrag);
+
+ }
+}
+
+
+function coords(el, container) {
+ var offset = container.getBoundingClientRect();
+ var rect = el.getBoundingClientRect();
+
+ return {
+ top: rect.top - offset.top,
+ left: rect.left - offset.left,
+ height: el.scrollHeight,
+ width: el.scrollWidth
+ };
+}
+
+
+function setCoords(el, coords) {
+ el.style.setProperty('top', `${coords.top}px`, 'important');
+ el.style.setProperty('left', `${coords.left}px`, 'important');
+ el.style.setProperty('height', `${coords.height}px`, 'important');
+ el.style.setProperty('width', `${coords.width}px`, 'important');
+}
+
+function contains(rect1, rect2) {
+ return (
+ (rect2.right <= rect1.right) &&
+ (rect2.left >= rect1.left) &&
+ (rect2.top >= rect1.top) &&
+ (rect2.bottom <= rect1.bottom)
+ );
+}
\ No newline at end of file
diff --git a/src/libs/marks/svg.js b/src/libs/marks/svg.js
new file mode 100644
index 0000000..45b3973
--- /dev/null
+++ b/src/libs/marks/svg.js
@@ -0,0 +1,7 @@
+export function createElement(name) {
+ return document.createElementNS('http://www.w3.org/2000/svg', name);
+}
+
+export default {
+ createElement: createElement
+};
\ No newline at end of file
diff --git a/src/managers/views/iframe.js b/src/managers/views/iframe.js
index 72fdd0a..0db71c5 100644
--- a/src/managers/views/iframe.js
+++ b/src/managers/views/iframe.js
@@ -3,7 +3,7 @@ import {extend, borders, uuid, isNumber, bounds, defer, createBlobUrl, revokeBlo
import EpubCFI from "../../epubcfi";
import Contents from "../../contents";
import { EVENTS } from "../../utils/constants";
-import { Pane, Highlight, Underline } from "marks-pane";
+import { Pane, Highlight, Underline } from "../../libs/marks/marks";
class IframeView {
constructor(section, options) {
@@ -603,32 +603,39 @@ class IframeView {
return this.elementBounds;
}
- highlight(cfiRange, data={}, cb, cb2, className = "epubjs-hl", styles = {}) {
+ highlight(cfiRange, data={}, cb, className = "epubjs-hl", styles = {}, cbOptions = {}) {
if (!this.contents) {
return;
}
+
+ // ensuring backward compatibility
+ const onClickCallback = cb || cbOptions.onClick;
+
+ const onMouseOverCallback = cbOptions.onMouseOver;
+ const onMouseOutCallback = cbOptions.onMouseOut;
+
const attributes = Object.assign({"fill": "yellow", "fill-opacity": "0.3", "mix-blend-mode": "multiply"}, styles);
let range = this.contents.range(cfiRange);
- const onMount = (container) => {
- container.style.pointerEvents = "all";
- };
+ // const onMount = (container) => {
+ // container.style.pointerEvents = "all";
+ // };
- const onEject = (container) => {
- // container.style.pointerEvents = "none";
- };
+ // const onEject = (container) => {
+ // container.style.pointerEvents = "none";
+ // };
const emitOnClick = () => {
this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
};
- const emitOnMouseOver = (el) => {
- this.emit(EVENTS.VIEWS.MARK_HOVERED, cfiRange, data);
- onMount(el);
+ const emitOnMouseOver = () => {
+ this.emit(EVENTS.VIEWS.MARK_MOUSEOVER, cfiRange, data);
+
};
- const emitOnMouseOut = (el) => {
- onEject(el);
+ const emitOnMouseOut = () => {
+ this.emit(EVENTS.VIEWS.MARK_MOUSEOUT, cfiRange, data);
};
data["epubcfi"] = cfiRange;
@@ -640,20 +647,48 @@ class IframeView {
let m = new Highlight(range, className, data, attributes);
let h = this.pane.addMark(m);
- this.highlights[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitOnClick, cb, cb2] };
+ const getListeners = () => {
+ let listeners = [
+ emitOnClick,
+ emitOnMouseOver,
+ emitOnMouseOut];
+
+ if(onClickCallback) {
+ listeners.push(onClickCallback);
+ }
+
+ if(onMouseOverCallback) {
+ listeners.push(onMouseOverCallback);
+ }
+
+ if(onMouseOutCallback) {
+ listeners.push(onMouseOutCallback);
+ }
+
+ return listeners;
+ };
+
+ this.highlights[cfiRange] = { "mark": h, "element": h.element, "listeners": getListeners() };
h.element.setAttribute("ref", className);
h.element.addEventListener("click", emitOnClick);
h.element.addEventListener("touchstart", emitOnClick);
- h.element.addEventListener("mouseover", emitOnMouseOver(this.pane.element));
- h.element.addEventListener("mouseout", emitOnMouseOut(this.pane.element));
+ h.element.addEventListener("mouseover", emitOnMouseOver);
+ h.element.addEventListener("mouseout", emitOnMouseOut);
- if (cb) {
- h.element.addEventListener("click", cb);
- h.element.addEventListener("touchstart", cb);
- h.element.addEventListener("mouseover", cb2);
- h.element.addEventListener("mouseout", ()=>{console.log('mouse is out');});
+ if (onClickCallback) {
+ h.element.addEventListener("click", onClickCallback);
+ h.element.addEventListener("touchstart", onClickCallback);
}
+
+ if(onMouseOverCallback) {
+ h.element.addEventListener("mouseover", onMouseOverCallback);
+ }
+
+ if(onMouseOutCallback) {
+ h.element.addEventListener("mouseout", onMouseOutCallback);
+ }
+
return h;
}
diff --git a/src/utils/constants.js b/src/utils/constants.js
index b4d5d33..0d3f423 100644
--- a/src/utils/constants.js
+++ b/src/utils/constants.js
@@ -1,7 +1,7 @@
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", "mouseover", "mouseenter"];
+export const DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "mousemove", "click", "touchend", "touchstart", "touchmove", "mouseover", "mouseout"];
export const EVENTS = {
BOOK : {
@@ -36,7 +36,8 @@ export const EVENTS = {
SHOWN : "shown",
HIDDEN : "hidden",
MARK_CLICKED : "markClicked",
- MARK_HOVERED: "markHovered"
+ MARK_MOUSEOVER: "markMouseover",
+ MARK_MOUSEOUT: "markMouseout",
},
RENDITION : {
STARTED : "started",