Skip to content

Commit 08c0095

Browse files
authored
Merge pull request #3156 from microsoft/u/nguyenvi/touch-double-tap
Touch Selection - handle double tab **Spec:** Double tap on a word: Highlight the word or closest word if the user tapped on a space in between 2 words and open the Floatie Ribbon **(-> browser handles this)** - note: if the user double taps on the open space after a word and there is no word on the right-side of the same, then highlight the first space of the wide gap - note: if a user double taps on a character like a comma, period, colon, or semi-colon, then highlight that character - note: if a user double taps on a bracket [,{,(,),},] that is next to a word, then highlight the word and not the bracket **(-> browser handles this)** **Changes:** - Fix: + reset `this.pointerEvent` to be `null `after trigger plugin event + use `setTimeout` to delay plugin event triggered for 200s to wait for new selection to be updated properly before defining the reposition (and also wait for `dblclick` event to check if it is double tab or single tab). - Listen to `dblclick` native event to trigger `pointerDoubleClick` if there is pointer event stored - Add new `pointerDoubleClick` plugin event and add handler in Touch Plugin - Add handler for 2 scenarios: + double clicked character is a punctuation mark: select that char only + double clicked character is a white space: check if right side has word, if yes, let browser handle it; if no, traverse to left and select the first white space char of the selected open space
2 parents 059454f + d187575 commit 08c0095

File tree

7 files changed

+110
-11
lines changed

7 files changed

+110
-11
lines changed

packages/roosterjs-content-model-core/lib/corePlugin/domEvent/DOMEventPlugin.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const EventTypeMap: Record<string, 'keyDown' | 'keyUp' | 'keyPress'> = {
1717
keyup: 'keyUp',
1818
keypress: 'keyPress',
1919
};
20+
const POINTER_DETECTION_DELAY = 200; // Delay time to wait for selection to be updated and also detect if pointerup is a tap or part of double tap
2021

2122
/**
2223
* DOMEventPlugin handles customized DOM events, including:
@@ -34,6 +35,8 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
3435
private disposer: (() => void) | null = null;
3536
private state: DOMEventPluginState;
3637
private pointerEvent: PointerEvent | null = null;
38+
private timer = 0;
39+
private isDblClicked: boolean = false;
3740

3841
/**
3942
* Construct a new instance of DOMEventPlugin
@@ -87,6 +90,7 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
8790

8891
// 5. Pointer event
8992
pointerdown: { beforeDispatch: (event: PointerEvent) => this.onPointerDown(event) },
93+
dblclick: { beforeDispatch: () => this.onDoubleClick() },
9094
};
9195

9296
this.disposer = this.editor.attachDomEvent(<Record<string, DOMEventRecord>>eventHandlers);
@@ -104,14 +108,18 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
104108
this.removeMouseUpEventListener();
105109

106110
const document = this.editor?.getDocument();
107-
108111
document?.defaultView?.removeEventListener('resize', this.onScroll);
109112
document?.defaultView?.removeEventListener('scroll', this.onScroll);
110113
this.state.scrollContainer.removeEventListener('scroll', this.onScroll);
111114
this.disposer?.();
112115
this.disposer = null;
113116
this.editor = null;
114117
this.pointerEvent = null;
118+
119+
if (this.timer) {
120+
document?.defaultView?.clearTimeout(this.timer);
121+
this.timer = 0;
122+
}
115123
}
116124

117125
/**
@@ -220,13 +228,40 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
220228
});
221229

222230
if (this.pointerEvent) {
223-
this.editor.triggerEvent('pointerUp', {
224-
rawEvent: this.pointerEvent,
225-
});
231+
const window = this.editor?.getDocument().defaultView;
232+
233+
if (!window) {
234+
return;
235+
}
236+
237+
if (this.timer) {
238+
window.clearTimeout(this.timer);
239+
}
240+
241+
this.timer = window.setTimeout(() => {
242+
this.timer = 0;
243+
if (this.editor && this.pointerEvent) {
244+
if (this.isDblClicked) {
245+
this.editor.triggerEvent('pointerDoubleClick', {
246+
rawEvent: this.pointerEvent,
247+
});
248+
} else {
249+
this.editor.triggerEvent('pointerUp', {
250+
rawEvent: this.pointerEvent,
251+
});
252+
}
253+
}
254+
this.pointerEvent = null;
255+
this.isDblClicked = false;
256+
}, POINTER_DETECTION_DELAY);
226257
}
227258
}
228259
};
229260

261+
private onDoubleClick = () => {
262+
this.isDblClicked = true;
263+
};
264+
230265
private onCompositionStart = () => {
231266
this.state.isInIME = true;
232267
};

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,57 @@ export class TouchPlugin implements EditorPlugin {
4646
case 'pointerUp':
4747
repositionTouchSelection(this.editor);
4848
break;
49+
case 'pointerDoubleClick':
50+
const selection = this.editor.getDocument()?.getSelection();
51+
if (!selection) {
52+
return;
53+
}
54+
55+
const node = selection.focusNode;
56+
if (node?.nodeType !== Node.TEXT_NODE) {
57+
return;
58+
}
59+
60+
const offset = selection.focusOffset;
61+
const text = node.nodeValue || '';
62+
const char = text.charAt(offset);
63+
64+
// Check if the clicked character is a punctuation mark, then highlight that character only
65+
if (/[.,;:]/.test(char)) {
66+
const newRange = this.editor.getDocument()?.createRange();
67+
if (newRange) {
68+
newRange.setStart(node, offset);
69+
newRange.setEnd(node, offset + 1);
70+
this.editor.setDOMSelection({
71+
type: 'range',
72+
range: newRange,
73+
isReverted: false,
74+
});
75+
}
76+
} else if (/\s/.test(char)) {
77+
// If the clicked character is an open space with no word of right side
78+
const rightSideOfChar = text.substring(offset, text.length);
79+
const isRightSideAllSpaces =
80+
rightSideOfChar.length > 0 && !/\S/.test(rightSideOfChar);
81+
if (isRightSideAllSpaces) {
82+
// select the first space only
83+
let start = offset;
84+
while (start > 0 && /\s/.test(text.charAt(start - 1))) {
85+
start--;
86+
}
87+
const newRange = this.editor.getDocument()?.createRange();
88+
if (newRange) {
89+
newRange.setStart(node, start);
90+
newRange.setEnd(node, start + 1);
91+
this.editor.setDOMSelection({
92+
type: 'range',
93+
range: newRange,
94+
isReverted: false,
95+
});
96+
}
97+
}
98+
}
99+
break;
49100
}
50101
}
51102
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ export function repositionTouchSelection(editor: IEditor) {
4444
// before selection marker + after selection marker
4545
if (segments.length === 2) {
4646
// 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
4748
const leftCursorWordLength = segments[0].text.length;
4849
const rightCursorWordLength = segments[1].text.length;
4950
let movingOffset: number =
50-
leftCursorWordLength > rightCursorWordLength
51+
leftCursorWordLength >= rightCursorWordLength
5152
? rightCursorWordLength
5253
: -leftCursorWordLength;
5354
movingOffset =

packages/roosterjs-content-model-types/lib/event/PluginEvent.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type { ScrollEvent } from './ScrollEvent';
2222
import type { SelectionChangedEvent } from './SelectionChangedEvent';
2323
import type { EnterShadowEditEvent, LeaveShadowEditEvent } from './ShadowEditEvent';
2424
import type { ZoomChangedEvent } from './ZoomChangedEvent';
25-
import type { PointerDownEvent, PointerUpEvent } from './PointerEvent';
25+
import type { PointerDownEvent, PointerUpEvent, PointerDoubleClickEvent } from './PointerEvent';
2626

2727
/**
2828
* Editor plugin event interface
@@ -56,4 +56,5 @@ export type PluginEvent =
5656
| SelectionChangedEvent
5757
| ZoomChangedEvent
5858
| PointerDownEvent
59-
| PointerUpEvent;
59+
| PointerUpEvent
60+
| PointerDoubleClickEvent;

packages/roosterjs-content-model-types/lib/event/PluginEventType.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,16 @@ export type PluginEventType =
149149
| 'beforeAddUndoSnapshot'
150150

151151
/**
152-
* HTML PointerDown event
152+
* HTML PointerDown event - for touch only
153153
*/
154154
| 'pointerDown'
155155

156156
/**
157-
* HTML PointerUp event
157+
* HTML PointerUp event - for touch only
158158
*/
159-
| 'pointerUp';
159+
| 'pointerUp'
160+
161+
/**
162+
* HTML double click with Pointer event - for touch only
163+
*/
164+
| 'pointerDoubleClick';

packages/roosterjs-content-model-types/lib/event/PointerEvent.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ export interface PointerDownEvent extends BasePluginDomEvent<'pointerDown', Poin
99
* This interface represents a PluginEvent wrapping native PointerUp event
1010
*/
1111
export interface PointerUpEvent extends BasePluginDomEvent<'pointerUp', PointerEvent> {}
12+
13+
/**
14+
* This interface represents a PluginEvent wrapping native double-click event with pointer information
15+
*/
16+
export interface PointerDoubleClickEvent
17+
extends BasePluginDomEvent<'pointerDoubleClick', PointerEvent> {}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,4 +495,4 @@ export { ScrollEvent } from './event/ScrollEvent';
495495
export { SelectionChangedEvent } from './event/SelectionChangedEvent';
496496
export { EnterShadowEditEvent, LeaveShadowEditEvent } from './event/ShadowEditEvent';
497497
export { ZoomChangedEvent } from './event/ZoomChangedEvent';
498-
export { PointerDownEvent, PointerUpEvent } from './event/PointerEvent';
498+
export { PointerDownEvent, PointerUpEvent, PointerDoubleClickEvent } from './event/PointerEvent';

0 commit comments

Comments
 (0)