Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block lifecycle hooks #906

Merged
merged 10 commits into from
Nov 21, 2019
11 changes: 6 additions & 5 deletions dist/editor.js

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,17 @@ class ListTool {
}
}
```

## Block Lifecycle hooks

### `rendered()`

Called after Block contents is added to the page

### `updated()`

Called each time Block contents is updated

### `removed()`

Called after Block contents is removed from the page but before Block instance deleted
2 changes: 1 addition & 1 deletion example/tools/header
2 changes: 1 addition & 1 deletion example/tools/inline-code
2 changes: 1 addition & 1 deletion example/tools/marker
71 changes: 53 additions & 18 deletions src/components/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down';
import SelectionUtils from './selection';

/**
* Available Block Tool API methods
*/
export enum BlockToolAPI {
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
/**
* @todo remove method in 3.0.0
* @deprecated — use 'rendered' hook instead
*/
APPEND_CALLBACK = 'appendCallback',
RENDERED = 'rendered',
UPDATED = 'updated',
REMOVED = 'removed',
ON_PASTE = 'onPaste',
}

/**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
*
Expand Down Expand Up @@ -337,6 +352,29 @@ export default class Block {
*/
private mutationObserver: MutationObserver;

/**
* Debounce Timer
* @type {number}
*/
private readonly modificationDebounceTimer = 450;

/**
* Is fired when DOM mutation has been happened
*/
private didMutated = _.debounce((): void => {
/**
* Drop cache
*/
this.cachedInputs = [];

/**
* Update current input
*/
this.updateCurrentInput();

this.call(BlockToolAPI.UPDATED);
}, this.modificationDebounceTimer);

/**
* @constructor
* @param {String} toolName - Tool name that passed on initialization
Expand Down Expand Up @@ -375,12 +413,16 @@ export default class Block {
* @param {String} methodName
* @param {Object} params
*/
public call(methodName: string, params: object) {
public call(methodName: string, params?: object) {
/**
* call Tool's method with the instance context
*/
if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
this.tool[methodName].call(this.tool, params);
try {
this.tool[methodName].call(this.tool, params);
} catch (e) {
_.log(`Error during '${methodName}' call: ${e.message}`, 'error');
}
}
}

Expand Down Expand Up @@ -485,7 +527,15 @@ export default class Block {
/**
* Observe DOM mutations to update Block inputs
*/
this.mutationObserver.observe(this.holder, {childList: true, subtree: true});
this.mutationObserver.observe(
this.holder.firstElementChild,
{
childList: true,
subtree: true,
characterData: true,
attributes: true,
},
);
}

/**
Expand All @@ -495,21 +545,6 @@ export default class Block {
this.mutationObserver.disconnect();
}

/**
* Is fired when DOM mutation has been happened
*/
private didMutated = (): void => {
/**
* Drop cache
*/
this.cachedInputs = [];

/**
* Update current input
*/
this.updateCurrentInput();
}

/**
* Make default Block wrappers and put Tool`s content there
* @returns {HTMLDivElement}
Expand Down
75 changes: 58 additions & 17 deletions src/components/blocks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from './utils';
import $ from './dom';
import Block from './block';
import Block, {BlockToolAPI} from './block';

/**
* @class Blocks
Expand All @@ -12,7 +12,6 @@ import Block from './block';
*
*/
export default class Blocks {

/**
* Get length of Block instances array
*
Expand Down Expand Up @@ -47,16 +46,27 @@ export default class Blocks {
* blocks[0] = new Block(...)
*
* @param {Blocks} instance — Blocks instance
* @param {Number|String} index — block index
* @param {Block} blockBlock to set
* @param {Number|String} property — block index or any Blocks class property to set
* @param {Block} valuevalue to set
* @returns {Boolean}
*/
public static set(instance: Blocks, index: number, block: Block) {
if (isNaN(Number(index))) {
return false;
public static set(instance: Blocks, property: number | string, value: Block | any) {

/**
* If property name is not a number (method or other property, access it via reflect
*/
if (isNaN(Number(property))) {
Reflect.set(instance, property, value);
return true;
}

instance.insert(+index, block);
/**
* If property is number, call insert method to emulate array behaviour
*
* @example
* blocks[0] = new Block();
*/
instance.insert(+property, value);

return true;
}
Expand All @@ -65,15 +75,22 @@ export default class Blocks {
* Proxy trap to implement array-like getter
*
* @param {Blocks} instance — Blocks instance
* @param {Number|String} indexBlock index
* @param {Number|String} propertyBlocks class property
* @returns {Block|*}
*/
public static get(instance: Blocks, index: number) {
if (isNaN(Number(index))) {
return instance[index];
public static get(instance: Blocks, property: any | number) {

/**
* If property is not a number, get it via Reflect object
*/
if (isNaN(Number(property))) {
return Reflect.get(instance, property);
}

return instance.get(+index);
/**
* If property is a number (Block index) return Block by passed index
*/
return instance.get(+property);
}

/**
Expand Down Expand Up @@ -103,7 +120,7 @@ export default class Blocks {
*/
public push(block: Block): void {
this.blocks.push(block);
this.workingArea.appendChild(block.holder);
this.insertToDOM(block);
}

/**
Expand Down Expand Up @@ -145,6 +162,7 @@ export default class Blocks {

if (replace) {
this.blocks[index].holder.remove();
this.blocks[index].call(BlockToolAPI.REMOVED);
}

const deleteCount = replace ? 1 : 0;
Expand All @@ -154,14 +172,14 @@ export default class Blocks {
if (index > 0) {
const previousBlock = this.blocks[index - 1];

previousBlock.holder.insertAdjacentElement('afterend', block.holder);
this.insertToDOM(block, 'afterend', previousBlock);
} else {
const nextBlock = this.blocks[index + 1];

if (nextBlock) {
nextBlock.holder.insertAdjacentElement('beforebegin', block.holder);
this.insertToDOM(block, 'beforebegin', nextBlock);
} else {
this.workingArea.appendChild(block.holder);
this.insertToDOM(block);
}
}
}
Expand All @@ -176,6 +194,9 @@ export default class Blocks {
}

this.blocks[index].holder.remove();

this.blocks[index].call(BlockToolAPI.REMOVED);

this.blocks.splice(index, 1);
}

Expand All @@ -184,6 +205,9 @@ export default class Blocks {
*/
public removeAll(): void {
this.workingArea.innerHTML = '';

this.blocks.forEach((block) => block.call(BlockToolAPI.REMOVED));

this.blocks.length = 0;
}

Expand Down Expand Up @@ -220,4 +244,21 @@ export default class Blocks {
public indexOf(block: Block): number {
return this.blocks.indexOf(block);
}

/**
* Insert new Block into DOM
*
* @param {Block} block - Block to insert
* @param {InsertPosition} position — insert position (if set, will use insertAdjacentElement)
* @param {Block} target — Block related to position
*/
private insertToDOM(block: Block, position?: InsertPosition, target?: Block): void {
if (position) {
target.holder.insertAdjacentElement(position, block.holder);
} else {
this.workingArea.appendChild(block.holder);
}

block.call(BlockToolAPI.RENDERED);
}
}
17 changes: 15 additions & 2 deletions src/components/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export default class Core {

_.log('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75');

setTimeout(() => {
setTimeout(async () => {
await this.render();

if ((this.configuration as EditorConfig).autofocus) {
const {BlockManager, Caret} = this.moduleInstances;

Expand Down Expand Up @@ -268,16 +270,27 @@ export default class Core {
}),
Promise.resolve(),
);
}

/**
* Render initial data
*/
private render(): Promise<void> {
return this.moduleInstances.Renderer.render(this.config.data.blocks);
}

/**
* Make modules instances and save it to the @property this.moduleInstances
*/
private constructModules(): void {
modules.forEach( (Module) => {
modules.forEach( (module) => {
/**
* If module has non-default exports, passed object contains them all and default export as 'default' property
*/
const Module = typeof module === 'function' ? module : module.default;
neSpecc marked this conversation as resolved.
Show resolved Hide resolved

try {

/**
* We use class name provided by displayName property
*
Expand Down
5 changes: 4 additions & 1 deletion src/components/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ export default class Dom {
* @param {Element|DocumentFragment} parent - where to append
* @param {Element|Element[]|Text|Text[]} elements - element or elements list
*/
public static append(parent: Element|DocumentFragment, elements: Element|Element[]|DocumentFragment|Text|Text[]): void {
public static append(
parent: Element|DocumentFragment,
elements: Element|Element[]|DocumentFragment|Text|Text[],
): void {
if ( Array.isArray(elements) ) {
elements.forEach( (el) => parent.appendChild(el) );
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/components/modules/blockManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
* @version 2.0.0
*/
import Block from '../block';
import Block, {BlockToolAPI} from '../block';
import Module from '../__module';
import $ from '../dom';
import _ from '../utils';
Expand Down Expand Up @@ -267,7 +267,7 @@ export default class BlockManager extends Module {
}

try {
block.call('onPaste', pasteEvent);
block.call(BlockToolAPI.ON_PASTE, pasteEvent);
} catch (e) {
_.log(`${toolName}: onPaste callback call is failed`, 'error', e);
}
Expand Down
27 changes: 27 additions & 0 deletions src/components/modules/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,33 @@ export default class Events extends Module {
this.subscribers[eventName].push(callback);
}

/**
* Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call.
*
* @param {String} eventName - event name
* @param {Function} callback - subscriber
*/
public once(eventName: string, callback: (data: any) => any) {
if (!(eventName in this.subscribers)) {
this.subscribers[eventName] = [];
}

const wrappedCallback = (data: any) => {
const result = callback(data);

const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback);

if (indexOfHandler !== -1) {
this.subscribers[eventName].splice(indexOfHandler, 1);
}

return result;
};

// group by events
this.subscribers[eventName].push(wrappedCallback);
}

/**
* Emit callbacks with passed data
*
Expand Down
Loading