Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions demo/scripts/controlsV2/demoButtons/exportContentButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { RibbonButton } from 'roosterjs-react';
export type ExportButtonStringKey =
| 'buttonNameExport'
| 'menuNameExportHTML'
| 'menuNameExportHTMLFast'
| 'menuNameExportText';

const callbacks: ModelToTextCallbacks = {
Expand All @@ -26,6 +27,7 @@ export const exportContentButton: RibbonButton<ExportButtonStringKey> = {
dropDownMenu: {
items: {
menuNameExportHTML: 'as HTML',
menuNameExportHTMLFast: 'as HTML (fast version)',
menuNameExportText: 'as Plain Text',
},
},
Expand All @@ -45,6 +47,8 @@ export const exportContentButton: RibbonButton<ExportButtonStringKey> = {
},
}) +
'</body></html>';
} else if (key == 'menuNameExportHTMLFast') {
html = exportContent(editor, 'HTMLFast');
} else if (key == 'menuNameExportText') {
html = `<pre>${exportContent(editor, 'PlainText', callbacks)}</pre>`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import {
contentModelToDom,
contentModelToText,
createModelToDomContext,
transformColor,
} from 'roosterjs-content-model-dom';
import type {
ContentModelDocument,
ExportContentMode,
IEditor,
ModelToDomOption,
Expand All @@ -13,11 +15,19 @@ import type {
/**
* Export HTML content. If there are entities, this will cause EntityOperation event with option = 'replaceTemporaryContent' to get a dehydrated entity
* @param editor The editor to get content from
* @param mode Specify HTML to get plain text result. This is the default option
* @param mode Specify HTML to get HTML. This is the default option
* @param options @optional Options for Model to DOM conversion
*/
export function exportContent(editor: IEditor, mode?: 'HTML', options?: ModelToDomOption): string;

/**
* Export HTML content. If there are entities, this will cause EntityOperation event with option = 'replaceTemporaryContent' to get a dehydrated entity.
* This is a fast version, it retrieve HTML content directly from editor without going through content model conversion.
* @param editor The editor to get content from
* @param mode Specify HTMLFast to get HTML result.
*/
export function exportContent(editor: IEditor, mode: 'HTMLFast'): string;

/**
* Export plain text content
* @param editor The editor to get content from
Expand All @@ -38,23 +48,45 @@ export function exportContent(
*/
export function exportContent(editor: IEditor, mode: 'PlainTextFast'): string;

// Here I didn't add 'HTMLFast' to ExportContentMode type because it will make this a breaking change and EditorAdapter will see build time error without bumping version
// Once we are confident that 'HTMLFast' is stable, we can fully switch 'HTML' to use the 'HTMLFast' approach
export function exportContent(
editor: IEditor,
mode: ExportContentMode = 'HTML',
mode: ExportContentMode | 'HTMLFast' = 'HTML',
optionsOrCallbacks?: ModelToDomOption | ModelToTextCallbacks
): string {
if (mode == 'PlainTextFast') {
return editor.getDOMHelper().getTextContent();
} else {
const model = editor.getContentModelCopy('clean');
let model: ContentModelDocument;

switch (mode) {
case 'PlainTextFast':
return editor.getDOMHelper().getTextContent();
case 'PlainText':
model = editor.getContentModelCopy('clean');

if (mode == 'PlainText') {
return contentModelToText(
model,
undefined /*separator*/,
optionsOrCallbacks as ModelToTextCallbacks
);
} else {

case 'HTMLFast':
const clonedRoot = editor.getDOMHelper().getClonedRoot();

if (editor.isDarkMode()) {
transformColor(
clonedRoot,
false /*includeSelf*/,
'darkToLight',
editor.getColorManager()
);
}

return getHTMLFromDOM(editor, clonedRoot);

case 'HTML':
default:
model = editor.getContentModelCopy('clean');

const doc = editor.getDocument();
const div = doc.createElement('div');

Expand All @@ -68,9 +100,12 @@ export function exportContent(
)
);

editor.triggerEvent('extractContentWithDom', { clonedRoot: div }, true /*broadcast*/);

return div.innerHTML;
}
return getHTMLFromDOM(editor, div);
}
}

function getHTMLFromDOM(editor: IEditor, root: HTMLElement): string {
editor.triggerEvent('extractContentWithDom', { clonedRoot: root }, true /*broadcast*/);

return root.innerHTML;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as contentModelToDom from 'roosterjs-content-model-dom/lib/modelToDom/contentModelToDom';
import * as contentModelToText from 'roosterjs-content-model-dom/lib/modelToText/contentModelToText';
import * as createModelToDomContext from 'roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext';
import * as transformColor from 'roosterjs-content-model-dom/lib/domUtils/style/transformColor';
import { exportContent } from '../../../lib/command/exportContent/exportContent';
import { IEditor } from 'roosterjs-content-model-types';

Expand Down Expand Up @@ -152,4 +153,82 @@ describe('exportContent', () => {
true
);
});

it('HTMLFast in light mode', () => {
const mockedHTML = 'HTML';
const mockedClonedRoot = {
innerHTML: mockedHTML,
};
const mockedDOMHelper = {
getClonedRoot: () => mockedClonedRoot,
} as any;
const getDOMHelperSpy = jasmine.createSpy('getDOMHelper').and.returnValue(mockedDOMHelper);
const mockedDiv = {} as any;
const mockedDoc = {
createElement: () => mockedDiv,
} as any;
const triggerEventSpy = jasmine.createSpy('triggerEvent');
const editor: IEditor = {
getDocument: () => mockedDoc,
triggerEvent: triggerEventSpy,
getDOMHelper: getDOMHelperSpy,
isDarkMode: () => false,
} as any;

const html = exportContent(editor, 'HTMLFast');

expect(html).toBe(mockedHTML);
expect(getDOMHelperSpy).toHaveBeenCalledWith();
expect(triggerEventSpy).toHaveBeenCalledWith(
'extractContentWithDom',
{ clonedRoot: mockedClonedRoot },
true
);
});

it('HTMLFast in dark mode', () => {
const mockedColorManager = 'COLOR_MANAGER' as any;
const transformedHTML = 'Transformed HTML';
const transformColorSpy = spyOn(transformColor, 'transformColor').and.callFake(
(rootNode: any) => {
rootNode.innerHTML = transformedHTML;
}
);
const mockedHTML = 'HTML';
const mockedClonedRoot = {
innerHTML: mockedHTML,
} as any;
const mockedDOMHelper = {
getClonedRoot: () => mockedClonedRoot,
} as any;
const getDOMHelperSpy = jasmine.createSpy('getDOMHelper').and.returnValue(mockedDOMHelper);
const mockedDiv = {} as any;
const mockedDoc = {
createElement: () => mockedDiv,
} as any;
const triggerEventSpy = jasmine.createSpy('triggerEvent');
const editor: IEditor = {
getDocument: () => mockedDoc,
triggerEvent: triggerEventSpy,
getDOMHelper: getDOMHelperSpy,
isDarkMode: () => true,
getColorManager: () => mockedColorManager,
} as any;

const html = exportContent(editor, 'HTMLFast');

expect(html).toBe(transformedHTML);
expect(getDOMHelperSpy).toHaveBeenCalledWith();
expect(triggerEventSpy).toHaveBeenCalledWith(
'extractContentWithDom',
{ clonedRoot: mockedClonedRoot },
true
);
expect(transformColorSpy).toHaveBeenCalledWith(
mockedClonedRoot,
false,
'darkToLight',
mockedColorManager
);
});
});
Loading