mirror of
https://github.com/codex-team/editor.js
synced 2024-05-15 21:06:52 +02:00
onChange improvements (#1678)
* onChange improvements * Return modifications observer module * Fix lint * Fix tests
This commit is contained in:
parent
51d94e1a11
commit
4e7b33c2b8
|
@ -1,5 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
### 2.22.0
|
||||
|
||||
- `New` - `onChange` callback now receive Block API object of affected block
|
||||
|
||||
### 2.21.0
|
||||
|
||||
- `New` - Blocks now have unique ids [#873](https://github.com/codex-team/editor.js/issues/873)
|
||||
|
|
|
@ -193,7 +193,7 @@ var editor = new EditorJS({
|
|||
/**
|
||||
* onChange callback
|
||||
*/
|
||||
onChange: () => {console.log('Now I know that Editor\'s content changed!')}
|
||||
onChange: (editorAPI, affectedBlockAPI) => {console.log('Now I know that Editor\'s content changed!')}
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
@ -319,8 +319,8 @@
|
|||
onReady: function(){
|
||||
saveButton.click();
|
||||
},
|
||||
onChange: function() {
|
||||
console.log('something changed');
|
||||
onChange: function(api, block) {
|
||||
console.log('something changed', block);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -281,8 +281,8 @@
|
|||
onReady: function(){
|
||||
saveButton.click();
|
||||
},
|
||||
onChange: function() {
|
||||
console.log('something changed');
|
||||
onChange: function(api, block) {
|
||||
console.log('something changed', block);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import BlockTool from '../tools/block';
|
|||
import BlockTune from '../tools/tune';
|
||||
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
||||
import ToolsCollection from '../tools/collection';
|
||||
import EventsDispatcher from '../utils/events';
|
||||
|
||||
/**
|
||||
* Interface describes Block class constructor argument
|
||||
|
@ -79,6 +80,11 @@ export enum BlockToolAPI {
|
|||
ON_PASTE = 'onPaste',
|
||||
}
|
||||
|
||||
/**
|
||||
* Names of events supported by Block class
|
||||
*/
|
||||
type BlockEvents = 'didMutated';
|
||||
|
||||
/**
|
||||
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
|
||||
*
|
||||
|
@ -86,7 +92,7 @@ export enum BlockToolAPI {
|
|||
* @property {HTMLElement} holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
|
||||
* @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function
|
||||
*/
|
||||
export default class Block {
|
||||
export default class Block extends EventsDispatcher<BlockEvents> {
|
||||
/**
|
||||
* CSS classes for the Block
|
||||
*
|
||||
|
@ -207,6 +213,8 @@ export default class Block {
|
|||
this.updateCurrentInput();
|
||||
|
||||
this.call(BlockToolAPI.UPDATED);
|
||||
|
||||
this.emit('didMutated', this);
|
||||
}, this.modificationDebounceTimer);
|
||||
|
||||
/**
|
||||
|
@ -230,6 +238,8 @@ export default class Block {
|
|||
readOnly,
|
||||
tunesData,
|
||||
}: BlockConstructorOptions) {
|
||||
super();
|
||||
|
||||
this.name = tool.name;
|
||||
this.id = id;
|
||||
this.settings = tool.settings;
|
||||
|
@ -680,6 +690,8 @@ export default class Block {
|
|||
* Call Tool instance destroy method
|
||||
*/
|
||||
public destroy(): void {
|
||||
super.destroy();
|
||||
|
||||
if (_.isFunction(this.toolInstance.destroy)) {
|
||||
this.toolInstance.destroy();
|
||||
}
|
||||
|
@ -777,6 +789,13 @@ export default class Block {
|
|||
private addInputEvents(): void {
|
||||
this.inputs.forEach(input => {
|
||||
input.addEventListener('focus', this.handleFocus);
|
||||
|
||||
/**
|
||||
* If input is native input add oninput listener to observe changes
|
||||
*/
|
||||
if ($.isNativeInput(input)) {
|
||||
input.addEventListener('input', this.didMutated);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -786,6 +805,10 @@ export default class Block {
|
|||
private removeInputEvents(): void {
|
||||
this.inputs.forEach(input => {
|
||||
input.removeEventListener('focus', this.handleFocus);
|
||||
|
||||
if ($.isNativeInput(input)) {
|
||||
input.removeEventListener('input', this.didMutated);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export default class SanitizerAPI extends Module {
|
|||
/**
|
||||
* Available methods
|
||||
*
|
||||
* @returns {Sanitizer}
|
||||
* @returns {SanitizerConfig}
|
||||
*/
|
||||
public get methods(): ISanitizer {
|
||||
return {
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as _ from '../utils';
|
|||
import Blocks from '../blocks';
|
||||
import { BlockToolData, PasteEvent } from '../../../types';
|
||||
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
||||
import BlockAPI from '../block/api';
|
||||
|
||||
/**
|
||||
* @typedef {BlockManager} BlockManager
|
||||
|
@ -290,6 +291,11 @@ export default class BlockManager extends Module {
|
|||
|
||||
this._blocks.insert(newIndex, block, replace);
|
||||
|
||||
/**
|
||||
* Force call of didMutated event on Block insertion
|
||||
*/
|
||||
this.blockDidMutated(block);
|
||||
|
||||
if (needToFocus) {
|
||||
this.currentBlockIndex = newIndex;
|
||||
} else if (newIndex <= this.currentBlockIndex) {
|
||||
|
@ -361,6 +367,11 @@ export default class BlockManager extends Module {
|
|||
|
||||
this._blocks[index] = block;
|
||||
|
||||
/**
|
||||
* Force call of didMutated event on Block insertion
|
||||
*/
|
||||
this.blockDidMutated(block);
|
||||
|
||||
if (needToFocus) {
|
||||
this.currentBlockIndex = index;
|
||||
} else if (index <= this.currentBlockIndex) {
|
||||
|
@ -426,8 +437,15 @@ export default class BlockManager extends Module {
|
|||
throw new Error('Can\'t find a Block to remove');
|
||||
}
|
||||
|
||||
const blockToRemove = this._blocks[index];
|
||||
|
||||
this._blocks.remove(index);
|
||||
|
||||
/**
|
||||
* Force call of didMutated event on Block removal
|
||||
*/
|
||||
this.blockDidMutated(blockToRemove);
|
||||
|
||||
if (this.currentBlockIndex >= index) {
|
||||
this.currentBlockIndex--;
|
||||
}
|
||||
|
@ -689,6 +707,11 @@ export default class BlockManager extends Module {
|
|||
|
||||
/** Now actual block moved so that current block index changed */
|
||||
this.currentBlockIndex = toIndex;
|
||||
|
||||
/**
|
||||
* Force call of didMutated event on Block movement
|
||||
*/
|
||||
this.blockDidMutated(this.currentBlock);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -754,6 +777,8 @@ export default class BlockManager extends Module {
|
|||
this.readOnlyMutableListeners.on(block.holder, 'dragleave', (event: DragEvent) => {
|
||||
BlockEvents.dragLeave(event);
|
||||
});
|
||||
|
||||
block.on('didMutated', (affectedBlock: Block) => this.blockDidMutated(affectedBlock));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -789,4 +814,15 @@ export default class BlockManager extends Module {
|
|||
private validateIndex(index: number): boolean {
|
||||
return !(index < 0 || index >= this._blocks.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block mutation callback
|
||||
*
|
||||
* @param block - mutated block
|
||||
*/
|
||||
private blockDidMutated(block: Block): Block {
|
||||
this.Editor.ModificationsObserver.onChange(new BlockAPI(block));
|
||||
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,195 +1,40 @@
|
|||
/**
|
||||
* @module ModificationsObserver
|
||||
*
|
||||
* Handles any mutations
|
||||
* and gives opportunity to handle outside
|
||||
*/
|
||||
|
||||
import Module from '../__module';
|
||||
import { BlockAPI } from '../../../types';
|
||||
import * as _ from '../utils';
|
||||
import Block from '../block';
|
||||
|
||||
/**
|
||||
*
|
||||
* Single entry point for Block mutation events
|
||||
*/
|
||||
export default class ModificationsObserver extends Module {
|
||||
/**
|
||||
* Debounce Timer
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public static readonly DebounceTimer = 450;
|
||||
|
||||
/**
|
||||
* MutationObserver instance
|
||||
*/
|
||||
private observer: MutationObserver;
|
||||
|
||||
/**
|
||||
* Allows to temporary disable mutations handling
|
||||
* Flag shows onChange event is disabled
|
||||
*/
|
||||
private disabled = false;
|
||||
|
||||
/**
|
||||
* Used to prevent several mutation callback execution
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
private mutationDebouncer = _.debounce(() => {
|
||||
this.updateNativeInputs();
|
||||
|
||||
if (_.isFunction(this.config.onChange)) {
|
||||
this.config.onChange(this.Editor.API.methods);
|
||||
}
|
||||
}, ModificationsObserver.DebounceTimer);
|
||||
|
||||
/**
|
||||
* Array of native inputs in Blocks.
|
||||
* Changes in native inputs are not handled by modification observer, so we need to set change event listeners on them
|
||||
*/
|
||||
private nativeInputs: HTMLElement[] = [];
|
||||
|
||||
/**
|
||||
* Clear timeout and set null to mutationDebouncer property
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.mutationDebouncer = null;
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
this.observer = null;
|
||||
this.nativeInputs.forEach((input) => this.listeners.off(input, 'input', this.mutationDebouncer));
|
||||
this.mutationDebouncer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set read-only state
|
||||
*
|
||||
* @param {boolean} readOnlyEnabled - read only flag value
|
||||
*/
|
||||
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
||||
if (readOnlyEnabled) {
|
||||
this.disableModule();
|
||||
} else {
|
||||
this.enableModule();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to disable observer,
|
||||
* for example when Editor wants to stealthy mutate DOM
|
||||
*/
|
||||
public disable(): void {
|
||||
this.disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables mutation handling
|
||||
* Should be called after .disable()
|
||||
* Enables onChange event
|
||||
*/
|
||||
public enable(): void {
|
||||
this.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* setObserver
|
||||
*
|
||||
* sets 'DOMSubtreeModified' listener on Editor's UI.nodes.redactor
|
||||
* so that User can handle outside from API
|
||||
* Disables onChange event
|
||||
*/
|
||||
private setObserver(): void {
|
||||
const { UI } = this.Editor;
|
||||
const observerOptions = {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
characterDataOldValue: true,
|
||||
};
|
||||
|
||||
this.observer = new MutationObserver((mutationList, observer) => {
|
||||
this.mutationHandler(mutationList, observer);
|
||||
});
|
||||
this.observer.observe(UI.nodes.redactor, observerOptions);
|
||||
public disable(): void {
|
||||
this.disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* MutationObserver events handler
|
||||
* Call onChange event passed to Editor.js configuration
|
||||
*
|
||||
* @param {MutationRecord[]} mutationList - list of mutations
|
||||
* @param {MutationObserver} observer - observer instance
|
||||
* @param block - changed Block
|
||||
*/
|
||||
private mutationHandler(mutationList: MutationRecord[], observer: MutationObserver): void {
|
||||
/**
|
||||
* Skip mutations in stealth mode
|
||||
*/
|
||||
if (this.disabled) {
|
||||
public onChange(block: BlockAPI): void {
|
||||
if (this.disabled || !_.isFunction(this.config.onChange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* We divide two Mutation types:
|
||||
* 1) mutations that concerns client changes: settings changes, symbol added, deletion, insertions and so on
|
||||
* 2) functional changes. On each client actions we set functional identifiers to interact with user
|
||||
*/
|
||||
let contentMutated = false;
|
||||
|
||||
mutationList.forEach((mutation) => {
|
||||
switch (mutation.type) {
|
||||
case 'childList':
|
||||
case 'characterData':
|
||||
contentMutated = true;
|
||||
break;
|
||||
case 'attributes':
|
||||
/**
|
||||
* Changes on Element.ce-block usually is functional
|
||||
*/
|
||||
if (!(mutation.target as Element).classList.contains(Block.CSS.wrapper)) {
|
||||
contentMutated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
/** call once */
|
||||
if (contentMutated) {
|
||||
this.mutationDebouncer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets native inputs and set oninput event handler
|
||||
*/
|
||||
private updateNativeInputs(): void {
|
||||
if (this.nativeInputs) {
|
||||
this.nativeInputs.forEach((input) => {
|
||||
this.listeners.off(input, 'input');
|
||||
});
|
||||
}
|
||||
|
||||
this.nativeInputs = Array.from(this.Editor.UI.nodes.redactor.querySelectorAll('textarea, input, select'));
|
||||
|
||||
this.nativeInputs.forEach((input) => this.listeners.on(input, 'input', this.mutationDebouncer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets observer and enables it
|
||||
*/
|
||||
private enableModule(): void {
|
||||
/**
|
||||
* wait till Browser render Editor's Blocks
|
||||
*/
|
||||
window.setTimeout(() => {
|
||||
this.setObserver();
|
||||
this.updateNativeInputs();
|
||||
this.enable();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables observer
|
||||
*/
|
||||
private disableModule(): void {
|
||||
this.disable();
|
||||
this.config.onChange(this.Editor.API.methods, block);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -639,7 +639,7 @@ export default class Paste extends Module {
|
|||
* @param {PasteData} dataToInsert - data of Block to insert
|
||||
*/
|
||||
private async processInlinePaste(dataToInsert: PasteData): Promise<void> {
|
||||
const { BlockManager, Caret, Tools } = this.Editor;
|
||||
const { BlockManager, Caret } = this.Editor;
|
||||
const { content } = dataToInsert;
|
||||
|
||||
const currentBlockIsDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;
|
||||
|
|
|
@ -48,8 +48,15 @@ export default class Renderer extends Module {
|
|||
public async render(blocks: OutputBlockData[]): Promise<void> {
|
||||
const chainData = blocks.map((block) => ({ function: (): Promise<void> => this.insertBlock(block) }));
|
||||
|
||||
/**
|
||||
* Disable onChange callback on render to not to spam those events
|
||||
*/
|
||||
this.Editor.ModificationsObserver.disable();
|
||||
|
||||
const sequence = await _.sequence(chainData as _.ChainData[]);
|
||||
|
||||
this.Editor.ModificationsObserver.enable();
|
||||
|
||||
this.Editor.UI.checkEmptiness();
|
||||
|
||||
return sequence;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
import Module from '../__module';
|
||||
import { OutputData } from '../../../types';
|
||||
import { ValidatedData } from '../../../types/data-formats';
|
||||
import { SavedData, ValidatedData } from '../../../types/data-formats';
|
||||
import Block from '../block';
|
||||
import * as _ from '../utils';
|
||||
import { sanitizeBlocks } from '../utils/sanitizer';
|
||||
|
@ -28,26 +28,28 @@ export default class Saver extends Module {
|
|||
* @returns {OutputData}
|
||||
*/
|
||||
public async save(): Promise<OutputData> {
|
||||
const { BlockManager, ModificationsObserver, Tools } = this.Editor;
|
||||
const { BlockManager, Tools, ModificationsObserver } = this.Editor;
|
||||
const blocks = BlockManager.blocks,
|
||||
chainData = [];
|
||||
|
||||
/**
|
||||
* Disable modifications observe while saving
|
||||
*/
|
||||
ModificationsObserver.disable();
|
||||
|
||||
try {
|
||||
/**
|
||||
* Disable onChange callback on save to not to spam those events
|
||||
*/
|
||||
ModificationsObserver.disable();
|
||||
|
||||
blocks.forEach((block: Block) => {
|
||||
chainData.push(this.getSavedData(block));
|
||||
});
|
||||
|
||||
const extractedData = await Promise.all(chainData);
|
||||
const extractedData = await Promise.all(chainData) as Array<Pick<SavedData, 'data' | 'tool'>>;
|
||||
const sanitizedData = await sanitizeBlocks(extractedData, (name) => {
|
||||
return Tools.blockTools.get(name).sanitizeConfig;
|
||||
});
|
||||
|
||||
return this.makeOutput(sanitizedData);
|
||||
} catch (e) {
|
||||
_.logLabeled(`Saving failed due to the Error %o`, 'error', e);
|
||||
} finally {
|
||||
ModificationsObserver.enable();
|
||||
}
|
||||
|
|
|
@ -9,10 +9,8 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
|
|||
import Shortcuts from '../../utils/shortcuts';
|
||||
import Tooltip from '../../utils/tooltip';
|
||||
import { ModuleConfig } from '../../../types-internal/module-config';
|
||||
import EventsDispatcher from '../../utils/events';
|
||||
import InlineTool from '../../tools/inline';
|
||||
import { CommonInternalSettings } from '../../tools/base';
|
||||
import BlockTool from '../../tools/block';
|
||||
|
||||
/**
|
||||
* Inline Toolbar elements
|
||||
|
|
|
@ -660,7 +660,7 @@ export function deprecationAssert(condition: boolean, oldProperty: string, newPr
|
|||
* @param propertyKey - method or accessor name
|
||||
* @param descriptor - property descriptor
|
||||
*/
|
||||
export function cacheable<Target, Value, Arguments extends any[] = any[]>(
|
||||
export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]>(
|
||||
target: Target,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
|
@ -672,7 +672,7 @@ export function cacheable<Target, Value, Arguments extends any[] = any[]>(
|
|||
/**
|
||||
* Override get or value descriptor property to cache return value
|
||||
*
|
||||
* @param args
|
||||
* @param args - method args
|
||||
*/
|
||||
descriptor[propertyToOverride] = function (...args: Arguments): Value {
|
||||
/**
|
||||
|
@ -688,12 +688,12 @@ export function cacheable<Target, Value, Arguments extends any[] = any[]>(
|
|||
/**
|
||||
* If get accessor has been overridden, we need to override set accessor to clear cache
|
||||
*
|
||||
* @param value
|
||||
* @param value - value to set
|
||||
*/
|
||||
if (propertyToOverride === 'get' && descriptor.set) {
|
||||
const originalSet = descriptor.set;
|
||||
|
||||
descriptor.set = function (value: any): void {
|
||||
descriptor.set = function (value: unknown): void {
|
||||
delete target[cacheKey];
|
||||
|
||||
originalSet.apply(this, value);
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
* @typedef {Events} Events
|
||||
* @property {object} subscribers - all subscribers grouped by event name
|
||||
*/
|
||||
export default class EventsDispatcher {
|
||||
export default class EventsDispatcher<Events extends string = string> {
|
||||
/**
|
||||
* Object with events` names as key and array of callback functions as value
|
||||
*
|
||||
|
@ -25,7 +25,7 @@ export default class EventsDispatcher {
|
|||
* @param {string} eventName - event name
|
||||
* @param {Function} callback - subscriber
|
||||
*/
|
||||
public on(eventName: string, callback: (data: object) => object): void {
|
||||
public on(eventName: Events, callback: (data: object) => object): void {
|
||||
if (!(eventName in this.subscribers)) {
|
||||
this.subscribers[eventName] = [];
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export default class EventsDispatcher {
|
|||
* @param {string} eventName - event name
|
||||
* @param {Function} callback - subscriber
|
||||
*/
|
||||
public once(eventName: string, callback: (data: object) => object): void {
|
||||
public once(eventName: Events, callback: (data: object) => object): void {
|
||||
if (!(eventName in this.subscribers)) {
|
||||
this.subscribers[eventName] = [];
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ export default class EventsDispatcher {
|
|||
* @param {string} eventName - event name
|
||||
* @param {object} data - subscribers get this data when they were fired
|
||||
*/
|
||||
public emit(eventName: string, data?: object): void {
|
||||
public emit(eventName: Events, data?: object): void {
|
||||
if (!this.subscribers[eventName]) {
|
||||
return;
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ export default class EventsDispatcher {
|
|||
* @param {string} eventName - event name
|
||||
* @param {Function} callback - event handler
|
||||
*/
|
||||
public off(eventName: string, callback: (data: object) => object): void {
|
||||
public off(eventName: Events, callback: (data: object) => object): void {
|
||||
for (let i = 0; i < this.subscribers[eventName].length; i++) {
|
||||
if (this.subscribers[eventName][i] === callback) {
|
||||
delete this.subscribers[eventName][i];
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/**
|
||||
* CodeX Sanitizer
|
||||
*
|
||||
|
|
4
src/types-internal/editor-modules.d.ts
vendored
4
src/types-internal/editor-modules.d.ts
vendored
|
@ -6,7 +6,6 @@ import Toolbox from '../components/modules/toolbar/toolbox';
|
|||
import BlockSettings from '../components/modules/toolbar/blockSettings';
|
||||
import Paste from '../components/modules/paste';
|
||||
import DragNDrop from '../components/modules/dragNDrop';
|
||||
import ModificationsObserver from '../components/modules/modificationsObserver';
|
||||
import Renderer from '../components/modules/renderer';
|
||||
import Tools from '../components/modules/tools';
|
||||
import API from '../components/modules/api/index';
|
||||
|
@ -32,6 +31,7 @@ import TooltipAPI from '../components/modules/api/tooltip';
|
|||
import ReadOnly from '../components/modules/readonly';
|
||||
import ReadOnlyAPI from '../components/modules/api/readonly';
|
||||
import I18nAPI from '../components/modules/api/i18n';
|
||||
import ModificationsObserver from '../components/modules/modificationsObserver';
|
||||
|
||||
export interface EditorModules {
|
||||
UI: UI;
|
||||
|
@ -45,7 +45,6 @@ export interface EditorModules {
|
|||
ConversionToolbar: ConversionToolbar;
|
||||
Paste: Paste;
|
||||
DragNDrop: DragNDrop;
|
||||
ModificationsObserver: ModificationsObserver;
|
||||
Renderer: Renderer;
|
||||
Tools: Tools;
|
||||
API: API;
|
||||
|
@ -68,4 +67,5 @@ export interface EditorModules {
|
|||
ReadOnly: ReadOnly;
|
||||
ReadOnlyAPI: ReadOnlyAPI;
|
||||
I18nAPI: I18nAPI;
|
||||
ModificationsObserver: ModificationsObserver;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* tslint:disable:no-var-requires */
|
||||
/**
|
||||
* This file contains connection of Cypres plugins
|
||||
|
@ -6,7 +7,7 @@ const webpackConfig = require('../../../webpack.config.js');
|
|||
const preprocessor = require('@cypress/webpack-preprocessor');
|
||||
const codeCoverageTask = require('@cypress/code-coverage/task');
|
||||
|
||||
module.exports = (on, config): any => {
|
||||
module.exports = (on, config): unknown => {
|
||||
/**
|
||||
* Add Cypress task to get code coverage
|
||||
*/
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/**
|
||||
* This file contains custom commands for Cypress.
|
||||
* Also it can override the existing commands.
|
||||
|
@ -40,7 +41,7 @@ Cypress.Commands.add('createEditor', (editorConfig: EditorConfig = {}): Chainabl
|
|||
/**
|
||||
* Paste command to dispatch paste event
|
||||
*
|
||||
* @usage
|
||||
* Usage
|
||||
* cy.get('div').paste({'text/plain': 'Text', 'text/html': '<b>Text</b>'})
|
||||
*
|
||||
* @param data - map with MIME type as a key and data as value
|
||||
|
@ -66,7 +67,7 @@ Cypress.Commands.add('paste', {
|
|||
/**
|
||||
* Copy command to dispatch copy event on subject
|
||||
*
|
||||
* @usage
|
||||
* Usage:
|
||||
* cy.get('div').copy().then(data => {})
|
||||
*/
|
||||
Cypress.Commands.add('copy', { prevSubject: true }, async (subject) => {
|
||||
|
@ -92,7 +93,7 @@ Cypress.Commands.add('copy', { prevSubject: true }, async (subject) => {
|
|||
/**
|
||||
* Cut command to dispatch cut event on subject
|
||||
*
|
||||
* @usage
|
||||
* Usage:
|
||||
* cy.get('div').cut().then(data => {})
|
||||
*/
|
||||
Cypress.Commands.add('cut', { prevSubject: true }, async (subject) => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/**
|
||||
* There will be described test cases of 'blocks.*' API
|
||||
*/
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Header from '../../../example/tools/header';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* tslint:disable:max-classes-per-file */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-ignore */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-ignore,@typescript-eslint/no-explicit-any,jsdoc/require-jsdoc */
|
||||
import Tools from '../../../../src/components/modules/tools';
|
||||
import { EditorConfig } from '../../../../types';
|
||||
import BlockTool from '../../../../src/components/tools/block';
|
||||
|
@ -86,8 +86,8 @@ describe('Tools module', () => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
public static prepare(): void {}
|
||||
} as any,
|
||||
inlineToolbar: ['inlineTool2'],
|
||||
tunes: ['blockTune2']
|
||||
inlineToolbar: [ 'inlineTool2' ],
|
||||
tunes: [ 'blockTune2' ],
|
||||
},
|
||||
withFailedPrepare: class {
|
||||
public static prepare(): void {
|
||||
|
|
127
test/cypress/tests/onchange.spec.ts
Normal file
127
test/cypress/tests/onchange.spec.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import Header from '../../../example/tools/header';
|
||||
|
||||
/**
|
||||
* @todo Add checks that correct block API object is passed to onChange
|
||||
* @todo Add cases for native inputs changes
|
||||
*/
|
||||
describe('onChange callback', () => {
|
||||
const config = {
|
||||
tools: {
|
||||
header: Header,
|
||||
},
|
||||
onChange: (): void => {
|
||||
console.log('something changed');
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
if (this && this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
} else {
|
||||
cy.spy(config, 'onChange').as('onChange');
|
||||
|
||||
cy.createEditor(config).as('editorInstance');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fire onChange callback on block insertion', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('{enter}');
|
||||
|
||||
cy.get('@onChange').should('be.called');
|
||||
});
|
||||
|
||||
it('should fire onChange callback on typing into block', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('some text');
|
||||
|
||||
cy.get('@onChange').should('be.called');
|
||||
});
|
||||
|
||||
it('should fire onChange callback on block replacement', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('li.ce-toolbox__button[data-tool=header]')
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should('be.calledWithMatch', Cypress.sinon.match.any, Cypress.sinon.match({ name: 'header' }));
|
||||
});
|
||||
|
||||
it('should fire onChange callback on tune modifier', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('li.ce-toolbox__button[data-tool=header]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('span.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('span.cdx-settings-button[data-level=1]')
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should('be.calledWithMatch', Cypress.sinon.match.any, Cypress.sinon.match({ name: 'header' }));
|
||||
});
|
||||
|
||||
it('should fire onChange callback when block is removed', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('span.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-settings__button--delete')
|
||||
.click()
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should('be.called');
|
||||
});
|
||||
|
||||
it('should fire onChange callback when block is moved', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('{enter}');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.last()
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('span.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-tune-move-up')
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should('be.called');
|
||||
});
|
||||
});
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
describe('Output sanitisation', () => {
|
||||
beforeEach(() => {
|
||||
if (this && this.editorInstance) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
import { BlockToolData, ToolSettings } from '../../../../types';
|
||||
import { ToolType } from '../../../../src/components/tools/base';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
import { ToolSettings } from '../../../../types';
|
||||
import { ToolType } from '../../../../src/components/tools/base';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
import { ToolSettings } from '../../../../types';
|
||||
import { ToolType } from '../../../../src/components/tools/base';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ToolsCollection from '../../../../src/components/tools/collection';
|
||||
import BlockTool from '../../../../src/components/tools/block';
|
||||
import InlineTool from '../../../../src/components/tools/inline';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import LinkInlineTool from '../../../../src/components/inline-tools/inline-tool-link';
|
||||
import MoveUpTune from '../../../../src/components/block-tunes/block-tune-move-up';
|
||||
import ToolsFactory from '../../../../src/components/tools/factory';
|
||||
|
|
5
types/configs/editor-config.d.ts
vendored
5
types/configs/editor-config.d.ts
vendored
|
@ -1,5 +1,5 @@
|
|||
import {ToolConstructable, ToolSettings} from '../tools';
|
||||
import {API, LogLevels, OutputData} from '../index';
|
||||
import {API, BlockAPI, LogLevels, OutputData} from '../index';
|
||||
import {SanitizerConfig} from './sanitizer-config';
|
||||
import {I18nConfig} from './i18n-config';
|
||||
|
||||
|
@ -88,8 +88,9 @@ export interface EditorConfig {
|
|||
/**
|
||||
* Fires when something changed in DOM
|
||||
* @param {API} api - editor.js api
|
||||
* @param block - changed block API
|
||||
*/
|
||||
onChange?(api: API): void;
|
||||
onChange?(api: API, block: BlockAPI): void;
|
||||
|
||||
/**
|
||||
* Defines default toolbar for all tools.
|
||||
|
|
Loading…
Reference in a new issue