Skip to content

Commit 1a5951e

Browse files
author
Vi Nguyen
committed
only take control of touch in some specific scenario
1 parent 2b64cdd commit 1a5951e

4 files changed

Lines changed: 96 additions & 65 deletions

File tree

packages/roosterjs-content-model-api/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,4 @@ export { matchLink } from './modelApi/link/matchLink';
6262
export { promoteLink } from './modelApi/link/promoteLink';
6363
export { getListAnnounceData } from './modelApi/list/getListAnnounceData';
6464
export { queryContentModelBlocks } from './modelApi/common/queryContentModelBlocks';
65+
export { adjustWordSelection } from './modelApi/selection/adjustWordSelection';

packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import type {
1313
} from 'roosterjs-content-model-types';
1414

1515
/**
16-
* @internal
16+
* Return specific word segment where the selection marker is located
17+
* @param model The model document
18+
* @param marker The selection marker
19+
* @returns An array of segments that form the word where the selection marker is located
1720
*/
1821
export function adjustWordSelection(
1922
model: ReadonlyContentModelDocument,

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

Lines changed: 7 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-content-model-types';
22
import { getNodePositionFromEvent } from '../utils/getNodePositionFromEvent';
3+
import { repositionTouchSelection } from './repositionTouchSelection';
34

4-
const MAX_TOUCH_MOVE_DISTANCE = 6; // the max number of offsets for the touch selection to move
5+
// const MAX_TOUCH_MOVE_DISTANCE = 6; // the max number of offsets for the touch selection to move
56
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
67
const PUNCTUATION_MATCHING_REGEX = /[.,;:!]/;
78
const SPACE_MATCHING_REGEX = /\s/;
9+
const CARET_CSS_RULE = 'caret-color: transparent';
10+
const HIDE_CURSOR_CSS_KEY = '_DOMSelectionHideCursor';
811

912
/**
1013
* Touch plugin to manage touch behaviors
@@ -59,80 +62,20 @@ export class TouchPlugin implements EditorPlugin {
5962
case 'pointerDown':
6063
this.isDblClicked = false;
6164
this.isTouchPenPointerEvent = true;
62-
event.originalEvent.preventDefault();
6365

6466
const targetWindow = this.editor.getDocument()?.defaultView || window;
6567
if (this.timer) {
6668
targetWindow.clearTimeout(this.timer);
6769
}
68-
70+
this.editor.setEditorStyle(HIDE_CURSOR_CSS_KEY, CARET_CSS_RULE);
6971
this.timer = targetWindow.setTimeout(() => {
7072
this.timer = 0;
7173

7274
if (!this.isDblClicked && this.editor) {
73-
const caretPosition = getNodePositionFromEvent(
74-
this.editor,
75-
event.rawEvent.x,
76-
event.rawEvent.y
77-
);
78-
79-
if (caretPosition) {
80-
const { node, offset } = caretPosition;
81-
82-
const nodeTextContent = node.textContent || '';
83-
const charAtSelection = nodeTextContent[offset];
84-
if (
85-
node.nodeType === Node.TEXT_NODE &&
86-
charAtSelection &&
87-
!SPACE_MATCHING_REGEX.test(charAtSelection) &&
88-
!PUNCTUATION_MATCHING_REGEX.test(charAtSelection)
89-
) {
90-
const { wordStart, wordEnd } = findWordBoundaries(
91-
nodeTextContent,
92-
offset
93-
);
94-
95-
// Move cursor to the calculated offset
96-
const leftCursorWordLength = offset - wordStart;
97-
const rightCursorWordLength = wordEnd - offset;
98-
let movingOffset: number =
99-
leftCursorWordLength >= rightCursorWordLength
100-
? rightCursorWordLength
101-
: -leftCursorWordLength;
102-
movingOffset =
103-
Math.abs(movingOffset) > MAX_TOUCH_MOVE_DISTANCE
104-
? 0
105-
: movingOffset;
106-
const newOffsetPosition = offset + movingOffset;
107-
if (
108-
movingOffset !== 0 &&
109-
nodeTextContent.length >= newOffsetPosition
110-
) {
111-
const newRange = this.editor.getDocument().createRange();
112-
newRange.setStart(node, newOffsetPosition);
113-
newRange.setEnd(node, newOffsetPosition);
114-
this.editor.setDOMSelection({
115-
type: 'range',
116-
range: newRange,
117-
isReverted: false,
118-
});
119-
return;
120-
}
121-
}
122-
123-
// Place cursor at same position of browser handler
124-
const newRange = this.editor.getDocument().createRange();
125-
newRange.setStart(node, offset);
126-
newRange.setEnd(node, offset);
127-
this.editor.setDOMSelection({
128-
type: 'range',
129-
range: newRange,
130-
isReverted: false,
131-
});
132-
}
133-
75+
repositionTouchSelection(this.editor);
13476
// reset values
13577
this.isTouchPenPointerEvent = false;
78+
this.editor.setEditorStyle(HIDE_CURSOR_CSS_KEY, null);
13679
}
13780
}, POINTER_DETECTION_DELAY);
13881

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { IEditor, ContentModelText } from 'roosterjs-content-model-types';
2+
import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom';
3+
import { adjustWordSelection } from 'roosterjs-content-model-api';
4+
5+
const MAX_TOUCH_MOVE_DISTANCE = 6; // the max number of offsets for the touch selection to move
6+
7+
/**
8+
* @internal
9+
* Touch selection, if tap within 6 characters of the beginning/end. Find the nearest edge of the word and move cursor there.
10+
*/
11+
export function repositionTouchSelection(editor: IEditor) {
12+
editor.formatContentModel(
13+
(model, _context) => {
14+
let segmentAndParagraphs = getSelectedSegmentsAndParagraphs(
15+
model,
16+
false /*includingFormatHolder*/,
17+
true /*includingEntity*/,
18+
true /*mutate*/
19+
);
20+
21+
const isCollapsedSelection =
22+
segmentAndParagraphs.length >= 1 &&
23+
segmentAndParagraphs.every(x => x[0].segmentType == 'SelectionMarker');
24+
25+
// 1. adjust selection to a word if selection is collapsed
26+
if (isCollapsedSelection) {
27+
const para = segmentAndParagraphs[0][1];
28+
const path = segmentAndParagraphs[0][2];
29+
30+
segmentAndParagraphs = adjustWordSelection(
31+
model,
32+
segmentAndParagraphs[0][0]
33+
).map(x => [x, para, path]);
34+
35+
// 2. Collect all text segments in selection
36+
const segments: ContentModelText[] = [];
37+
segmentAndParagraphs.forEach(item => {
38+
if (item[0].segmentType == 'Text') {
39+
segments.push(item[0]);
40+
}
41+
});
42+
43+
// If there are 3 text segment in the Word, selection is in middle of the word
44+
// before selection marker + after selection marker
45+
if (segments.length === 2) {
46+
// 3. Calculate the offset to move cursor to the nearest edge of the word if within 6 characters
47+
// default to end of the word if user tapped in the middle
48+
const leftCursorWordLength = segments[0].text.length;
49+
const rightCursorWordLength = segments[1].text.length;
50+
let movingOffset: number =
51+
leftCursorWordLength >= rightCursorWordLength
52+
? rightCursorWordLength
53+
: -leftCursorWordLength;
54+
movingOffset =
55+
Math.abs(movingOffset) > MAX_TOUCH_MOVE_DISTANCE ? 0 : movingOffset;
56+
57+
// 4. Move cursor to the calculated offset
58+
const selection = editor.getDOMSelection();
59+
if (selection?.type == 'range' && movingOffset !== 0) {
60+
const selectedRange = selection.range;
61+
const newRange = editor.getDocument().createRange();
62+
newRange.setStart(
63+
selectedRange.startContainer,
64+
selectedRange.startOffset + movingOffset
65+
);
66+
newRange.setEnd(
67+
selectedRange.endContainer,
68+
selectedRange.endOffset + movingOffset
69+
);
70+
editor.setDOMSelection({
71+
type: 'range',
72+
range: newRange,
73+
isReverted: false,
74+
});
75+
}
76+
}
77+
}
78+
return false;
79+
},
80+
{
81+
apiName: 'TouchSelection',
82+
}
83+
);
84+
}

0 commit comments

Comments
 (0)