Skip to content

Commit d82be10

Browse files
authored
Merge pull request #3174 from microsoft/u/nguyenvi/touch-selection-handle-entire-flow
Touch Plugin - Handle entire flow of selection in editor
2 parents cb4e7c9 + 867e3ad commit d82be10

File tree

1 file changed

+56
-95
lines changed

1 file changed

+56
-95
lines changed

packages/roosterjs-content-model-plugins/lib/touch/TouchPlugin.ts

Lines changed: 56 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
1-
import type {
2-
EditorPlugin,
3-
IEditor,
4-
PluginEvent,
5-
ReadonlyContentModelDocument,
6-
} from 'roosterjs-content-model-types';
1+
import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-content-model-types';
72
import { getNodePositionFromEvent } from '../utils/getNodePositionFromEvent';
8-
import {
9-
getSelectedSegmentsAndParagraphs,
10-
mutateBlock,
11-
createSelectionMarker,
12-
} from 'roosterjs-content-model-dom';
13-
import { adjustWordSelection } from 'roosterjs-content-model-api';
143

154
const MAX_TOUCH_MOVE_DISTANCE = 6; // the max number of offsets for the touch selection to move
165
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
176
const PUNCTUATION_MATCHING_REGEX = /[.,;:!]/;
187
const SPACE_MATCHING_REGEX = /\s/;
19-
const CARET_CSS_RULE = 'caret-color: transparent';
20-
const HIDE_CURSOR_CSS_KEY = '_DOMSelectionHideCursor';
218

229
/**
2310
* Touch plugin to manage touch behaviors
@@ -72,7 +59,7 @@ export class TouchPlugin implements EditorPlugin {
7259
case 'pointerDown':
7360
this.isDblClicked = false;
7461
this.isTouchPenPointerEvent = true;
75-
this.editor.setEditorStyle(HIDE_CURSOR_CSS_KEY, CARET_CSS_RULE);
62+
event.originalEvent.preventDefault();
7663

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

8572
if (this.editor) {
8673
if (!this.isDblClicked) {
87-
this.editor.formatContentModel(model =>
88-
this.repositionTouchSelection(model)
74+
this.editor.focus();
75+
const caretPosition = getNodePositionFromEvent(
76+
this.editor,
77+
event.rawEvent.x,
78+
event.rawEvent.y
8979
);
80+
81+
const newRange = this.editor.getDocument().createRange();
82+
if (caretPosition) {
83+
const { node, offset } = caretPosition;
84+
85+
// Place cursor at same position of browser handler by default
86+
newRange.setStart(node, offset);
87+
newRange.setEnd(node, offset);
88+
89+
const nodeTextContent = node.textContent || '';
90+
const charAtSelection = nodeTextContent[offset];
91+
if (
92+
node.nodeType === Node.TEXT_NODE &&
93+
charAtSelection &&
94+
!SPACE_MATCHING_REGEX.test(charAtSelection) &&
95+
!PUNCTUATION_MATCHING_REGEX.test(charAtSelection)
96+
) {
97+
const { wordStart, wordEnd } = findWordBoundaries(
98+
nodeTextContent,
99+
offset
100+
);
101+
102+
// Move cursor to the calculated offset
103+
const leftCursorWordLength = offset - wordStart;
104+
const rightCursorWordLength = wordEnd - offset;
105+
let movingOffset: number =
106+
leftCursorWordLength >= rightCursorWordLength
107+
? rightCursorWordLength
108+
: -leftCursorWordLength;
109+
movingOffset =
110+
Math.abs(movingOffset) > MAX_TOUCH_MOVE_DISTANCE
111+
? 0
112+
: movingOffset;
113+
const newOffsetPosition = offset + movingOffset;
114+
if (
115+
movingOffset !== 0 &&
116+
nodeTextContent.length >= newOffsetPosition
117+
) {
118+
newRange.setStart(node, newOffsetPosition);
119+
newRange.setEnd(node, newOffsetPosition);
120+
}
121+
}
122+
}
123+
this.editor.setDOMSelection({
124+
type: 'range',
125+
range: newRange,
126+
isReverted: false,
127+
});
128+
90129
// reset values
91130
this.isTouchPenPointerEvent = false;
92131
}
93-
this.editor.setEditorStyle(HIDE_CURSOR_CSS_KEY, null);
94132
}
95133
}, POINTER_DETECTION_DELAY);
96-
97134
break;
98135
case 'doubleClick':
99136
if (this.isTouchPenPointerEvent) {
@@ -177,82 +214,6 @@ export class TouchPlugin implements EditorPlugin {
177214
break;
178215
}
179216
}
180-
181-
repositionTouchSelection = (model: ReadonlyContentModelDocument) => {
182-
const segmentAndParagraphs = getSelectedSegmentsAndParagraphs(
183-
model,
184-
false /*includingFormatHolder*/,
185-
true /*includingEntity*/,
186-
true /*mutate*/
187-
);
188-
189-
const isCollapsedSelection =
190-
segmentAndParagraphs.length >= 1 &&
191-
segmentAndParagraphs.every(x => x[0].segmentType == 'SelectionMarker');
192-
193-
// 1. adjust selection to a word if selection is collapsed
194-
if (isCollapsedSelection) {
195-
const para = segmentAndParagraphs[0][1];
196-
const segments = adjustWordSelection(model, segmentAndParagraphs[0][0]);
197-
198-
if (
199-
segments.length > 2 &&
200-
segments.some(x => x.segmentType == 'Text' && !x.isSelected) &&
201-
para
202-
) {
203-
const selectionMarkerIndexInWord = segments.findIndex(
204-
segment => segment.segmentType == 'SelectionMarker'
205-
);
206-
const selectionMarkerIndexInPara = para.segments.findIndex(
207-
segment => segment.segmentType == 'SelectionMarker'
208-
);
209-
const leftSelectionSegmentsInWord = segments[selectionMarkerIndexInWord - 1];
210-
const rightSelectionSegmentsInWord = segments[selectionMarkerIndexInWord + 1];
211-
const leftCursorWordLength =
212-
leftSelectionSegmentsInWord.segmentType == 'Text'
213-
? leftSelectionSegmentsInWord.text.length
214-
: 0;
215-
const rightCursorWordLength =
216-
rightSelectionSegmentsInWord.segmentType == 'Text'
217-
? rightSelectionSegmentsInWord.text.length
218-
: 0;
219-
220-
// Move the cursor to the closest edge of the word if the distance is within threshold = 6
221-
if (rightCursorWordLength > leftCursorWordLength) {
222-
if (leftCursorWordLength < MAX_TOUCH_MOVE_DISTANCE) {
223-
// Move cursor to beginning of word
224-
// Remove old marker
225-
mutateBlock(para).segments.splice(selectionMarkerIndexInPara, 1);
226-
227-
// Add new marker
228-
const indexSegmentBeforeMarker = para.segments.findIndex(
229-
segment => segment === leftSelectionSegmentsInWord
230-
);
231-
const marker = createSelectionMarker(
232-
segments[selectionMarkerIndexInPara]?.format || para.format
233-
);
234-
mutateBlock(para).segments.splice(indexSegmentBeforeMarker, 0, marker);
235-
}
236-
} else {
237-
// Move cursor to end of word
238-
if (rightCursorWordLength < MAX_TOUCH_MOVE_DISTANCE) {
239-
// Add new marker
240-
const indexSegmentAfterMarker = para.segments.findIndex(
241-
segment => segment === rightSelectionSegmentsInWord
242-
);
243-
const marker = createSelectionMarker(
244-
segments[selectionMarkerIndexInPara]?.format || para.format
245-
);
246-
mutateBlock(para).segments.splice(indexSegmentAfterMarker + 1, 0, marker);
247-
248-
// Remove old marker
249-
mutateBlock(para).segments.splice(selectionMarkerIndexInPara, 1);
250-
}
251-
}
252-
}
253-
}
254-
return true;
255-
};
256217
}
257218

258219
/**

0 commit comments

Comments
 (0)