1
0
Fork 0
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:
Do Park 2023-10-28 14:35:38 -07:00
parent 83b0f7755f
commit 583577b72a
7 changed files with 453 additions and 38 deletions

View file

@ -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();

View file

@ -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
View 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
View 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
View file

@ -0,0 +1,7 @@
export function createElement(name) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
export default {
createElement: createElement
};

View file

@ -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;
}

View file

@ -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",