This commit is contained in:
gohabereg 2019-02-04 10:06:35 +03:00
parent b89f756a03
commit 6047e83272
38 changed files with 26077 additions and 147 deletions

25372
dist/codex-editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,7 @@
import {EditorModules} from '../types-internal/editor-modules';
import {EditorConfig} from '../../types';
import {ModuleConfig} from '../types-internal/module-config';
import {EventEmitter} from './eventEmitter';
/**
* @abstract
@ -11,7 +12,7 @@ import {ModuleConfig} from '../types-internal/module-config';
* @property {Object} config - Editor user settings
* @property {EditorModules} Editor - List of Editor modules
*/
export default class Module {
export default class Module extends EventEmitter {
/**
* Editor modules list
@ -30,6 +31,8 @@ export default class Module {
* @param {EditorConfig}
*/
constructor({config}: ModuleConfig) {
super();
if (new.target === Module) {
throw new TypeError('Constructors for abstract class Module are not allowed.');
}

View file

@ -63,7 +63,7 @@ export default class DeleteTune implements BlockTune {
public render() {
this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {});
this.nodes.button.appendChild($.svg('cross', 12, 12));
this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false);
this.api.listeners.add(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false);
return this.nodes.button;
}
@ -85,14 +85,14 @@ export default class DeleteTune implements BlockTune {
* When toolbar block settings is closed but block deletion is not confirmed,
* then reset confirmation state
*/
this.api.events.on('block-settings-closed', this.resetConfirmation);
this.api.toolbar.on('block-settings/closed', this.resetConfirmation);
} else {
/**
* Unsubscribe from block-settings closing event
*/
this.api.events.off('block-settings-closed', this.resetConfirmation);
this.api.toolbar.off('block-settings/closed', this.resetConfirmation);
this.api.blocks.delete();

View file

@ -40,7 +40,7 @@ export default class MoveDownTune implements BlockTune {
public render() {
const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
moveDownButton.appendChild($.svg('arrow-down', 14, 14));
this.api.listeners.on(
this.api.listeners.add(
moveDownButton,
'click',
(event) => this.handleClick(event as MouseEvent, moveDownButton),

View file

@ -41,7 +41,7 @@ export default class MoveUpTune implements BlockTune {
public render(): HTMLElement {
const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
moveUpButton.appendChild($.svg('arrow-up', 14, 14));
this.api.listeners.on(
this.api.listeners.add(
moveUpButton,
'click',
(event) => this.handleClick(event as MouseEvent, moveUpButton),

View file

@ -27,6 +27,7 @@ import MoveUpTune from './block-tunes/block-tune-move-up';
import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down';
import SelectionUtils from './selection';
import {EventEmitter} from './eventEmitter';
/**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
@ -36,7 +37,7 @@ import SelectionUtils from './selection';
* @property holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
* @property pluginsContent - HTML content that returns by Tool's render function
*/
export default class Block {
export default class Block extends EventEmitter {
/**
* CSS classes for the Block
@ -326,6 +327,8 @@ export default class Block {
settings: ToolConfig,
apiMethods: API,
) {
super();
this.name = toolName;
this.tool = toolInstance;
this.class = toolClass;
@ -334,6 +337,7 @@ export default class Block {
this.holder = this.compose();
this.mutationObserver = new MutationObserver(this.didMutated);
this.holder.addEventListener('beforeinput', this.onInput);
/**
* @type {BlockTune[]}
@ -484,6 +488,83 @@ export default class Block {
this.updateCurrentInput();
}
private onInput = (e) => {
let value = e.data;
const selection = SelectionUtils.get();
const range = SelectionUtils.range;
const tempWrapper = $.make('div');
const rangeContents = range.cloneContents();
tempWrapper.appendChild(rangeContents);
let html = tempWrapper.innerHTML;
const inputs = this.inputs;
const inputIndex = inputs.findIndex((i) => i.contains(selection.anchorNode));
if (inputIndex === -1) {
return;
}
const input = inputs[inputIndex];
const nodes = [];
const findNodes = (node: Node) => {
if (node === input) {
return;
}
nodes.push(Array.from(node.parentNode.childNodes).indexOf(node as ChildNode));
findNodes(node.parentNode as Node);
};
findNodes(selection.anchorNode);
nodes.reverse();
switch (e.inputType) {
case 'insertText':
this.emit('text/insert', {
position: {
input: inputIndex,
nodes,
startOffset: range.startOffset,
endOffset: range.startOffset + value.length,
},
data: {
text: value,
html,
},
});
break;
case 'deleteContentBackward':
value = tempWrapper.textContent;
const {endOffset} = range;
let {startOffset} = range;
if (!html.length) {
html = selection.anchorNode.textContent[range.startOffset - 1];
value = html;
startOffset -= 1;
}
this.emit('text/delete', {
position: {
input: inputIndex,
startOffset,
endOffset,
nodes,
},
data: {
text: value,
html,
},
});
}
}
/**
* Make default Block wrappers and put Tool`s content there
* @returns {HTMLDivElement}

View file

@ -65,7 +65,7 @@ export default class Core {
this.isReady = new Promise((resolve, reject) => {
onReady = resolve;
onFail = reject;
});
}) as Promise<void>;
Promise.resolve()
.then(async () => {
@ -249,6 +249,7 @@ export default class Core {
'DragNDrop',
'ModificationsObserver',
'BlockSelection',
'OTManager',
];
await modulesToPrepare.reduce(

View file

@ -1,4 +1,4 @@
import Module from '../__module';
import Module from './__module';
/**
* @module eventDispatcher
@ -10,10 +10,10 @@ import Module from '../__module';
*
* @version 1.0.0
*
* @typedef {Events} Events
* @typedef {EventEmitter} EventEmitter
* @property {Object} subscribers - all subscribers grouped by event name
*/
export default class Events extends Module {
export class EventEmitter {
/**
* Object with events` names as key and array of callback functions as value
@ -77,3 +77,5 @@ export default class Events extends Module {
this.subscribers = null;
}
}
export default new EventEmitter();

View file

@ -0,0 +1,198 @@
import Module from '../__module';
import {BlockDeleteOperation, BlockInsertOperation} from '../ot/blocks';
import Block from '../block';
import Operation from '../ot/base';
import {TextDeleteOperation, TextInsertOperation} from '../ot/text';
import SelectionUtils from '../selection';
import $ from '../dom';
export default class OTManager extends Module {
public stack: Operation[] = [];
public prepare() {
this.Editor.Renderer.on('render/finished', () => {
this.Editor.BlockManager.on('block/insert', this.onBlockInsert);
this.Editor.BlockManager.on('block/delete', this.onBlockDelete);
this.Editor.BlockManager.on('text/insert', this.onTextInsert);
this.Editor.BlockManager.on('text/delete', this.onTextDelete);
});
this.Editor.Shortcuts.add({name: 'CMD+Z', handler: this.handleUndo});
this.Editor.Shortcuts.add({name: 'CMD+Y', handler: this.handleRedo});
}
public apply(operation: Operation) {
const {BlockManager, Caret} = this.Editor;
let range, input, node, selection;
switch (operation.type) {
case BlockInsertOperation.TYPE:
BlockManager.currentBlockIndex = operation.block - 1;
BlockManager.insert(operation.data.tool, operation.data.data);
Caret.setToBlock(BlockManager.currentBlock, Caret.positions.END);
break;
case BlockDeleteOperation.TYPE:
BlockManager.removeBlock(operation.block);
Caret.setToBlock(BlockManager.blocks[operation.block - 1], Caret.positions.END);
break;
case TextInsertOperation.TYPE:
BlockManager.currentBlockIndex = operation.block;
input = BlockManager.currentBlock.inputs[operation.input];
range = document.createRange();
selection = SelectionUtils.get();
selection.removeAllRanges();
selection.addRange(range);
node = operation.nodes.reduce((anchorNode: Node, i: number) => {
return anchorNode.childNodes[i];
}, input);
range.setStart(node, operation.startOffset);
const tempWrapper = $.make('div');
tempWrapper.innerHTML = operation.data.value;
const fragment = document.createDocumentFragment();
tempWrapper.childNodes.forEach((child) => {
fragment.appendChild(child);
});
range.insertNode(fragment);
input.normalize();
range.collapse();
break;
case TextDeleteOperation.TYPE:
BlockManager.currentBlockIndex = operation.block;
input = BlockManager.currentBlock.inputs[operation.input];
range = document.createRange();
selection = SelectionUtils.get();
selection.removeAllRanges();
selection.addRange(range);
node = operation.nodes.reduce((anchorNode: Node, i: number) => {
return anchorNode.childNodes[i];
}, input);
range.setStart(node, operation.startOffset);
range.setEnd(node, operation.endOffset);
range.extractContents();
}
}
private handleUndo = (e) => {
e.preventDefault();
const op = this.stack.find((o) => !o.reversed && !o.canceled);
if (!op) {
return;
}
op.canceled = true;
const newOp = op.reverse();
this.apply(newOp);
if (newOp.needForceUpdate) {
this.stack.unshift(newOp);
} else {
setImmediate(() => {
this.stack[0].reversed = true;
});
}
console.log(this.stack);
}
private handleRedo = (e) => {
e.preventDefault();
const op = this.stack.find((o) => o.reversed && !o.canceled);
if (!op) {
return;
}
op.canceled = true;
const newOp = op.reverse();
newOp.reversed = false;
this.apply(newOp);
if (newOp.needForceUpdate) {
this.stack.unshift(newOp);
}
console.log(this.stack);
}
private onBlockInsert = async ({index, block}: {index: number, block: Block, data: any}) => {
const operation = new BlockInsertOperation(index, block.name, await block.data);
this.stack.unshift(operation);
}
private onBlockDelete = async ({index, block}: {index: number, block: Block}) => {
const operation = new BlockDeleteOperation(index, block.name, await block.data);
this.stack.unshift(operation);
}
private onTextInsert = ({position, data, block}) => {
const operation = new TextInsertOperation(
data.html,
block,
position.input,
position.nodes,
position.startOffset,
position.endOffset,
);
const prevOp = this.stack[0];
const prevOpIsMergable = prevOp && !prevOp.reversed && !prevOp.canceled && prevOp.mergeable;
const newOpIsMergeable = operation.mergeable && !operation.reversed;
const mergeable = prevOpIsMergable && newOpIsMergeable && prevOp.type === operation.type;
if (!mergeable || !(prevOp as TextInsertOperation).merge(operation)) {
this.stack.unshift(operation);
}
}
private onTextDelete = ({data, block, position}) => {
const operation = new TextDeleteOperation(
data.html,
block,
position.input,
position.nodes,
position.startOffset,
position.endOffset,
);
const prevOp = this.stack[0];
const prevOpIsMergable = prevOp && !prevOp.reversed && !prevOp.canceled && prevOp.mergeable;
const newOpIsMergeable = operation.mergeable && !operation.reversed;
const mergeable = prevOpIsMergable && newOpIsMergeable && prevOp.type === operation.type;
console.log(prevOp, mergeable);
if (!mergeable || !(prevOp as TextDeleteOperation).merge(operation)) {
this.stack.unshift(operation);
}
}
}

View file

@ -1,48 +0,0 @@
import Module from '../../__module';
import {Events} from '../../../../types/api';
/**
* @class EventsAPI
* provides with methods working with Toolbar
*/
export default class EventsAPI extends Module {
/**
* Available methods
* @return {Events}
*/
get methods(): Events {
return {
emit: (eventName: string, data: object) => this.emit(eventName, data),
off: (eventName: string, callback: () => void) => this.off(eventName, callback),
on: (eventName: string, callback: () => void) => this.on(eventName, callback),
};
}
/**
* Subscribe on Events
* @param {String} eventName
* @param {Function} callback
*/
public on(eventName, callback): void {
this.Editor.Events.on(eventName, callback);
}
/**
* Emit event with data
* @param {String} eventName
* @param {Object} data
*/
public emit(eventName, data): void {
this.Editor.Events.emit(eventName, data);
}
/**
* Unsubscribe from Event
* @param {String} eventName
* @param {Function} callback
*/
public off(eventName, callback): void {
this.Editor.Events.off(eventName, callback);
}
}

View file

@ -16,7 +16,6 @@ export default class API extends Module {
return {
blocks: this.Editor.BlocksAPI.methods,
caret: this.Editor.CaretAPI.methods,
events: this.Editor.EventsAPI.methods,
listeners: this.Editor.ListenersAPI.methods,
notifier: this.Editor.NotifierAPI.methods,
sanitizer: this.Editor.SanitizerAPI.methods,

View file

@ -12,9 +12,9 @@ export default class ListenersAPI extends Module {
*/
get methods(): Listeners {
return {
on: (element: HTMLElement, eventType, handler, useCapture) => this.on(element, eventType, handler, useCapture),
off: (element, eventType, handler) => this.off(element, eventType, handler),
};
add: (element: HTMLElement, eventType, handler, useCapture) => this.add(element, eventType, handler, useCapture),
remove: (element, eventType, handler) => this.remove(element, eventType, handler),
} as Listeners;
}
/**
@ -25,8 +25,8 @@ export default class ListenersAPI extends Module {
* @param {() => void} handler
* @param {boolean} useCapture
*/
public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void {
this.Editor.Listeners.on(element, eventType, handler, useCapture);
public add(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void {
this.Editor.Listeners.add(element, eventType, handler, useCapture);
}
/**
@ -36,7 +36,7 @@ export default class ListenersAPI extends Module {
* @param eventType
* @param handler
*/
public off(element, eventType, handler): void {
this.Editor.Listeners.off(element, eventType, handler);
public remove(element, eventType, handler): void {
this.Editor.Listeners.remove(element, eventType, handler);
}
}

View file

@ -14,7 +14,7 @@ export default class SaverAPI extends Module {
get methods(): Saver {
return {
save: () => this.save(),
};
} as Saver;
}
/**

View file

@ -14,7 +14,9 @@ export default class ToolbarAPI extends Module {
return {
close: () => this.close(),
open: () => this.open(),
};
on: (event: string, callback: (data: any) => void) => this.Editor.Toolbar.on(event, callback),
off: (event: string, callback: (data: any) => void) => this.Editor.Toolbar.off(event, callback),
} as Toolbar;
}
/**

View file

@ -195,6 +195,9 @@ export default class BlockManager extends Module {
const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable;
const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API.methods);
block.on('text/insert', (d) => this.emit('text/insert', {...d, block: this.currentBlockIndex}));
block.on('text/delete', (d) => this.emit('text/delete', {...d, block: this.currentBlockIndex}));
this.bindEvents(block);
return block;
@ -220,6 +223,13 @@ export default class BlockManager extends Module {
const block = this.composeBlock(toolName, data, settings);
this._blocks[newIndex] = block;
this.emit('block/insert', {
block,
index: newIndex,
data,
});
return block;
}
@ -270,6 +280,12 @@ export default class BlockManager extends Module {
this.currentBlockIndex++;
}
this.emit('block/insert', {
block,
index,
data: {},
});
return block;
}
@ -321,6 +337,12 @@ export default class BlockManager extends Module {
if (index === undefined) {
index = this.currentBlockIndex;
}
this.emit('block/delete', {
block: this.blocks[index],
index,
});
this._blocks.remove(index);
if (this.currentBlockIndex >= index) {
@ -397,6 +419,12 @@ export default class BlockManager extends Module {
this._blocks.insert(this.currentBlockIndex, block, true);
this.emit('block/insert', {
block,
index: this.currentBlockIndex,
data,
});
return block;
}
@ -544,10 +572,10 @@ export default class BlockManager extends Module {
private bindEvents(block: Block): void {
const {BlockEvents, Listeners} = this.Editor;
Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true);
Listeners.on(block.holder, 'mouseup', (event) => BlockEvents.mouseUp(event));
Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));
Listeners.on(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));
Listeners.add(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true);
Listeners.add(block.holder, 'mouseup', (event) => BlockEvents.mouseUp(event));
Listeners.add(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
Listeners.add(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));
Listeners.add(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));
}
}

View file

@ -25,9 +25,9 @@ export default class DragNDrop extends Module {
* @private
*/
private bindEvents(): void {
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'drop', this.processDrop, true);
this.Editor.Listeners.add(this.Editor.UI.nodes.holder, 'drop', this.processDrop, true);
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragstart', (dragEvent: DragEvent) => {
this.Editor.Listeners.add(this.Editor.UI.nodes.holder, 'dragstart', (dragEvent: DragEvent) => {
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
this.isStartedAtEditor = true;
@ -37,7 +37,7 @@ export default class DragNDrop extends Module {
});
/* Prevent default browser behavior to allow drop on non-contenteditable elements */
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragover', (e) => e.preventDefault(), true);
this.Editor.Listeners.add(this.Editor.UI.nodes.holder, 'dragover', (e) => e.preventDefault(), true);
}
/**

View file

@ -59,7 +59,7 @@ export default class Listeners extends Module {
* @param {Function} handler - method that will be fired on event
* @param {Boolean} useCapture - use event bubbling
*/
public on(
public add(
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
@ -88,7 +88,7 @@ export default class Listeners extends Module {
* @param {Function} handler - remove handler, if element listens several handlers on the same event type
* @param {Boolean} useCapture - use event bubbling
*/
public off(
public remove(
element: EventTarget,
eventType: string,
handler: (event: Event) => void,

View file

@ -179,7 +179,7 @@ export default class Paste extends Module {
private setCallback(): void {
const {Listeners, UI} = this.Editor;
Listeners.on(document, 'paste', this.handlePasteEvent);
Listeners.add(document, 'paste', this.handlePasteEvent);
}
/**

View file

@ -42,10 +42,14 @@ export default class Renderer extends Module {
* Make plugin blocks from array of plugin`s data
* @param {RendererBlocks[]} blocks
*/
public render(blocks: BlockToolData[]): Promise<void> {
public async render(blocks: BlockToolData[]): Promise<void> {
this.emit('render/started');
const chainData = blocks.map((block) => ({function: () => this.insertBlock(block)}));
return _.sequence(chainData as ChainData[]);
await _.sequence(chainData as ChainData[]);
this.emit('render/finished');
}
/**

View file

@ -30,6 +30,7 @@ export default class Saver extends Module {
const blocks = BlockManager.blocks,
chainData = [];
this.emit('saving/started');
/**
* Disable modifications observe while saving
*/
@ -44,7 +45,11 @@ export default class Saver extends Module {
ModificationsObserver.enable();
return this.makeOutput(sanitizedData);
const output = this.makeOutput(sanitizedData);
this.emit('saving/finished', output);
return output;
}
/**

View file

@ -20,8 +20,8 @@ export default class BlockSettings extends Module {
*/
public get events(): {opened: string, closed: string} {
return {
opened: 'block-settings-opened',
closed: 'block-settings-closed',
opened: 'block-settings/opened',
closed: 'block-settings/closed',
};
}
@ -91,7 +91,7 @@ export default class BlockSettings extends Module {
this.addDefaultSettings();
/** Tell to subscribers that block settings is opened */
this.Editor.Events.emit(this.events.opened);
this.emit(this.events.opened);
}
/**
@ -105,7 +105,7 @@ export default class BlockSettings extends Module {
this.nodes.defaultSettings.innerHTML = '';
/** Tell to subscribers that block settings is closed */
this.Editor.Events.emit(this.events.closed);
this.emit(this.events.closed);
}
/**

View file

@ -114,7 +114,7 @@ export default class Toolbar extends Module {
this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.append(this.nodes.content, this.nodes.plusButton);
this.Editor.Listeners.on(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false);
this.Editor.Listeners.add(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false);
/**
* Make a Toolbox
@ -275,7 +275,7 @@ export default class Toolbar extends Module {
/**
* Settings toggler
*/
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());
this.Editor.Listeners.add(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());
}
/**

View file

@ -110,7 +110,7 @@ export default class InlineToolbar extends Module {
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
// To prevent reset of a selection when click on the wrapper
this.Editor.Listeners.on(this.nodes.wrapper, 'mousedown', (event) => {
this.Editor.Listeners.add(this.nodes.wrapper, 'mousedown', (event) => {
event.preventDefault();
});
@ -342,7 +342,7 @@ export default class InlineToolbar extends Module {
this.nodes.actions.appendChild(actions);
}
Listeners.on(button, 'click', (event) => {
Listeners.add(button, 'click', (event) => {
this.toolClicked(tool);
event.preventDefault();
});

View file

@ -268,7 +268,7 @@ export default class Toolbox extends Module {
/**
* Add click listener
*/
this.Editor.Listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.Editor.Listeners.add(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, toolName);
});

View file

@ -183,14 +183,14 @@ export default class UI extends Module {
* Bind events on the CodeX Editor interface
*/
private bindEvents(): void {
this.Editor.Listeners.on(
this.Editor.Listeners.add(
this.nodes.redactor,
'click',
(event) => this.redactorClicked(event as MouseEvent),
false,
);
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true );
this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), false );
this.Editor.Listeners.add(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true );
this.Editor.Listeners.add(document, 'click', (event) => this.documentClicked(event as MouseEvent), false );
}
/**

50
src/components/ot/base.ts Normal file
View file

@ -0,0 +1,50 @@
export interface OperationData {
type: string;
block: number;
input?: number;
nodes?: number[];
startOffset?: number;
endOffset?: number;
data: any;
}
export default abstract class Operation {
public type: string;
public block: number;
public input?: number;
public nodes?: number[];
public startOffset?: number;
public endOffset?: number;
public data: any;
public reversed: boolean = false;
public canceled: boolean = false;
public needForceUpdate?: boolean = false;
public mergeable?: boolean = false;
protected constructor(opData: OperationData) {
this.type = opData.type;
this.block = opData.block;
this.input = opData.input;
this.nodes = opData.nodes;
this.startOffset = opData.startOffset;
this.endOffset = opData.endOffset;
this.data = opData.data;
}
public abstract transform(opertaion: Operation): Operation;
public abstract reverse(): Operation;
protected clone(): Operation {
return new (this as any).constructor({
type: this.type,
block: this.block,
input: this.input,
nodes: this.nodes,
startOffset: this.startOffset,
endOffset: this.endOffset,
data: Object.assign({}, this.data),
});
}
}

View file

@ -0,0 +1,43 @@
import Operation from '../base';
import {BlockInsertOperation} from './index';
export class BlockDeleteOperation extends Operation {
public static TYPE = 'block/delete';
constructor(index: number, tool: string, data: any) {
super({
type: BlockDeleteOperation.TYPE,
block: index,
data: {
tool,
data,
},
});
}
public transform(operation: Operation): Operation {
const clone = this.clone();
switch (operation.type) {
case BlockInsertOperation.TYPE:
if (operation.block <= clone.block) {
clone.block++;
}
break;
case BlockDeleteOperation.TYPE:
if (operation.block <= clone.block) {
clone.block--;
}
}
return clone;
}
public reverse(): Operation {
const op = new BlockInsertOperation(this.block, this.data.tool, this.data.data);
op.reversed = true;
return op;
}
}

View file

@ -0,0 +1,2 @@
export * from './delete';
export * from './insert';

View file

@ -0,0 +1,43 @@
import Operation from '../base';
import { BlockDeleteOperation } from './index';
export class BlockInsertOperation extends Operation {
public static TYPE = 'block/insert';
constructor(index: number, tool: string, data: any) {
super({
type: BlockInsertOperation.TYPE,
block: index,
data: {
tool,
data,
},
});
}
public transform(operation: Operation): Operation {
const newOp = this.clone();
switch (operation.type) {
case BlockInsertOperation.TYPE:
if (operation.block <= newOp.block) {
newOp.block++;
}
break;
case BlockDeleteOperation.TYPE:
if (operation.block <= newOp.block) {
newOp.block--;
}
}
return newOp;
}
public reverse(): Operation {
const op = new BlockDeleteOperation(this.block, this.data.tool, this.data.data);
op.reversed = true;
return op;
}
}

View file

@ -0,0 +1,101 @@
import Operation from '../base';
import { BlockDeleteOperation, BlockInsertOperation } from '../blocks';
import {TextInsertOperation} from './index';
export class TextDeleteOperation extends Operation {
public static TYPE = 'text/delete';
public needForceUpdate = true;
public mergeable = true;
constructor(value: string, block: number, input: number, nodes: number[], startOffset: number, endOffset: number) {
super({
type: TextDeleteOperation.TYPE,
block,
input,
nodes,
startOffset,
endOffset,
data: {
value,
},
});
}
public transform(operation: Operation): Operation {
const newOp = this.clone();
switch (operation.type) {
case BlockInsertOperation.TYPE:
if (operation.block <= newOp.block) {
newOp.block++;
}
break;
case BlockDeleteOperation.TYPE:
if (operation.block <= newOp.block) {
newOp.block--;
}
break;
case TextInsertOperation.TYPE:
if (operation.startOffset <= newOp.startOffset) {
const length = operation.endOffset - operation.startOffset;
newOp.startOffset += length;
newOp.endOffset += length;
}
break;
case TextDeleteOperation.TYPE:
if (operation.startOffset <= newOp.startOffset) {
const length = operation.endOffset - operation.startOffset;
newOp.startOffset -= length;
newOp.endOffset -= length;
}
}
return newOp;
}
public reverse(): Operation {
const op = new TextInsertOperation(
this.data.value,
this.block,
this.input,
this.nodes,
this.startOffset,
this.endOffset,
);
op.reversed = true;
return op;
}
public merge(operation: TextDeleteOperation): boolean {
if (this.block !== operation.block) {
return false;
}
if (this.input !== operation.input) {
return false;
}
if (this.nodes.length !== operation.nodes.length) {
return false;
}
if (!this.nodes.every((e, i) => operation.nodes[i] === e)) {
return false;
}
if (this.startOffset !== operation.endOffset) {
return false;
}
this.data.value = operation.data.value + this.data.value;
this.startOffset = operation.startOffset;
return true;
}
}

View file

@ -0,0 +1,2 @@
export * from './insert';
export * from './delete';

View file

@ -0,0 +1,101 @@
import Operation from '../base';
import { BlockDeleteOperation, BlockInsertOperation } from '../blocks';
import {TextDeleteOperation} from './index';
export class TextInsertOperation extends Operation {
public static TYPE = 'text/insert';
public needForceUpdate = true;
public mergeable = true;
constructor(value: string, block: number, input: number, nodes: number[], startOffset: number, endOffset: number) {
super({
type: TextInsertOperation.TYPE,
block,
input,
nodes,
startOffset,
endOffset,
data: {
value,
},
});
}
public transform(operation: Operation): Operation {
const newOp = this.clone();
switch (operation.type) {
case BlockInsertOperation.TYPE:
if (operation.block <= newOp.block) {
newOp.block++;
}
break;
case BlockDeleteOperation.TYPE:
if (operation.block <= newOp.block) {
newOp.block--;
}
break;
case TextInsertOperation.TYPE:
if (operation.startOffset <= newOp.startOffset) {
const length = operation.endOffset - operation.startOffset;
newOp.startOffset += length;
newOp.endOffset += length;
}
break;
case TextDeleteOperation.TYPE:
if (operation.startOffset <= newOp.startOffset) {
const length = operation.endOffset - operation.startOffset;
newOp.startOffset -= length;
newOp.endOffset -= length;
}
}
return newOp;
}
public reverse(): Operation {
const op = new TextDeleteOperation(
this.data.value,
this.block,
this.input,
this.nodes,
this.startOffset,
this.endOffset,
);
op.reversed = true;
return op;
}
public merge(operation: TextInsertOperation): boolean {
if (this.block !== operation.block) {
return false;
}
if (this.input !== operation.input) {
return false;
}
if (this.nodes.length !== operation.nodes.length) {
return false;
}
if (!this.nodes.every((e, i) => operation.nodes[i] === e)) {
return false;
}
if (this.endOffset !== operation.startOffset) {
return false;
}
this.data.value += operation.data.value;
this.endOffset = operation.endOffset;
return true;
}
}

View file

@ -5,7 +5,7 @@ import Toolbar from '../components/modules/toolbar/index';
import InlineToolbar from '../components/modules/toolbar/inline';
import Toolbox from '../components/modules/toolbar/toolbox';
import BlockSettings from '../components/modules/toolbar/blockSettings';
import Events from '../components/modules/events';
import {EventEmitter} from '../components/eventEmitter';
import Shortcuts from '../components/modules/shortcuts';
import Paste from '../components/modules/paste';
import Notifier from '../components/modules/notifier';
@ -19,7 +19,6 @@ import Caret from '../components/modules/caret';
import BlockManager from '../components/modules/blockManager';
import BlocksAPI from '../components/modules/api/blocks';
import CaretAPI from '../components/modules/api/caret';
import EventsAPI from '../components/modules/api/events';
import ListenersAPI from '../components/modules/api/listeners';
import SanitizerAPI from '../components/modules/api/sanitizer';
import ToolbarAPI from '../components/modules/api/toolbar';
@ -29,6 +28,7 @@ import NotifierAPI from '../components/modules/api/notifier';
import SaverAPI from '../components/modules/api/saver';
import Saver from '../components/modules/saver';
import BlockSelection from '../components/modules/blockSelection';
import OTManager from '../components/modules/OTManager';
export interface EditorModules {
UI: UI;
@ -39,7 +39,6 @@ export interface EditorModules {
InlineToolbar: InlineToolbar;
Toolbox: Toolbox;
BlockSettings: BlockSettings;
Events: Events;
Shortcuts: Shortcuts;
Paste: Paste;
DragNDrop: DragNDrop;
@ -54,7 +53,6 @@ export interface EditorModules {
BlockManager: BlockManager;
BlocksAPI: BlocksAPI;
CaretAPI: CaretAPI;
EventsAPI: EventsAPI;
ListenersAPI: ListenersAPI;
SanitizerAPI: SanitizerAPI;
SaverAPI: SaverAPI;
@ -62,4 +60,5 @@ export interface EditorModules {
StylesAPI: StylesAPI;
ToolbarAPI: ToolbarAPI;
NotifierAPI: NotifierAPI;
OTManager: OTManager;
}

28
types/api/events.d.ts vendored
View file

@ -1,28 +0,0 @@
/**
* Describes Editor`s events API
*/
export interface Events {
/**
* Emits event
*
* @param {string} eventName
* @param {any} data
*/
emit(eventName: string, data: any): void;
/**
* Unsubscribe from event
*
* @param {string} eventName
* @param {(data: any) => void} callback
*/
off(eventName: string, callback: (data?: any) => void): void;
/**
* Subscribe to event
*
* @param {string} eventName
* @param {(data: any) => void} callback
*/
on(eventName: string, callback: (data?: any) => void): void;
}

View file

@ -1,5 +1,4 @@
export * from './blocks';
export * from './events';
export * from './listeners';
export * from './sanitizer';
export * from './saver';

View file

@ -10,7 +10,7 @@ export interface Listeners {
* @param {(event: Event) => void}handler
* @param {boolean} useCapture
*/
on(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void;
add(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void;
/**
* Unsubscribe from event dispatched on passed element
@ -20,5 +20,5 @@ export interface Listeners {
* @param {(event: Event) => void}handler
* @param {boolean} useCapture
*/
off(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void;
remove(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void;
}

View file

@ -11,4 +11,7 @@ export interface Toolbar {
* Opens Toolbar
*/
open(): void;
on(event: string, callback: (data: any) => void);
off(event: string, callback: (data: any) => void);
}

2
types/index.d.ts vendored
View file

@ -5,7 +5,7 @@
*/
import {EditorConfig} from './configs';
import {Blocks, Caret, Events, Listeners, Notifier, Sanitizer, Saver, Selection, Styles, Toolbar} from './api';
import {Blocks, Caret, Listeners, Notifier, Sanitizer, Saver, Selection, Styles, Toolbar} from './api';
/**
* Interfaces used for development