Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 56 additions & 95 deletions packages/roosterjs-content-model-plugins/lib/touch/TouchPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import type {
EditorPlugin,
IEditor,
PluginEvent,
ReadonlyContentModelDocument,
} from 'roosterjs-content-model-types';
import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-content-model-types';
import { getNodePositionFromEvent } from '../utils/getNodePositionFromEvent';
import {
getSelectedSegmentsAndParagraphs,
mutateBlock,
createSelectionMarker,
} from 'roosterjs-content-model-dom';
import { adjustWordSelection } from 'roosterjs-content-model-api';

const MAX_TOUCH_MOVE_DISTANCE = 6; // the max number of offsets for the touch selection to move
const POINTER_DETECTION_DELAY = 150; // Delay time to wait for selection to be updated and also detect if pointerup is a tap or part of double tap
const PUNCTUATION_MATCHING_REGEX = /[.,;:!]/;
const SPACE_MATCHING_REGEX = /\s/;
const CARET_CSS_RULE = 'caret-color: transparent';
const HIDE_CURSOR_CSS_KEY = '_DOMSelectionHideCursor';

/**
* Touch plugin to manage touch behaviors
Expand Down Expand Up @@ -72,7 +59,7 @@ export class TouchPlugin implements EditorPlugin {
case 'pointerDown':
this.isDblClicked = false;
this.isTouchPenPointerEvent = true;
this.editor.setEditorStyle(HIDE_CURSOR_CSS_KEY, CARET_CSS_RULE);
event.originalEvent.preventDefault();

const targetWindow = this.editor.getDocument()?.defaultView || window;
if (this.timer) {
Expand All @@ -84,16 +71,66 @@ export class TouchPlugin implements EditorPlugin {

if (this.editor) {
if (!this.isDblClicked) {
this.editor.formatContentModel(model =>
this.repositionTouchSelection(model)
this.editor.focus();
const caretPosition = getNodePositionFromEvent(
this.editor,
event.rawEvent.x,
event.rawEvent.y
);

const newRange = this.editor.getDocument().createRange();
if (caretPosition) {
const { node, offset } = caretPosition;

// Place cursor at same position of browser handler by default
newRange.setStart(node, offset);
newRange.setEnd(node, offset);

const nodeTextContent = node.textContent || '';
const charAtSelection = nodeTextContent[offset];
if (
node.nodeType === Node.TEXT_NODE &&
charAtSelection &&
!SPACE_MATCHING_REGEX.test(charAtSelection) &&
!PUNCTUATION_MATCHING_REGEX.test(charAtSelection)
) {
const { wordStart, wordEnd } = findWordBoundaries(
nodeTextContent,
offset
);

// Move cursor to the calculated offset
const leftCursorWordLength = offset - wordStart;
const rightCursorWordLength = wordEnd - offset;
let movingOffset: number =
leftCursorWordLength >= rightCursorWordLength
? rightCursorWordLength
: -leftCursorWordLength;
movingOffset =
Math.abs(movingOffset) > MAX_TOUCH_MOVE_DISTANCE
? 0
: movingOffset;
const newOffsetPosition = offset + movingOffset;
if (
movingOffset !== 0 &&
nodeTextContent.length >= newOffsetPosition
) {
newRange.setStart(node, newOffsetPosition);
newRange.setEnd(node, newOffsetPosition);
}
}
}
this.editor.setDOMSelection({
type: 'range',
range: newRange,
isReverted: false,
});

// reset values
this.isTouchPenPointerEvent = false;
}
this.editor.setEditorStyle(HIDE_CURSOR_CSS_KEY, null);
}
}, POINTER_DETECTION_DELAY);

break;
case 'doubleClick':
if (this.isTouchPenPointerEvent) {
Expand Down Expand Up @@ -177,82 +214,6 @@ export class TouchPlugin implements EditorPlugin {
break;
}
}

repositionTouchSelection = (model: ReadonlyContentModelDocument) => {
const segmentAndParagraphs = getSelectedSegmentsAndParagraphs(
model,
false /*includingFormatHolder*/,
true /*includingEntity*/,
true /*mutate*/
);

const isCollapsedSelection =
segmentAndParagraphs.length >= 1 &&
segmentAndParagraphs.every(x => x[0].segmentType == 'SelectionMarker');

// 1. adjust selection to a word if selection is collapsed
if (isCollapsedSelection) {
const para = segmentAndParagraphs[0][1];
const segments = adjustWordSelection(model, segmentAndParagraphs[0][0]);

if (
segments.length > 2 &&
segments.some(x => x.segmentType == 'Text' && !x.isSelected) &&
para
) {
const selectionMarkerIndexInWord = segments.findIndex(
segment => segment.segmentType == 'SelectionMarker'
);
const selectionMarkerIndexInPara = para.segments.findIndex(
segment => segment.segmentType == 'SelectionMarker'
);
const leftSelectionSegmentsInWord = segments[selectionMarkerIndexInWord - 1];
const rightSelectionSegmentsInWord = segments[selectionMarkerIndexInWord + 1];
const leftCursorWordLength =
leftSelectionSegmentsInWord.segmentType == 'Text'
? leftSelectionSegmentsInWord.text.length
: 0;
const rightCursorWordLength =
rightSelectionSegmentsInWord.segmentType == 'Text'
? rightSelectionSegmentsInWord.text.length
: 0;

// Move the cursor to the closest edge of the word if the distance is within threshold = 6
if (rightCursorWordLength > leftCursorWordLength) {
if (leftCursorWordLength < MAX_TOUCH_MOVE_DISTANCE) {
// Move cursor to beginning of word
// Remove old marker
mutateBlock(para).segments.splice(selectionMarkerIndexInPara, 1);

// Add new marker
const indexSegmentBeforeMarker = para.segments.findIndex(
segment => segment === leftSelectionSegmentsInWord
);
const marker = createSelectionMarker(
segments[selectionMarkerIndexInPara]?.format || para.format
);
mutateBlock(para).segments.splice(indexSegmentBeforeMarker, 0, marker);
}
} else {
// Move cursor to end of word
if (rightCursorWordLength < MAX_TOUCH_MOVE_DISTANCE) {
// Add new marker
const indexSegmentAfterMarker = para.segments.findIndex(
segment => segment === rightSelectionSegmentsInWord
);
const marker = createSelectionMarker(
segments[selectionMarkerIndexInPara]?.format || para.format
);
mutateBlock(para).segments.splice(indexSegmentAfterMarker + 1, 0, marker);

// Remove old marker
mutateBlock(para).segments.splice(selectionMarkerIndexInPara, 1);
}
}
}
}
return true;
};
}

/**
Expand Down
Loading