Skip to content

Commit 924f0ff

Browse files
Add support for OneNote document pasting (#2983)
* Add support for OneNote document pasting and validation * Enhance OneNote paste handling * Address comment * Rename file
1 parent 0996a11 commit 924f0ff

File tree

12 files changed

+5539
-11
lines changed

12 files changed

+5539
-11
lines changed

packages/roosterjs-content-model-dom/lib/domUtils/readFile.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
* If fail to read, dataUrl will be null
66
*/
77
export function readFile(file: File, callback: (dataUrl: string | null) => void) {
8-
if (file) {
9-
const reader = new FileReader();
10-
reader.onload = () => {
11-
callback(reader.result as string);
12-
};
13-
reader.onerror = () => {
14-
callback(null);
15-
};
16-
reader.readAsDataURL(file);
8+
try {
9+
if (file) {
10+
const reader = new FileReader();
11+
reader.onload = () => {
12+
callback(reader.result as string);
13+
};
14+
reader.onerror = () => {
15+
callback(null);
16+
};
17+
reader.readAsDataURL(file);
18+
}
19+
} catch {
20+
callback(null);
1721
}
1822
}

packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ export const handleList: ContentModelBlockHandler<ContentModelListItem> = (
3333
if (
3434
stackLevel.listType != itemLevel.listType ||
3535
stackLevel.dataset?.editingInfo != itemLevel.dataset.editingInfo ||
36-
(itemLevel.listType == 'OL' && typeof itemLevel.format.startNumberOverride === 'number')
36+
(itemLevel.listType == 'OL' &&
37+
typeof itemLevel.format.startNumberOverride === 'number') ||
38+
(itemLevel.listType == 'UL' &&
39+
itemLevel.format.listStyleType != stackLevel.format?.listStyleType)
3740
) {
3841
break;
3942
}

packages/roosterjs-content-model-plugins/lib/paste/PastePlugin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getPasteSource } from './pasteSourceValidations/getPasteSource';
77
import { parseLink } from './utils/linkParser';
88
import { PastePropertyNames } from './pasteSourceValidations/constants';
99
import { processPastedContentFromExcel } from './Excel/processPastedContentFromExcel';
10+
import { processPastedContentFromOneNote } from './oneNote/processPastedContentFromOneNote';
1011
import { processPastedContentFromPowerPoint } from './PowerPoint/processPastedContentFromPowerPoint';
1112
import { processPastedContentFromWordDesktop } from './WordDesktop/processPastedContentFromWordDesktop';
1213
import { processPastedContentWacComponents } from './WacComponents/processPastedContentWacComponents';
@@ -125,6 +126,10 @@ export class PastePlugin implements EditorPlugin {
125126
case 'powerPointDesktop':
126127
processPastedContentFromPowerPoint(event, this.editor.getDOMCreator());
127128
break;
129+
130+
case 'oneNoteDesktop':
131+
processPastedContentFromOneNote(event);
132+
break;
128133
}
129134

130135
addParser(event.domToModelOption, 'link', parseLink);
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { setProcessor } from '../utils/setProcessor';
2+
import type {
3+
BeforePasteEvent,
4+
DomToModelContext,
5+
ElementProcessor,
6+
} from 'roosterjs-content-model-types';
7+
8+
const OrderedListStyleMap = {
9+
1: 'decimal',
10+
a: 'lower-alpha',
11+
A: 'upper-alpha',
12+
i: 'lower-roman',
13+
I: 'upper-roman',
14+
} as const;
15+
16+
const UnorderedListStyleMap = {
17+
disc: 'disc',
18+
circle: 'circle',
19+
square: 'square',
20+
} as const;
21+
22+
/**
23+
* @internal
24+
*/
25+
export interface OneNoteListContext {
26+
listStyleType?: string;
27+
startNumberOverride?: number;
28+
}
29+
30+
/**
31+
* @internal
32+
*/
33+
export interface OneNoteDomToModelContext extends DomToModelContext {
34+
oneNoteListContext?: OneNoteListContext;
35+
}
36+
37+
/**
38+
* @internal
39+
* Processes the content pasted from OneNote by setting up custom processors
40+
* for ordered lists (`<ol>`) and list items (`<li>`). These processors handle
41+
* specific list styles and numbering overrides that may be present in OneNote
42+
* content.
43+
*
44+
* @param event - The `BeforePasteEvent` containing the DOM-to-model options
45+
* and other context information for the paste operation.
46+
*/
47+
export function processPastedContentFromOneNote(event: BeforePasteEvent): void {
48+
setProcessor(event.domToModelOption, 'ol', processOrderedList);
49+
setProcessor(event.domToModelOption, 'ul', processUnorderedList);
50+
setProcessor(event.domToModelOption, 'li', processListItem);
51+
}
52+
53+
/**
54+
* @internal exported only for unit test
55+
* Content from OneNote may have ordered lists with specific styles and start numbers.
56+
* This function processes the `<ol>` elements, extracting the `type` and `start` custom attributes
57+
* to set the appropriate list style and starting number in the `oneNoteListContext` of the provided context.
58+
* Which is then used to format the list items within the list.
59+
*/
60+
export const processOrderedList: ElementProcessor<HTMLOListElement> = (
61+
group,
62+
element,
63+
cmContext
64+
) => {
65+
const context = ensureOneNoteListContext(cmContext);
66+
67+
if (context.oneNoteListContext) {
68+
const typeOfList = element.getAttribute('type');
69+
if (typeOfList) {
70+
const listStyle = OrderedListStyleMap[typeOfList as keyof typeof OrderedListStyleMap];
71+
const startNumberOverride = parseInt(element.getAttribute('start') || '1') || 1;
72+
73+
context.oneNoteListContext.listStyleType = listStyle;
74+
context.oneNoteListContext.startNumberOverride = startNumberOverride;
75+
}
76+
}
77+
78+
context.defaultElementProcessors.ol?.(group, element, context);
79+
};
80+
81+
/**
82+
* @internal exported only for unit test
83+
* Content from OneNote may have ordered lists with specific styles and start numbers.
84+
* This function processes the `<ul>` elements, extracting the `type` custom attribute
85+
* to set the appropriate list style in the `oneNoteListContext` of the provided context.
86+
* Which is then used to format the list items within the list.
87+
*/
88+
export const processUnorderedList: ElementProcessor<HTMLUListElement> = (
89+
group,
90+
element,
91+
cmContext
92+
) => {
93+
const context = ensureOneNoteListContext(cmContext);
94+
95+
if (context.oneNoteListContext) {
96+
const typeOfList = element.getAttribute('type');
97+
if (typeOfList) {
98+
const listStyle =
99+
UnorderedListStyleMap[typeOfList as keyof typeof UnorderedListStyleMap];
100+
context.oneNoteListContext.listStyleType = listStyle;
101+
}
102+
}
103+
104+
context.defaultElementProcessors.ul?.(group, element, context);
105+
};
106+
107+
/**
108+
* @internal exported only for unit test
109+
* Processes the `<li>` elements within a list. It checks if the `oneNoteListContext`
110+
* is present in the provided context. If so, it applies the list style type and
111+
* start number override to the last level of the list format.
112+
* This ensures that the list items are formatted correctly according to the
113+
* OneNote list context.
114+
*/
115+
export const processListItem: ElementProcessor<HTMLLIElement> = (group, element, cmContext) => {
116+
const context = ensureOneNoteListContext(cmContext);
117+
let removeStartNumberOverride = false;
118+
119+
if (context.oneNoteListContext) {
120+
const { listStyleType, startNumberOverride } = context.oneNoteListContext;
121+
if (listStyleType) {
122+
const lastLevel = context.listFormat.levels[context.listFormat.levels.length - 1];
123+
lastLevel.format.listStyleType = listStyleType;
124+
125+
if (startNumberOverride) {
126+
removeStartNumberOverride = true;
127+
lastLevel.format.startNumberOverride = startNumberOverride;
128+
129+
delete context.oneNoteListContext.startNumberOverride;
130+
}
131+
delete context.oneNoteListContext.listStyleType;
132+
}
133+
}
134+
135+
context.defaultElementProcessors.li?.(group, element, context);
136+
137+
if (removeStartNumberOverride) {
138+
delete context.listFormat.levels[context.listFormat.levels.length - 1].format
139+
.startNumberOverride;
140+
}
141+
};
142+
143+
function ensureOneNoteListContext(cmContext: DomToModelContext): OneNoteDomToModelContext {
144+
const context = cmContext as OneNoteDomToModelContext;
145+
146+
if (!context.oneNoteListContext) {
147+
context.oneNoteListContext = {};
148+
}
149+
150+
return context;
151+
}

packages/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { isExcelDesktopDocument } from './isExcelDesktopDocument';
33
import { isExcelNotNativeEvent } from './isExcelNonNativeEvent';
44
import { isExcelOnlineDocument } from './isExcelOnlineDocument';
55
import { isGoogleSheetDocument } from './isGoogleSheetDocument';
6+
import { isOneNoteDesktopDocument } from './isOneNoteDocument';
67
import { isPowerPointDesktopDocument } from './isPowerPointDesktopDocument';
78
import { isWordDesktopDocument } from './isWordDesktopDocument';
89
import { shouldConvertToSingleImage } from './shouldConvertToSingleImage';
@@ -31,7 +32,8 @@ export type KnownPasteSourceType =
3132
| 'wacComponents'
3233
| 'default'
3334
| 'singleImage'
34-
| 'excelNonNativeEvent';
35+
| 'excelNonNativeEvent'
36+
| 'oneNoteDesktop';
3537

3638
/**
3739
* @internal
@@ -47,6 +49,7 @@ const getSourceFunctions = new Map<KnownPasteSourceType, GetSourceFunction>([
4749
['googleSheets', isGoogleSheetDocument],
4850
['singleImage', shouldConvertToSingleImage],
4951
['excelNonNativeEvent', isExcelNotNativeEvent],
52+
['oneNoteDesktop', isOneNoteDesktopDocument],
5053
]);
5154

5255
/**
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { PastePropertyNames } from './constants';
2+
import type { GetSourceFunction } from './getPasteSource';
3+
4+
const ONE_NOTE_ATTRIBUTE_VALUE = 'OneNote.File';
5+
6+
/**
7+
* @internal
8+
* Checks whether the provided HTML attributes identify a OneNote Desktop document
9+
* @param props Properties related to the PasteEvent
10+
* @returns True if the document is identified as a OneNote Desktop document, otherwise false
11+
*/
12+
export const isOneNoteDesktopDocument: GetSourceFunction = props => {
13+
const { htmlAttributes } = props;
14+
// The presence of this attribute confirms its origin from OneNote Desktop
15+
return htmlAttributes[PastePropertyNames.PROG_ID_NAME] == ONE_NOTE_ATTRIBUTE_VALUE;
16+
};

0 commit comments

Comments
 (0)