Skip to content

Commit 7cead40

Browse files
ianeli1JiuqingSongBryanValverdeUjuliaroldiCopilot
authored
Bump to 9.29.0 (#3053)
* Use FormatContainer to represent DIV with id (#3003) * Fix #3005 (#3007) * Fix a cache issue (#3006) * Refactor getStyleMetadata to not rely on DomCreator and only use String handling (#3010) * Refactor paste plugin to remove unused DOMCreator parameter and enhance style extraction logic * fix test * Change search string to lowercase * Clean image edit when undo (#3015) * undo image * undo image * undo image * Add 'CustomCopyCut' experimental feature to fix some copy cut bugs (#3000) * Add 'CustomCopyCut' experimental feature to enhance copy/cut behavior * Implement pruneUnselectedModel utility for optimizing copy/paste behavior * Try fix iuld * Address comment and fix broken tests * Revert unneeded change * Refactor pruneUnselectedModel --------- Co-authored-by: Jiuqing Song <[email protected]> * Demo site: Add preset content for undeleteable anchor (#3014) Co-authored-by: Bryan Valverde U <[email protected]> * Revert "Refactor getStyleMetadata to not rely on DomCreator and only use Stri…" (#3020) This reverts commit 5bbab35. * Add API playground for createModelFromHTML (#3019) * Add API playground for createModelFromHTML * imporve --------- Co-authored-by: Bryan Valverde U <[email protected]> * Do not copy div ID on Enter (#3011) * wip * insertCustom * refactor * formatKeys * Add image hidden marker (#3021) Instead of using a dataset to store the isEditing property, a hidden property is now used. To support this, get/set functions and the ImageMarkerFormat were introduced. The imageMarker property can now be accessed through the format property of the image. This change eliminates the need to manually remove the dataset from the image element when extracting content from the DOM. * Include ImageMetadata in FormatState (#3023) * Support List Pasting from PowerPoint Desktop (#3012) * Refactor paste parsers: add removeNegativeTextIndentParser and deprecatedBorderColorParser; update imports and constants for bullet list types * Update packages/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts Co-authored-by: Copilot <[email protected]> * Refactor bullet list constants and improve format parser signatures --------- Co-authored-by: Copilot <[email protected]> * Remove comments `<!--` and `-->` from styles and re apply fix for Word Desktop Pasting (#3024) * Update dependencies and enhance paste functionality by cleaning HTML comments in style tags * Reapply "Refactor getStyleMetadata to not rely on DomCreator and only use Stri…" (#3020) This reverts commit 32f47bf. * Enhance cleanHtmlComments to handle both HTML comment formats in style tags * Set original DOMPurify * Update packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts Co-authored-by: Copilot <[email protected]> * Ensure headEndIndex is valid * address comment * Address comments --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Jiuqing Song <[email protected]> * insert link in the image (#3027) When the image is selected, do not replace the image with the link, add the link to image segment. * square (#3029) Instead of using a square character, this change updates the square style to use the 'square' style. * Normalize default format (#3028) * Normalize default format * improve --------- Co-authored-by: Bryan Valverde U <[email protected]> * auto link (#3026) * Add margin-inline-start to watermark styles for improved positioning (#3031) * Allow queryContentModelBlocks to query blocks in entities (#3032) * Allow queryContentModelBlocks to search children of entity * Expect EditorContext instead * Break out createEditorContextForEntity function into separate file and add tests * Fix 353323: Keep indentation when start a list, and refactor (#3033) * 353323 * fix build, add test * improve * Edit plugin Options (#3036) * options * add test * Do not indent on TAB (#3039) * keyboard tab * remove import * Fix 354663 (#3038) * Fix 354663 * export the new function * Fix Word Desktop paste case (#3034) Co-authored-by: Jiuqing Song <[email protected]> * Add height property to table rows in paste tests and processor (#3045) * Add height property to table rows in paste tests and processor * Fix build * Remove local change * Fix A11y bug with table selection (#3041) * Fix A11y bug with table selection * Add comment * Fix 341291 (#3046) * Fix Word Desktop pasting when using Safari (#3047) * Enhance paste functionality: support additional document types and extract HTML head content * Fix paste source validations for Safari: update environment handling and improve document detection logic * Add environment param back * Prevent multiple event attachments for mousemove in SelectionPlugin (#3049) * Prevent multiple event attachments for mousemove in SelectionPlugin * Update packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts * Refactor mouse event handling in SelectionPlugin to ensure proper cleanup and re-attachment on mouseDown events * Only paste text content of button (#3050) * Enhance paste functionality: Update setProcessor call counts in tests and add pasteButtonProcessor unit tests * Refactor pasteButtonProcessor: Enhance button element processing and add comprehensive unit tests * Support table edit with logical root (#3048) * Support table edit with logical root * fix build and test --------- Co-authored-by: Bryan Valverde U <[email protected]> * Bump to 9.29.0 * Fix broken code --------- Co-authored-by: Jiuqing Song <[email protected]> Co-authored-by: Bryan Valverde U <[email protected]> Co-authored-by: Julia Roldi <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: florian-msft <[email protected]>
1 parent e175370 commit 7cead40

File tree

76 files changed

+4602
-608
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+4602
-608
lines changed

packages/roosterjs-content-model-api/lib/modelApi/list/setListType.ts

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { splitSelectedParagraphByBr } from '../block/splitSelectedParagraphByBr';
22
import {
3+
copyFormat,
34
createListItem,
45
createListLevel,
56
getOperationalBlocks,
67
isBlockGroupOfType,
8+
ListFormats,
9+
ListFormatsToKeep,
10+
ListFormatsToMove,
711
mutateBlock,
812
normalizeContentModel,
913
setParagraphNotImplicit,
1014
updateListMetadata,
1115
} from 'roosterjs-content-model-dom';
1216
import type {
17+
ContentModelBlockFormat,
1318
ContentModelListItem,
1419
ReadonlyContentModelBlock,
1520
ReadonlyContentModelDocument,
@@ -54,19 +59,9 @@ export function setListType(
5459
}
5560

5661
if (alreadyInExpectedType) {
57-
//if the list item has margins or textAlign, we need to apply them to the block to preserve the indention and alignment
62+
// if the list item has margins or textAlign, we need to apply them to the block to preserve the indention and alignment
5863
block.blocks.forEach(x => {
59-
if (block.format.marginLeft) {
60-
x.format.marginLeft = block.format.marginLeft;
61-
}
62-
63-
if (block.format.marginRight) {
64-
x.format.marginRight = block.format.marginRight;
65-
}
66-
67-
if (block.format.textAlign) {
68-
x.format.textAlign = block.format.textAlign;
69-
}
64+
copyFormat<ContentModelBlockFormat>(x.format, block.format, ListFormats);
7065
});
7166
}
7267
} else {
@@ -109,19 +104,17 @@ export function setListType(
109104

110105
newListItem.blocks.push(mutableBlock);
111106

112-
if (mutableBlock.format.marginRight) {
113-
newListItem.format.marginRight = mutableBlock.format.marginRight;
114-
mutableBlock.format.marginRight = undefined;
115-
}
116-
117-
if (mutableBlock.format.marginLeft) {
118-
newListItem.format.marginLeft = mutableBlock.format.marginLeft;
119-
mutableBlock.format.marginLeft = undefined;
120-
}
121-
122-
if (mutableBlock.format.textAlign) {
123-
newListItem.format.textAlign = mutableBlock.format.textAlign;
124-
}
107+
copyFormat<ContentModelBlockFormat>(
108+
newListItem.format,
109+
mutableBlock.format,
110+
ListFormatsToMove,
111+
true /*deleteOriginalFormat*/
112+
);
113+
copyFormat<ContentModelBlockFormat>(
114+
newListItem.format,
115+
mutableBlock.format,
116+
ListFormatsToKeep
117+
);
125118

126119
mutateBlock(parent).blocks.splice(index, 1, newListItem);
127120
existingListItems.push(newListItem);

packages/roosterjs-content-model-api/test/modelApi/list/setListTypeTest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ describe('indent', () => {
470470
},
471471
format: {
472472
textAlign: 'start',
473+
direction: 'rtl',
473474
},
474475
},
475476
],

packages/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot/restoreSnapshotHTML.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
getAllEntityWrappers,
33
isBlockEntityContainer,
4+
isEntityDelimiter,
45
isEntityElement,
56
isNodeOfType,
67
parseEntityFormat,
@@ -25,6 +26,15 @@ export function restoreSnapshotHTML(core: EditorCore, snapshot: Snapshot) {
2526
const originalEntityElement = tryGetEntityElement(entityMap, currentNode);
2627

2728
if (originalEntityElement) {
29+
// After restoring the snapshot, we need to clear the delimiter indexes since cached model will be cleared
30+
if (isBlockEntityContainer(originalEntityElement)) {
31+
for (let node = originalEntityElement.firstChild; node; node = node.nextSibling) {
32+
if (isNodeOfType(node, 'ELEMENT_NODE') && isEntityDelimiter(node)) {
33+
core.cache.domIndexer?.clearIndex(node);
34+
}
35+
}
36+
}
37+
2838
refNode = reuseCachedElement(physicalRoot, originalEntityElement, refNode);
2939
} else {
3040
physicalRoot.insertBefore(currentNode, refNode);

packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ export class DomIndexerImpl implements DomIndexer {
218218
}
219219
}
220220

221+
clearIndex(container: Node) {
222+
internalClearIndex(container);
223+
}
224+
221225
reconcileSelection(
222226
model: ContentModelDocument,
223227
newSelection: DOMSelection,
@@ -711,3 +715,11 @@ function getFirstLeaf(node: Node | null): Node | null {
711715

712716
return node;
713717
}
718+
719+
function internalClearIndex(container: Node) {
720+
unindex(container as IndexedSegmentNode);
721+
722+
for (let node = container.firstChild; node; node = node.nextSibling) {
723+
internalClearIndex(node);
724+
}
725+
}

packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,13 @@ export function handleDelimiterKeyDownEvent(editor: IEditor, event: KeyDownEvent
213213

214214
switch (key) {
215215
case 'Enter':
216-
if (range.collapsed) {
217-
handleInputOnDelimiter(editor, range, getFocusedElement(selection), rawEvent);
218-
} else {
219-
const helper = editor.getDOMHelper();
220-
const entity = findClosestEntityWrapper(range.startContainer, helper);
221-
222-
if (
223-
entity &&
224-
isNodeOfType(entity, 'ELEMENT_NODE') &&
225-
helper.isNodeInEditor(entity)
226-
) {
227-
triggerEntityEventOnEnter(editor, entity, rawEvent);
228-
}
216+
const helper = editor.getDOMHelper();
217+
const entity = findClosestEntityWrapper(range.startContainer, helper);
218+
219+
if (entity && isNodeOfType(entity, 'ELEMENT_NODE') && helper.isNodeInEditor(entity)) {
220+
triggerEntityEventOnEnter(editor, entity, rawEvent);
229221
}
222+
230223
break;
231224

232225
case 'ArrowLeft':

packages/roosterjs-content-model-core/lib/corePlugin/format/FormatPlugin.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isCharacterValue,
77
isCursorMovingKey,
88
isNodeOfType,
9+
normalizeFontFamily,
910
normalizeSegmentFormat,
1011
} from 'roosterjs-content-model-dom';
1112
import type {
@@ -55,6 +56,12 @@ class FormatPlugin implements PluginWithState<FormatPluginState> {
5556
pendingFormat: null,
5657
};
5758

59+
const defaultFormat = this.state.defaultFormat;
60+
61+
if (defaultFormat.fontFamily) {
62+
defaultFormat.fontFamily = normalizeFontFamily(defaultFormat.fontFamily);
63+
}
64+
5865
this.defaultFormatKeys = new Set<keyof CSSStyleDeclaration>();
5966

6067
getObjectKeys(DefaultStyleKeyMap).forEach(key => {

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
219219
}
220220
}
221221

222+
this.state.mouseDisposer?.();
222223
this.state.mouseDisposer = editor.attachDomEvent({
223224
mousemove: {
224225
beforeDispatch: this.onMouseMove,
@@ -350,6 +351,26 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
350351
break;
351352

352353
case 'table':
354+
// After a content change event is handled tableSelection state is reset to null
355+
// Since we have table selection from DOMSelection, we can use it to re-create the tableSelection state
356+
if (this.state.tableSelection == null) {
357+
const { table, firstRow, firstColumn, lastRow, lastColumn } = selection;
358+
359+
const parsedTable = parseTableCells(table);
360+
if (parsedTable) {
361+
const firstCo = { row: firstRow, col: firstColumn };
362+
const lastCo = { row: lastRow, col: lastColumn };
363+
364+
// Create the tableSelection with current table info
365+
this.state.tableSelection = {
366+
table,
367+
parsedTable,
368+
firstCo,
369+
lastCo,
370+
startNode: findTableCellElement(parsedTable, firstCo)?.cell || table,
371+
};
372+
}
373+
}
353374
if (this.state.tableSelection?.lastCo) {
354375
const { shiftKey, key } = rawEvent;
355376

packages/roosterjs-content-model-core/lib/editor/Editor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export class Editor implements IEditor {
4949
constructor(contentDiv: HTMLDivElement, options: EditorOptions = {}) {
5050
this.core = createEditorCore(contentDiv, options);
5151

52-
const initialModel = options.initialModel ?? createEmptyModel(options.defaultSegmentFormat);
52+
const initialModel =
53+
options.initialModel ?? createEmptyModel(this.core.format.defaultFormat);
5354

5455
this.core.api.setContentModel(
5556
this.core,

packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ describe('paste with content model & paste plugin', () => {
167167

168168
paste(editor!, clipboardData);
169169

170-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1);
170+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
171171
expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5);
172172
expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1);
173173
});
@@ -178,7 +178,7 @@ describe('paste with content model & paste plugin', () => {
178178

179179
paste(editor!, clipboardData);
180180

181-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
181+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(3);
182182
expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7);
183183
expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(1);
184184
});
@@ -189,7 +189,7 @@ describe('paste with content model & paste plugin', () => {
189189

190190
paste(editor!, clipboardData);
191191

192-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1);
192+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
193193
expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1);
194194
expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(1);
195195
});
@@ -200,7 +200,7 @@ describe('paste with content model & paste plugin', () => {
200200

201201
paste(editor!, clipboardData);
202202

203-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1);
203+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
204204
expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1);
205205
expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(1);
206206
});
@@ -211,7 +211,7 @@ describe('paste with content model & paste plugin', () => {
211211

212212
paste(editor!, clipboardData);
213213

214-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1);
214+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
215215
expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1);
216216
expect(PPT.processPastedContentFromPowerPoint).toHaveBeenCalledTimes(1);
217217
});
@@ -223,7 +223,7 @@ describe('paste with content model & paste plugin', () => {
223223

224224
paste(editor!, clipboardData, 'asPlainText');
225225

226-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1);
226+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
227227
expect(addParserF.addParser).toHaveBeenCalledTimes(9);
228228
expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1);
229229
});
@@ -234,7 +234,7 @@ describe('paste with content model & paste plugin', () => {
234234

235235
paste(editor!, clipboardData, 'asPlainText');
236236

237-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
237+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(3);
238238
expect(addParserF.addParser).toHaveBeenCalledTimes(11);
239239
expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(1);
240240
});
@@ -245,7 +245,7 @@ describe('paste with content model & paste plugin', () => {
245245

246246
paste(editor!, clipboardData, 'asPlainText');
247247

248-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0);
248+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1);
249249
expect(addParserF.addParser).toHaveBeenCalledTimes(4);
250250
expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(0);
251251
});
@@ -256,7 +256,7 @@ describe('paste with content model & paste plugin', () => {
256256

257257
paste(editor!, clipboardData, 'asPlainText');
258258

259-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0);
259+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1);
260260
expect(addParserF.addParser).toHaveBeenCalledTimes(4);
261261
expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(0);
262262
});
@@ -267,7 +267,7 @@ describe('paste with content model & paste plugin', () => {
267267

268268
paste(editor!, clipboardData, 'asPlainText');
269269

270-
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1);
270+
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
271271
expect(addParserF.addParser).toHaveBeenCalledTimes(5);
272272
expect(PPT.processPastedContentFromPowerPoint).toHaveBeenCalledTimes(1);
273273
});

packages/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshot/restoreSnapshotHTMLTest.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,53 @@ describe('restoreSnapshotHTML', () => {
11571157
expect(div.childNodes[1]).toBe(container2);
11581158
expect(div.childNodes[1].firstChild).toBe(entityWrapper2);
11591159
});
1160+
1161+
it('HTML with block entity at root level, need to clear index of entity delimiter', () => {
1162+
const snapshot: Snapshot = {
1163+
html:
1164+
'<div class="_E_EBlockEntityContainer"><span class="entityDelimiterBefore">\u200B</span><div class="_Entity _EType_A _EId_B1"><br></div><span class="entityDelimiterAfter">\u200B</span></div>',
1165+
} as any;
1166+
1167+
const entityWrapper1 = document.createElement('DIV');
1168+
const delimiterBefore = document.createElement('span');
1169+
const delimiterAfter = document.createElement('span');
1170+
const wrapper = document.createElement('div');
1171+
1172+
delimiterBefore.className = 'entityDelimiterBefore';
1173+
delimiterBefore.innerHTML = '\u200B';
1174+
delimiterAfter.className = 'entityDelimiterAfter';
1175+
delimiterAfter.innerHTML = '\u200B';
1176+
entityWrapper1.id = 'div1';
1177+
1178+
wrapper.className = '_E_EBlockEntityContainer';
1179+
wrapper.appendChild(delimiterBefore);
1180+
wrapper.appendChild(entityWrapper1);
1181+
wrapper.appendChild(delimiterAfter);
1182+
1183+
div.appendChild(wrapper);
1184+
1185+
core.entity.entityMap.B1 = {
1186+
element: entityWrapper1,
1187+
canPersist: true,
1188+
};
1189+
1190+
const clearIndexSpy = jasmine.createSpy('clearIndex');
1191+
core.cache = {
1192+
domIndexer: {
1193+
clearIndex: clearIndexSpy,
1194+
} as any,
1195+
};
1196+
1197+
restoreSnapshotHTML(core, snapshot);
1198+
1199+
expect(div.innerHTML).toBe(
1200+
'<div class="_E_EBlockEntityContainer"><span class="entityDelimiterBefore">\u200B</span><div id="div1"></div><span class="entityDelimiterAfter">\u200B</span></div>'
1201+
);
1202+
expect(div.childNodes[0]).toBe(wrapper);
1203+
expect(clearIndexSpy).toHaveBeenCalledTimes(2);
1204+
expect(clearIndexSpy).toHaveBeenCalledWith(delimiterBefore);
1205+
expect(clearIndexSpy).toHaveBeenCalledWith(delimiterAfter);
1206+
});
11601207
});
11611208

11621209
function wrapInContainer(entity: HTMLElement) {

0 commit comments

Comments
 (0)