mirror of
https://github.com/codex-team/editor.js
synced 2024-05-27 11:02:27 +02:00
initial
This commit is contained in:
parent
b89f756a03
commit
6047e83272
25372
dist/codex-editor.js
vendored
25372
dist/codex-editor.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,7 @@
|
||||||
import {EditorModules} from '../types-internal/editor-modules';
|
import {EditorModules} from '../types-internal/editor-modules';
|
||||||
import {EditorConfig} from '../../types';
|
import {EditorConfig} from '../../types';
|
||||||
import {ModuleConfig} from '../types-internal/module-config';
|
import {ModuleConfig} from '../types-internal/module-config';
|
||||||
|
import {EventEmitter} from './eventEmitter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @abstract
|
* @abstract
|
||||||
|
@ -11,7 +12,7 @@ import {ModuleConfig} from '../types-internal/module-config';
|
||||||
* @property {Object} config - Editor user settings
|
* @property {Object} config - Editor user settings
|
||||||
* @property {EditorModules} Editor - List of Editor modules
|
* @property {EditorModules} Editor - List of Editor modules
|
||||||
*/
|
*/
|
||||||
export default class Module {
|
export default class Module extends EventEmitter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor modules list
|
* Editor modules list
|
||||||
|
@ -30,6 +31,8 @@ export default class Module {
|
||||||
* @param {EditorConfig}
|
* @param {EditorConfig}
|
||||||
*/
|
*/
|
||||||
constructor({config}: ModuleConfig) {
|
constructor({config}: ModuleConfig) {
|
||||||
|
super();
|
||||||
|
|
||||||
if (new.target === Module) {
|
if (new.target === Module) {
|
||||||
throw new TypeError('Constructors for abstract class Module are not allowed.');
|
throw new TypeError('Constructors for abstract class Module are not allowed.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ export default class DeleteTune implements BlockTune {
|
||||||
public render() {
|
public render() {
|
||||||
this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {});
|
this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {});
|
||||||
this.nodes.button.appendChild($.svg('cross', 12, 12));
|
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;
|
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,
|
* When toolbar block settings is closed but block deletion is not confirmed,
|
||||||
* then reset confirmation state
|
* then reset confirmation state
|
||||||
*/
|
*/
|
||||||
this.api.events.on('block-settings-closed', this.resetConfirmation);
|
this.api.toolbar.on('block-settings/closed', this.resetConfirmation);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from block-settings closing event
|
* 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();
|
this.api.blocks.delete();
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default class MoveDownTune implements BlockTune {
|
||||||
public render() {
|
public render() {
|
||||||
const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
|
const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
|
||||||
moveDownButton.appendChild($.svg('arrow-down', 14, 14));
|
moveDownButton.appendChild($.svg('arrow-down', 14, 14));
|
||||||
this.api.listeners.on(
|
this.api.listeners.add(
|
||||||
moveDownButton,
|
moveDownButton,
|
||||||
'click',
|
'click',
|
||||||
(event) => this.handleClick(event as MouseEvent, moveDownButton),
|
(event) => this.handleClick(event as MouseEvent, moveDownButton),
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default class MoveUpTune implements BlockTune {
|
||||||
public render(): HTMLElement {
|
public render(): HTMLElement {
|
||||||
const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
|
const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
|
||||||
moveUpButton.appendChild($.svg('arrow-up', 14, 14));
|
moveUpButton.appendChild($.svg('arrow-up', 14, 14));
|
||||||
this.api.listeners.on(
|
this.api.listeners.add(
|
||||||
moveUpButton,
|
moveUpButton,
|
||||||
'click',
|
'click',
|
||||||
(event) => this.handleClick(event as MouseEvent, moveUpButton),
|
(event) => this.handleClick(event as MouseEvent, moveUpButton),
|
||||||
|
|
|
@ -27,6 +27,7 @@ import MoveUpTune from './block-tunes/block-tune-move-up';
|
||||||
import DeleteTune from './block-tunes/block-tune-delete';
|
import DeleteTune from './block-tunes/block-tune-delete';
|
||||||
import MoveDownTune from './block-tunes/block-tune-move-down';
|
import MoveDownTune from './block-tunes/block-tune-move-down';
|
||||||
import SelectionUtils from './selection';
|
import SelectionUtils from './selection';
|
||||||
|
import {EventEmitter} from './eventEmitter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
|
* @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 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
|
* @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
|
* CSS classes for the Block
|
||||||
|
@ -326,6 +327,8 @@ export default class Block {
|
||||||
settings: ToolConfig,
|
settings: ToolConfig,
|
||||||
apiMethods: API,
|
apiMethods: API,
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
this.name = toolName;
|
this.name = toolName;
|
||||||
this.tool = toolInstance;
|
this.tool = toolInstance;
|
||||||
this.class = toolClass;
|
this.class = toolClass;
|
||||||
|
@ -334,6 +337,7 @@ export default class Block {
|
||||||
this.holder = this.compose();
|
this.holder = this.compose();
|
||||||
|
|
||||||
this.mutationObserver = new MutationObserver(this.didMutated);
|
this.mutationObserver = new MutationObserver(this.didMutated);
|
||||||
|
this.holder.addEventListener('beforeinput', this.onInput);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {BlockTune[]}
|
* @type {BlockTune[]}
|
||||||
|
@ -484,6 +488,83 @@ export default class Block {
|
||||||
this.updateCurrentInput();
|
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
|
* Make default Block wrappers and put Tool`s content there
|
||||||
* @returns {HTMLDivElement}
|
* @returns {HTMLDivElement}
|
||||||
|
|
|
@ -65,7 +65,7 @@ export default class Core {
|
||||||
this.isReady = new Promise((resolve, reject) => {
|
this.isReady = new Promise((resolve, reject) => {
|
||||||
onReady = resolve;
|
onReady = resolve;
|
||||||
onFail = reject;
|
onFail = reject;
|
||||||
});
|
}) as Promise<void>;
|
||||||
|
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
@ -249,6 +249,7 @@ export default class Core {
|
||||||
'DragNDrop',
|
'DragNDrop',
|
||||||
'ModificationsObserver',
|
'ModificationsObserver',
|
||||||
'BlockSelection',
|
'BlockSelection',
|
||||||
|
'OTManager',
|
||||||
];
|
];
|
||||||
|
|
||||||
await modulesToPrepare.reduce(
|
await modulesToPrepare.reduce(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Module from '../__module';
|
import Module from './__module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module eventDispatcher
|
* @module eventDispatcher
|
||||||
|
@ -10,10 +10,10 @@ import Module from '../__module';
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*
|
*
|
||||||
* @typedef {Events} Events
|
* @typedef {EventEmitter} EventEmitter
|
||||||
* @property {Object} subscribers - all subscribers grouped by event name
|
* @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
|
* 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;
|
this.subscribers = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default new EventEmitter();
|
198
src/components/modules/OTManager.ts
Normal file
198
src/components/modules/OTManager.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -16,7 +16,6 @@ export default class API extends Module {
|
||||||
return {
|
return {
|
||||||
blocks: this.Editor.BlocksAPI.methods,
|
blocks: this.Editor.BlocksAPI.methods,
|
||||||
caret: this.Editor.CaretAPI.methods,
|
caret: this.Editor.CaretAPI.methods,
|
||||||
events: this.Editor.EventsAPI.methods,
|
|
||||||
listeners: this.Editor.ListenersAPI.methods,
|
listeners: this.Editor.ListenersAPI.methods,
|
||||||
notifier: this.Editor.NotifierAPI.methods,
|
notifier: this.Editor.NotifierAPI.methods,
|
||||||
sanitizer: this.Editor.SanitizerAPI.methods,
|
sanitizer: this.Editor.SanitizerAPI.methods,
|
||||||
|
|
|
@ -12,9 +12,9 @@ export default class ListenersAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get methods(): Listeners {
|
get methods(): Listeners {
|
||||||
return {
|
return {
|
||||||
on: (element: HTMLElement, eventType, handler, useCapture) => this.on(element, eventType, handler, useCapture),
|
add: (element: HTMLElement, eventType, handler, useCapture) => this.add(element, eventType, handler, useCapture),
|
||||||
off: (element, eventType, handler) => this.off(element, eventType, handler),
|
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 {() => void} handler
|
||||||
* @param {boolean} useCapture
|
* @param {boolean} useCapture
|
||||||
*/
|
*/
|
||||||
public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void {
|
public add(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void {
|
||||||
this.Editor.Listeners.on(element, eventType, handler, useCapture);
|
this.Editor.Listeners.add(element, eventType, handler, useCapture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,7 +36,7 @@ export default class ListenersAPI extends Module {
|
||||||
* @param eventType
|
* @param eventType
|
||||||
* @param handler
|
* @param handler
|
||||||
*/
|
*/
|
||||||
public off(element, eventType, handler): void {
|
public remove(element, eventType, handler): void {
|
||||||
this.Editor.Listeners.off(element, eventType, handler);
|
this.Editor.Listeners.remove(element, eventType, handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class SaverAPI extends Module {
|
||||||
get methods(): Saver {
|
get methods(): Saver {
|
||||||
return {
|
return {
|
||||||
save: () => this.save(),
|
save: () => this.save(),
|
||||||
};
|
} as Saver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,7 +14,9 @@ export default class ToolbarAPI extends Module {
|
||||||
return {
|
return {
|
||||||
close: () => this.close(),
|
close: () => this.close(),
|
||||||
open: () => this.open(),
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -195,6 +195,9 @@ export default class BlockManager extends Module {
|
||||||
const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable;
|
const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable;
|
||||||
const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API.methods);
|
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);
|
this.bindEvents(block);
|
||||||
|
|
||||||
return block;
|
return block;
|
||||||
|
@ -220,6 +223,13 @@ export default class BlockManager extends Module {
|
||||||
const block = this.composeBlock(toolName, data, settings);
|
const block = this.composeBlock(toolName, data, settings);
|
||||||
|
|
||||||
this._blocks[newIndex] = block;
|
this._blocks[newIndex] = block;
|
||||||
|
|
||||||
|
this.emit('block/insert', {
|
||||||
|
block,
|
||||||
|
index: newIndex,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,6 +280,12 @@ export default class BlockManager extends Module {
|
||||||
this.currentBlockIndex++;
|
this.currentBlockIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('block/insert', {
|
||||||
|
block,
|
||||||
|
index,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,6 +337,12 @@ export default class BlockManager extends Module {
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
index = this.currentBlockIndex;
|
index = this.currentBlockIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('block/delete', {
|
||||||
|
block: this.blocks[index],
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
|
||||||
this._blocks.remove(index);
|
this._blocks.remove(index);
|
||||||
|
|
||||||
if (this.currentBlockIndex >= index) {
|
if (this.currentBlockIndex >= index) {
|
||||||
|
@ -397,6 +419,12 @@ export default class BlockManager extends Module {
|
||||||
|
|
||||||
this._blocks.insert(this.currentBlockIndex, block, true);
|
this._blocks.insert(this.currentBlockIndex, block, true);
|
||||||
|
|
||||||
|
this.emit('block/insert', {
|
||||||
|
block,
|
||||||
|
index: this.currentBlockIndex,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,10 +572,10 @@ export default class BlockManager extends Module {
|
||||||
private bindEvents(block: Block): void {
|
private bindEvents(block: Block): void {
|
||||||
const {BlockEvents, Listeners} = this.Editor;
|
const {BlockEvents, Listeners} = this.Editor;
|
||||||
|
|
||||||
Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true);
|
Listeners.add(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true);
|
||||||
Listeners.on(block.holder, 'mouseup', (event) => BlockEvents.mouseUp(event));
|
Listeners.add(block.holder, 'mouseup', (event) => BlockEvents.mouseUp(event));
|
||||||
Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
|
Listeners.add(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
|
||||||
Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));
|
Listeners.add(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));
|
||||||
Listeners.on(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));
|
Listeners.add(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,9 @@ export default class DragNDrop extends Module {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private bindEvents(): void {
|
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) {
|
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
|
||||||
this.isStartedAtEditor = true;
|
this.isStartedAtEditor = true;
|
||||||
|
@ -37,7 +37,7 @@ export default class DragNDrop extends Module {
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Prevent default browser behavior to allow drop on non-contenteditable elements */
|
/* 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default class Listeners extends Module {
|
||||||
* @param {Function} handler - method that will be fired on event
|
* @param {Function} handler - method that will be fired on event
|
||||||
* @param {Boolean} useCapture - use event bubbling
|
* @param {Boolean} useCapture - use event bubbling
|
||||||
*/
|
*/
|
||||||
public on(
|
public add(
|
||||||
element: EventTarget,
|
element: EventTarget,
|
||||||
eventType: string,
|
eventType: string,
|
||||||
handler: (event: Event) => void,
|
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 {Function} handler - remove handler, if element listens several handlers on the same event type
|
||||||
* @param {Boolean} useCapture - use event bubbling
|
* @param {Boolean} useCapture - use event bubbling
|
||||||
*/
|
*/
|
||||||
public off(
|
public remove(
|
||||||
element: EventTarget,
|
element: EventTarget,
|
||||||
eventType: string,
|
eventType: string,
|
||||||
handler: (event: Event) => void,
|
handler: (event: Event) => void,
|
||||||
|
|
|
@ -179,7 +179,7 @@ export default class Paste extends Module {
|
||||||
private setCallback(): void {
|
private setCallback(): void {
|
||||||
const {Listeners, UI} = this.Editor;
|
const {Listeners, UI} = this.Editor;
|
||||||
|
|
||||||
Listeners.on(document, 'paste', this.handlePasteEvent);
|
Listeners.add(document, 'paste', this.handlePasteEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -42,10 +42,14 @@ export default class Renderer extends Module {
|
||||||
* Make plugin blocks from array of plugin`s data
|
* Make plugin blocks from array of plugin`s data
|
||||||
* @param {RendererBlocks[]} blocks
|
* @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)}));
|
const chainData = blocks.map((block) => ({function: () => this.insertBlock(block)}));
|
||||||
|
|
||||||
return _.sequence(chainData as ChainData[]);
|
await _.sequence(chainData as ChainData[]);
|
||||||
|
|
||||||
|
this.emit('render/finished');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -30,6 +30,7 @@ export default class Saver extends Module {
|
||||||
const blocks = BlockManager.blocks,
|
const blocks = BlockManager.blocks,
|
||||||
chainData = [];
|
chainData = [];
|
||||||
|
|
||||||
|
this.emit('saving/started');
|
||||||
/**
|
/**
|
||||||
* Disable modifications observe while saving
|
* Disable modifications observe while saving
|
||||||
*/
|
*/
|
||||||
|
@ -44,7 +45,11 @@ export default class Saver extends Module {
|
||||||
|
|
||||||
ModificationsObserver.enable();
|
ModificationsObserver.enable();
|
||||||
|
|
||||||
return this.makeOutput(sanitizedData);
|
const output = this.makeOutput(sanitizedData);
|
||||||
|
|
||||||
|
this.emit('saving/finished', output);
|
||||||
|
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,8 +20,8 @@ export default class BlockSettings extends Module {
|
||||||
*/
|
*/
|
||||||
public get events(): {opened: string, closed: string} {
|
public get events(): {opened: string, closed: string} {
|
||||||
return {
|
return {
|
||||||
opened: 'block-settings-opened',
|
opened: 'block-settings/opened',
|
||||||
closed: 'block-settings-closed',
|
closed: 'block-settings/closed',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ export default class BlockSettings extends Module {
|
||||||
this.addDefaultSettings();
|
this.addDefaultSettings();
|
||||||
|
|
||||||
/** Tell to subscribers that block settings is opened */
|
/** 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 = '';
|
this.nodes.defaultSettings.innerHTML = '';
|
||||||
|
|
||||||
/** Tell to subscribers that block settings is closed */
|
/** Tell to subscribers that block settings is closed */
|
||||||
this.Editor.Events.emit(this.events.closed);
|
this.emit(this.events.closed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default class Toolbar extends Module {
|
||||||
this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
|
this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
|
||||||
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
|
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
|
||||||
$.append(this.nodes.content, this.nodes.plusButton);
|
$.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
|
* Make a Toolbox
|
||||||
|
@ -275,7 +275,7 @@ export default class Toolbar extends Module {
|
||||||
/**
|
/**
|
||||||
* Settings toggler
|
* Settings toggler
|
||||||
*/
|
*/
|
||||||
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());
|
this.Editor.Listeners.add(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -110,7 +110,7 @@ export default class InlineToolbar extends Module {
|
||||||
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
|
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
|
||||||
|
|
||||||
// To prevent reset of a selection when click on the wrapper
|
// 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();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -342,7 +342,7 @@ export default class InlineToolbar extends Module {
|
||||||
this.nodes.actions.appendChild(actions);
|
this.nodes.actions.appendChild(actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
Listeners.on(button, 'click', (event) => {
|
Listeners.add(button, 'click', (event) => {
|
||||||
this.toolClicked(tool);
|
this.toolClicked(tool);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
|
@ -268,7 +268,7 @@ export default class Toolbox extends Module {
|
||||||
/**
|
/**
|
||||||
* Add click listener
|
* 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);
|
this.toolButtonActivate(event, toolName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -183,14 +183,14 @@ export default class UI extends Module {
|
||||||
* Bind events on the CodeX Editor interface
|
* Bind events on the CodeX Editor interface
|
||||||
*/
|
*/
|
||||||
private bindEvents(): void {
|
private bindEvents(): void {
|
||||||
this.Editor.Listeners.on(
|
this.Editor.Listeners.add(
|
||||||
this.nodes.redactor,
|
this.nodes.redactor,
|
||||||
'click',
|
'click',
|
||||||
(event) => this.redactorClicked(event as MouseEvent),
|
(event) => this.redactorClicked(event as MouseEvent),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true );
|
this.Editor.Listeners.add(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, 'click', (event) => this.documentClicked(event as MouseEvent), false );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
50
src/components/ot/base.ts
Normal file
50
src/components/ot/base.ts
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
43
src/components/ot/blocks/delete.ts
Normal file
43
src/components/ot/blocks/delete.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
2
src/components/ot/blocks/index.ts
Normal file
2
src/components/ot/blocks/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './delete';
|
||||||
|
export * from './insert';
|
43
src/components/ot/blocks/insert.ts
Normal file
43
src/components/ot/blocks/insert.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
101
src/components/ot/text/delete.ts
Normal file
101
src/components/ot/text/delete.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
2
src/components/ot/text/index.ts
Normal file
2
src/components/ot/text/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './insert';
|
||||||
|
export * from './delete';
|
101
src/components/ot/text/insert.ts
Normal file
101
src/components/ot/text/insert.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
7
src/types-internal/editor-modules.d.ts
vendored
7
src/types-internal/editor-modules.d.ts
vendored
|
@ -5,7 +5,7 @@ import Toolbar from '../components/modules/toolbar/index';
|
||||||
import InlineToolbar from '../components/modules/toolbar/inline';
|
import InlineToolbar from '../components/modules/toolbar/inline';
|
||||||
import Toolbox from '../components/modules/toolbar/toolbox';
|
import Toolbox from '../components/modules/toolbar/toolbox';
|
||||||
import BlockSettings from '../components/modules/toolbar/blockSettings';
|
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 Shortcuts from '../components/modules/shortcuts';
|
||||||
import Paste from '../components/modules/paste';
|
import Paste from '../components/modules/paste';
|
||||||
import Notifier from '../components/modules/notifier';
|
import Notifier from '../components/modules/notifier';
|
||||||
|
@ -19,7 +19,6 @@ import Caret from '../components/modules/caret';
|
||||||
import BlockManager from '../components/modules/blockManager';
|
import BlockManager from '../components/modules/blockManager';
|
||||||
import BlocksAPI from '../components/modules/api/blocks';
|
import BlocksAPI from '../components/modules/api/blocks';
|
||||||
import CaretAPI from '../components/modules/api/caret';
|
import CaretAPI from '../components/modules/api/caret';
|
||||||
import EventsAPI from '../components/modules/api/events';
|
|
||||||
import ListenersAPI from '../components/modules/api/listeners';
|
import ListenersAPI from '../components/modules/api/listeners';
|
||||||
import SanitizerAPI from '../components/modules/api/sanitizer';
|
import SanitizerAPI from '../components/modules/api/sanitizer';
|
||||||
import ToolbarAPI from '../components/modules/api/toolbar';
|
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 SaverAPI from '../components/modules/api/saver';
|
||||||
import Saver from '../components/modules/saver';
|
import Saver from '../components/modules/saver';
|
||||||
import BlockSelection from '../components/modules/blockSelection';
|
import BlockSelection from '../components/modules/blockSelection';
|
||||||
|
import OTManager from '../components/modules/OTManager';
|
||||||
|
|
||||||
export interface EditorModules {
|
export interface EditorModules {
|
||||||
UI: UI;
|
UI: UI;
|
||||||
|
@ -39,7 +39,6 @@ export interface EditorModules {
|
||||||
InlineToolbar: InlineToolbar;
|
InlineToolbar: InlineToolbar;
|
||||||
Toolbox: Toolbox;
|
Toolbox: Toolbox;
|
||||||
BlockSettings: BlockSettings;
|
BlockSettings: BlockSettings;
|
||||||
Events: Events;
|
|
||||||
Shortcuts: Shortcuts;
|
Shortcuts: Shortcuts;
|
||||||
Paste: Paste;
|
Paste: Paste;
|
||||||
DragNDrop: DragNDrop;
|
DragNDrop: DragNDrop;
|
||||||
|
@ -54,7 +53,6 @@ export interface EditorModules {
|
||||||
BlockManager: BlockManager;
|
BlockManager: BlockManager;
|
||||||
BlocksAPI: BlocksAPI;
|
BlocksAPI: BlocksAPI;
|
||||||
CaretAPI: CaretAPI;
|
CaretAPI: CaretAPI;
|
||||||
EventsAPI: EventsAPI;
|
|
||||||
ListenersAPI: ListenersAPI;
|
ListenersAPI: ListenersAPI;
|
||||||
SanitizerAPI: SanitizerAPI;
|
SanitizerAPI: SanitizerAPI;
|
||||||
SaverAPI: SaverAPI;
|
SaverAPI: SaverAPI;
|
||||||
|
@ -62,4 +60,5 @@ export interface EditorModules {
|
||||||
StylesAPI: StylesAPI;
|
StylesAPI: StylesAPI;
|
||||||
ToolbarAPI: ToolbarAPI;
|
ToolbarAPI: ToolbarAPI;
|
||||||
NotifierAPI: NotifierAPI;
|
NotifierAPI: NotifierAPI;
|
||||||
|
OTManager: OTManager;
|
||||||
}
|
}
|
||||||
|
|
28
types/api/events.d.ts
vendored
28
types/api/events.d.ts
vendored
|
@ -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;
|
|
||||||
}
|
|
1
types/api/index.d.ts
vendored
1
types/api/index.d.ts
vendored
|
@ -1,5 +1,4 @@
|
||||||
export * from './blocks';
|
export * from './blocks';
|
||||||
export * from './events';
|
|
||||||
export * from './listeners';
|
export * from './listeners';
|
||||||
export * from './sanitizer';
|
export * from './sanitizer';
|
||||||
export * from './saver';
|
export * from './saver';
|
||||||
|
|
4
types/api/listeners.d.ts
vendored
4
types/api/listeners.d.ts
vendored
|
@ -10,7 +10,7 @@ export interface Listeners {
|
||||||
* @param {(event: Event) => void}handler
|
* @param {(event: Event) => void}handler
|
||||||
* @param {boolean} useCapture
|
* @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
|
* Unsubscribe from event dispatched on passed element
|
||||||
|
@ -20,5 +20,5 @@ export interface Listeners {
|
||||||
* @param {(event: Event) => void}handler
|
* @param {(event: Event) => void}handler
|
||||||
* @param {boolean} useCapture
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
3
types/api/toolbar.d.ts
vendored
3
types/api/toolbar.d.ts
vendored
|
@ -11,4 +11,7 @@ export interface Toolbar {
|
||||||
* Opens Toolbar
|
* Opens Toolbar
|
||||||
*/
|
*/
|
||||||
open(): void;
|
open(): void;
|
||||||
|
|
||||||
|
on(event: string, callback: (data: any) => void);
|
||||||
|
off(event: string, callback: (data: any) => void);
|
||||||
}
|
}
|
||||||
|
|
2
types/index.d.ts
vendored
2
types/index.d.ts
vendored
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {EditorConfig} from './configs';
|
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
|
* Interfaces used for development
|
||||||
|
|
Loading…
Reference in a new issue