mirror of
https://github.com/futurepress/epub.js.git
synced 2025-10-02 14:49:16 +02:00
awesome hack to make the mouse interaction working 😏
This commit is contained in:
parent
83b0f7755f
commit
583577b72a
7 changed files with 453 additions and 38 deletions
|
@ -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();
|
||||
|
||||
|
|
|
@ -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") {
|
||||
|
|
124
src/libs/marks/events.js
Normal file
124
src/libs/marks/events.js
Normal file
|
@ -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;
|
||||
}
|
237
src/libs/marks/marks.js
Normal file
237
src/libs/marks/marks.js
Normal file
|
@ -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)
|
||||
);
|
||||
}
|
7
src/libs/marks/svg.js
Normal file
7
src/libs/marks/svg.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export function createElement(name) {
|
||||
return document.createElementNS('http://www.w3.org/2000/svg', name);
|
||||
}
|
||||
|
||||
export default {
|
||||
createElement: createElement
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue