Skip to content

Commit

Permalink
Support adding custom data when creating editor (#320)
Browse files Browse the repository at this point in the history
* customdata & undo

* custom data

* fix merge issue
  • Loading branch information
JiuqingSong authored Jul 31, 2019
1 parent 0f6d063 commit 921bb34
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 15 deletions.
4 changes: 2 additions & 2 deletions packages/roosterjs-editor-core/lib/coreAPI/getCustomData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import EditorCore, { GetCustomData } from '../interfaces/EditorCore';
* @param core The EditorCore object
* @param key Key of the custom data
* @param getter Getter function. If custom data for the given key doesn't exist,
* call this function to get one and store it.
* call this function to get one and store it if it is specified. Otherwise return undefined
* @param disposer An optional disposer function to dispose this custom data when
* dispose editor.
*/
Expand All @@ -16,7 +16,7 @@ export const getCustomData: GetCustomData = <T>(
disposer?: (value: T) => void
): T => {
return (core.customData[key] = core.customData[key] || {
value: getter(),
value: getter ? getter() : undefined,
disposer,
}).value as T;
};
4 changes: 2 additions & 2 deletions packages/roosterjs-editor-core/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,11 +776,11 @@ export default class Editor {
* Get custom data related to this editor
* @param key Key of the custom data
* @param getter Getter function. If custom data for the given key doesn't exist,
* call this function to get one and store it.
* call this function to get one and store it if it is specified. Otherwise return undefined
* @param disposer An optional disposer function to dispose this custom data when
* dispose editor.
*/
public getCustomData<T>(key: string, getter: () => T, disposer?: (value: T) => void): T {
public getCustomData<T>(key: string, getter?: () => T, disposer?: (value: T) => void): T {
return this.core.api.getCustomData(this.core, key, getter, disposer);
}

Expand Down
21 changes: 19 additions & 2 deletions packages/roosterjs-editor-core/lib/editor/createEditorCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Undo from '../undo/Undo';
import { attachDomEvent } from '../coreAPI/attachDomEvent';
import { Browser } from 'roosterjs-editor-dom';
import { calculateDefaultFormat } from '../coreAPI/calculateDefaultFormat';
import { CustomDataMap } from '../interfaces/CustomData';
import { editWithUndo } from '../coreAPI/editWithUndo';
import { focus } from '../coreAPI/focus';
import { getCustomData } from '../coreAPI/getCustomData';
Expand Down Expand Up @@ -45,10 +46,14 @@ export default function createEditorCore(
return {
contentDiv,
document: contentDiv.ownerDocument,
defaultFormat: calculateDefaultFormat(contentDiv, options.defaultFormat, options.inDarkMode),
defaultFormat: calculateDefaultFormat(
contentDiv,
options.defaultFormat,
options.inDarkMode
),
corePlugins,
currentUndoSnapshot: null,
customData: {},
customData: createCustomData(options.customData || {}),
cachedSelectionRange: null,
plugins: allPlugins,
eventHandlerPlugins: eventHandlerPlugins,
Expand Down Expand Up @@ -87,3 +92,15 @@ function createCoreApiMap(map?: Partial<CoreApiMap>): CoreApiMap {
triggerEvent: map.triggerEvent || triggerEvent,
};
}

function createCustomData(initValue: { [key: string]: any }): CustomDataMap {
return Object.keys(initValue).reduce(
(result, key) => {
result[key] = {
value: initValue[key],
};
return result;
},
<CustomDataMap>{}
);
}
1 change: 1 addition & 0 deletions packages/roosterjs-editor-core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
GenericContentEditFeature,
Keys,
} from './interfaces/ContentEditFeature';
export { default as CustomData, CustomDataMap } from './interfaces/CustomData';
export {
default as EditorCore,
CorePlugins,
Expand Down
20 changes: 20 additions & 0 deletions packages/roosterjs-editor-core/lib/interfaces/CustomData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Custom data stored in editor
*/
export default interface CustomData {
/**
* Value of this custom data
*/
value: any;

/**
* Optional disposer function of the custom data.
* When a valid value is set, it will be invoked when editor is disposing
*/
disposer?: (value: any) => void;
}

/**
* Define the type of a map from key to custom data
*/
export type CustomDataMap = { [key: string]: CustomData };
10 changes: 3 additions & 7 deletions packages/roosterjs-editor-core/lib/interfaces/EditorCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import FirefoxTypeAfterLink from '../corePlugins/FirefoxTypeAfterLink';
import MouseUpPlugin from '../corePlugins/MouseUpPlugin';
import TypeInContainerPlugin from '../corePlugins/TypeInContainerPlugin';
import UndoService from './UndoService';
import { CustomDataMap } from './CustomData';
import {
ChangeSource,
DefaultFormat,
Expand Down Expand Up @@ -94,12 +95,7 @@ export default interface EditorCore {
/**
* Custom data of this editor
*/
readonly customData: {
[Key: string]: {
value: any;
disposer: (value: any) => void;
};
};
readonly customData: CustomDataMap;

/**
* Core API map of this editor
Expand Down Expand Up @@ -170,7 +166,7 @@ export type Focus = (core: EditorCore) => void;
* @param core The EditorCore object
* @param key Key of the custom data
* @param getter Getter function. If custom data for the given key doesn't exist,
* call this function to get one and store it.
* call this function to get one and store it if it is specified. Otherwise return undefined
* @param disposer An optional disposer function to dispose this custom data when
* dispose editor.
*/
Expand Down
10 changes: 10 additions & 0 deletions packages/roosterjs-editor-core/lib/interfaces/EditorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,14 @@ export default interface EditorOptions {
* Dark mode options for default format and paste handler
*/
darkModeOptions?: DarkModeOptions;

/**
* Initial custom data.
* Use this option to set custom data before any plugin is initialized,
* so that plugins can access the custom data safely.
* The value of this map is the value of each custom data. No disposer function to specify here.
* Because when set custom data via this way, it means the custom data value is created before editor,
* so editor shouldn't control the lifecycle of these objects, and caller need to manage its lifecycle.
*/
customData?: { [key: string]: any };
}
8 changes: 7 additions & 1 deletion packages/roosterjs-editor-core/lib/test/TestHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import UndoService from '../interfaces/UndoService';

export * from 'roosterjs-editor-dom/lib/test/DomTestHelper';

export function initEditor(id: string, plugins?: EditorPlugin[], undo?: UndoService) {
export function initEditor(
id: string,
plugins?: EditorPlugin[],
undo?: UndoService,
customData?: { [key: string]: any }
) {
let node = document.createElement('div');
node.id = id;
document.body.insertBefore(node, document.body.childNodes[0]);
Expand All @@ -18,6 +23,7 @@ export function initEditor(id: string, plugins?: EditorPlugin[], undo?: UndoServ
fontSize: '11pt',
textColor: '#000000',
},
customData: customData,
};

let editor = new Editor(node as HTMLDivElement, options);
Expand Down
64 changes: 64 additions & 0 deletions packages/roosterjs-editor-core/lib/test/editor/EditorTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,67 @@ describe('Editor contains()', () => {
expect(containsNode).toBe(false);
});
});

describe('Editor getCustomData()', () => {
const CustomDataKey = 'DATA';
it('Get custom data with getter', () => {
editor = TestHelper.initEditor(testID);

let data = {
test: 'result',
};
let callCount = 0;
let callback = () => {
callCount++;
return data;
};
let result = editor.getCustomData(CustomDataKey, callback);
expect(result).toBe(data);
expect(callCount).toBe(1);

result = null;
result = editor.getCustomData(CustomDataKey, callback);
expect(result).toBe(data);
expect(callCount).toBe(1);
});

it('Get custom data without getter', () => {
editor = TestHelper.initEditor(testID);

let result = editor.getCustomData(CustomDataKey);
expect(result).toBeUndefined();
});

it('Get custom data with disposer', () => {
editor = TestHelper.initEditor(testID);

let objCount = 1;
let data = {
test: 'result',
};

editor.getCustomData(
CustomDataKey,
() => data,
() => {
objCount--;
}
);
expect(objCount).toBe(1);

editor.dispose();
expect(objCount).toBe(0);
});

it('Get predefined custom data', () => {
let data = {
test: 'result',
};
editor = TestHelper.initEditor(testID, null, null, {
[CustomDataKey]: data,
});

let result = editor.getCustomData(CustomDataKey);
expect(result).toBe(data);
});
});
6 changes: 5 additions & 1 deletion packages/roosterjs-editor-core/lib/undo/Undo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ export default class Undo implements UndoService {

switch (event.eventType) {
case PluginEventType.EditorReady:
this.addUndoSnapshot();
if (!this.preserveSnapshots || (!this.canUndo() && !this.canRedo())) {
// Only add initial snapshot when we don't need to preserve snapshots or there is no existing snapshot
// Otherwise preserved undo/redo state may be ruined
this.addUndoSnapshot();
}
break;
case PluginEventType.KeyDown:
this.onKeyDown(event.rawEvent);
Expand Down

0 comments on commit 921bb34

Please sign in to comment.