roosterjs-content-model-plugins 9.17.0 → 9.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,11 @@ export declare type EditOptions = {
7
7
  * Whether to handle Tab key in keyboard. @default true
8
8
  */
9
9
  handleTabKey?: boolean;
10
+ /**
11
+ * Whether expanded selection within a text node should be handled by CM when pressing Backspace/Delete key.
12
+ * @default true
13
+ */
14
+ handleExpandedSelectionOnDelete?: boolean;
10
15
  };
11
16
  /**
12
17
  * Edit plugins helps editor to do editing operation on top of content model.
@@ -17,6 +17,7 @@ var DELETE_KEY = 46;
17
17
  var DEAD_KEY = 229;
18
18
  var DefaultOptions = {
19
19
  handleTabKey: true,
20
+ handleExpandedSelectionOnDelete: true,
20
21
  };
21
22
  /**
22
23
  * Edit plugins helps editor to do editing operation on top of content model.
@@ -137,14 +138,14 @@ var EditPlugin = /** @class */ (function () {
137
138
  case 'Backspace':
138
139
  // Use our API to handle BACKSPACE/DELETE key.
139
140
  // 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
140
- (0, keyboardDelete_1.keyboardDelete)(editor, rawEvent);
141
+ (0, keyboardDelete_1.keyboardDelete)(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
141
142
  break;
142
143
  case 'Delete':
143
144
  // Use our API to handle BACKSPACE/DELETE key.
144
145
  // 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
145
146
  // And leave it to browser when shift key is pressed so that browser will trigger cut event
146
147
  if (!event.rawEvent.shiftKey) {
147
- (0, keyboardDelete_1.keyboardDelete)(editor, rawEvent);
148
+ (0, keyboardDelete_1.keyboardDelete)(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
148
149
  }
149
150
  break;
150
151
  case 'Tab':
@@ -186,14 +187,14 @@ var EditPlugin = /** @class */ (function () {
186
187
  key: 'Backspace',
187
188
  keyCode: BACKSPACE_KEY,
188
189
  which: BACKSPACE_KEY,
189
- }));
190
+ }), this.options.handleExpandedSelectionOnDelete);
190
191
  break;
191
192
  case 'deleteContentForward':
192
193
  handled = (0, keyboardDelete_1.keyboardDelete)(editor, new KeyboardEvent('keydown', {
193
194
  key: 'Delete',
194
195
  keyCode: DELETE_KEY,
195
196
  which: DELETE_KEY,
196
- }));
197
+ }), this.options.handleExpandedSelectionOnDelete);
197
198
  break;
198
199
  }
199
200
  if (handled) {
@@ -1 +1 @@
1
- {"version":3,"file":"EditPlugin.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { keyboardDelete } from './keyboardDelete';\nimport { keyboardEnter } from './keyboardEnter';\nimport { keyboardInput } from './keyboardInput';\nimport { keyboardTab } from './keyboardTab';\nimport { parseTableCells } from 'roosterjs-content-model-dom';\nimport type {\n DOMSelection,\n EditorPlugin,\n IEditor,\n KeyDownEvent,\n PluginEvent,\n} from 'roosterjs-content-model-types';\n\n/**\n * Options to customize the keyboard handling behavior of Edit plugin\n */\nexport type EditOptions = {\n /**\n * Whether to handle Tab key in keyboard. @default true\n */\n handleTabKey?: boolean;\n};\n\nconst BACKSPACE_KEY = 8;\nconst DELETE_KEY = 46;\n/**\n * According to https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html\n * 229 can be sent in variants generated when Long press (iOS) or using IM.\n *\n * Other cases: https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229\n */\nconst DEAD_KEY = 229;\n\nconst DefaultOptions: Partial<EditOptions> = {\n handleTabKey: true,\n};\n\n/**\n * Edit plugins helps editor to do editing operation on top of content model.\n * This includes:\n * 1. Delete Key\n * 2. Backspace Key\n * 3. Tab Key\n */\nexport class EditPlugin implements EditorPlugin {\n private editor: IEditor | null = null;\n private disposer: (() => void) | null = null;\n private shouldHandleNextInputEvent = false;\n private selectionAfterDelete: DOMSelection | null = null;\n private handleNormalEnter = false;\n\n /**\n * @param options An optional parameter that takes in an object of type EditOptions, which includes the following properties:\n * handleTabKey: A boolean that enables or disables Tab key handling. Defaults to true.\n */\n constructor(private options: EditOptions = DefaultOptions) {}\n\n /**\n * Get name of this plugin\n */\n getName() {\n return 'Edit';\n }\n\n /**\n * The first method that editor will call to a plugin when editor is initializing.\n * It will pass in the editor instance, plugin should take this chance to save the\n * editor reference so that it can call to any editor method or format API later.\n * @param editor The editor object\n */\n initialize(editor: IEditor) {\n this.editor = editor;\n this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('HandleEnterKey');\n\n if (editor.getEnvironment().isAndroid) {\n this.disposer = this.editor.attachDomEvent({\n beforeinput: {\n beforeDispatch: e => this.handleBeforeInputEvent(editor, e),\n },\n });\n }\n }\n\n /**\n * The last method that editor will call to a plugin before it is disposed.\n * Plugin can take this chance to clear the reference to editor. After this method is\n * called, plugin should not call to any editor method since it will result in error.\n */\n dispose() {\n this.editor = null;\n this.disposer?.();\n this.disposer = null;\n }\n\n /**\n * Core method for a plugin. Once an event happens in editor, editor will call this\n * method of each plugin to handle the event as long as the event is not handled\n * exclusively by another plugin.\n * @param event The event to handle:\n */\n onPluginEvent(event: PluginEvent) {\n if (this.editor) {\n switch (event.eventType) {\n case 'keyDown':\n this.handleKeyDownEvent(this.editor, event);\n break;\n case 'keyUp':\n if (this.selectionAfterDelete) {\n this.editor.setDOMSelection(this.selectionAfterDelete);\n this.selectionAfterDelete = null;\n }\n break;\n }\n }\n }\n\n /**\n * Check if the plugin should handle the given event exclusively.\n * Handle an event exclusively means other plugin will not receive this event in\n * onPluginEvent method.\n * If two plugins will return true in willHandleEventExclusively() for the same event,\n * the final result depends on the order of the plugins are added into editor\n * @param event The event to check:\n */\n willHandleEventExclusively(event: PluginEvent) {\n if (\n this.editor &&\n this.options.handleTabKey &&\n event.eventType == 'keyDown' &&\n event.rawEvent.key == 'Tab' &&\n !event.rawEvent.shiftKey\n ) {\n const selection = this.editor.getDOMSelection();\n const startContainer =\n selection?.type == 'range' && selection.range.collapsed\n ? selection.range.startContainer\n : null;\n const table = startContainer\n ? this.editor.getDOMHelper().findClosestElementAncestor(startContainer, 'table')\n : null;\n const parsedTable = table && parseTableCells(table);\n\n if (parsedTable) {\n const lastRow = parsedTable[parsedTable.length - 1];\n const lastCell = lastRow && lastRow[lastRow.length - 1];\n\n if (typeof lastCell == 'object' && lastCell.contains(startContainer)) {\n // When TAB in the last cell of a table, we will generate new table row, so prevent other plugins handling this event\n // e.g. SelectionPlugin will move the focus out of table, which is conflict with this behavior\n return true;\n }\n }\n }\n\n return false;\n }\n\n private handleKeyDownEvent(editor: IEditor, event: KeyDownEvent) {\n const rawEvent = event.rawEvent;\n const hasCtrlOrMetaKey = rawEvent.ctrlKey || rawEvent.metaKey;\n\n if (!rawEvent.defaultPrevented && !event.handledByEditFeature) {\n switch (rawEvent.key) {\n case 'Backspace':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n keyboardDelete(editor, rawEvent);\n break;\n\n case 'Delete':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n // And leave it to browser when shift key is pressed so that browser will trigger cut event\n if (!event.rawEvent.shiftKey) {\n keyboardDelete(editor, rawEvent);\n }\n break;\n\n case 'Tab':\n if (this.options.handleTabKey && !hasCtrlOrMetaKey) {\n keyboardTab(editor, rawEvent);\n }\n break;\n case 'Unidentified':\n if (editor.getEnvironment().isAndroid) {\n this.shouldHandleNextInputEvent = true;\n }\n break;\n\n case 'Enter':\n if (\n !hasCtrlOrMetaKey &&\n !event.rawEvent.isComposing &&\n event.rawEvent.keyCode !== DEAD_KEY\n ) {\n keyboardEnter(editor, rawEvent, this.handleNormalEnter);\n }\n break;\n\n default:\n keyboardInput(editor, rawEvent);\n break;\n }\n }\n }\n\n private handleBeforeInputEvent(editor: IEditor, rawEvent: Event) {\n // Some Android IMEs doesn't fire correct keydown event for BACKSPACE/DELETE key\n // Here we translate input event to BACKSPACE/DELETE keydown event to be compatible with existing logic\n if (\n !this.shouldHandleNextInputEvent ||\n !(rawEvent instanceof InputEvent) ||\n rawEvent.defaultPrevented\n ) {\n return;\n }\n this.shouldHandleNextInputEvent = false;\n\n let handled = false;\n switch (rawEvent.inputType) {\n case 'deleteContentBackward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Backspace',\n keyCode: BACKSPACE_KEY,\n which: BACKSPACE_KEY,\n })\n );\n break;\n case 'deleteContentForward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Delete',\n keyCode: DELETE_KEY,\n which: DELETE_KEY,\n })\n );\n break;\n }\n\n if (handled) {\n rawEvent.preventDefault();\n\n // Restore the selection on keyup event to avoid the cursor jump issue\n // See: https://issues.chromium.org/issues/330596261\n this.selectionAfterDelete = editor.getDOMSelection();\n }\n }\n}\n"]}
1
+ {"version":3,"file":"EditPlugin.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts"],"names":[],"mappings":";;;AAAA,mDAAkD;AAClD,iDAAgD;AAChD,iDAAgD;AAChD,6CAA4C;AAC5C,2EAA8D;AAyB9D,IAAM,aAAa,GAAG,CAAC,CAAC;AACxB,IAAM,UAAU,GAAG,EAAE,CAAC;AACtB;;;;;GAKG;AACH,IAAM,QAAQ,GAAG,GAAG,CAAC;AAErB,IAAM,cAAc,GAAyB;IACzC,YAAY,EAAE,IAAI;IAClB,+BAA+B,EAAE,IAAI;CACxC,CAAC;AAEF;;;;;;GAMG;AACH;IAOI;;;OAGG;IACH,oBAAoB,OAAqC;QAArC,wBAAA,EAAA,wBAAqC;QAArC,YAAO,GAAP,OAAO,CAA8B;QAVjD,WAAM,GAAmB,IAAI,CAAC;QAC9B,aAAQ,GAAwB,IAAI,CAAC;QACrC,+BAA0B,GAAG,KAAK,CAAC;QACnC,yBAAoB,GAAwB,IAAI,CAAC;QACjD,sBAAiB,GAAG,KAAK,CAAC;IAM0B,CAAC;IAE7D;;OAEG;IACH,4BAAO,GAAP;QACI,OAAO,MAAM,CAAC;IAClB,CAAC;IAED;;;;;OAKG;IACH,+BAAU,GAAV,UAAW,MAAe;QAA1B,iBAWC;QAVG,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,MAAM,CAAC,4BAA4B,CAAC,gBAAgB,CAAC,CAAC;QAEpF,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC,SAAS,EAAE;YACnC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;gBACvC,WAAW,EAAE;oBACT,cAAc,EAAE,UAAA,CAAC,IAAI,OAAA,KAAI,CAAC,sBAAsB,CAAC,MAAM,EAAE,CAAC,CAAC,EAAtC,CAAsC;iBAC9D;aACJ,CAAC,CAAC;SACN;IACL,CAAC;IAED;;;;OAIG;IACH,4BAAO,GAAP;;QACI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,MAAA,IAAI,CAAC,QAAQ,+CAAb,IAAI,CAAa,CAAC;QAClB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACzB,CAAC;IAED;;;;;OAKG;IACH,kCAAa,GAAb,UAAc,KAAkB;QAC5B,IAAI,IAAI,CAAC,MAAM,EAAE;YACb,QAAQ,KAAK,CAAC,SAAS,EAAE;gBACrB,KAAK,SAAS;oBACV,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;oBAC5C,MAAM;gBACV,KAAK,OAAO;oBACR,IAAI,IAAI,CAAC,oBAAoB,EAAE;wBAC3B,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;wBACvD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;qBACpC;oBACD,MAAM;aACb;SACJ;IACL,CAAC;IAED;;;;;;;OAOG;IACH,+CAA0B,GAA1B,UAA2B,KAAkB;QACzC,IACI,IAAI,CAAC,MAAM;YACX,IAAI,CAAC,OAAO,CAAC,YAAY;YACzB,KAAK,CAAC,SAAS,IAAI,SAAS;YAC5B,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI,KAAK;YAC3B,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAC1B;YACE,IAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;YAChD,IAAM,cAAc,GAChB,CAAA,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,IAAI,KAAI,OAAO,IAAI,SAAS,CAAC,KAAK,CAAC,SAAS;gBACnD,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,cAAc;gBAChC,CAAC,CAAC,IAAI,CAAC;YACf,IAAM,KAAK,GAAG,cAAc;gBACxB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,0BAA0B,CAAC,cAAc,EAAE,OAAO,CAAC;gBAChF,CAAC,CAAC,IAAI,CAAC;YACX,IAAM,WAAW,GAAG,KAAK,IAAI,IAAA,6CAAe,EAAC,KAAK,CAAC,CAAC;YAEpD,IAAI,WAAW,EAAE;gBACb,IAAM,OAAO,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBACpD,IAAM,QAAQ,GAAG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAExD,IAAI,OAAO,QAAQ,IAAI,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE;oBAClE,qHAAqH;oBACrH,8FAA8F;oBAC9F,OAAO,IAAI,CAAC;iBACf;aACJ;SACJ;QAED,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,uCAAkB,GAA1B,UAA2B,MAAe,EAAE,KAAmB;QAC3D,IAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAChC,IAAM,gBAAgB,GAAG,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC;QAE9D,IAAI,CAAC,QAAQ,CAAC,gBAAgB,IAAI,CAAC,KAAK,CAAC,oBAAoB,EAAE;YAC3D,QAAQ,QAAQ,CAAC,GAAG,EAAE;gBAClB,KAAK,WAAW;oBACZ,8CAA8C;oBAC9C,qIAAqI;oBACrI,IAAA,+BAAc,EAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;oBAC/E,MAAM;gBAEV,KAAK,QAAQ;oBACT,8CAA8C;oBAC9C,qIAAqI;oBACrI,2FAA2F;oBAC3F,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE;wBAC1B,IAAA,+BAAc,EACV,MAAM,EACN,QAAQ,EACR,IAAI,CAAC,OAAO,CAAC,+BAA+B,CAC/C,CAAC;qBACL;oBACD,MAAM;gBAEV,KAAK,KAAK;oBACN,IAAI,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,CAAC,gBAAgB,EAAE;wBAChD,IAAA,yBAAW,EAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;qBACjC;oBACD,MAAM;gBACV,KAAK,cAAc;oBACf,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC,SAAS,EAAE;wBACnC,IAAI,CAAC,0BAA0B,GAAG,IAAI,CAAC;qBAC1C;oBACD,MAAM;gBAEV,KAAK,OAAO;oBACR,IACI,CAAC,gBAAgB;wBACjB,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW;wBAC3B,KAAK,CAAC,QAAQ,CAAC,OAAO,KAAK,QAAQ,EACrC;wBACE,IAAA,6BAAa,EAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;qBAC3D;oBACD,MAAM;gBAEV;oBACI,IAAA,6BAAa,EAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;oBAChC,MAAM;aACb;SACJ;IACL,CAAC;IAEO,2CAAsB,GAA9B,UAA+B,MAAe,EAAE,QAAe;QAC3D,gFAAgF;QAChF,uGAAuG;QACvG,IACI,CAAC,IAAI,CAAC,0BAA0B;YAChC,CAAC,CAAC,QAAQ,YAAY,UAAU,CAAC;YACjC,QAAQ,CAAC,gBAAgB,EAC3B;YACE,OAAO;SACV;QACD,IAAI,CAAC,0BAA0B,GAAG,KAAK,CAAC;QAExC,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,QAAQ,QAAQ,CAAC,SAAS,EAAE;YACxB,KAAK,uBAAuB;gBACxB,OAAO,GAAG,IAAA,+BAAc,EACpB,MAAM,EACN,IAAI,aAAa,CAAC,SAAS,EAAE;oBACzB,GAAG,EAAE,WAAW;oBAChB,OAAO,EAAE,aAAa;oBACtB,KAAK,EAAE,aAAa;iBACvB,CAAC,EACF,IAAI,CAAC,OAAO,CAAC,+BAA+B,CAC/C,CAAC;gBACF,MAAM;YACV,KAAK,sBAAsB;gBACvB,OAAO,GAAG,IAAA,+BAAc,EACpB,MAAM,EACN,IAAI,aAAa,CAAC,SAAS,EAAE;oBACzB,GAAG,EAAE,QAAQ;oBACb,OAAO,EAAE,UAAU;oBACnB,KAAK,EAAE,UAAU;iBACpB,CAAC,EACF,IAAI,CAAC,OAAO,CAAC,+BAA+B,CAC/C,CAAC;gBACF,MAAM;SACb;QAED,IAAI,OAAO,EAAE;YACT,QAAQ,CAAC,cAAc,EAAE,CAAC;YAE1B,sEAAsE;YACtE,oDAAoD;YACpD,IAAI,CAAC,oBAAoB,GAAG,MAAM,CAAC,eAAe,EAAE,CAAC;SACxD;IACL,CAAC;IACL,iBAAC;AAAD,CAAC,AApND,IAoNC;AApNY,gCAAU","sourcesContent":["import { keyboardDelete } from './keyboardDelete';\nimport { keyboardEnter } from './keyboardEnter';\nimport { keyboardInput } from './keyboardInput';\nimport { keyboardTab } from './keyboardTab';\nimport { parseTableCells } from 'roosterjs-content-model-dom';\nimport type {\n DOMSelection,\n EditorPlugin,\n IEditor,\n KeyDownEvent,\n PluginEvent,\n} from 'roosterjs-content-model-types';\n\n/**\n * Options to customize the keyboard handling behavior of Edit plugin\n */\nexport type EditOptions = {\n /**\n * Whether to handle Tab key in keyboard. @default true\n */\n handleTabKey?: boolean;\n\n /**\n * Whether expanded selection within a text node should be handled by CM when pressing Backspace/Delete key.\n * @default true\n */\n handleExpandedSelectionOnDelete?: boolean;\n};\n\nconst BACKSPACE_KEY = 8;\nconst DELETE_KEY = 46;\n/**\n * According to https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html\n * 229 can be sent in variants generated when Long press (iOS) or using IM.\n *\n * Other cases: https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229\n */\nconst DEAD_KEY = 229;\n\nconst DefaultOptions: Partial<EditOptions> = {\n handleTabKey: true,\n handleExpandedSelectionOnDelete: true,\n};\n\n/**\n * Edit plugins helps editor to do editing operation on top of content model.\n * This includes:\n * 1. Delete Key\n * 2. Backspace Key\n * 3. Tab Key\n */\nexport class EditPlugin implements EditorPlugin {\n private editor: IEditor | null = null;\n private disposer: (() => void) | null = null;\n private shouldHandleNextInputEvent = false;\n private selectionAfterDelete: DOMSelection | null = null;\n private handleNormalEnter = false;\n\n /**\n * @param options An optional parameter that takes in an object of type EditOptions, which includes the following properties:\n * handleTabKey: A boolean that enables or disables Tab key handling. Defaults to true.\n */\n constructor(private options: EditOptions = DefaultOptions) {}\n\n /**\n * Get name of this plugin\n */\n getName() {\n return 'Edit';\n }\n\n /**\n * The first method that editor will call to a plugin when editor is initializing.\n * It will pass in the editor instance, plugin should take this chance to save the\n * editor reference so that it can call to any editor method or format API later.\n * @param editor The editor object\n */\n initialize(editor: IEditor) {\n this.editor = editor;\n this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('HandleEnterKey');\n\n if (editor.getEnvironment().isAndroid) {\n this.disposer = this.editor.attachDomEvent({\n beforeinput: {\n beforeDispatch: e => this.handleBeforeInputEvent(editor, e),\n },\n });\n }\n }\n\n /**\n * The last method that editor will call to a plugin before it is disposed.\n * Plugin can take this chance to clear the reference to editor. After this method is\n * called, plugin should not call to any editor method since it will result in error.\n */\n dispose() {\n this.editor = null;\n this.disposer?.();\n this.disposer = null;\n }\n\n /**\n * Core method for a plugin. Once an event happens in editor, editor will call this\n * method of each plugin to handle the event as long as the event is not handled\n * exclusively by another plugin.\n * @param event The event to handle:\n */\n onPluginEvent(event: PluginEvent) {\n if (this.editor) {\n switch (event.eventType) {\n case 'keyDown':\n this.handleKeyDownEvent(this.editor, event);\n break;\n case 'keyUp':\n if (this.selectionAfterDelete) {\n this.editor.setDOMSelection(this.selectionAfterDelete);\n this.selectionAfterDelete = null;\n }\n break;\n }\n }\n }\n\n /**\n * Check if the plugin should handle the given event exclusively.\n * Handle an event exclusively means other plugin will not receive this event in\n * onPluginEvent method.\n * If two plugins will return true in willHandleEventExclusively() for the same event,\n * the final result depends on the order of the plugins are added into editor\n * @param event The event to check:\n */\n willHandleEventExclusively(event: PluginEvent) {\n if (\n this.editor &&\n this.options.handleTabKey &&\n event.eventType == 'keyDown' &&\n event.rawEvent.key == 'Tab' &&\n !event.rawEvent.shiftKey\n ) {\n const selection = this.editor.getDOMSelection();\n const startContainer =\n selection?.type == 'range' && selection.range.collapsed\n ? selection.range.startContainer\n : null;\n const table = startContainer\n ? this.editor.getDOMHelper().findClosestElementAncestor(startContainer, 'table')\n : null;\n const parsedTable = table && parseTableCells(table);\n\n if (parsedTable) {\n const lastRow = parsedTable[parsedTable.length - 1];\n const lastCell = lastRow && lastRow[lastRow.length - 1];\n\n if (typeof lastCell == 'object' && lastCell.contains(startContainer)) {\n // When TAB in the last cell of a table, we will generate new table row, so prevent other plugins handling this event\n // e.g. SelectionPlugin will move the focus out of table, which is conflict with this behavior\n return true;\n }\n }\n }\n\n return false;\n }\n\n private handleKeyDownEvent(editor: IEditor, event: KeyDownEvent) {\n const rawEvent = event.rawEvent;\n const hasCtrlOrMetaKey = rawEvent.ctrlKey || rawEvent.metaKey;\n\n if (!rawEvent.defaultPrevented && !event.handledByEditFeature) {\n switch (rawEvent.key) {\n case 'Backspace':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n keyboardDelete(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);\n break;\n\n case 'Delete':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n // And leave it to browser when shift key is pressed so that browser will trigger cut event\n if (!event.rawEvent.shiftKey) {\n keyboardDelete(\n editor,\n rawEvent,\n this.options.handleExpandedSelectionOnDelete\n );\n }\n break;\n\n case 'Tab':\n if (this.options.handleTabKey && !hasCtrlOrMetaKey) {\n keyboardTab(editor, rawEvent);\n }\n break;\n case 'Unidentified':\n if (editor.getEnvironment().isAndroid) {\n this.shouldHandleNextInputEvent = true;\n }\n break;\n\n case 'Enter':\n if (\n !hasCtrlOrMetaKey &&\n !event.rawEvent.isComposing &&\n event.rawEvent.keyCode !== DEAD_KEY\n ) {\n keyboardEnter(editor, rawEvent, this.handleNormalEnter);\n }\n break;\n\n default:\n keyboardInput(editor, rawEvent);\n break;\n }\n }\n }\n\n private handleBeforeInputEvent(editor: IEditor, rawEvent: Event) {\n // Some Android IMEs doesn't fire correct keydown event for BACKSPACE/DELETE key\n // Here we translate input event to BACKSPACE/DELETE keydown event to be compatible with existing logic\n if (\n !this.shouldHandleNextInputEvent ||\n !(rawEvent instanceof InputEvent) ||\n rawEvent.defaultPrevented\n ) {\n return;\n }\n this.shouldHandleNextInputEvent = false;\n\n let handled = false;\n switch (rawEvent.inputType) {\n case 'deleteContentBackward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Backspace',\n keyCode: BACKSPACE_KEY,\n which: BACKSPACE_KEY,\n }),\n this.options.handleExpandedSelectionOnDelete\n );\n break;\n case 'deleteContentForward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Delete',\n keyCode: DELETE_KEY,\n which: DELETE_KEY,\n }),\n this.options.handleExpandedSelectionOnDelete\n );\n break;\n }\n\n if (handled) {\n rawEvent.preventDefault();\n\n // Restore the selection on keyup event to avoid the cursor jump issue\n // See: https://issues.chromium.org/issues/330596261\n this.selectionAfterDelete = editor.getDOMSelection();\n }\n }\n}\n"]}
@@ -4,6 +4,7 @@ import type { IEditor } from 'roosterjs-content-model-types';
4
4
  * Do keyboard event handling for DELETE/BACKSPACE key
5
5
  * @param editor The editor object
6
6
  * @param rawEvent DOM keyboard event
7
+ * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM
7
8
  * @returns True if the event is handled by content model, otherwise false
8
9
  */
9
- export declare function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent): boolean;
10
+ export declare function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent, handleExpandedSelection?: boolean): boolean;
@@ -13,12 +13,14 @@ var deleteCollapsedSelection_1 = require("./deleteSteps/deleteCollapsedSelection
13
13
  * Do keyboard event handling for DELETE/BACKSPACE key
14
14
  * @param editor The editor object
15
15
  * @param rawEvent DOM keyboard event
16
+ * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM
16
17
  * @returns True if the event is handled by content model, otherwise false
17
18
  */
18
- function keyboardDelete(editor, rawEvent) {
19
+ function keyboardDelete(editor, rawEvent, handleExpandedSelection) {
20
+ if (handleExpandedSelection === void 0) { handleExpandedSelection = true; }
19
21
  var handled = false;
20
22
  var selection = editor.getDOMSelection();
21
- if (shouldDeleteWithContentModel(selection, rawEvent)) {
23
+ if (shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection)) {
22
24
  editor.formatContentModel(function (model, context) {
23
25
  var result = (0, roosterjs_content_model_dom_1.deleteSelection)(model, getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac), context).deleteResult;
24
26
  handled = (0, handleKeyboardEventCommon_1.handleKeyboardEventResult)(editor, model, rawEvent, result, context);
@@ -54,12 +56,24 @@ function getDeleteSteps(rawEvent, isMac) {
54
56
  deleteQuote,
55
57
  ];
56
58
  }
57
- function shouldDeleteWithContentModel(selection, rawEvent) {
59
+ function shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection) {
60
+ var _a, _b;
58
61
  if (!selection) {
59
62
  return false; // Nothing to delete
60
63
  }
61
- else if (selection.type != 'range' || !selection.range.collapsed) {
62
- return true; // Selection is not collapsed, need to delete all selections
64
+ else if (selection.type != 'range') {
65
+ return true;
66
+ }
67
+ else if (!selection.range.collapsed) {
68
+ if (handleExpandedSelection) {
69
+ return true; // Selection is not collapsed, need to delete all selections
70
+ }
71
+ var range = selection.range;
72
+ var _c = selection.range, startContainer = _c.startContainer, endContainer = _c.endContainer;
73
+ var isInSameTextNode = startContainer === endContainer && (0, roosterjs_content_model_dom_1.isNodeOfType)(startContainer, 'TEXT_NODE');
74
+ return !(isInSameTextNode &&
75
+ !(0, roosterjs_content_model_dom_1.isModifierKey)(rawEvent) &&
76
+ range.endOffset - range.startOffset < ((_b = (_a = startContainer.nodeValue) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0));
63
77
  }
64
78
  else {
65
79
  var range = selection.range;
@@ -1 +1 @@
1
- {"version":3,"file":"keyboardDelete.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts"],"names":[],"mappings":"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","sourcesContent":["import { deleteAllSegmentBefore } from './deleteSteps/deleteAllSegmentBefore';\nimport { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote';\nimport { deleteList } from './deleteSteps/deleteList';\nimport {\n ChangeSource,\n deleteSelection,\n isModifierKey,\n isNodeOfType,\n} from 'roosterjs-content-model-dom';\nimport {\n handleKeyboardEventResult,\n shouldDeleteAllSegmentsBefore,\n shouldDeleteWord,\n} from './handleKeyboardEventCommon';\nimport {\n backwardDeleteWordSelection,\n forwardDeleteWordSelection,\n} from './deleteSteps/deleteWordSelection';\nimport {\n backwardDeleteCollapsedSelection,\n forwardDeleteCollapsedSelection,\n} from './deleteSteps/deleteCollapsedSelection';\nimport type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-content-model-types';\n\n/**\n * @internal\n * Do keyboard event handling for DELETE/BACKSPACE key\n * @param editor The editor object\n * @param rawEvent DOM keyboard event\n * @returns True if the event is handled by content model, otherwise false\n */\nexport function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent) {\n let handled = false;\n const selection = editor.getDOMSelection();\n\n if (shouldDeleteWithContentModel(selection, rawEvent)) {\n editor.formatContentModel(\n (model, context) => {\n const result = deleteSelection(\n model,\n getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac),\n context\n ).deleteResult;\n\n handled = handleKeyboardEventResult(editor, model, rawEvent, result, context);\n return handled;\n },\n {\n rawEvent,\n changeSource: ChangeSource.Keyboard,\n getChangeData: () => rawEvent.which,\n scrollCaretIntoView: true,\n apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey',\n }\n );\n }\n\n return handled;\n}\n\nfunction getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {\n const isForward = rawEvent.key == 'Delete';\n const deleteAllSegmentBeforeStep =\n shouldDeleteAllSegmentsBefore(rawEvent) && !isForward ? deleteAllSegmentBefore : null;\n const deleteWordSelection = shouldDeleteWord(rawEvent, isMac)\n ? isForward\n ? forwardDeleteWordSelection\n : backwardDeleteWordSelection\n : null;\n const deleteCollapsedSelection = isForward\n ? forwardDeleteCollapsedSelection\n : backwardDeleteCollapsedSelection;\n const deleteQuote = !isForward ? deleteEmptyQuote : null;\n return [\n deleteAllSegmentBeforeStep,\n deleteWordSelection,\n isForward ? null : deleteList,\n deleteCollapsedSelection,\n deleteQuote,\n ];\n}\n\nfunction shouldDeleteWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) {\n if (!selection) {\n return false; // Nothing to delete\n } else if (selection.type != 'range' || !selection.range.collapsed) {\n return true; // Selection is not collapsed, need to delete all selections\n } else {\n const range = selection.range;\n\n // When selection is collapsed and is in middle of text node, no need to use Content Model to delete\n return !(\n isNodeOfType(range.startContainer, 'TEXT_NODE') &&\n !isModifierKey(rawEvent) &&\n (canDeleteBefore(rawEvent, range) || canDeleteAfter(rawEvent, range))\n );\n }\n}\n\nfunction canDeleteBefore(rawEvent: KeyboardEvent, range: Range) {\n return rawEvent.key == 'Backspace' && range.startOffset > 1;\n}\n\nfunction canDeleteAfter(rawEvent: KeyboardEvent, range: Range) {\n return (\n rawEvent.key == 'Delete' &&\n range.startOffset < (range.startContainer.nodeValue?.length ?? 0) - 1\n );\n}\n"]}
1
+ {"version":3,"file":"keyboardDelete.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts"],"names":[],"mappings":"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","sourcesContent":["import { deleteAllSegmentBefore } from './deleteSteps/deleteAllSegmentBefore';\nimport { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote';\nimport { deleteList } from './deleteSteps/deleteList';\nimport {\n ChangeSource,\n deleteSelection,\n isModifierKey,\n isNodeOfType,\n} from 'roosterjs-content-model-dom';\nimport {\n handleKeyboardEventResult,\n shouldDeleteAllSegmentsBefore,\n shouldDeleteWord,\n} from './handleKeyboardEventCommon';\nimport {\n backwardDeleteWordSelection,\n forwardDeleteWordSelection,\n} from './deleteSteps/deleteWordSelection';\nimport {\n backwardDeleteCollapsedSelection,\n forwardDeleteCollapsedSelection,\n} from './deleteSteps/deleteCollapsedSelection';\nimport type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-content-model-types';\n\n/**\n * @internal\n * Do keyboard event handling for DELETE/BACKSPACE key\n * @param editor The editor object\n * @param rawEvent DOM keyboard event\n * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM\n * @returns True if the event is handled by content model, otherwise false\n */\nexport function keyboardDelete(\n editor: IEditor,\n rawEvent: KeyboardEvent,\n handleExpandedSelection: boolean = true\n) {\n let handled = false;\n const selection = editor.getDOMSelection();\n\n if (shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection)) {\n editor.formatContentModel(\n (model, context) => {\n const result = deleteSelection(\n model,\n getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac),\n context\n ).deleteResult;\n\n handled = handleKeyboardEventResult(editor, model, rawEvent, result, context);\n return handled;\n },\n {\n rawEvent,\n changeSource: ChangeSource.Keyboard,\n getChangeData: () => rawEvent.which,\n scrollCaretIntoView: true,\n apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey',\n }\n );\n }\n\n return handled;\n}\n\nfunction getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {\n const isForward = rawEvent.key == 'Delete';\n const deleteAllSegmentBeforeStep =\n shouldDeleteAllSegmentsBefore(rawEvent) && !isForward ? deleteAllSegmentBefore : null;\n const deleteWordSelection = shouldDeleteWord(rawEvent, isMac)\n ? isForward\n ? forwardDeleteWordSelection\n : backwardDeleteWordSelection\n : null;\n const deleteCollapsedSelection = isForward\n ? forwardDeleteCollapsedSelection\n : backwardDeleteCollapsedSelection;\n const deleteQuote = !isForward ? deleteEmptyQuote : null;\n return [\n deleteAllSegmentBeforeStep,\n deleteWordSelection,\n isForward ? null : deleteList,\n deleteCollapsedSelection,\n deleteQuote,\n ];\n}\n\nfunction shouldDeleteWithContentModel(\n selection: DOMSelection | null,\n rawEvent: KeyboardEvent,\n handleExpandedSelection: boolean\n) {\n if (!selection) {\n return false; // Nothing to delete\n } else if (selection.type != 'range') {\n return true;\n } else if (!selection.range.collapsed) {\n if (handleExpandedSelection) {\n return true; // Selection is not collapsed, need to delete all selections\n }\n\n const range = selection.range;\n const { startContainer, endContainer } = selection.range;\n const isInSameTextNode =\n startContainer === endContainer && isNodeOfType(startContainer, 'TEXT_NODE');\n return !(\n isInSameTextNode &&\n !isModifierKey(rawEvent) &&\n range.endOffset - range.startOffset < (startContainer.nodeValue?.length ?? 0)\n );\n } else {\n const range = selection.range;\n\n // When selection is collapsed and is in middle of text node, no need to use Content Model to delete\n return !(\n isNodeOfType(range.startContainer, 'TEXT_NODE') &&\n !isModifierKey(rawEvent) &&\n (canDeleteBefore(rawEvent, range) || canDeleteAfter(rawEvent, range))\n );\n }\n}\n\nfunction canDeleteBefore(rawEvent: KeyboardEvent, range: Range) {\n return rawEvent.key == 'Backspace' && range.startOffset > 1;\n}\n\nfunction canDeleteAfter(rawEvent: KeyboardEvent, range: Range) {\n return (\n rawEvent.key == 'Delete' &&\n range.startOffset < (range.startContainer.nodeValue?.length ?? 0) - 1\n );\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { BeforePasteEvent, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';
1
+ import type { BeforePasteEvent, ClipboardData, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';
2
2
  /**
3
3
  * @internal
4
4
  * Convert pasted content from Excel, add borders when source doc doesn't have a border
@@ -10,6 +10,11 @@ export declare function processPastedContentFromExcel(event: BeforePasteEvent, d
10
10
  * Exported only for unit test
11
11
  */
12
12
  export declare const childProcessor: ElementProcessor<ParentNode>;
13
+ /**
14
+ * @internal
15
+ * Exported only for unit test
16
+ */
17
+ export declare function validateExcelFragment(fragment: DocumentFragment, domCreator: DOMCreator, htmlBefore: string, clipboardData: ClipboardData, htmlAfter: string): void;
13
18
  /**
14
19
  * @internal Export for test only
15
20
  * @param html Source html
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.excelHandler = exports.childProcessor = exports.processPastedContentFromExcel = void 0;
3
+ exports.excelHandler = exports.validateExcelFragment = exports.childProcessor = exports.processPastedContentFromExcel = void 0;
4
4
  var tslib_1 = require("tslib");
5
5
  var addParser_1 = require("../utils/addParser");
6
6
  var roosterjs_content_model_dom_1 = require("roosterjs-content-model-dom");
@@ -10,18 +10,15 @@ var LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i;
10
10
  var LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;
11
11
  var LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;
12
12
  var DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';
13
+ var TABLE_SELECTOR = 'table';
13
14
  /**
14
15
  * @internal
15
16
  * Convert pasted content from Excel, add borders when source doc doesn't have a border
16
17
  * @param event The BeforePaste event
17
18
  */
18
19
  function processPastedContentFromExcel(event, domCreator, allowExcelNoBorderTable) {
19
- var fragment = event.fragment, htmlBefore = event.htmlBefore, clipboardData = event.clipboardData;
20
- var html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;
21
- if (html && clipboardData.html != html) {
22
- var doc = domCreator.htmlToDOM(html);
23
- (0, roosterjs_content_model_dom_1.moveChildNodes)(fragment, doc === null || doc === void 0 ? void 0 : doc.body);
24
- }
20
+ var fragment = event.fragment, htmlBefore = event.htmlBefore, htmlAfter = event.htmlAfter, clipboardData = event.clipboardData;
21
+ validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);
25
22
  // For Excel Online
26
23
  var firstChild = fragment.firstChild;
27
24
  if ((0, roosterjs_content_model_dom_1.isNodeOfType)(firstChild, 'ELEMENT_NODE') &&
@@ -70,22 +67,59 @@ var childProcessor = function (group, element, context) {
70
67
  }
71
68
  };
72
69
  exports.childProcessor = childProcessor;
70
+ /**
71
+ * @internal
72
+ * Exported only for unit test
73
+ */
74
+ function validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter) {
75
+ // Clipboard content of Excel may contain the <StartFragment> and EndFragment comment tags inside the table
76
+ //
77
+ // @example
78
+ // <table>
79
+ // <!--StartFragment-->
80
+ // <tr>...</tr>
81
+ // <!--EndFragment-->
82
+ // </table>
83
+ //
84
+ // This causes that the fragment is not properly created and the table is not extracted.
85
+ // The content that is before the StartFragment is htmlBefore and the content that is after the EndFragment is htmlAfter.
86
+ // So attempt to create a new document fragment with the content of htmlBefore + clipboardData.html + htmlAfter
87
+ // If a table is found, replace the fragment with the new fragment
88
+ var result = !fragment.querySelector(TABLE_SELECTOR) &&
89
+ domCreator.htmlToDOM(htmlBefore + clipboardData.html + htmlAfter);
90
+ if (result && result.querySelector(TABLE_SELECTOR)) {
91
+ (0, roosterjs_content_model_dom_1.moveChildNodes)(fragment, result === null || result === void 0 ? void 0 : result.body);
92
+ }
93
+ else {
94
+ // If the table is still not found, try to extract the table from the clipboard data using Regex
95
+ var html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;
96
+ if (html && clipboardData.html != html) {
97
+ var doc = domCreator.htmlToDOM(html);
98
+ (0, roosterjs_content_model_dom_1.moveChildNodes)(fragment, doc === null || doc === void 0 ? void 0 : doc.body);
99
+ }
100
+ }
101
+ }
102
+ exports.validateExcelFragment = validateExcelFragment;
73
103
  /**
74
104
  * @internal Export for test only
75
105
  * @param html Source html
76
106
  */
77
107
  function excelHandler(html, htmlBefore) {
78
- if (html.match(LAST_TD_END_REGEX)) {
79
- var trMatch = htmlBefore.match(LAST_TR_REGEX);
80
- var tr = trMatch ? trMatch[0] : '<TR>';
81
- html = tr + html + '</TR>';
108
+ try {
109
+ if (html.match(LAST_TD_END_REGEX)) {
110
+ var trMatch = htmlBefore.match(LAST_TR_REGEX);
111
+ var tr = trMatch ? trMatch[0] : '<TR>';
112
+ html = tr + html + '</TR>';
113
+ }
114
+ if (html.match(LAST_TR_END_REGEX)) {
115
+ var tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
116
+ var table = tableMatch ? tableMatch[0] : '<TABLE>';
117
+ html = table + html + '</TABLE>';
118
+ }
82
119
  }
83
- if (html.match(LAST_TR_END_REGEX)) {
84
- var tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
85
- var table = tableMatch ? tableMatch[0] : '<TABLE>';
86
- html = table + html + '</TABLE>';
120
+ finally {
121
+ return html;
87
122
  }
88
- return html;
89
123
  }
90
124
  exports.excelHandler = excelHandler;
91
125
  //# sourceMappingURL=processPastedContentFromExcel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"processPastedContentFromExcel.js","sourceRoot":"","sources":["../../../../../packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts"],"names":[],"mappings":"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","sourcesContent":["import { addParser } from '../utils/addParser';\nimport { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';\nimport { setProcessor } from '../utils/setProcessor';\nimport type { BeforePasteEvent, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';\n\nconst LAST_TD_END_REGEX = /<\\/\\s*td\\s*>((?!<\\/\\s*tr\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_END_REGEX = /<\\/\\s*tr\\s*>((?!<\\/\\s*table\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;\nconst LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;\nconst DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';\n\n/**\n * @internal\n * Convert pasted content from Excel, add borders when source doc doesn't have a border\n * @param event The BeforePaste event\n */\n\nexport function processPastedContentFromExcel(\n event: BeforePasteEvent,\n domCreator: DOMCreator,\n allowExcelNoBorderTable?: boolean\n) {\n const { fragment, htmlBefore, clipboardData } = event;\n const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;\n\n if (html && clipboardData.html != html) {\n const doc = domCreator.htmlToDOM(html);\n moveChildNodes(fragment, doc?.body);\n }\n\n // For Excel Online\n const firstChild = fragment.firstChild;\n if (\n isNodeOfType(firstChild, 'ELEMENT_NODE') &&\n firstChild.tagName == 'div' &&\n firstChild.firstChild\n ) {\n const tableFound = Array.from(firstChild.childNodes).every((child: Node) => {\n // Tables pasted from Excel Online should be of the format: 0 to N META tags and 1 TABLE tag\n const tagName = isNodeOfType(child, 'ELEMENT_NODE') && child.tagName;\n\n return tagName == 'META'\n ? true\n : tagName == 'TABLE'\n ? child == firstChild.lastChild\n : false;\n });\n\n // Extract Table from Div\n if (tableFound && firstChild.lastChild) {\n event.fragment.replaceChildren(firstChild.lastChild);\n }\n }\n\n addParser(event.domToModelOption, 'tableCell', (format, element) => {\n if (!allowExcelNoBorderTable && element.style.borderStyle === 'none') {\n format.borderBottom = DEFAULT_BORDER_STYLE;\n format.borderLeft = DEFAULT_BORDER_STYLE;\n format.borderRight = DEFAULT_BORDER_STYLE;\n format.borderTop = DEFAULT_BORDER_STYLE;\n }\n });\n\n setProcessor(event.domToModelOption, 'child', childProcessor);\n}\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {\n const segmentFormat = { ...context.segmentFormat };\n if (\n group.blockGroupType === 'TableCell' &&\n group.format.textColor &&\n !context.segmentFormat.textColor\n ) {\n context.segmentFormat.textColor = group.format.textColor;\n }\n\n context.defaultElementProcessors.child(group, element, context);\n\n if (group.blockGroupType === 'TableCell' && group.format.textColor) {\n context.segmentFormat = segmentFormat;\n delete group.format.textColor;\n }\n};\n\n/**\n * @internal Export for test only\n * @param html Source html\n */\n\nexport function excelHandler(html: string, htmlBefore: string): string {\n if (html.match(LAST_TD_END_REGEX)) {\n const trMatch = htmlBefore.match(LAST_TR_REGEX);\n const tr = trMatch ? trMatch[0] : '<TR>';\n html = tr + html + '</TR>';\n }\n if (html.match(LAST_TR_END_REGEX)) {\n const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);\n const table = tableMatch ? tableMatch[0] : '<TABLE>';\n html = table + html + '</TABLE>';\n }\n\n return html;\n}\n"]}
1
+ {"version":3,"file":"processPastedContentFromExcel.js","sourceRoot":"","sources":["../../../../../packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts"],"names":[],"mappings":"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","sourcesContent":["import { addParser } from '../utils/addParser';\nimport { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';\nimport { setProcessor } from '../utils/setProcessor';\nimport type {\n BeforePasteEvent,\n ClipboardData,\n DOMCreator,\n ElementProcessor,\n} from 'roosterjs-content-model-types';\n\nconst LAST_TD_END_REGEX = /<\\/\\s*td\\s*>((?!<\\/\\s*tr\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_END_REGEX = /<\\/\\s*tr\\s*>((?!<\\/\\s*table\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;\nconst LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;\nconst DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';\nconst TABLE_SELECTOR = 'table';\n\n/**\n * @internal\n * Convert pasted content from Excel, add borders when source doc doesn't have a border\n * @param event The BeforePaste event\n */\n\nexport function processPastedContentFromExcel(\n event: BeforePasteEvent,\n domCreator: DOMCreator,\n allowExcelNoBorderTable?: boolean\n) {\n const { fragment, htmlBefore, htmlAfter, clipboardData } = event;\n\n validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);\n\n // For Excel Online\n const firstChild = fragment.firstChild;\n if (\n isNodeOfType(firstChild, 'ELEMENT_NODE') &&\n firstChild.tagName == 'div' &&\n firstChild.firstChild\n ) {\n const tableFound = Array.from(firstChild.childNodes).every((child: Node) => {\n // Tables pasted from Excel Online should be of the format: 0 to N META tags and 1 TABLE tag\n const tagName = isNodeOfType(child, 'ELEMENT_NODE') && child.tagName;\n\n return tagName == 'META'\n ? true\n : tagName == 'TABLE'\n ? child == firstChild.lastChild\n : false;\n });\n\n // Extract Table from Div\n if (tableFound && firstChild.lastChild) {\n event.fragment.replaceChildren(firstChild.lastChild);\n }\n }\n\n addParser(event.domToModelOption, 'tableCell', (format, element) => {\n if (!allowExcelNoBorderTable && element.style.borderStyle === 'none') {\n format.borderBottom = DEFAULT_BORDER_STYLE;\n format.borderLeft = DEFAULT_BORDER_STYLE;\n format.borderRight = DEFAULT_BORDER_STYLE;\n format.borderTop = DEFAULT_BORDER_STYLE;\n }\n });\n\n setProcessor(event.domToModelOption, 'child', childProcessor);\n}\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {\n const segmentFormat = { ...context.segmentFormat };\n if (\n group.blockGroupType === 'TableCell' &&\n group.format.textColor &&\n !context.segmentFormat.textColor\n ) {\n context.segmentFormat.textColor = group.format.textColor;\n }\n\n context.defaultElementProcessors.child(group, element, context);\n\n if (group.blockGroupType === 'TableCell' && group.format.textColor) {\n context.segmentFormat = segmentFormat;\n delete group.format.textColor;\n }\n};\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport function validateExcelFragment(\n fragment: DocumentFragment,\n domCreator: DOMCreator,\n htmlBefore: string,\n clipboardData: ClipboardData,\n htmlAfter: string\n) {\n // Clipboard content of Excel may contain the <StartFragment> and EndFragment comment tags inside the table\n //\n // @example\n // <table>\n // <!--StartFragment-->\n // <tr>...</tr>\n // <!--EndFragment-->\n // </table>\n //\n // This causes that the fragment is not properly created and the table is not extracted.\n // The content that is before the StartFragment is htmlBefore and the content that is after the EndFragment is htmlAfter.\n // So attempt to create a new document fragment with the content of htmlBefore + clipboardData.html + htmlAfter\n // If a table is found, replace the fragment with the new fragment\n const result =\n !fragment.querySelector(TABLE_SELECTOR) &&\n domCreator.htmlToDOM(htmlBefore + clipboardData.html + htmlAfter);\n if (result && result.querySelector(TABLE_SELECTOR)) {\n moveChildNodes(fragment, result?.body);\n } else {\n // If the table is still not found, try to extract the table from the clipboard data using Regex\n const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;\n\n if (html && clipboardData.html != html) {\n const doc = domCreator.htmlToDOM(html);\n moveChildNodes(fragment, doc?.body);\n }\n }\n}\n\n/**\n * @internal Export for test only\n * @param html Source html\n */\nexport function excelHandler(html: string, htmlBefore: string): string {\n try {\n if (html.match(LAST_TD_END_REGEX)) {\n const trMatch = htmlBefore.match(LAST_TR_REGEX);\n const tr = trMatch ? trMatch[0] : '<TR>';\n html = tr + html + '</TR>';\n }\n if (html.match(LAST_TR_END_REGEX)) {\n const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);\n const table = tableMatch ? tableMatch[0] : '<TABLE>';\n html = table + html + '</TABLE>';\n }\n } finally {\n return html;\n }\n}\n"]}
@@ -7,6 +7,11 @@ export declare type EditOptions = {
7
7
  * Whether to handle Tab key in keyboard. @default true
8
8
  */
9
9
  handleTabKey?: boolean;
10
+ /**
11
+ * Whether expanded selection within a text node should be handled by CM when pressing Backspace/Delete key.
12
+ * @default true
13
+ */
14
+ handleExpandedSelectionOnDelete?: boolean;
10
15
  };
11
16
  /**
12
17
  * Edit plugins helps editor to do editing operation on top of content model.
@@ -13,6 +13,7 @@ define(["require", "exports", "./keyboardDelete", "./keyboardEnter", "./keyboard
13
13
  var DEAD_KEY = 229;
14
14
  var DefaultOptions = {
15
15
  handleTabKey: true,
16
+ handleExpandedSelectionOnDelete: true,
16
17
  };
17
18
  /**
18
19
  * Edit plugins helps editor to do editing operation on top of content model.
@@ -133,14 +134,14 @@ define(["require", "exports", "./keyboardDelete", "./keyboardEnter", "./keyboard
133
134
  case 'Backspace':
134
135
  // Use our API to handle BACKSPACE/DELETE key.
135
136
  // 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
136
- (0, keyboardDelete_1.keyboardDelete)(editor, rawEvent);
137
+ (0, keyboardDelete_1.keyboardDelete)(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
137
138
  break;
138
139
  case 'Delete':
139
140
  // Use our API to handle BACKSPACE/DELETE key.
140
141
  // 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
141
142
  // And leave it to browser when shift key is pressed so that browser will trigger cut event
142
143
  if (!event.rawEvent.shiftKey) {
143
- (0, keyboardDelete_1.keyboardDelete)(editor, rawEvent);
144
+ (0, keyboardDelete_1.keyboardDelete)(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
144
145
  }
145
146
  break;
146
147
  case 'Tab':
@@ -182,14 +183,14 @@ define(["require", "exports", "./keyboardDelete", "./keyboardEnter", "./keyboard
182
183
  key: 'Backspace',
183
184
  keyCode: BACKSPACE_KEY,
184
185
  which: BACKSPACE_KEY,
185
- }));
186
+ }), this.options.handleExpandedSelectionOnDelete);
186
187
  break;
187
188
  case 'deleteContentForward':
188
189
  handled = (0, keyboardDelete_1.keyboardDelete)(editor, new KeyboardEvent('keydown', {
189
190
  key: 'Delete',
190
191
  keyCode: DELETE_KEY,
191
192
  which: DELETE_KEY,
192
- }));
193
+ }), this.options.handleExpandedSelectionOnDelete);
193
194
  break;
194
195
  }
195
196
  if (handled) {
@@ -1 +1 @@
1
- {"version":3,"file":"EditPlugin.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { keyboardDelete } from './keyboardDelete';\nimport { keyboardEnter } from './keyboardEnter';\nimport { keyboardInput } from './keyboardInput';\nimport { keyboardTab } from './keyboardTab';\nimport { parseTableCells } from 'roosterjs-content-model-dom';\nimport type {\n DOMSelection,\n EditorPlugin,\n IEditor,\n KeyDownEvent,\n PluginEvent,\n} from 'roosterjs-content-model-types';\n\n/**\n * Options to customize the keyboard handling behavior of Edit plugin\n */\nexport type EditOptions = {\n /**\n * Whether to handle Tab key in keyboard. @default true\n */\n handleTabKey?: boolean;\n};\n\nconst BACKSPACE_KEY = 8;\nconst DELETE_KEY = 46;\n/**\n * According to https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html\n * 229 can be sent in variants generated when Long press (iOS) or using IM.\n *\n * Other cases: https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229\n */\nconst DEAD_KEY = 229;\n\nconst DefaultOptions: Partial<EditOptions> = {\n handleTabKey: true,\n};\n\n/**\n * Edit plugins helps editor to do editing operation on top of content model.\n * This includes:\n * 1. Delete Key\n * 2. Backspace Key\n * 3. Tab Key\n */\nexport class EditPlugin implements EditorPlugin {\n private editor: IEditor | null = null;\n private disposer: (() => void) | null = null;\n private shouldHandleNextInputEvent = false;\n private selectionAfterDelete: DOMSelection | null = null;\n private handleNormalEnter = false;\n\n /**\n * @param options An optional parameter that takes in an object of type EditOptions, which includes the following properties:\n * handleTabKey: A boolean that enables or disables Tab key handling. Defaults to true.\n */\n constructor(private options: EditOptions = DefaultOptions) {}\n\n /**\n * Get name of this plugin\n */\n getName() {\n return 'Edit';\n }\n\n /**\n * The first method that editor will call to a plugin when editor is initializing.\n * It will pass in the editor instance, plugin should take this chance to save the\n * editor reference so that it can call to any editor method or format API later.\n * @param editor The editor object\n */\n initialize(editor: IEditor) {\n this.editor = editor;\n this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('HandleEnterKey');\n\n if (editor.getEnvironment().isAndroid) {\n this.disposer = this.editor.attachDomEvent({\n beforeinput: {\n beforeDispatch: e => this.handleBeforeInputEvent(editor, e),\n },\n });\n }\n }\n\n /**\n * The last method that editor will call to a plugin before it is disposed.\n * Plugin can take this chance to clear the reference to editor. After this method is\n * called, plugin should not call to any editor method since it will result in error.\n */\n dispose() {\n this.editor = null;\n this.disposer?.();\n this.disposer = null;\n }\n\n /**\n * Core method for a plugin. Once an event happens in editor, editor will call this\n * method of each plugin to handle the event as long as the event is not handled\n * exclusively by another plugin.\n * @param event The event to handle:\n */\n onPluginEvent(event: PluginEvent) {\n if (this.editor) {\n switch (event.eventType) {\n case 'keyDown':\n this.handleKeyDownEvent(this.editor, event);\n break;\n case 'keyUp':\n if (this.selectionAfterDelete) {\n this.editor.setDOMSelection(this.selectionAfterDelete);\n this.selectionAfterDelete = null;\n }\n break;\n }\n }\n }\n\n /**\n * Check if the plugin should handle the given event exclusively.\n * Handle an event exclusively means other plugin will not receive this event in\n * onPluginEvent method.\n * If two plugins will return true in willHandleEventExclusively() for the same event,\n * the final result depends on the order of the plugins are added into editor\n * @param event The event to check:\n */\n willHandleEventExclusively(event: PluginEvent) {\n if (\n this.editor &&\n this.options.handleTabKey &&\n event.eventType == 'keyDown' &&\n event.rawEvent.key == 'Tab' &&\n !event.rawEvent.shiftKey\n ) {\n const selection = this.editor.getDOMSelection();\n const startContainer =\n selection?.type == 'range' && selection.range.collapsed\n ? selection.range.startContainer\n : null;\n const table = startContainer\n ? this.editor.getDOMHelper().findClosestElementAncestor(startContainer, 'table')\n : null;\n const parsedTable = table && parseTableCells(table);\n\n if (parsedTable) {\n const lastRow = parsedTable[parsedTable.length - 1];\n const lastCell = lastRow && lastRow[lastRow.length - 1];\n\n if (typeof lastCell == 'object' && lastCell.contains(startContainer)) {\n // When TAB in the last cell of a table, we will generate new table row, so prevent other plugins handling this event\n // e.g. SelectionPlugin will move the focus out of table, which is conflict with this behavior\n return true;\n }\n }\n }\n\n return false;\n }\n\n private handleKeyDownEvent(editor: IEditor, event: KeyDownEvent) {\n const rawEvent = event.rawEvent;\n const hasCtrlOrMetaKey = rawEvent.ctrlKey || rawEvent.metaKey;\n\n if (!rawEvent.defaultPrevented && !event.handledByEditFeature) {\n switch (rawEvent.key) {\n case 'Backspace':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n keyboardDelete(editor, rawEvent);\n break;\n\n case 'Delete':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n // And leave it to browser when shift key is pressed so that browser will trigger cut event\n if (!event.rawEvent.shiftKey) {\n keyboardDelete(editor, rawEvent);\n }\n break;\n\n case 'Tab':\n if (this.options.handleTabKey && !hasCtrlOrMetaKey) {\n keyboardTab(editor, rawEvent);\n }\n break;\n case 'Unidentified':\n if (editor.getEnvironment().isAndroid) {\n this.shouldHandleNextInputEvent = true;\n }\n break;\n\n case 'Enter':\n if (\n !hasCtrlOrMetaKey &&\n !event.rawEvent.isComposing &&\n event.rawEvent.keyCode !== DEAD_KEY\n ) {\n keyboardEnter(editor, rawEvent, this.handleNormalEnter);\n }\n break;\n\n default:\n keyboardInput(editor, rawEvent);\n break;\n }\n }\n }\n\n private handleBeforeInputEvent(editor: IEditor, rawEvent: Event) {\n // Some Android IMEs doesn't fire correct keydown event for BACKSPACE/DELETE key\n // Here we translate input event to BACKSPACE/DELETE keydown event to be compatible with existing logic\n if (\n !this.shouldHandleNextInputEvent ||\n !(rawEvent instanceof InputEvent) ||\n rawEvent.defaultPrevented\n ) {\n return;\n }\n this.shouldHandleNextInputEvent = false;\n\n let handled = false;\n switch (rawEvent.inputType) {\n case 'deleteContentBackward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Backspace',\n keyCode: BACKSPACE_KEY,\n which: BACKSPACE_KEY,\n })\n );\n break;\n case 'deleteContentForward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Delete',\n keyCode: DELETE_KEY,\n which: DELETE_KEY,\n })\n );\n break;\n }\n\n if (handled) {\n rawEvent.preventDefault();\n\n // Restore the selection on keyup event to avoid the cursor jump issue\n // See: https://issues.chromium.org/issues/330596261\n this.selectionAfterDelete = editor.getDOMSelection();\n }\n }\n}\n"]}
1
+ {"version":3,"file":"EditPlugin.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { keyboardDelete } from './keyboardDelete';\nimport { keyboardEnter } from './keyboardEnter';\nimport { keyboardInput } from './keyboardInput';\nimport { keyboardTab } from './keyboardTab';\nimport { parseTableCells } from 'roosterjs-content-model-dom';\nimport type {\n DOMSelection,\n EditorPlugin,\n IEditor,\n KeyDownEvent,\n PluginEvent,\n} from 'roosterjs-content-model-types';\n\n/**\n * Options to customize the keyboard handling behavior of Edit plugin\n */\nexport type EditOptions = {\n /**\n * Whether to handle Tab key in keyboard. @default true\n */\n handleTabKey?: boolean;\n\n /**\n * Whether expanded selection within a text node should be handled by CM when pressing Backspace/Delete key.\n * @default true\n */\n handleExpandedSelectionOnDelete?: boolean;\n};\n\nconst BACKSPACE_KEY = 8;\nconst DELETE_KEY = 46;\n/**\n * According to https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html\n * 229 can be sent in variants generated when Long press (iOS) or using IM.\n *\n * Other cases: https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229\n */\nconst DEAD_KEY = 229;\n\nconst DefaultOptions: Partial<EditOptions> = {\n handleTabKey: true,\n handleExpandedSelectionOnDelete: true,\n};\n\n/**\n * Edit plugins helps editor to do editing operation on top of content model.\n * This includes:\n * 1. Delete Key\n * 2. Backspace Key\n * 3. Tab Key\n */\nexport class EditPlugin implements EditorPlugin {\n private editor: IEditor | null = null;\n private disposer: (() => void) | null = null;\n private shouldHandleNextInputEvent = false;\n private selectionAfterDelete: DOMSelection | null = null;\n private handleNormalEnter = false;\n\n /**\n * @param options An optional parameter that takes in an object of type EditOptions, which includes the following properties:\n * handleTabKey: A boolean that enables or disables Tab key handling. Defaults to true.\n */\n constructor(private options: EditOptions = DefaultOptions) {}\n\n /**\n * Get name of this plugin\n */\n getName() {\n return 'Edit';\n }\n\n /**\n * The first method that editor will call to a plugin when editor is initializing.\n * It will pass in the editor instance, plugin should take this chance to save the\n * editor reference so that it can call to any editor method or format API later.\n * @param editor The editor object\n */\n initialize(editor: IEditor) {\n this.editor = editor;\n this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('HandleEnterKey');\n\n if (editor.getEnvironment().isAndroid) {\n this.disposer = this.editor.attachDomEvent({\n beforeinput: {\n beforeDispatch: e => this.handleBeforeInputEvent(editor, e),\n },\n });\n }\n }\n\n /**\n * The last method that editor will call to a plugin before it is disposed.\n * Plugin can take this chance to clear the reference to editor. After this method is\n * called, plugin should not call to any editor method since it will result in error.\n */\n dispose() {\n this.editor = null;\n this.disposer?.();\n this.disposer = null;\n }\n\n /**\n * Core method for a plugin. Once an event happens in editor, editor will call this\n * method of each plugin to handle the event as long as the event is not handled\n * exclusively by another plugin.\n * @param event The event to handle:\n */\n onPluginEvent(event: PluginEvent) {\n if (this.editor) {\n switch (event.eventType) {\n case 'keyDown':\n this.handleKeyDownEvent(this.editor, event);\n break;\n case 'keyUp':\n if (this.selectionAfterDelete) {\n this.editor.setDOMSelection(this.selectionAfterDelete);\n this.selectionAfterDelete = null;\n }\n break;\n }\n }\n }\n\n /**\n * Check if the plugin should handle the given event exclusively.\n * Handle an event exclusively means other plugin will not receive this event in\n * onPluginEvent method.\n * If two plugins will return true in willHandleEventExclusively() for the same event,\n * the final result depends on the order of the plugins are added into editor\n * @param event The event to check:\n */\n willHandleEventExclusively(event: PluginEvent) {\n if (\n this.editor &&\n this.options.handleTabKey &&\n event.eventType == 'keyDown' &&\n event.rawEvent.key == 'Tab' &&\n !event.rawEvent.shiftKey\n ) {\n const selection = this.editor.getDOMSelection();\n const startContainer =\n selection?.type == 'range' && selection.range.collapsed\n ? selection.range.startContainer\n : null;\n const table = startContainer\n ? this.editor.getDOMHelper().findClosestElementAncestor(startContainer, 'table')\n : null;\n const parsedTable = table && parseTableCells(table);\n\n if (parsedTable) {\n const lastRow = parsedTable[parsedTable.length - 1];\n const lastCell = lastRow && lastRow[lastRow.length - 1];\n\n if (typeof lastCell == 'object' && lastCell.contains(startContainer)) {\n // When TAB in the last cell of a table, we will generate new table row, so prevent other plugins handling this event\n // e.g. SelectionPlugin will move the focus out of table, which is conflict with this behavior\n return true;\n }\n }\n }\n\n return false;\n }\n\n private handleKeyDownEvent(editor: IEditor, event: KeyDownEvent) {\n const rawEvent = event.rawEvent;\n const hasCtrlOrMetaKey = rawEvent.ctrlKey || rawEvent.metaKey;\n\n if (!rawEvent.defaultPrevented && !event.handledByEditFeature) {\n switch (rawEvent.key) {\n case 'Backspace':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n keyboardDelete(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);\n break;\n\n case 'Delete':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n // And leave it to browser when shift key is pressed so that browser will trigger cut event\n if (!event.rawEvent.shiftKey) {\n keyboardDelete(\n editor,\n rawEvent,\n this.options.handleExpandedSelectionOnDelete\n );\n }\n break;\n\n case 'Tab':\n if (this.options.handleTabKey && !hasCtrlOrMetaKey) {\n keyboardTab(editor, rawEvent);\n }\n break;\n case 'Unidentified':\n if (editor.getEnvironment().isAndroid) {\n this.shouldHandleNextInputEvent = true;\n }\n break;\n\n case 'Enter':\n if (\n !hasCtrlOrMetaKey &&\n !event.rawEvent.isComposing &&\n event.rawEvent.keyCode !== DEAD_KEY\n ) {\n keyboardEnter(editor, rawEvent, this.handleNormalEnter);\n }\n break;\n\n default:\n keyboardInput(editor, rawEvent);\n break;\n }\n }\n }\n\n private handleBeforeInputEvent(editor: IEditor, rawEvent: Event) {\n // Some Android IMEs doesn't fire correct keydown event for BACKSPACE/DELETE key\n // Here we translate input event to BACKSPACE/DELETE keydown event to be compatible with existing logic\n if (\n !this.shouldHandleNextInputEvent ||\n !(rawEvent instanceof InputEvent) ||\n rawEvent.defaultPrevented\n ) {\n return;\n }\n this.shouldHandleNextInputEvent = false;\n\n let handled = false;\n switch (rawEvent.inputType) {\n case 'deleteContentBackward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Backspace',\n keyCode: BACKSPACE_KEY,\n which: BACKSPACE_KEY,\n }),\n this.options.handleExpandedSelectionOnDelete\n );\n break;\n case 'deleteContentForward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Delete',\n keyCode: DELETE_KEY,\n which: DELETE_KEY,\n }),\n this.options.handleExpandedSelectionOnDelete\n );\n break;\n }\n\n if (handled) {\n rawEvent.preventDefault();\n\n // Restore the selection on keyup event to avoid the cursor jump issue\n // See: https://issues.chromium.org/issues/330596261\n this.selectionAfterDelete = editor.getDOMSelection();\n }\n }\n}\n"]}
@@ -4,6 +4,7 @@ import type { IEditor } from 'roosterjs-content-model-types';
4
4
  * Do keyboard event handling for DELETE/BACKSPACE key
5
5
  * @param editor The editor object
6
6
  * @param rawEvent DOM keyboard event
7
+ * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM
7
8
  * @returns True if the event is handled by content model, otherwise false
8
9
  */
9
- export declare function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent): boolean;
10
+ export declare function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent, handleExpandedSelection?: boolean): boolean;
@@ -7,12 +7,14 @@ define(["require", "exports", "./deleteSteps/deleteAllSegmentBefore", "./deleteS
7
7
  * Do keyboard event handling for DELETE/BACKSPACE key
8
8
  * @param editor The editor object
9
9
  * @param rawEvent DOM keyboard event
10
+ * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM
10
11
  * @returns True if the event is handled by content model, otherwise false
11
12
  */
12
- function keyboardDelete(editor, rawEvent) {
13
+ function keyboardDelete(editor, rawEvent, handleExpandedSelection) {
14
+ if (handleExpandedSelection === void 0) { handleExpandedSelection = true; }
13
15
  var handled = false;
14
16
  var selection = editor.getDOMSelection();
15
- if (shouldDeleteWithContentModel(selection, rawEvent)) {
17
+ if (shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection)) {
16
18
  editor.formatContentModel(function (model, context) {
17
19
  var result = (0, roosterjs_content_model_dom_1.deleteSelection)(model, getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac), context).deleteResult;
18
20
  handled = (0, handleKeyboardEventCommon_1.handleKeyboardEventResult)(editor, model, rawEvent, result, context);
@@ -48,12 +50,24 @@ define(["require", "exports", "./deleteSteps/deleteAllSegmentBefore", "./deleteS
48
50
  deleteQuote,
49
51
  ];
50
52
  }
51
- function shouldDeleteWithContentModel(selection, rawEvent) {
53
+ function shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection) {
54
+ var _a, _b;
52
55
  if (!selection) {
53
56
  return false; // Nothing to delete
54
57
  }
55
- else if (selection.type != 'range' || !selection.range.collapsed) {
56
- return true; // Selection is not collapsed, need to delete all selections
58
+ else if (selection.type != 'range') {
59
+ return true;
60
+ }
61
+ else if (!selection.range.collapsed) {
62
+ if (handleExpandedSelection) {
63
+ return true; // Selection is not collapsed, need to delete all selections
64
+ }
65
+ var range = selection.range;
66
+ var _c = selection.range, startContainer = _c.startContainer, endContainer = _c.endContainer;
67
+ var isInSameTextNode = startContainer === endContainer && (0, roosterjs_content_model_dom_1.isNodeOfType)(startContainer, 'TEXT_NODE');
68
+ return !(isInSameTextNode &&
69
+ !(0, roosterjs_content_model_dom_1.isModifierKey)(rawEvent) &&
70
+ range.endOffset - range.startOffset < ((_b = (_a = startContainer.nodeValue) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0));
57
71
  }
58
72
  else {
59
73
  var range = selection.range;
@@ -1 +1 @@
1
- {"version":3,"file":"keyboardDelete.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts"],"names":[],"mappings":"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","sourcesContent":["import { deleteAllSegmentBefore } from './deleteSteps/deleteAllSegmentBefore';\nimport { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote';\nimport { deleteList } from './deleteSteps/deleteList';\nimport {\n ChangeSource,\n deleteSelection,\n isModifierKey,\n isNodeOfType,\n} from 'roosterjs-content-model-dom';\nimport {\n handleKeyboardEventResult,\n shouldDeleteAllSegmentsBefore,\n shouldDeleteWord,\n} from './handleKeyboardEventCommon';\nimport {\n backwardDeleteWordSelection,\n forwardDeleteWordSelection,\n} from './deleteSteps/deleteWordSelection';\nimport {\n backwardDeleteCollapsedSelection,\n forwardDeleteCollapsedSelection,\n} from './deleteSteps/deleteCollapsedSelection';\nimport type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-content-model-types';\n\n/**\n * @internal\n * Do keyboard event handling for DELETE/BACKSPACE key\n * @param editor The editor object\n * @param rawEvent DOM keyboard event\n * @returns True if the event is handled by content model, otherwise false\n */\nexport function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent) {\n let handled = false;\n const selection = editor.getDOMSelection();\n\n if (shouldDeleteWithContentModel(selection, rawEvent)) {\n editor.formatContentModel(\n (model, context) => {\n const result = deleteSelection(\n model,\n getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac),\n context\n ).deleteResult;\n\n handled = handleKeyboardEventResult(editor, model, rawEvent, result, context);\n return handled;\n },\n {\n rawEvent,\n changeSource: ChangeSource.Keyboard,\n getChangeData: () => rawEvent.which,\n scrollCaretIntoView: true,\n apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey',\n }\n );\n }\n\n return handled;\n}\n\nfunction getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {\n const isForward = rawEvent.key == 'Delete';\n const deleteAllSegmentBeforeStep =\n shouldDeleteAllSegmentsBefore(rawEvent) && !isForward ? deleteAllSegmentBefore : null;\n const deleteWordSelection = shouldDeleteWord(rawEvent, isMac)\n ? isForward\n ? forwardDeleteWordSelection\n : backwardDeleteWordSelection\n : null;\n const deleteCollapsedSelection = isForward\n ? forwardDeleteCollapsedSelection\n : backwardDeleteCollapsedSelection;\n const deleteQuote = !isForward ? deleteEmptyQuote : null;\n return [\n deleteAllSegmentBeforeStep,\n deleteWordSelection,\n isForward ? null : deleteList,\n deleteCollapsedSelection,\n deleteQuote,\n ];\n}\n\nfunction shouldDeleteWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) {\n if (!selection) {\n return false; // Nothing to delete\n } else if (selection.type != 'range' || !selection.range.collapsed) {\n return true; // Selection is not collapsed, need to delete all selections\n } else {\n const range = selection.range;\n\n // When selection is collapsed and is in middle of text node, no need to use Content Model to delete\n return !(\n isNodeOfType(range.startContainer, 'TEXT_NODE') &&\n !isModifierKey(rawEvent) &&\n (canDeleteBefore(rawEvent, range) || canDeleteAfter(rawEvent, range))\n );\n }\n}\n\nfunction canDeleteBefore(rawEvent: KeyboardEvent, range: Range) {\n return rawEvent.key == 'Backspace' && range.startOffset > 1;\n}\n\nfunction canDeleteAfter(rawEvent: KeyboardEvent, range: Range) {\n return (\n rawEvent.key == 'Delete' &&\n range.startOffset < (range.startContainer.nodeValue?.length ?? 0) - 1\n );\n}\n"]}
1
+ {"version":3,"file":"keyboardDelete.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts"],"names":[],"mappings":"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","sourcesContent":["import { deleteAllSegmentBefore } from './deleteSteps/deleteAllSegmentBefore';\nimport { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote';\nimport { deleteList } from './deleteSteps/deleteList';\nimport {\n ChangeSource,\n deleteSelection,\n isModifierKey,\n isNodeOfType,\n} from 'roosterjs-content-model-dom';\nimport {\n handleKeyboardEventResult,\n shouldDeleteAllSegmentsBefore,\n shouldDeleteWord,\n} from './handleKeyboardEventCommon';\nimport {\n backwardDeleteWordSelection,\n forwardDeleteWordSelection,\n} from './deleteSteps/deleteWordSelection';\nimport {\n backwardDeleteCollapsedSelection,\n forwardDeleteCollapsedSelection,\n} from './deleteSteps/deleteCollapsedSelection';\nimport type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-content-model-types';\n\n/**\n * @internal\n * Do keyboard event handling for DELETE/BACKSPACE key\n * @param editor The editor object\n * @param rawEvent DOM keyboard event\n * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM\n * @returns True if the event is handled by content model, otherwise false\n */\nexport function keyboardDelete(\n editor: IEditor,\n rawEvent: KeyboardEvent,\n handleExpandedSelection: boolean = true\n) {\n let handled = false;\n const selection = editor.getDOMSelection();\n\n if (shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection)) {\n editor.formatContentModel(\n (model, context) => {\n const result = deleteSelection(\n model,\n getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac),\n context\n ).deleteResult;\n\n handled = handleKeyboardEventResult(editor, model, rawEvent, result, context);\n return handled;\n },\n {\n rawEvent,\n changeSource: ChangeSource.Keyboard,\n getChangeData: () => rawEvent.which,\n scrollCaretIntoView: true,\n apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey',\n }\n );\n }\n\n return handled;\n}\n\nfunction getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {\n const isForward = rawEvent.key == 'Delete';\n const deleteAllSegmentBeforeStep =\n shouldDeleteAllSegmentsBefore(rawEvent) && !isForward ? deleteAllSegmentBefore : null;\n const deleteWordSelection = shouldDeleteWord(rawEvent, isMac)\n ? isForward\n ? forwardDeleteWordSelection\n : backwardDeleteWordSelection\n : null;\n const deleteCollapsedSelection = isForward\n ? forwardDeleteCollapsedSelection\n : backwardDeleteCollapsedSelection;\n const deleteQuote = !isForward ? deleteEmptyQuote : null;\n return [\n deleteAllSegmentBeforeStep,\n deleteWordSelection,\n isForward ? null : deleteList,\n deleteCollapsedSelection,\n deleteQuote,\n ];\n}\n\nfunction shouldDeleteWithContentModel(\n selection: DOMSelection | null,\n rawEvent: KeyboardEvent,\n handleExpandedSelection: boolean\n) {\n if (!selection) {\n return false; // Nothing to delete\n } else if (selection.type != 'range') {\n return true;\n } else if (!selection.range.collapsed) {\n if (handleExpandedSelection) {\n return true; // Selection is not collapsed, need to delete all selections\n }\n\n const range = selection.range;\n const { startContainer, endContainer } = selection.range;\n const isInSameTextNode =\n startContainer === endContainer && isNodeOfType(startContainer, 'TEXT_NODE');\n return !(\n isInSameTextNode &&\n !isModifierKey(rawEvent) &&\n range.endOffset - range.startOffset < (startContainer.nodeValue?.length ?? 0)\n );\n } else {\n const range = selection.range;\n\n // When selection is collapsed and is in middle of text node, no need to use Content Model to delete\n return !(\n isNodeOfType(range.startContainer, 'TEXT_NODE') &&\n !isModifierKey(rawEvent) &&\n (canDeleteBefore(rawEvent, range) || canDeleteAfter(rawEvent, range))\n );\n }\n}\n\nfunction canDeleteBefore(rawEvent: KeyboardEvent, range: Range) {\n return rawEvent.key == 'Backspace' && range.startOffset > 1;\n}\n\nfunction canDeleteAfter(rawEvent: KeyboardEvent, range: Range) {\n return (\n rawEvent.key == 'Delete' &&\n range.startOffset < (range.startContainer.nodeValue?.length ?? 0) - 1\n );\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { BeforePasteEvent, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';
1
+ import type { BeforePasteEvent, ClipboardData, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';
2
2
  /**
3
3
  * @internal
4
4
  * Convert pasted content from Excel, add borders when source doc doesn't have a border
@@ -10,6 +10,11 @@ export declare function processPastedContentFromExcel(event: BeforePasteEvent, d
10
10
  * Exported only for unit test
11
11
  */
12
12
  export declare const childProcessor: ElementProcessor<ParentNode>;
13
+ /**
14
+ * @internal
15
+ * Exported only for unit test
16
+ */
17
+ export declare function validateExcelFragment(fragment: DocumentFragment, domCreator: DOMCreator, htmlBefore: string, clipboardData: ClipboardData, htmlAfter: string): void;
13
18
  /**
14
19
  * @internal Export for test only
15
20
  * @param html Source html
@@ -1,24 +1,21 @@
1
1
  define(["require", "exports", "tslib", "../utils/addParser", "roosterjs-content-model-dom", "../utils/setProcessor"], function (require, exports, tslib_1, addParser_1, roosterjs_content_model_dom_1, setProcessor_1) {
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.excelHandler = exports.childProcessor = exports.processPastedContentFromExcel = void 0;
4
+ exports.excelHandler = exports.validateExcelFragment = exports.childProcessor = exports.processPastedContentFromExcel = void 0;
5
5
  var LAST_TD_END_REGEX = /<\/\s*td\s*>((?!<\/\s*tr\s*>)[\s\S])*$/i;
6
6
  var LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i;
7
7
  var LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;
8
8
  var LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;
9
9
  var DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';
10
+ var TABLE_SELECTOR = 'table';
10
11
  /**
11
12
  * @internal
12
13
  * Convert pasted content from Excel, add borders when source doc doesn't have a border
13
14
  * @param event The BeforePaste event
14
15
  */
15
16
  function processPastedContentFromExcel(event, domCreator, allowExcelNoBorderTable) {
16
- var fragment = event.fragment, htmlBefore = event.htmlBefore, clipboardData = event.clipboardData;
17
- var html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;
18
- if (html && clipboardData.html != html) {
19
- var doc = domCreator.htmlToDOM(html);
20
- (0, roosterjs_content_model_dom_1.moveChildNodes)(fragment, doc === null || doc === void 0 ? void 0 : doc.body);
21
- }
17
+ var fragment = event.fragment, htmlBefore = event.htmlBefore, htmlAfter = event.htmlAfter, clipboardData = event.clipboardData;
18
+ validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);
22
19
  // For Excel Online
23
20
  var firstChild = fragment.firstChild;
24
21
  if ((0, roosterjs_content_model_dom_1.isNodeOfType)(firstChild, 'ELEMENT_NODE') &&
@@ -67,22 +64,59 @@ define(["require", "exports", "tslib", "../utils/addParser", "roosterjs-content-
67
64
  }
68
65
  };
69
66
  exports.childProcessor = childProcessor;
67
+ /**
68
+ * @internal
69
+ * Exported only for unit test
70
+ */
71
+ function validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter) {
72
+ // Clipboard content of Excel may contain the <StartFragment> and EndFragment comment tags inside the table
73
+ //
74
+ // @example
75
+ // <table>
76
+ // <!--StartFragment-->
77
+ // <tr>...</tr>
78
+ // <!--EndFragment-->
79
+ // </table>
80
+ //
81
+ // This causes that the fragment is not properly created and the table is not extracted.
82
+ // The content that is before the StartFragment is htmlBefore and the content that is after the EndFragment is htmlAfter.
83
+ // So attempt to create a new document fragment with the content of htmlBefore + clipboardData.html + htmlAfter
84
+ // If a table is found, replace the fragment with the new fragment
85
+ var result = !fragment.querySelector(TABLE_SELECTOR) &&
86
+ domCreator.htmlToDOM(htmlBefore + clipboardData.html + htmlAfter);
87
+ if (result && result.querySelector(TABLE_SELECTOR)) {
88
+ (0, roosterjs_content_model_dom_1.moveChildNodes)(fragment, result === null || result === void 0 ? void 0 : result.body);
89
+ }
90
+ else {
91
+ // If the table is still not found, try to extract the table from the clipboard data using Regex
92
+ var html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;
93
+ if (html && clipboardData.html != html) {
94
+ var doc = domCreator.htmlToDOM(html);
95
+ (0, roosterjs_content_model_dom_1.moveChildNodes)(fragment, doc === null || doc === void 0 ? void 0 : doc.body);
96
+ }
97
+ }
98
+ }
99
+ exports.validateExcelFragment = validateExcelFragment;
70
100
  /**
71
101
  * @internal Export for test only
72
102
  * @param html Source html
73
103
  */
74
104
  function excelHandler(html, htmlBefore) {
75
- if (html.match(LAST_TD_END_REGEX)) {
76
- var trMatch = htmlBefore.match(LAST_TR_REGEX);
77
- var tr = trMatch ? trMatch[0] : '<TR>';
78
- html = tr + html + '</TR>';
105
+ try {
106
+ if (html.match(LAST_TD_END_REGEX)) {
107
+ var trMatch = htmlBefore.match(LAST_TR_REGEX);
108
+ var tr = trMatch ? trMatch[0] : '<TR>';
109
+ html = tr + html + '</TR>';
110
+ }
111
+ if (html.match(LAST_TR_END_REGEX)) {
112
+ var tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
113
+ var table = tableMatch ? tableMatch[0] : '<TABLE>';
114
+ html = table + html + '</TABLE>';
115
+ }
79
116
  }
80
- if (html.match(LAST_TR_END_REGEX)) {
81
- var tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
82
- var table = tableMatch ? tableMatch[0] : '<TABLE>';
83
- html = table + html + '</TABLE>';
117
+ finally {
118
+ return html;
84
119
  }
85
- return html;
86
120
  }
87
121
  exports.excelHandler = excelHandler;
88
122
  });
@@ -1 +1 @@
1
- {"version":3,"file":"processPastedContentFromExcel.js","sourceRoot":"","sources":["../../../../../packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts"],"names":[],"mappings":"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","sourcesContent":["import { addParser } from '../utils/addParser';\nimport { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';\nimport { setProcessor } from '../utils/setProcessor';\nimport type { BeforePasteEvent, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';\n\nconst LAST_TD_END_REGEX = /<\\/\\s*td\\s*>((?!<\\/\\s*tr\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_END_REGEX = /<\\/\\s*tr\\s*>((?!<\\/\\s*table\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;\nconst LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;\nconst DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';\n\n/**\n * @internal\n * Convert pasted content from Excel, add borders when source doc doesn't have a border\n * @param event The BeforePaste event\n */\n\nexport function processPastedContentFromExcel(\n event: BeforePasteEvent,\n domCreator: DOMCreator,\n allowExcelNoBorderTable?: boolean\n) {\n const { fragment, htmlBefore, clipboardData } = event;\n const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;\n\n if (html && clipboardData.html != html) {\n const doc = domCreator.htmlToDOM(html);\n moveChildNodes(fragment, doc?.body);\n }\n\n // For Excel Online\n const firstChild = fragment.firstChild;\n if (\n isNodeOfType(firstChild, 'ELEMENT_NODE') &&\n firstChild.tagName == 'div' &&\n firstChild.firstChild\n ) {\n const tableFound = Array.from(firstChild.childNodes).every((child: Node) => {\n // Tables pasted from Excel Online should be of the format: 0 to N META tags and 1 TABLE tag\n const tagName = isNodeOfType(child, 'ELEMENT_NODE') && child.tagName;\n\n return tagName == 'META'\n ? true\n : tagName == 'TABLE'\n ? child == firstChild.lastChild\n : false;\n });\n\n // Extract Table from Div\n if (tableFound && firstChild.lastChild) {\n event.fragment.replaceChildren(firstChild.lastChild);\n }\n }\n\n addParser(event.domToModelOption, 'tableCell', (format, element) => {\n if (!allowExcelNoBorderTable && element.style.borderStyle === 'none') {\n format.borderBottom = DEFAULT_BORDER_STYLE;\n format.borderLeft = DEFAULT_BORDER_STYLE;\n format.borderRight = DEFAULT_BORDER_STYLE;\n format.borderTop = DEFAULT_BORDER_STYLE;\n }\n });\n\n setProcessor(event.domToModelOption, 'child', childProcessor);\n}\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {\n const segmentFormat = { ...context.segmentFormat };\n if (\n group.blockGroupType === 'TableCell' &&\n group.format.textColor &&\n !context.segmentFormat.textColor\n ) {\n context.segmentFormat.textColor = group.format.textColor;\n }\n\n context.defaultElementProcessors.child(group, element, context);\n\n if (group.blockGroupType === 'TableCell' && group.format.textColor) {\n context.segmentFormat = segmentFormat;\n delete group.format.textColor;\n }\n};\n\n/**\n * @internal Export for test only\n * @param html Source html\n */\n\nexport function excelHandler(html: string, htmlBefore: string): string {\n if (html.match(LAST_TD_END_REGEX)) {\n const trMatch = htmlBefore.match(LAST_TR_REGEX);\n const tr = trMatch ? trMatch[0] : '<TR>';\n html = tr + html + '</TR>';\n }\n if (html.match(LAST_TR_END_REGEX)) {\n const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);\n const table = tableMatch ? tableMatch[0] : '<TABLE>';\n html = table + html + '</TABLE>';\n }\n\n return html;\n}\n"]}
1
+ {"version":3,"file":"processPastedContentFromExcel.js","sourceRoot":"","sources":["../../../../../packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts"],"names":[],"mappings":"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","sourcesContent":["import { addParser } from '../utils/addParser';\nimport { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';\nimport { setProcessor } from '../utils/setProcessor';\nimport type {\n BeforePasteEvent,\n ClipboardData,\n DOMCreator,\n ElementProcessor,\n} from 'roosterjs-content-model-types';\n\nconst LAST_TD_END_REGEX = /<\\/\\s*td\\s*>((?!<\\/\\s*tr\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_END_REGEX = /<\\/\\s*tr\\s*>((?!<\\/\\s*table\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;\nconst LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;\nconst DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';\nconst TABLE_SELECTOR = 'table';\n\n/**\n * @internal\n * Convert pasted content from Excel, add borders when source doc doesn't have a border\n * @param event The BeforePaste event\n */\n\nexport function processPastedContentFromExcel(\n event: BeforePasteEvent,\n domCreator: DOMCreator,\n allowExcelNoBorderTable?: boolean\n) {\n const { fragment, htmlBefore, htmlAfter, clipboardData } = event;\n\n validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);\n\n // For Excel Online\n const firstChild = fragment.firstChild;\n if (\n isNodeOfType(firstChild, 'ELEMENT_NODE') &&\n firstChild.tagName == 'div' &&\n firstChild.firstChild\n ) {\n const tableFound = Array.from(firstChild.childNodes).every((child: Node) => {\n // Tables pasted from Excel Online should be of the format: 0 to N META tags and 1 TABLE tag\n const tagName = isNodeOfType(child, 'ELEMENT_NODE') && child.tagName;\n\n return tagName == 'META'\n ? true\n : tagName == 'TABLE'\n ? child == firstChild.lastChild\n : false;\n });\n\n // Extract Table from Div\n if (tableFound && firstChild.lastChild) {\n event.fragment.replaceChildren(firstChild.lastChild);\n }\n }\n\n addParser(event.domToModelOption, 'tableCell', (format, element) => {\n if (!allowExcelNoBorderTable && element.style.borderStyle === 'none') {\n format.borderBottom = DEFAULT_BORDER_STYLE;\n format.borderLeft = DEFAULT_BORDER_STYLE;\n format.borderRight = DEFAULT_BORDER_STYLE;\n format.borderTop = DEFAULT_BORDER_STYLE;\n }\n });\n\n setProcessor(event.domToModelOption, 'child', childProcessor);\n}\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {\n const segmentFormat = { ...context.segmentFormat };\n if (\n group.blockGroupType === 'TableCell' &&\n group.format.textColor &&\n !context.segmentFormat.textColor\n ) {\n context.segmentFormat.textColor = group.format.textColor;\n }\n\n context.defaultElementProcessors.child(group, element, context);\n\n if (group.blockGroupType === 'TableCell' && group.format.textColor) {\n context.segmentFormat = segmentFormat;\n delete group.format.textColor;\n }\n};\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport function validateExcelFragment(\n fragment: DocumentFragment,\n domCreator: DOMCreator,\n htmlBefore: string,\n clipboardData: ClipboardData,\n htmlAfter: string\n) {\n // Clipboard content of Excel may contain the <StartFragment> and EndFragment comment tags inside the table\n //\n // @example\n // <table>\n // <!--StartFragment-->\n // <tr>...</tr>\n // <!--EndFragment-->\n // </table>\n //\n // This causes that the fragment is not properly created and the table is not extracted.\n // The content that is before the StartFragment is htmlBefore and the content that is after the EndFragment is htmlAfter.\n // So attempt to create a new document fragment with the content of htmlBefore + clipboardData.html + htmlAfter\n // If a table is found, replace the fragment with the new fragment\n const result =\n !fragment.querySelector(TABLE_SELECTOR) &&\n domCreator.htmlToDOM(htmlBefore + clipboardData.html + htmlAfter);\n if (result && result.querySelector(TABLE_SELECTOR)) {\n moveChildNodes(fragment, result?.body);\n } else {\n // If the table is still not found, try to extract the table from the clipboard data using Regex\n const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;\n\n if (html && clipboardData.html != html) {\n const doc = domCreator.htmlToDOM(html);\n moveChildNodes(fragment, doc?.body);\n }\n }\n}\n\n/**\n * @internal Export for test only\n * @param html Source html\n */\nexport function excelHandler(html: string, htmlBefore: string): string {\n try {\n if (html.match(LAST_TD_END_REGEX)) {\n const trMatch = htmlBefore.match(LAST_TR_REGEX);\n const tr = trMatch ? trMatch[0] : '<TR>';\n html = tr + html + '</TR>';\n }\n if (html.match(LAST_TR_END_REGEX)) {\n const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);\n const table = tableMatch ? tableMatch[0] : '<TABLE>';\n html = table + html + '</TABLE>';\n }\n } finally {\n return html;\n }\n}\n"]}
@@ -7,6 +7,11 @@ export declare type EditOptions = {
7
7
  * Whether to handle Tab key in keyboard. @default true
8
8
  */
9
9
  handleTabKey?: boolean;
10
+ /**
11
+ * Whether expanded selection within a text node should be handled by CM when pressing Backspace/Delete key.
12
+ * @default true
13
+ */
14
+ handleExpandedSelectionOnDelete?: boolean;
10
15
  };
11
16
  /**
12
17
  * Edit plugins helps editor to do editing operation on top of content model.
@@ -14,6 +14,7 @@ var DELETE_KEY = 46;
14
14
  var DEAD_KEY = 229;
15
15
  var DefaultOptions = {
16
16
  handleTabKey: true,
17
+ handleExpandedSelectionOnDelete: true,
17
18
  };
18
19
  /**
19
20
  * Edit plugins helps editor to do editing operation on top of content model.
@@ -134,14 +135,14 @@ var EditPlugin = /** @class */ (function () {
134
135
  case 'Backspace':
135
136
  // Use our API to handle BACKSPACE/DELETE key.
136
137
  // 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
137
- keyboardDelete(editor, rawEvent);
138
+ keyboardDelete(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
138
139
  break;
139
140
  case 'Delete':
140
141
  // Use our API to handle BACKSPACE/DELETE key.
141
142
  // 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
142
143
  // And leave it to browser when shift key is pressed so that browser will trigger cut event
143
144
  if (!event.rawEvent.shiftKey) {
144
- keyboardDelete(editor, rawEvent);
145
+ keyboardDelete(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
145
146
  }
146
147
  break;
147
148
  case 'Tab':
@@ -183,14 +184,14 @@ var EditPlugin = /** @class */ (function () {
183
184
  key: 'Backspace',
184
185
  keyCode: BACKSPACE_KEY,
185
186
  which: BACKSPACE_KEY,
186
- }));
187
+ }), this.options.handleExpandedSelectionOnDelete);
187
188
  break;
188
189
  case 'deleteContentForward':
189
190
  handled = keyboardDelete(editor, new KeyboardEvent('keydown', {
190
191
  key: 'Delete',
191
192
  keyCode: DELETE_KEY,
192
193
  which: DELETE_KEY,
193
- }));
194
+ }), this.options.handleExpandedSelectionOnDelete);
194
195
  break;
195
196
  }
196
197
  if (handled) {
@@ -1 +1 @@
1
- {"version":3,"file":"EditPlugin.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAmB9D,IAAM,aAAa,GAAG,CAAC,CAAC;AACxB,IAAM,UAAU,GAAG,EAAE,CAAC;AACtB;;;;;GAKG;AACH,IAAM,QAAQ,GAAG,GAAG,CAAC;AAErB,IAAM,cAAc,GAAyB;IACzC,YAAY,EAAE,IAAI;CACrB,CAAC;AAEF;;;;;;GAMG;AACH;IAOI;;;OAGG;IACH,oBAAoB,OAAqC;QAArC,wBAAA,EAAA,wBAAqC;QAArC,YAAO,GAAP,OAAO,CAA8B;QAVjD,WAAM,GAAmB,IAAI,CAAC;QAC9B,aAAQ,GAAwB,IAAI,CAAC;QACrC,+BAA0B,GAAG,KAAK,CAAC;QACnC,yBAAoB,GAAwB,IAAI,CAAC;QACjD,sBAAiB,GAAG,KAAK,CAAC;IAM0B,CAAC;IAE7D;;OAEG;IACH,4BAAO,GAAP;QACI,OAAO,MAAM,CAAC;IAClB,CAAC;IAED;;;;;OAKG;IACH,+BAAU,GAAV,UAAW,MAAe;QAA1B,iBAWC;QAVG,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,MAAM,CAAC,4BAA4B,CAAC,gBAAgB,CAAC,CAAC;QAEpF,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC,SAAS,EAAE;YACnC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;gBACvC,WAAW,EAAE;oBACT,cAAc,EAAE,UAAA,CAAC,IAAI,OAAA,KAAI,CAAC,sBAAsB,CAAC,MAAM,EAAE,CAAC,CAAC,EAAtC,CAAsC;iBAC9D;aACJ,CAAC,CAAC;SACN;IACL,CAAC;IAED;;;;OAIG;IACH,4BAAO,GAAP;;QACI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,MAAA,IAAI,CAAC,QAAQ,+CAAb,IAAI,CAAa,CAAC;QAClB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACzB,CAAC;IAED;;;;;OAKG;IACH,kCAAa,GAAb,UAAc,KAAkB;QAC5B,IAAI,IAAI,CAAC,MAAM,EAAE;YACb,QAAQ,KAAK,CAAC,SAAS,EAAE;gBACrB,KAAK,SAAS;oBACV,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;oBAC5C,MAAM;gBACV,KAAK,OAAO;oBACR,IAAI,IAAI,CAAC,oBAAoB,EAAE;wBAC3B,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;wBACvD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;qBACpC;oBACD,MAAM;aACb;SACJ;IACL,CAAC;IAED;;;;;;;OAOG;IACH,+CAA0B,GAA1B,UAA2B,KAAkB;QACzC,IACI,IAAI,CAAC,MAAM;YACX,IAAI,CAAC,OAAO,CAAC,YAAY;YACzB,KAAK,CAAC,SAAS,IAAI,SAAS;YAC5B,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI,KAAK;YAC3B,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAC1B;YACE,IAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;YAChD,IAAM,cAAc,GAChB,CAAA,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,IAAI,KAAI,OAAO,IAAI,SAAS,CAAC,KAAK,CAAC,SAAS;gBACnD,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,cAAc;gBAChC,CAAC,CAAC,IAAI,CAAC;YACf,IAAM,KAAK,GAAG,cAAc;gBACxB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,0BAA0B,CAAC,cAAc,EAAE,OAAO,CAAC;gBAChF,CAAC,CAAC,IAAI,CAAC;YACX,IAAM,WAAW,GAAG,KAAK,IAAI,eAAe,CAAC,KAAK,CAAC,CAAC;YAEpD,IAAI,WAAW,EAAE;gBACb,IAAM,OAAO,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBACpD,IAAM,QAAQ,GAAG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAExD,IAAI,OAAO,QAAQ,IAAI,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE;oBAClE,qHAAqH;oBACrH,8FAA8F;oBAC9F,OAAO,IAAI,CAAC;iBACf;aACJ;SACJ;QAED,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,uCAAkB,GAA1B,UAA2B,MAAe,EAAE,KAAmB;QAC3D,IAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAChC,IAAM,gBAAgB,GAAG,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC;QAE9D,IAAI,CAAC,QAAQ,CAAC,gBAAgB,IAAI,CAAC,KAAK,CAAC,oBAAoB,EAAE;YAC3D,QAAQ,QAAQ,CAAC,GAAG,EAAE;gBAClB,KAAK,WAAW;oBACZ,8CAA8C;oBAC9C,qIAAqI;oBACrI,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;oBACjC,MAAM;gBAEV,KAAK,QAAQ;oBACT,8CAA8C;oBAC9C,qIAAqI;oBACrI,2FAA2F;oBAC3F,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE;wBAC1B,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;qBACpC;oBACD,MAAM;gBAEV,KAAK,KAAK;oBACN,IAAI,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,CAAC,gBAAgB,EAAE;wBAChD,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;qBACjC;oBACD,MAAM;gBACV,KAAK,cAAc;oBACf,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC,SAAS,EAAE;wBACnC,IAAI,CAAC,0BAA0B,GAAG,IAAI,CAAC;qBAC1C;oBACD,MAAM;gBAEV,KAAK,OAAO;oBACR,IACI,CAAC,gBAAgB;wBACjB,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW;wBAC3B,KAAK,CAAC,QAAQ,CAAC,OAAO,KAAK,QAAQ,EACrC;wBACE,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;qBAC3D;oBACD,MAAM;gBAEV;oBACI,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;oBAChC,MAAM;aACb;SACJ;IACL,CAAC;IAEO,2CAAsB,GAA9B,UAA+B,MAAe,EAAE,QAAe;QAC3D,gFAAgF;QAChF,uGAAuG;QACvG,IACI,CAAC,IAAI,CAAC,0BAA0B;YAChC,CAAC,CAAC,QAAQ,YAAY,UAAU,CAAC;YACjC,QAAQ,CAAC,gBAAgB,EAC3B;YACE,OAAO;SACV;QACD,IAAI,CAAC,0BAA0B,GAAG,KAAK,CAAC;QAExC,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,QAAQ,QAAQ,CAAC,SAAS,EAAE;YACxB,KAAK,uBAAuB;gBACxB,OAAO,GAAG,cAAc,CACpB,MAAM,EACN,IAAI,aAAa,CAAC,SAAS,EAAE;oBACzB,GAAG,EAAE,WAAW;oBAChB,OAAO,EAAE,aAAa;oBACtB,KAAK,EAAE,aAAa;iBACvB,CAAC,CACL,CAAC;gBACF,MAAM;YACV,KAAK,sBAAsB;gBACvB,OAAO,GAAG,cAAc,CACpB,MAAM,EACN,IAAI,aAAa,CAAC,SAAS,EAAE;oBACzB,GAAG,EAAE,QAAQ;oBACb,OAAO,EAAE,UAAU;oBACnB,KAAK,EAAE,UAAU;iBACpB,CAAC,CACL,CAAC;gBACF,MAAM;SACb;QAED,IAAI,OAAO,EAAE;YACT,QAAQ,CAAC,cAAc,EAAE,CAAC;YAE1B,sEAAsE;YACtE,oDAAoD;YACpD,IAAI,CAAC,oBAAoB,GAAG,MAAM,CAAC,eAAe,EAAE,CAAC;SACxD;IACL,CAAC;IACL,iBAAC;AAAD,CAAC,AA9MD,IA8MC","sourcesContent":["import { keyboardDelete } from './keyboardDelete';\nimport { keyboardEnter } from './keyboardEnter';\nimport { keyboardInput } from './keyboardInput';\nimport { keyboardTab } from './keyboardTab';\nimport { parseTableCells } from 'roosterjs-content-model-dom';\nimport type {\n DOMSelection,\n EditorPlugin,\n IEditor,\n KeyDownEvent,\n PluginEvent,\n} from 'roosterjs-content-model-types';\n\n/**\n * Options to customize the keyboard handling behavior of Edit plugin\n */\nexport type EditOptions = {\n /**\n * Whether to handle Tab key in keyboard. @default true\n */\n handleTabKey?: boolean;\n};\n\nconst BACKSPACE_KEY = 8;\nconst DELETE_KEY = 46;\n/**\n * According to https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html\n * 229 can be sent in variants generated when Long press (iOS) or using IM.\n *\n * Other cases: https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229\n */\nconst DEAD_KEY = 229;\n\nconst DefaultOptions: Partial<EditOptions> = {\n handleTabKey: true,\n};\n\n/**\n * Edit plugins helps editor to do editing operation on top of content model.\n * This includes:\n * 1. Delete Key\n * 2. Backspace Key\n * 3. Tab Key\n */\nexport class EditPlugin implements EditorPlugin {\n private editor: IEditor | null = null;\n private disposer: (() => void) | null = null;\n private shouldHandleNextInputEvent = false;\n private selectionAfterDelete: DOMSelection | null = null;\n private handleNormalEnter = false;\n\n /**\n * @param options An optional parameter that takes in an object of type EditOptions, which includes the following properties:\n * handleTabKey: A boolean that enables or disables Tab key handling. Defaults to true.\n */\n constructor(private options: EditOptions = DefaultOptions) {}\n\n /**\n * Get name of this plugin\n */\n getName() {\n return 'Edit';\n }\n\n /**\n * The first method that editor will call to a plugin when editor is initializing.\n * It will pass in the editor instance, plugin should take this chance to save the\n * editor reference so that it can call to any editor method or format API later.\n * @param editor The editor object\n */\n initialize(editor: IEditor) {\n this.editor = editor;\n this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('HandleEnterKey');\n\n if (editor.getEnvironment().isAndroid) {\n this.disposer = this.editor.attachDomEvent({\n beforeinput: {\n beforeDispatch: e => this.handleBeforeInputEvent(editor, e),\n },\n });\n }\n }\n\n /**\n * The last method that editor will call to a plugin before it is disposed.\n * Plugin can take this chance to clear the reference to editor. After this method is\n * called, plugin should not call to any editor method since it will result in error.\n */\n dispose() {\n this.editor = null;\n this.disposer?.();\n this.disposer = null;\n }\n\n /**\n * Core method for a plugin. Once an event happens in editor, editor will call this\n * method of each plugin to handle the event as long as the event is not handled\n * exclusively by another plugin.\n * @param event The event to handle:\n */\n onPluginEvent(event: PluginEvent) {\n if (this.editor) {\n switch (event.eventType) {\n case 'keyDown':\n this.handleKeyDownEvent(this.editor, event);\n break;\n case 'keyUp':\n if (this.selectionAfterDelete) {\n this.editor.setDOMSelection(this.selectionAfterDelete);\n this.selectionAfterDelete = null;\n }\n break;\n }\n }\n }\n\n /**\n * Check if the plugin should handle the given event exclusively.\n * Handle an event exclusively means other plugin will not receive this event in\n * onPluginEvent method.\n * If two plugins will return true in willHandleEventExclusively() for the same event,\n * the final result depends on the order of the plugins are added into editor\n * @param event The event to check:\n */\n willHandleEventExclusively(event: PluginEvent) {\n if (\n this.editor &&\n this.options.handleTabKey &&\n event.eventType == 'keyDown' &&\n event.rawEvent.key == 'Tab' &&\n !event.rawEvent.shiftKey\n ) {\n const selection = this.editor.getDOMSelection();\n const startContainer =\n selection?.type == 'range' && selection.range.collapsed\n ? selection.range.startContainer\n : null;\n const table = startContainer\n ? this.editor.getDOMHelper().findClosestElementAncestor(startContainer, 'table')\n : null;\n const parsedTable = table && parseTableCells(table);\n\n if (parsedTable) {\n const lastRow = parsedTable[parsedTable.length - 1];\n const lastCell = lastRow && lastRow[lastRow.length - 1];\n\n if (typeof lastCell == 'object' && lastCell.contains(startContainer)) {\n // When TAB in the last cell of a table, we will generate new table row, so prevent other plugins handling this event\n // e.g. SelectionPlugin will move the focus out of table, which is conflict with this behavior\n return true;\n }\n }\n }\n\n return false;\n }\n\n private handleKeyDownEvent(editor: IEditor, event: KeyDownEvent) {\n const rawEvent = event.rawEvent;\n const hasCtrlOrMetaKey = rawEvent.ctrlKey || rawEvent.metaKey;\n\n if (!rawEvent.defaultPrevented && !event.handledByEditFeature) {\n switch (rawEvent.key) {\n case 'Backspace':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n keyboardDelete(editor, rawEvent);\n break;\n\n case 'Delete':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n // And leave it to browser when shift key is pressed so that browser will trigger cut event\n if (!event.rawEvent.shiftKey) {\n keyboardDelete(editor, rawEvent);\n }\n break;\n\n case 'Tab':\n if (this.options.handleTabKey && !hasCtrlOrMetaKey) {\n keyboardTab(editor, rawEvent);\n }\n break;\n case 'Unidentified':\n if (editor.getEnvironment().isAndroid) {\n this.shouldHandleNextInputEvent = true;\n }\n break;\n\n case 'Enter':\n if (\n !hasCtrlOrMetaKey &&\n !event.rawEvent.isComposing &&\n event.rawEvent.keyCode !== DEAD_KEY\n ) {\n keyboardEnter(editor, rawEvent, this.handleNormalEnter);\n }\n break;\n\n default:\n keyboardInput(editor, rawEvent);\n break;\n }\n }\n }\n\n private handleBeforeInputEvent(editor: IEditor, rawEvent: Event) {\n // Some Android IMEs doesn't fire correct keydown event for BACKSPACE/DELETE key\n // Here we translate input event to BACKSPACE/DELETE keydown event to be compatible with existing logic\n if (\n !this.shouldHandleNextInputEvent ||\n !(rawEvent instanceof InputEvent) ||\n rawEvent.defaultPrevented\n ) {\n return;\n }\n this.shouldHandleNextInputEvent = false;\n\n let handled = false;\n switch (rawEvent.inputType) {\n case 'deleteContentBackward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Backspace',\n keyCode: BACKSPACE_KEY,\n which: BACKSPACE_KEY,\n })\n );\n break;\n case 'deleteContentForward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Delete',\n keyCode: DELETE_KEY,\n which: DELETE_KEY,\n })\n );\n break;\n }\n\n if (handled) {\n rawEvent.preventDefault();\n\n // Restore the selection on keyup event to avoid the cursor jump issue\n // See: https://issues.chromium.org/issues/330596261\n this.selectionAfterDelete = editor.getDOMSelection();\n }\n }\n}\n"]}
1
+ {"version":3,"file":"EditPlugin.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { keyboardDelete } from './keyboardDelete';\nimport { keyboardEnter } from './keyboardEnter';\nimport { keyboardInput } from './keyboardInput';\nimport { keyboardTab } from './keyboardTab';\nimport { parseTableCells } from 'roosterjs-content-model-dom';\nimport type {\n DOMSelection,\n EditorPlugin,\n IEditor,\n KeyDownEvent,\n PluginEvent,\n} from 'roosterjs-content-model-types';\n\n/**\n * Options to customize the keyboard handling behavior of Edit plugin\n */\nexport type EditOptions = {\n /**\n * Whether to handle Tab key in keyboard. @default true\n */\n handleTabKey?: boolean;\n\n /**\n * Whether expanded selection within a text node should be handled by CM when pressing Backspace/Delete key.\n * @default true\n */\n handleExpandedSelectionOnDelete?: boolean;\n};\n\nconst BACKSPACE_KEY = 8;\nconst DELETE_KEY = 46;\n/**\n * According to https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html\n * 229 can be sent in variants generated when Long press (iOS) or using IM.\n *\n * Other cases: https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229\n */\nconst DEAD_KEY = 229;\n\nconst DefaultOptions: Partial<EditOptions> = {\n handleTabKey: true,\n handleExpandedSelectionOnDelete: true,\n};\n\n/**\n * Edit plugins helps editor to do editing operation on top of content model.\n * This includes:\n * 1. Delete Key\n * 2. Backspace Key\n * 3. Tab Key\n */\nexport class EditPlugin implements EditorPlugin {\n private editor: IEditor | null = null;\n private disposer: (() => void) | null = null;\n private shouldHandleNextInputEvent = false;\n private selectionAfterDelete: DOMSelection | null = null;\n private handleNormalEnter = false;\n\n /**\n * @param options An optional parameter that takes in an object of type EditOptions, which includes the following properties:\n * handleTabKey: A boolean that enables or disables Tab key handling. Defaults to true.\n */\n constructor(private options: EditOptions = DefaultOptions) {}\n\n /**\n * Get name of this plugin\n */\n getName() {\n return 'Edit';\n }\n\n /**\n * The first method that editor will call to a plugin when editor is initializing.\n * It will pass in the editor instance, plugin should take this chance to save the\n * editor reference so that it can call to any editor method or format API later.\n * @param editor The editor object\n */\n initialize(editor: IEditor) {\n this.editor = editor;\n this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('HandleEnterKey');\n\n if (editor.getEnvironment().isAndroid) {\n this.disposer = this.editor.attachDomEvent({\n beforeinput: {\n beforeDispatch: e => this.handleBeforeInputEvent(editor, e),\n },\n });\n }\n }\n\n /**\n * The last method that editor will call to a plugin before it is disposed.\n * Plugin can take this chance to clear the reference to editor. After this method is\n * called, plugin should not call to any editor method since it will result in error.\n */\n dispose() {\n this.editor = null;\n this.disposer?.();\n this.disposer = null;\n }\n\n /**\n * Core method for a plugin. Once an event happens in editor, editor will call this\n * method of each plugin to handle the event as long as the event is not handled\n * exclusively by another plugin.\n * @param event The event to handle:\n */\n onPluginEvent(event: PluginEvent) {\n if (this.editor) {\n switch (event.eventType) {\n case 'keyDown':\n this.handleKeyDownEvent(this.editor, event);\n break;\n case 'keyUp':\n if (this.selectionAfterDelete) {\n this.editor.setDOMSelection(this.selectionAfterDelete);\n this.selectionAfterDelete = null;\n }\n break;\n }\n }\n }\n\n /**\n * Check if the plugin should handle the given event exclusively.\n * Handle an event exclusively means other plugin will not receive this event in\n * onPluginEvent method.\n * If two plugins will return true in willHandleEventExclusively() for the same event,\n * the final result depends on the order of the plugins are added into editor\n * @param event The event to check:\n */\n willHandleEventExclusively(event: PluginEvent) {\n if (\n this.editor &&\n this.options.handleTabKey &&\n event.eventType == 'keyDown' &&\n event.rawEvent.key == 'Tab' &&\n !event.rawEvent.shiftKey\n ) {\n const selection = this.editor.getDOMSelection();\n const startContainer =\n selection?.type == 'range' && selection.range.collapsed\n ? selection.range.startContainer\n : null;\n const table = startContainer\n ? this.editor.getDOMHelper().findClosestElementAncestor(startContainer, 'table')\n : null;\n const parsedTable = table && parseTableCells(table);\n\n if (parsedTable) {\n const lastRow = parsedTable[parsedTable.length - 1];\n const lastCell = lastRow && lastRow[lastRow.length - 1];\n\n if (typeof lastCell == 'object' && lastCell.contains(startContainer)) {\n // When TAB in the last cell of a table, we will generate new table row, so prevent other plugins handling this event\n // e.g. SelectionPlugin will move the focus out of table, which is conflict with this behavior\n return true;\n }\n }\n }\n\n return false;\n }\n\n private handleKeyDownEvent(editor: IEditor, event: KeyDownEvent) {\n const rawEvent = event.rawEvent;\n const hasCtrlOrMetaKey = rawEvent.ctrlKey || rawEvent.metaKey;\n\n if (!rawEvent.defaultPrevented && !event.handledByEditFeature) {\n switch (rawEvent.key) {\n case 'Backspace':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n keyboardDelete(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);\n break;\n\n case 'Delete':\n // Use our API to handle BACKSPACE/DELETE key.\n // 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\n // And leave it to browser when shift key is pressed so that browser will trigger cut event\n if (!event.rawEvent.shiftKey) {\n keyboardDelete(\n editor,\n rawEvent,\n this.options.handleExpandedSelectionOnDelete\n );\n }\n break;\n\n case 'Tab':\n if (this.options.handleTabKey && !hasCtrlOrMetaKey) {\n keyboardTab(editor, rawEvent);\n }\n break;\n case 'Unidentified':\n if (editor.getEnvironment().isAndroid) {\n this.shouldHandleNextInputEvent = true;\n }\n break;\n\n case 'Enter':\n if (\n !hasCtrlOrMetaKey &&\n !event.rawEvent.isComposing &&\n event.rawEvent.keyCode !== DEAD_KEY\n ) {\n keyboardEnter(editor, rawEvent, this.handleNormalEnter);\n }\n break;\n\n default:\n keyboardInput(editor, rawEvent);\n break;\n }\n }\n }\n\n private handleBeforeInputEvent(editor: IEditor, rawEvent: Event) {\n // Some Android IMEs doesn't fire correct keydown event for BACKSPACE/DELETE key\n // Here we translate input event to BACKSPACE/DELETE keydown event to be compatible with existing logic\n if (\n !this.shouldHandleNextInputEvent ||\n !(rawEvent instanceof InputEvent) ||\n rawEvent.defaultPrevented\n ) {\n return;\n }\n this.shouldHandleNextInputEvent = false;\n\n let handled = false;\n switch (rawEvent.inputType) {\n case 'deleteContentBackward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Backspace',\n keyCode: BACKSPACE_KEY,\n which: BACKSPACE_KEY,\n }),\n this.options.handleExpandedSelectionOnDelete\n );\n break;\n case 'deleteContentForward':\n handled = keyboardDelete(\n editor,\n new KeyboardEvent('keydown', {\n key: 'Delete',\n keyCode: DELETE_KEY,\n which: DELETE_KEY,\n }),\n this.options.handleExpandedSelectionOnDelete\n );\n break;\n }\n\n if (handled) {\n rawEvent.preventDefault();\n\n // Restore the selection on keyup event to avoid the cursor jump issue\n // See: https://issues.chromium.org/issues/330596261\n this.selectionAfterDelete = editor.getDOMSelection();\n }\n }\n}\n"]}
@@ -4,6 +4,7 @@ import type { IEditor } from 'roosterjs-content-model-types';
4
4
  * Do keyboard event handling for DELETE/BACKSPACE key
5
5
  * @param editor The editor object
6
6
  * @param rawEvent DOM keyboard event
7
+ * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM
7
8
  * @returns True if the event is handled by content model, otherwise false
8
9
  */
9
- export declare function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent): boolean;
10
+ export declare function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent, handleExpandedSelection?: boolean): boolean;
@@ -10,12 +10,14 @@ import { backwardDeleteCollapsedSelection, forwardDeleteCollapsedSelection, } fr
10
10
  * Do keyboard event handling for DELETE/BACKSPACE key
11
11
  * @param editor The editor object
12
12
  * @param rawEvent DOM keyboard event
13
+ * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM
13
14
  * @returns True if the event is handled by content model, otherwise false
14
15
  */
15
- export function keyboardDelete(editor, rawEvent) {
16
+ export function keyboardDelete(editor, rawEvent, handleExpandedSelection) {
17
+ if (handleExpandedSelection === void 0) { handleExpandedSelection = true; }
16
18
  var handled = false;
17
19
  var selection = editor.getDOMSelection();
18
- if (shouldDeleteWithContentModel(selection, rawEvent)) {
20
+ if (shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection)) {
19
21
  editor.formatContentModel(function (model, context) {
20
22
  var result = deleteSelection(model, getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac), context).deleteResult;
21
23
  handled = handleKeyboardEventResult(editor, model, rawEvent, result, context);
@@ -50,12 +52,24 @@ function getDeleteSteps(rawEvent, isMac) {
50
52
  deleteQuote,
51
53
  ];
52
54
  }
53
- function shouldDeleteWithContentModel(selection, rawEvent) {
55
+ function shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection) {
56
+ var _a, _b;
54
57
  if (!selection) {
55
58
  return false; // Nothing to delete
56
59
  }
57
- else if (selection.type != 'range' || !selection.range.collapsed) {
58
- return true; // Selection is not collapsed, need to delete all selections
60
+ else if (selection.type != 'range') {
61
+ return true;
62
+ }
63
+ else if (!selection.range.collapsed) {
64
+ if (handleExpandedSelection) {
65
+ return true; // Selection is not collapsed, need to delete all selections
66
+ }
67
+ var range = selection.range;
68
+ var _c = selection.range, startContainer = _c.startContainer, endContainer = _c.endContainer;
69
+ var isInSameTextNode = startContainer === endContainer && isNodeOfType(startContainer, 'TEXT_NODE');
70
+ return !(isInSameTextNode &&
71
+ !isModifierKey(rawEvent) &&
72
+ range.endOffset - range.startOffset < ((_b = (_a = startContainer.nodeValue) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0));
59
73
  }
60
74
  else {
61
75
  var range = selection.range;
@@ -1 +1 @@
1
- {"version":3,"file":"keyboardDelete.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts"],"names":[],"mappings":"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","sourcesContent":["import { deleteAllSegmentBefore } from './deleteSteps/deleteAllSegmentBefore';\nimport { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote';\nimport { deleteList } from './deleteSteps/deleteList';\nimport {\n ChangeSource,\n deleteSelection,\n isModifierKey,\n isNodeOfType,\n} from 'roosterjs-content-model-dom';\nimport {\n handleKeyboardEventResult,\n shouldDeleteAllSegmentsBefore,\n shouldDeleteWord,\n} from './handleKeyboardEventCommon';\nimport {\n backwardDeleteWordSelection,\n forwardDeleteWordSelection,\n} from './deleteSteps/deleteWordSelection';\nimport {\n backwardDeleteCollapsedSelection,\n forwardDeleteCollapsedSelection,\n} from './deleteSteps/deleteCollapsedSelection';\nimport type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-content-model-types';\n\n/**\n * @internal\n * Do keyboard event handling for DELETE/BACKSPACE key\n * @param editor The editor object\n * @param rawEvent DOM keyboard event\n * @returns True if the event is handled by content model, otherwise false\n */\nexport function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent) {\n let handled = false;\n const selection = editor.getDOMSelection();\n\n if (shouldDeleteWithContentModel(selection, rawEvent)) {\n editor.formatContentModel(\n (model, context) => {\n const result = deleteSelection(\n model,\n getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac),\n context\n ).deleteResult;\n\n handled = handleKeyboardEventResult(editor, model, rawEvent, result, context);\n return handled;\n },\n {\n rawEvent,\n changeSource: ChangeSource.Keyboard,\n getChangeData: () => rawEvent.which,\n scrollCaretIntoView: true,\n apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey',\n }\n );\n }\n\n return handled;\n}\n\nfunction getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {\n const isForward = rawEvent.key == 'Delete';\n const deleteAllSegmentBeforeStep =\n shouldDeleteAllSegmentsBefore(rawEvent) && !isForward ? deleteAllSegmentBefore : null;\n const deleteWordSelection = shouldDeleteWord(rawEvent, isMac)\n ? isForward\n ? forwardDeleteWordSelection\n : backwardDeleteWordSelection\n : null;\n const deleteCollapsedSelection = isForward\n ? forwardDeleteCollapsedSelection\n : backwardDeleteCollapsedSelection;\n const deleteQuote = !isForward ? deleteEmptyQuote : null;\n return [\n deleteAllSegmentBeforeStep,\n deleteWordSelection,\n isForward ? null : deleteList,\n deleteCollapsedSelection,\n deleteQuote,\n ];\n}\n\nfunction shouldDeleteWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) {\n if (!selection) {\n return false; // Nothing to delete\n } else if (selection.type != 'range' || !selection.range.collapsed) {\n return true; // Selection is not collapsed, need to delete all selections\n } else {\n const range = selection.range;\n\n // When selection is collapsed and is in middle of text node, no need to use Content Model to delete\n return !(\n isNodeOfType(range.startContainer, 'TEXT_NODE') &&\n !isModifierKey(rawEvent) &&\n (canDeleteBefore(rawEvent, range) || canDeleteAfter(rawEvent, range))\n );\n }\n}\n\nfunction canDeleteBefore(rawEvent: KeyboardEvent, range: Range) {\n return rawEvent.key == 'Backspace' && range.startOffset > 1;\n}\n\nfunction canDeleteAfter(rawEvent: KeyboardEvent, range: Range) {\n return (\n rawEvent.key == 'Delete' &&\n range.startOffset < (range.startContainer.nodeValue?.length ?? 0) - 1\n );\n}\n"]}
1
+ {"version":3,"file":"keyboardDelete.js","sourceRoot":"","sources":["../../../../packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts"],"names":[],"mappings":"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","sourcesContent":["import { deleteAllSegmentBefore } from './deleteSteps/deleteAllSegmentBefore';\nimport { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote';\nimport { deleteList } from './deleteSteps/deleteList';\nimport {\n ChangeSource,\n deleteSelection,\n isModifierKey,\n isNodeOfType,\n} from 'roosterjs-content-model-dom';\nimport {\n handleKeyboardEventResult,\n shouldDeleteAllSegmentsBefore,\n shouldDeleteWord,\n} from './handleKeyboardEventCommon';\nimport {\n backwardDeleteWordSelection,\n forwardDeleteWordSelection,\n} from './deleteSteps/deleteWordSelection';\nimport {\n backwardDeleteCollapsedSelection,\n forwardDeleteCollapsedSelection,\n} from './deleteSteps/deleteCollapsedSelection';\nimport type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-content-model-types';\n\n/**\n * @internal\n * Do keyboard event handling for DELETE/BACKSPACE key\n * @param editor The editor object\n * @param rawEvent DOM keyboard event\n * @param handleExpandedSelection Whether to handle expanded selection within a text node by CM\n * @returns True if the event is handled by content model, otherwise false\n */\nexport function keyboardDelete(\n editor: IEditor,\n rawEvent: KeyboardEvent,\n handleExpandedSelection: boolean = true\n) {\n let handled = false;\n const selection = editor.getDOMSelection();\n\n if (shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection)) {\n editor.formatContentModel(\n (model, context) => {\n const result = deleteSelection(\n model,\n getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac),\n context\n ).deleteResult;\n\n handled = handleKeyboardEventResult(editor, model, rawEvent, result, context);\n return handled;\n },\n {\n rawEvent,\n changeSource: ChangeSource.Keyboard,\n getChangeData: () => rawEvent.which,\n scrollCaretIntoView: true,\n apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey',\n }\n );\n }\n\n return handled;\n}\n\nfunction getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {\n const isForward = rawEvent.key == 'Delete';\n const deleteAllSegmentBeforeStep =\n shouldDeleteAllSegmentsBefore(rawEvent) && !isForward ? deleteAllSegmentBefore : null;\n const deleteWordSelection = shouldDeleteWord(rawEvent, isMac)\n ? isForward\n ? forwardDeleteWordSelection\n : backwardDeleteWordSelection\n : null;\n const deleteCollapsedSelection = isForward\n ? forwardDeleteCollapsedSelection\n : backwardDeleteCollapsedSelection;\n const deleteQuote = !isForward ? deleteEmptyQuote : null;\n return [\n deleteAllSegmentBeforeStep,\n deleteWordSelection,\n isForward ? null : deleteList,\n deleteCollapsedSelection,\n deleteQuote,\n ];\n}\n\nfunction shouldDeleteWithContentModel(\n selection: DOMSelection | null,\n rawEvent: KeyboardEvent,\n handleExpandedSelection: boolean\n) {\n if (!selection) {\n return false; // Nothing to delete\n } else if (selection.type != 'range') {\n return true;\n } else if (!selection.range.collapsed) {\n if (handleExpandedSelection) {\n return true; // Selection is not collapsed, need to delete all selections\n }\n\n const range = selection.range;\n const { startContainer, endContainer } = selection.range;\n const isInSameTextNode =\n startContainer === endContainer && isNodeOfType(startContainer, 'TEXT_NODE');\n return !(\n isInSameTextNode &&\n !isModifierKey(rawEvent) &&\n range.endOffset - range.startOffset < (startContainer.nodeValue?.length ?? 0)\n );\n } else {\n const range = selection.range;\n\n // When selection is collapsed and is in middle of text node, no need to use Content Model to delete\n return !(\n isNodeOfType(range.startContainer, 'TEXT_NODE') &&\n !isModifierKey(rawEvent) &&\n (canDeleteBefore(rawEvent, range) || canDeleteAfter(rawEvent, range))\n );\n }\n}\n\nfunction canDeleteBefore(rawEvent: KeyboardEvent, range: Range) {\n return rawEvent.key == 'Backspace' && range.startOffset > 1;\n}\n\nfunction canDeleteAfter(rawEvent: KeyboardEvent, range: Range) {\n return (\n rawEvent.key == 'Delete' &&\n range.startOffset < (range.startContainer.nodeValue?.length ?? 0) - 1\n );\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { BeforePasteEvent, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';
1
+ import type { BeforePasteEvent, ClipboardData, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';
2
2
  /**
3
3
  * @internal
4
4
  * Convert pasted content from Excel, add borders when source doc doesn't have a border
@@ -10,6 +10,11 @@ export declare function processPastedContentFromExcel(event: BeforePasteEvent, d
10
10
  * Exported only for unit test
11
11
  */
12
12
  export declare const childProcessor: ElementProcessor<ParentNode>;
13
+ /**
14
+ * @internal
15
+ * Exported only for unit test
16
+ */
17
+ export declare function validateExcelFragment(fragment: DocumentFragment, domCreator: DOMCreator, htmlBefore: string, clipboardData: ClipboardData, htmlAfter: string): void;
13
18
  /**
14
19
  * @internal Export for test only
15
20
  * @param html Source html
@@ -7,18 +7,15 @@ var LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i;
7
7
  var LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;
8
8
  var LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;
9
9
  var DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';
10
+ var TABLE_SELECTOR = 'table';
10
11
  /**
11
12
  * @internal
12
13
  * Convert pasted content from Excel, add borders when source doc doesn't have a border
13
14
  * @param event The BeforePaste event
14
15
  */
15
16
  export function processPastedContentFromExcel(event, domCreator, allowExcelNoBorderTable) {
16
- var fragment = event.fragment, htmlBefore = event.htmlBefore, clipboardData = event.clipboardData;
17
- var html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;
18
- if (html && clipboardData.html != html) {
19
- var doc = domCreator.htmlToDOM(html);
20
- moveChildNodes(fragment, doc === null || doc === void 0 ? void 0 : doc.body);
21
- }
17
+ var fragment = event.fragment, htmlBefore = event.htmlBefore, htmlAfter = event.htmlAfter, clipboardData = event.clipboardData;
18
+ validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);
22
19
  // For Excel Online
23
20
  var firstChild = fragment.firstChild;
24
21
  if (isNodeOfType(firstChild, 'ELEMENT_NODE') &&
@@ -65,21 +62,57 @@ export var childProcessor = function (group, element, context) {
65
62
  delete group.format.textColor;
66
63
  }
67
64
  };
65
+ /**
66
+ * @internal
67
+ * Exported only for unit test
68
+ */
69
+ export function validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter) {
70
+ // Clipboard content of Excel may contain the <StartFragment> and EndFragment comment tags inside the table
71
+ //
72
+ // @example
73
+ // <table>
74
+ // <!--StartFragment-->
75
+ // <tr>...</tr>
76
+ // <!--EndFragment-->
77
+ // </table>
78
+ //
79
+ // This causes that the fragment is not properly created and the table is not extracted.
80
+ // The content that is before the StartFragment is htmlBefore and the content that is after the EndFragment is htmlAfter.
81
+ // So attempt to create a new document fragment with the content of htmlBefore + clipboardData.html + htmlAfter
82
+ // If a table is found, replace the fragment with the new fragment
83
+ var result = !fragment.querySelector(TABLE_SELECTOR) &&
84
+ domCreator.htmlToDOM(htmlBefore + clipboardData.html + htmlAfter);
85
+ if (result && result.querySelector(TABLE_SELECTOR)) {
86
+ moveChildNodes(fragment, result === null || result === void 0 ? void 0 : result.body);
87
+ }
88
+ else {
89
+ // If the table is still not found, try to extract the table from the clipboard data using Regex
90
+ var html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;
91
+ if (html && clipboardData.html != html) {
92
+ var doc = domCreator.htmlToDOM(html);
93
+ moveChildNodes(fragment, doc === null || doc === void 0 ? void 0 : doc.body);
94
+ }
95
+ }
96
+ }
68
97
  /**
69
98
  * @internal Export for test only
70
99
  * @param html Source html
71
100
  */
72
101
  export function excelHandler(html, htmlBefore) {
73
- if (html.match(LAST_TD_END_REGEX)) {
74
- var trMatch = htmlBefore.match(LAST_TR_REGEX);
75
- var tr = trMatch ? trMatch[0] : '<TR>';
76
- html = tr + html + '</TR>';
102
+ try {
103
+ if (html.match(LAST_TD_END_REGEX)) {
104
+ var trMatch = htmlBefore.match(LAST_TR_REGEX);
105
+ var tr = trMatch ? trMatch[0] : '<TR>';
106
+ html = tr + html + '</TR>';
107
+ }
108
+ if (html.match(LAST_TR_END_REGEX)) {
109
+ var tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
110
+ var table = tableMatch ? tableMatch[0] : '<TABLE>';
111
+ html = table + html + '</TABLE>';
112
+ }
77
113
  }
78
- if (html.match(LAST_TR_END_REGEX)) {
79
- var tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
80
- var table = tableMatch ? tableMatch[0] : '<TABLE>';
81
- html = table + html + '</TABLE>';
114
+ finally {
115
+ return html;
82
116
  }
83
- return html;
84
117
  }
85
118
  //# sourceMappingURL=processPastedContentFromExcel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"processPastedContentFromExcel.js","sourceRoot":"","sources":["../../../../../packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts"],"names":[],"mappings":"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","sourcesContent":["import { addParser } from '../utils/addParser';\nimport { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';\nimport { setProcessor } from '../utils/setProcessor';\nimport type { BeforePasteEvent, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';\n\nconst LAST_TD_END_REGEX = /<\\/\\s*td\\s*>((?!<\\/\\s*tr\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_END_REGEX = /<\\/\\s*tr\\s*>((?!<\\/\\s*table\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;\nconst LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;\nconst DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';\n\n/**\n * @internal\n * Convert pasted content from Excel, add borders when source doc doesn't have a border\n * @param event The BeforePaste event\n */\n\nexport function processPastedContentFromExcel(\n event: BeforePasteEvent,\n domCreator: DOMCreator,\n allowExcelNoBorderTable?: boolean\n) {\n const { fragment, htmlBefore, clipboardData } = event;\n const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;\n\n if (html && clipboardData.html != html) {\n const doc = domCreator.htmlToDOM(html);\n moveChildNodes(fragment, doc?.body);\n }\n\n // For Excel Online\n const firstChild = fragment.firstChild;\n if (\n isNodeOfType(firstChild, 'ELEMENT_NODE') &&\n firstChild.tagName == 'div' &&\n firstChild.firstChild\n ) {\n const tableFound = Array.from(firstChild.childNodes).every((child: Node) => {\n // Tables pasted from Excel Online should be of the format: 0 to N META tags and 1 TABLE tag\n const tagName = isNodeOfType(child, 'ELEMENT_NODE') && child.tagName;\n\n return tagName == 'META'\n ? true\n : tagName == 'TABLE'\n ? child == firstChild.lastChild\n : false;\n });\n\n // Extract Table from Div\n if (tableFound && firstChild.lastChild) {\n event.fragment.replaceChildren(firstChild.lastChild);\n }\n }\n\n addParser(event.domToModelOption, 'tableCell', (format, element) => {\n if (!allowExcelNoBorderTable && element.style.borderStyle === 'none') {\n format.borderBottom = DEFAULT_BORDER_STYLE;\n format.borderLeft = DEFAULT_BORDER_STYLE;\n format.borderRight = DEFAULT_BORDER_STYLE;\n format.borderTop = DEFAULT_BORDER_STYLE;\n }\n });\n\n setProcessor(event.domToModelOption, 'child', childProcessor);\n}\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {\n const segmentFormat = { ...context.segmentFormat };\n if (\n group.blockGroupType === 'TableCell' &&\n group.format.textColor &&\n !context.segmentFormat.textColor\n ) {\n context.segmentFormat.textColor = group.format.textColor;\n }\n\n context.defaultElementProcessors.child(group, element, context);\n\n if (group.blockGroupType === 'TableCell' && group.format.textColor) {\n context.segmentFormat = segmentFormat;\n delete group.format.textColor;\n }\n};\n\n/**\n * @internal Export for test only\n * @param html Source html\n */\n\nexport function excelHandler(html: string, htmlBefore: string): string {\n if (html.match(LAST_TD_END_REGEX)) {\n const trMatch = htmlBefore.match(LAST_TR_REGEX);\n const tr = trMatch ? trMatch[0] : '<TR>';\n html = tr + html + '</TR>';\n }\n if (html.match(LAST_TR_END_REGEX)) {\n const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);\n const table = tableMatch ? tableMatch[0] : '<TABLE>';\n html = table + html + '</TABLE>';\n }\n\n return html;\n}\n"]}
1
+ {"version":3,"file":"processPastedContentFromExcel.js","sourceRoot":"","sources":["../../../../../packages/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts"],"names":[],"mappings":"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","sourcesContent":["import { addParser } from '../utils/addParser';\nimport { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';\nimport { setProcessor } from '../utils/setProcessor';\nimport type {\n BeforePasteEvent,\n ClipboardData,\n DOMCreator,\n ElementProcessor,\n} from 'roosterjs-content-model-types';\n\nconst LAST_TD_END_REGEX = /<\\/\\s*td\\s*>((?!<\\/\\s*tr\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_END_REGEX = /<\\/\\s*tr\\s*>((?!<\\/\\s*table\\s*>)[\\s\\S])*$/i;\nconst LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;\nconst LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;\nconst DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';\nconst TABLE_SELECTOR = 'table';\n\n/**\n * @internal\n * Convert pasted content from Excel, add borders when source doc doesn't have a border\n * @param event The BeforePaste event\n */\n\nexport function processPastedContentFromExcel(\n event: BeforePasteEvent,\n domCreator: DOMCreator,\n allowExcelNoBorderTable?: boolean\n) {\n const { fragment, htmlBefore, htmlAfter, clipboardData } = event;\n\n validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);\n\n // For Excel Online\n const firstChild = fragment.firstChild;\n if (\n isNodeOfType(firstChild, 'ELEMENT_NODE') &&\n firstChild.tagName == 'div' &&\n firstChild.firstChild\n ) {\n const tableFound = Array.from(firstChild.childNodes).every((child: Node) => {\n // Tables pasted from Excel Online should be of the format: 0 to N META tags and 1 TABLE tag\n const tagName = isNodeOfType(child, 'ELEMENT_NODE') && child.tagName;\n\n return tagName == 'META'\n ? true\n : tagName == 'TABLE'\n ? child == firstChild.lastChild\n : false;\n });\n\n // Extract Table from Div\n if (tableFound && firstChild.lastChild) {\n event.fragment.replaceChildren(firstChild.lastChild);\n }\n }\n\n addParser(event.domToModelOption, 'tableCell', (format, element) => {\n if (!allowExcelNoBorderTable && element.style.borderStyle === 'none') {\n format.borderBottom = DEFAULT_BORDER_STYLE;\n format.borderLeft = DEFAULT_BORDER_STYLE;\n format.borderRight = DEFAULT_BORDER_STYLE;\n format.borderTop = DEFAULT_BORDER_STYLE;\n }\n });\n\n setProcessor(event.domToModelOption, 'child', childProcessor);\n}\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {\n const segmentFormat = { ...context.segmentFormat };\n if (\n group.blockGroupType === 'TableCell' &&\n group.format.textColor &&\n !context.segmentFormat.textColor\n ) {\n context.segmentFormat.textColor = group.format.textColor;\n }\n\n context.defaultElementProcessors.child(group, element, context);\n\n if (group.blockGroupType === 'TableCell' && group.format.textColor) {\n context.segmentFormat = segmentFormat;\n delete group.format.textColor;\n }\n};\n\n/**\n * @internal\n * Exported only for unit test\n */\nexport function validateExcelFragment(\n fragment: DocumentFragment,\n domCreator: DOMCreator,\n htmlBefore: string,\n clipboardData: ClipboardData,\n htmlAfter: string\n) {\n // Clipboard content of Excel may contain the <StartFragment> and EndFragment comment tags inside the table\n //\n // @example\n // <table>\n // <!--StartFragment-->\n // <tr>...</tr>\n // <!--EndFragment-->\n // </table>\n //\n // This causes that the fragment is not properly created and the table is not extracted.\n // The content that is before the StartFragment is htmlBefore and the content that is after the EndFragment is htmlAfter.\n // So attempt to create a new document fragment with the content of htmlBefore + clipboardData.html + htmlAfter\n // If a table is found, replace the fragment with the new fragment\n const result =\n !fragment.querySelector(TABLE_SELECTOR) &&\n domCreator.htmlToDOM(htmlBefore + clipboardData.html + htmlAfter);\n if (result && result.querySelector(TABLE_SELECTOR)) {\n moveChildNodes(fragment, result?.body);\n } else {\n // If the table is still not found, try to extract the table from the clipboard data using Regex\n const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;\n\n if (html && clipboardData.html != html) {\n const doc = domCreator.htmlToDOM(html);\n moveChildNodes(fragment, doc?.body);\n }\n }\n}\n\n/**\n * @internal Export for test only\n * @param html Source html\n */\nexport function excelHandler(html: string, htmlBefore: string): string {\n try {\n if (html.match(LAST_TD_END_REGEX)) {\n const trMatch = htmlBefore.match(LAST_TR_REGEX);\n const tr = trMatch ? trMatch[0] : '<TR>';\n html = tr + html + '</TR>';\n }\n if (html.match(LAST_TR_END_REGEX)) {\n const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);\n const table = tableMatch ? tableMatch[0] : '<TABLE>';\n html = table + html + '</TABLE>';\n }\n } finally {\n return html;\n }\n}\n"]}
package/package.json CHANGED
@@ -3,12 +3,12 @@
3
3
  "description": "Plugins for roosterjs",
4
4
  "dependencies": {
5
5
  "tslib": "^2.3.1",
6
- "roosterjs-content-model-core": "^9.17.0",
7
- "roosterjs-content-model-dom": "^9.17.0",
8
- "roosterjs-content-model-types": "^9.17.0",
9
- "roosterjs-content-model-api": "^9.17.0"
6
+ "roosterjs-content-model-core": "^9.18.0",
7
+ "roosterjs-content-model-dom": "^9.18.0",
8
+ "roosterjs-content-model-types": "^9.18.0",
9
+ "roosterjs-content-model-api": "^9.18.0"
10
10
  },
11
- "version": "9.17.0",
11
+ "version": "9.18.0",
12
12
  "main": "./lib/index.js",
13
13
  "typings": "./lib/index.d.ts",
14
14
  "module": "./lib-mjs/index.js",