Skip to content

Commit 8b68ed4

Browse files
vinguyen12Vi NguyenJiuqingSongisninehaven2world
authored
Bump version to 9.37.0 (#3160)
* add repositionTouchSelection handler * fix * define constant * fix * fix * revert previous PR change * add Touch Plugin * add repositionTouchSelection in Touch Plugin * small fix * add touch plugin to demo page * add comment * fix * export pointer event * add touchplugin test * reset pointerEvent to null after trigger action and settimeout to wait for updated cursor position * Remove tslint from recommended extension list (#3153) * default to end of the word if user tapped in the middle * add pointer event double click * handle selecting first space if selection is an open space with no word on right side * remove console.log * clearTimeout when dispose plugin * address Copilot comment * fix * Address comment * Lint code * handle the entire flow of touch selection, not rely on browser * remove test * move timeout and prevent default to within touch plugin, use doubleclick as general mouse event instead of pointer * address Copilot comments * add comment * fix comment * create const for regex * resuse getNodePositionFromEvent * remove unused change * fix issue with selection in middle of word * fix * address comments * Add comments * Remove auto-capitalizatio for first character * Remove Boolean() wrap * [Image Edit] Image with borders (#3155) recalculate the image wrapper size considering the border width * fix * remove word matching regex but using !space regex and !punctuation regex instead * fix version number --------- Co-authored-by: Vi Nguyen <nguyenvi@microsoft.com> Co-authored-by: Jiuqing Song <jisong@microsoft.com> Co-authored-by: Zander Wang <xiaozwan@microsoft.com> Co-authored-by: Haowen Chen <haowchen@microsoft.com> Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com>
1 parent 83a7588 commit 8b68ed4

File tree

26 files changed

+1053
-117
lines changed

26 files changed

+1053
-117
lines changed

.vscode/extensions.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"recommendations": [
33
"esbenp.prettier-vscode",
4-
"ms-vscode.vscode-typescript-tslint-plugin",
54
"miclo.sort-typescript-imports",
65
"streetsidesoftware.code-spell-checker"
76
]

demo/scripts/controlsV2/mainPane/MainPane.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
ShortcutPlugin,
7171
TableEditPlugin,
7272
WatermarkPlugin,
73+
TouchPlugin,
7374
} from 'roosterjs-content-model-plugins';
7475
import DOMPurify = require('dompurify');
7576

@@ -559,6 +560,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
559560
new HiddenPropertyPlugin({
560561
undeletableLinkChecker: undeletableLinkChecker,
561562
}),
563+
pluginList.touch && new TouchPlugin(),
562564
].filter(x => !!x);
563565
}
564566
}

demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const initialState: OptionState = {
2222
hyperlink: true,
2323
customReplace: true,
2424
hiddenProperty: true,
25+
touch: true,
2526
},
2627
defaultFormat: {
2728
fontFamily: 'Calibri',

demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface BuildInPluginList {
2323
imageEditPlugin: boolean;
2424
customReplace: boolean;
2525
hiddenProperty: boolean;
26+
touch: boolean;
2627
}
2728

2829
export interface OptionState {

demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ export class Plugins extends PluginsBase<keyof BuildInPluginList> {
328328
</>
329329
)}
330330
{this.renderPluginItem('hiddenProperty', 'Hidden Property')}
331+
{this.renderPluginItem('touch', 'Touch')}
331332
</tbody>
332333
</table>
333334
);

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
3333
private editor: IEditor | null = null;
3434
private disposer: (() => void) | null = null;
3535
private state: DOMEventPluginState;
36+
private pointerEvent: PointerEvent | null = null;
37+
private timer = 0;
3638

3739
/**
3840
* Construct a new instance of DOMEventPlugin
@@ -75,6 +77,7 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
7577

7678
// 2. Mouse event
7779
mousedown: { beforeDispatch: this.onMouseDown },
80+
dblclick: { beforeDispatch: (event: MouseEvent) => this.onDoubleClick(event) },
7881

7982
// 3. IME state management
8083
compositionstart: { beforeDispatch: this.onCompositionStart },
@@ -83,6 +86,9 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
8386
// 4. Drag and Drop event
8487
dragstart: { beforeDispatch: this.onDragStart },
8588
drop: { beforeDispatch: this.onDrop },
89+
90+
// 5. Pointer event
91+
pointerdown: { beforeDispatch: (event: PointerEvent) => this.onPointerDown(event) },
8692
};
8793

8894
this.disposer = this.editor.attachDomEvent(<Record<string, DOMEventRecord>>eventHandlers);
@@ -100,13 +106,18 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
100106
this.removeMouseUpEventListener();
101107

102108
const document = this.editor?.getDocument();
103-
104109
document?.defaultView?.removeEventListener('resize', this.onScroll);
105110
document?.defaultView?.removeEventListener('scroll', this.onScroll);
106111
this.state.scrollContainer.removeEventListener('scroll', this.onScroll);
107112
this.disposer?.();
108113
this.disposer = null;
109114
this.editor = null;
115+
this.pointerEvent = null;
116+
117+
if (this.timer) {
118+
document?.defaultView?.clearTimeout(this.timer);
119+
this.timer = 0;
120+
}
110121
}
111122

112123
/**
@@ -197,6 +208,16 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
197208
this.editor.triggerEvent('mouseDown', {
198209
rawEvent: event,
199210
});
211+
212+
if (event.defaultPrevented) {
213+
this.pointerEvent = null;
214+
}
215+
if (this.pointerEvent) {
216+
this.editor.triggerEvent('pointerDown', {
217+
rawEvent: this.pointerEvent,
218+
originalEvent: event,
219+
});
220+
}
200221
}
201222
};
202223

@@ -210,6 +231,17 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
210231
this.state.mouseDownY == rawEvent.pageY,
211232
});
212233
}
234+
if (this.pointerEvent) {
235+
this.pointerEvent = null;
236+
}
237+
};
238+
239+
private onDoubleClick = (event: MouseEvent) => {
240+
if (this.editor) {
241+
this.editor.triggerEvent('doubleClick', {
242+
rawEvent: event,
243+
});
244+
}
213245
};
214246

215247
private onCompositionStart = () => {
@@ -229,6 +261,12 @@ class DOMEventPlugin implements PluginWithState<DOMEventPluginState> {
229261
this.editor.getDocument().removeEventListener('mouseup', this.onMouseUp, true);
230262
}
231263
}
264+
265+
private onPointerDown = (e: PointerEvent) => {
266+
if (e.pointerType === 'touch' || e.pointerType === 'pen') {
267+
this.pointerEvent = e;
268+
}
269+
};
232270
}
233271

234272
/**

packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import type {
2121
ParsedTable,
2222
TableSelectionInfo,
2323
TableCellCoordinate,
24-
DOMEventRecord,
2524
} from 'roosterjs-content-model-types';
2625

2726
const MouseLeftButton = 0;
@@ -49,7 +48,6 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
4948
private isSafari = false;
5049
private isMac = false;
5150
private scrollTopCache: number = 0;
52-
private pointerEvent: PointerEvent | null = null;
5351

5452
constructor(options: EditorOptions) {
5553
this.state = {
@@ -101,15 +99,18 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
10199
this.isSafari = !!env.isSafari;
102100
this.isMac = !!env.isMac;
103101
document.addEventListener('selectionchange', this.onSelectionChange);
104-
const eventHandlers: Partial<
105-
{ [P in keyof HTMLElementEventMap]: DOMEventRecord<HTMLElementEventMap[P]> }
106-
> = {
107-
focus: { beforeDispatch: this.onFocus },
108-
drop: { beforeDispatch: this.onDrop },
109-
blur: this.isSafari ? undefined : { beforeDispatch: this.onBlur },
110-
pointerdown: { beforeDispatch: (event: PointerEvent) => this.onPointerDown(event) },
111-
};
112-
this.disposer = this.editor.attachDomEvent(<Record<string, DOMEventRecord>>eventHandlers);
102+
if (this.isSafari) {
103+
this.disposer = this.editor.attachDomEvent({
104+
focus: { beforeDispatch: this.onFocus },
105+
drop: { beforeDispatch: this.onDrop },
106+
});
107+
} else {
108+
this.disposer = this.editor.attachDomEvent({
109+
focus: { beforeDispatch: this.onFocus },
110+
blur: { beforeDispatch: this.onBlur },
111+
drop: { beforeDispatch: this.onDrop },
112+
});
113+
}
113114
}
114115

115116
dispose() {
@@ -245,10 +246,6 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
245246
},
246247
});
247248
}
248-
249-
if (rawEvent.defaultPrevented) {
250-
this.pointerEvent = null;
251-
}
252249
}
253250

254251
private onMouseMove = (event: Event) => {
@@ -313,27 +310,12 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
313310

314311
private onMouseUp() {
315312
this.detachMouseEvent();
316-
if (this.pointerEvent && this.pointerEvent.pointerType === 'touch') {
317-
requestAnimationFrame(() => {
318-
if (this.editor && this.pointerEvent) {
319-
// Handle touch selection here since the cursor position is updated
320-
// Work-in-progress
321-
}
322-
this.pointerEvent = null;
323-
});
324-
}
325313
}
326314

327315
private onDrop = () => {
328316
this.detachMouseEvent();
329317
};
330318

331-
private onPointerDown = (e: PointerEvent) => {
332-
if (e.pointerType === 'touch' || e.pointerType === 'pen') {
333-
this.pointerEvent = e;
334-
}
335-
};
336-
337319
private onKeyDown(editor: IEditor, rawEvent: KeyboardEvent) {
338320
const key = rawEvent.key;
339321
const selection = editor.getDOMSelection();

packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ export type EditOptions = {
3333
* @returns A boolean
3434
*/
3535
shouldHandleEnterKey?: ((editor: IEditor) => boolean) | boolean;
36+
37+
/**
38+
* Callback or boolean to determine whether the browser (not Content Model) should handle the Backspace key press.
39+
* If the value/callback returns true, Rooster will NOT handle Backspace and will defer to the browser's native behavior.
40+
* @param editor - The editor instance (when using callback).
41+
* @returns A boolean
42+
*/
43+
shouldHandleBackspaceKey?: ((editor: IEditor) => boolean) | boolean;
3644
};
3745

3846
const BACKSPACE_KEY = 8;
@@ -201,7 +209,13 @@ export class EditPlugin implements EditorPlugin {
201209
case 'Backspace':
202210
// Use our API to handle BACKSPACE/DELETE key.
203211
// No need to clear cache here since if we rely on browser's behavior, there will be Input event and its handler will reconcile cache
204-
keyboardDelete(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
212+
if (!this.shouldBrowserHandleBackspace(editor)) {
213+
keyboardDelete(
214+
editor,
215+
rawEvent,
216+
this.options.handleExpandedSelectionOnDelete
217+
);
218+
}
205219
break;
206220

207221
case 'Delete':
@@ -260,15 +274,18 @@ export class EditPlugin implements EditorPlugin {
260274
let handled = false;
261275
switch (rawEvent.inputType) {
262276
case 'deleteContentBackward':
263-
handled = keyboardDelete(
264-
editor,
265-
new KeyboardEvent('keydown', {
266-
key: 'Backspace',
267-
keyCode: BACKSPACE_KEY,
268-
which: BACKSPACE_KEY,
269-
}),
270-
this.options.handleExpandedSelectionOnDelete
271-
);
277+
if (!this.shouldBrowserHandleBackspace(editor)) {
278+
// This logic is Android specific. It's because some Android keyboard doesn't support key and keycode, the value of them is always Unidentified, so we have to manually create a new one.
279+
handled = keyboardDelete(
280+
editor,
281+
new KeyboardEvent('keydown', {
282+
key: 'Backspace',
283+
keyCode: BACKSPACE_KEY,
284+
which: BACKSPACE_KEY,
285+
}),
286+
this.options.handleExpandedSelectionOnDelete
287+
);
288+
}
272289
break;
273290
case 'deleteContentForward':
274291
handled = keyboardDelete(
@@ -291,4 +308,16 @@ export class EditPlugin implements EditorPlugin {
291308
this.selectionAfterDelete = editor.getDOMSelection();
292309
}
293310
}
294-
}
311+
312+
private shouldBrowserHandleBackspace(editor: IEditor): boolean {
313+
const opt = this.options.shouldHandleBackspaceKey;
314+
switch (typeof opt) {
315+
case 'function':
316+
return opt(editor);
317+
case 'boolean':
318+
return opt;
319+
default:
320+
return false;
321+
}
322+
}
323+
}

packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,7 @@ export function keyboardDelete(
4141
let handled = false;
4242
const selection = editor.getDOMSelection();
4343

44-
if (
45-
shouldDeleteWithContentModel(
46-
selection,
47-
rawEvent,
48-
handleExpandedSelection,
49-
editor.getEnvironment().isIOS ?? false
50-
)
51-
) {
44+
if (shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection)) {
5245
editor.formatContentModel(
5346
(model, context) => {
5447
const result = deleteSelection(
@@ -99,8 +92,7 @@ function getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelecti
9992
function shouldDeleteWithContentModel(
10093
selection: DOMSelection | null,
10194
rawEvent: KeyboardEvent,
102-
handleExpandedSelection: boolean,
103-
isIOS: boolean
95+
handleExpandedSelection: boolean
10496
) {
10597
if (!selection) {
10698
return false; // Nothing to delete
@@ -129,20 +121,16 @@ function shouldDeleteWithContentModel(
129121
return !(
130122
isNodeOfType(startContainer, 'TEXT_NODE') &&
131123
!isModifierKey(rawEvent) &&
132-
(canDeleteBefore(rawEvent, startContainer, startOffset, isIOS) ||
124+
(canDeleteBefore(rawEvent, startContainer, startOffset) ||
133125
canDeleteAfter(rawEvent, startContainer, startOffset))
134126
);
135127
}
136128
}
137129

138-
function canDeleteBefore(rawEvent: KeyboardEvent, text: Text, offset: number, isIOS: boolean) {
139-
if (rawEvent.key != 'Backspace') {
130+
function canDeleteBefore(rawEvent: KeyboardEvent, text: Text, offset: number) {
131+
if (rawEvent.key != 'Backspace' || offset <= 1) {
140132
return false;
141133
}
142-
if (offset <= 1) {
143-
// For iOS, allow browser to handle deletion of first character on iOS to preserve auto-capitalization
144-
return offset === 1 && isIOS;
145-
}
146134

147135
const length = text.nodeValue?.length ?? 0;
148136

packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,8 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
597597
this.wrapper,
598598
undefined /* resizers */,
599599
undefined /* croppers */,
600-
isRTL
600+
isRTL,
601+
true /* isRotating */
601602
);
602603
this.updateRotateHandleState(
603604
editor,

0 commit comments

Comments
 (0)