Skip to content

Commit

Permalink
EPUB/Snapshot: Prevent text selection when annotating in Safari
Browse files Browse the repository at this point in the history
Disable text selection completely when annotating/resizing, and reenable
it temporarily every time caretPositionFromPoint() is called.
  • Loading branch information
AbeJellinek committed Nov 4, 2024
1 parent 9ca45a5 commit 3262119
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 95 deletions.
27 changes: 13 additions & 14 deletions src/dom/common/dom-view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @ts-ignore
import injectCSS from './stylesheets/inject.scss';
// @ts-ignore
import annotationsCSS from './stylesheets/annotations.scss';

import {
Annotation,
Expand Down Expand Up @@ -561,6 +563,10 @@ abstract class DOMView<State extends DOMViewState, Data> {
this._iframeDocument.addEventListener('wheel', this._handleWheelCapture.bind(this), { passive: false, capture: true });
this._iframeDocument.addEventListener('selectionchange', this._handleSelectionChange.bind(this));

let injectStyle = this._iframeDocument.createElement('style');
injectStyle.innerHTML = injectCSS;
this._iframeDocument.head.append(injectStyle);

let annotationOverlay = this._iframeDocument.createElement('div');
annotationOverlay.id = 'annotation-overlay';
this._annotationShadowRoot = annotationOverlay.attachShadow({ mode: 'open' });
Expand All @@ -571,9 +577,9 @@ abstract class DOMView<State extends DOMViewState, Data> {
this._annotationShadowRoot.append(this._annotationRenderRootEl);
this._annotationRenderRoot = createRoot(this._annotationRenderRootEl);

let style = this._iframeDocument.createElement('style');
style.innerHTML = injectCSS;
this._annotationShadowRoot.append(style);
let annotationsStyle = this._iframeDocument.createElement('style');
annotationsStyle.innerHTML = annotationsCSS;
this._annotationShadowRoot.append(annotationsStyle);

this._iframeDocument.documentElement.classList.toggle('is-safari', isSafari);

Expand Down Expand Up @@ -996,21 +1002,12 @@ abstract class DOMView<State extends DOMViewState, Data> {
private _handleAnnotationResizeStart = (_id: string) => {
this._resizing = true;
this._options.onSetAnnotationPopup(null);
if (isSafari) {
// Capturing the pointer doesn't stop text selection in Safari. We could set user-select: none
// (-webkit-user-select: none, actually), but that breaks caretPositionFromPoint (only in Safari).
// So we make the selection invisible instead.
this._iframeDocument.documentElement.style.setProperty('--selection-color', 'transparent');
}
this._iframeDocument.body.classList.add('resizing-annotation');
};

private _handleAnnotationResizeEnd = (id: string, range: Range, cancelled: boolean) => {
this._resizing = false;
if (isSafari) {
// See above - this resets the highlight color and clears the selection
this.setTool(this._tool);
this._iframeDocument.getSelection()?.removeAllRanges();
}
this._iframeDocument.body.classList.remove('resizing-annotation');
if (cancelled) {
return;
}
Expand Down Expand Up @@ -1063,6 +1060,7 @@ abstract class DOMView<State extends DOMViewState, Data> {
if ((event.pointerType === 'touch' || event.pointerType === 'pen')
&& (this._tool.type === 'highlight' || this._tool.type === 'underline')) {
this._touchAnnotationStartPosition = caretPositionFromPoint(this._iframeDocument, event.clientX, event.clientY);
this._iframeDocument.body.classList.add('creating-touch-annotation');
event.stopPropagation();
}
}
Expand Down Expand Up @@ -1110,6 +1108,7 @@ abstract class DOMView<State extends DOMViewState, Data> {
this._tryUseTool();
}
this._touchAnnotationStartPosition = null;
this._iframeDocument.body.classList.remove('creating-touch-annotation');
}

protected _handlePointerMove(event: PointerEvent) {
Expand Down
35 changes: 22 additions & 13 deletions src/dom/common/lib/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,21 +173,30 @@ export function supportsCaretPositionFromPoint(): boolean {
}

export function caretPositionFromPoint(doc: Document, x: number, y: number): CaretPosition | null {
if (typeof doc.caretPositionFromPoint == 'function') {
return doc.caretPositionFromPoint(x, y);
}
else if (typeof doc.caretRangeFromPoint == 'function') {
const range = doc.caretRangeFromPoint(x, y);
if (!range) {
return null;
// Make sure text selection is enabled everywhere
// We need this for WebKit because user-select: none disables
// caretRangeFromPoint()
doc.body.classList.add('force-enable-selection-everywhere');
try {
if (typeof doc.caretPositionFromPoint == 'function') {
return doc.caretPositionFromPoint(x, y);
}
else if (typeof doc.caretRangeFromPoint == 'function') {
const range = doc.caretRangeFromPoint(x, y);
if (!range) {
return null;
}
return {
offsetNode: range.startContainer,
offset: range.startOffset,
getClientRect: () => range.getBoundingClientRect()
};
}
return {
offsetNode: range.startContainer,
offset: range.startOffset,
getClientRect: () => range.getBoundingClientRect()
};
return null;
}
finally {
doc.body.classList.remove('force-enable-selection-everywhere');
}
return null;
}

export function getStartElement(range: Range | PersistentRange): Element | null {
Expand Down
73 changes: 73 additions & 0 deletions src/dom/common/stylesheets/annotations.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.annotation-container {
z-index: 9999;
pointer-events: none;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: transparent;
overflow: visible;
}

.annotation-container.blended {
mix-blend-mode: multiply;
}

.annotation-div {
cursor: default;
width: 100%;
height: 100%;
pointer-events: auto;
}

.inherit-pointer-events {
pointer-events: auto;
}

.disable-pointer-events {
pointer-events: none !important;

.inherit-pointer-events {
pointer-events: none !important;
}

.needs-pointer-events {
display: none !important;
}
}

.resizer {
cursor: col-resize;
touch-action: none;
}

@media (any-pointer: coarse) {
.resizer {
stroke: transparent;
stroke-width: 20px;
margin: -10px;
}
}

@mixin -dark-rules() {
.annotation-container.blended {
mix-blend-mode: screen;
}
}

@media (prefers-color-scheme: dark) {
#annotation-render-root:not(.disable-dark-mode) {
@include -dark-rules();
}
}

#annotation-render-root[data-color-scheme=dark]:not(.disable-dark-mode) {
@include -dark-rules();
}

@media print {
:host {
display: none !important;
}
}
76 changes: 8 additions & 68 deletions src/dom/common/stylesheets/inject.scss
Original file line number Diff line number Diff line change
@@ -1,73 +1,13 @@
.annotation-container {
z-index: 9999;
pointer-events: none;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: transparent;
overflow: visible;
}

.annotation-container.blended {
mix-blend-mode: multiply;
}

.annotation-div {
cursor: default;
width: 100%;
height: 100%;
pointer-events: auto;
}

.inherit-pointer-events {
pointer-events: auto;
}

.disable-pointer-events {
pointer-events: none !important;

.inherit-pointer-events {
pointer-events: none !important;
}

.needs-pointer-events {
display: none !important;
}
}

.resizer {
cursor: col-resize;
touch-action: none;
}

@media (any-pointer: coarse) {
.resizer {
stroke: transparent;
stroke-width: 20px;
margin: -10px;
}
}

@mixin -dark-rules() {
.annotation-container.blended {
mix-blend-mode: screen;
}
}

@media (prefers-color-scheme: dark) {
#annotation-render-root:not(.disable-dark-mode) {
@include -dark-rules();
body.resizing-annotation, body.creating-touch-annotation {
&, & * {
user-select: none !important;
-webkit-user-select: none !important;
}
}

#annotation-render-root[data-color-scheme=dark]:not(.disable-dark-mode) {
@include -dark-rules();
}

@media print {
:host {
display: none !important;
body.force-enable-selection-everywhere {
&, & * {
user-select: auto !important;
-webkit-user-select: auto !important;
}
}

0 comments on commit 3262119

Please sign in to comment.