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

Destroyer #392

Merged
merged 4 commits into from
Aug 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 55 additions & 43 deletions build/codex-editor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/codex-editor.js.map

Large diffs are not rendered by default.

23 changes: 17 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# CodeX Editor API

Blocks have access to the public methods provided by CodeX Editor API Module. Plugin and Tune Developers
Blocks have access to the public methods provided by CodeX Editor API Module. Plugin and Tune Developers
can use Editor API as they want.

## Api object description

Common API interface.
Expand All @@ -22,7 +22,7 @@ Methods that working with Blocks

```swap(fromIndex, toIndex)``` - swaps two Blocks by their positions

```delete(blockIndex?: Number)``` - deletes Block with passed index
```delete(blockIndex?: Number)``` - deletes Block with passed index

```getCurrentBlockIndex()``` - current Block index

Expand All @@ -32,7 +32,7 @@ Methods that working with Blocks

```stretchBlock(index: number, status: boolean)``` - make Block stretched

```insertNewBlock()``` - insert new Block after working place
```insertNewBlock()``` - insert new Block after working place

#### ISanitizerAPI

Expand Down Expand Up @@ -73,8 +73,19 @@ Methods that allows to subscribe on CodeX Editor events
### IListenerAPI

Methods that allows to work with DOM listener. Useful when you forgot to remove listener.
Module collects all listeners and destroys automatically
Module collects all listeners and destroys automatically

```on(element: HTMLElement, eventType: string, handler: Function, useCapture?: boolean)``` - add event listener to HTML element

```off(element: HTMLElement, eventType: string, handler: Function)``` - remove event handler from HTML element
```off(element: HTMLElement, eventType: string, handler: Function)``` - remove event handler from HTML element


### Destroy API
If there are necessity to remove CodeX Editor instance from the page you can use `destroy()` method.

It makes following steps:
1. Clear the holder element by setting it\`s innerHTML to empty string
2. Remove all event listeners related to CodeX Editor
3. Delete all properties from instance object and set it\`s prototype to `null`

After executing the `destroy` method, editor inctance becomes an empty object. This way you will free occupied JS Heap on your page.
302 changes: 34 additions & 268 deletions src/codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,6 @@
*
*/

/**
* @typedef {CodexEditor} CodexEditor - editor class
*/

/**
* Dynamically imported utils
*
* @typedef {Dom} $ - {@link components/dom.js}
* @typedef {Util} _ - {@link components/utils.js}
*/

'use strict';

/**
Expand All @@ -58,288 +47,65 @@
import 'babel-core/register';
import 'babel-polyfill';
import 'components/polyfills';
import Core from './components/core';

/**
* Require Editor modules places in components/modules dir
*/
// eslint-disable-next-line
let modules = editorModules.map( module => require('./components/modules/' + module ));

/**
* @class
*
* @classdesc CodeX Editor base class
*
* @property this.config - all settings
* @property this.moduleInstances - constructed editor components
* @property {Promise} isReady - resolved promise if CodeX Editor start was successfull, rejected promise otherwise
*
* @type {CodexEditor}
*/
export default class CodexEditor {
/** Editor version */
static get version() {
return VERSION;
}

/**
* @param {EditorConfig} config - user configuration
* @constructor
*
* @param {EditorConfig} configuration - user configuration
*/
constructor(config) {
/**
* Configuration object
* @type {EditorConfig}
*/
this.config = {};

/**
* @typedef {Object} EditorComponents
* @property {BlockManager} BlockManager
* @property {Tools} Tools
* @property {Events} Events
* @property {UI} UI
* @property {Toolbar} Toolbar
* @property {Toolbox} Toolbox
* @property {BlockSettings} BlockSettings
* @property {Renderer} Renderer
* @property {InlineToolbar} InlineToolbar
*/
this.moduleInstances = {};

/**
* Ready promise. Resolved if CodeX Editor is ready to work, rejected otherwise
*/
let onReady, onFail;

this.isReady = new Promise((resolve, reject) => {
onReady = resolve;
onFail = reject;
});

Promise.resolve()
.then(() => {
this.configuration = config;
})
.then(() => this.validate())
.then(() => this.init())
.then(() => this.start())
.then(() => {
let methods = this.moduleInstances.API.methods;

/**
* Make API methods available from inside easier
*/
for (let method in methods) {
this[method] = methods[method];
}

// todo Is it necessary?
delete this.moduleInstances;
})
.then(() => {
_.log('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧');

setTimeout(() => {
this.config.onReady.call();
onReady();
}, 500);
})
.catch(error => {
_.log(`CodeX Editor does not ready because of ${error}`, 'error');
onFail(error);
});
}

/**
* Setting for configuration
* @param {EditorConfig|string|null} config
*/
set configuration(config) {
/**
* Process zero-configuration or with only holderId
*/
if (typeof config === 'string' || typeof config === 'undefined') {
config = {
holderId: config
};
}
constructor(configuration) {
let {onReady} = configuration;

/**
* If initial Block's Tool was not passed, use the Paragraph Tool
*/
this.config.initialBlock = config.initialBlock || 'paragraph';
onReady = onReady && typeof onReady === 'function' ? onReady : () => {};

/**
* Initial block type
* Uses in case when there is no blocks passed
* @type {{type: (*), data: {text: null}}}
*/
let initialBlockData = {
type : this.config.initialBlock,
data : {}
};

this.config.holderId = config.holderId || 'codex-editor';
this.config.placeholder = config.placeholder || 'write your story...';
this.config.sanitizer = config.sanitizer || {
p: true,
b: true,
a: true
};
configuration.onReady = () => {};

this.config.hideToolbar = config.hideToolbar ? config.hideToolbar : false;
this.config.tools = config.tools || {};
this.config.data = config.data || {};
this.config.onReady = config.onReady && typeof config.onReady === 'function' ? config.onReady : function () {};
const editor = new Core(configuration);

/**
* Initialize Blocks to pass data to the Renderer
* We need to export isReady promise in the constructor as it can be used before other API methods are exported
* @type {Promise<any | never>}
*/
if (_.isEmpty(this.config.data)) {
this.config.data = {};
this.config.data.blocks = [ initialBlockData ];
} else {
if (!this.config.data.blocks || this.config.data.blocks.length === 0) {
this.config.data.blocks = [ initialBlockData ];
}
}
}

/**
* Returns private property
* @returns {EditorConfig}
*/
get configuration() {
return this.config;
}

/**
* Checks for required fields in Editor's config
* @returns {void|Promise<string>}
*/
validate() {
/**
* Check if holderId is not empty
*/
if (!this.config.holderId) {
return Promise.reject('«holderId» param must being not empty');
}

/**
* Check for a holder element's existence
*/
if (!$.get(this.config.holderId)) {
return Promise.reject(`element with ID «${this.config.holderId}» is missing. Pass correct holder's ID.`);
}

/**
* Check Tools for a class containing
*/
for (let toolName in this.config.tools) {
const tool = this.config.tools[toolName];

if (!_.isFunction(tool) && !_.isFunction(tool.class)) {
return Promise.reject(`Tool «${toolName}» must be a constructor function or an object with that function in the «class» property`);
}
}
}

/**
* Initializes modules:
* - make and save instances
* - configure
*/
init() {
/**
* Make modules instances and save it to the @property this.moduleInstances
*/
this.constructModules();

/**
* Modules configuration
*/
this.configureModules();
}

/**
* Make modules instances and save it to the @property this.moduleInstances
*/
constructModules() {
modules.forEach( Module => {
try {
/**
* We use class name provided by displayName property
*
* On build, Babel will transform all Classes to the Functions so, name will always be 'Function'
* To prevent this, we use 'babel-plugin-class-display-name' plugin
* @see https://www.npmjs.com/package/babel-plugin-class-display-name
*/
this.moduleInstances[Module.displayName] = new Module({
config : this.configuration
});
} catch ( e ) {
console.log('Module %o skipped because %o', Module, e);
}
this.isReady = editor.isReady.then(() => {
this.exportAPI(editor);
onReady();
});
}

/**
* Modules instances configuration:
* - pass other modules to the 'state' property
* - ...
*/
configureModules() {
for(let name in this.moduleInstances) {
/**
* Module does not need self-instance
*/
this.moduleInstances[name].state = this.getModulesDiff( name );
}
}

/**
* Return modules without passed name
* Export external API methods
*
* @param editor
*/
getModulesDiff( name ) {
let diff = {};

for(let moduleName in this.moduleInstances) {
/**
* Skip module with passed name
*/
if (moduleName === name) {
continue;
exportAPI(editor) {
const fieldsToExport = [ 'configuration' ];
const destroy = () => {
editor.moduleInstances.Listeners.removeAll();
editor.moduleInstances.UI.destroy();
editor = null;

for (const field in this) {
delete this[field];
}
diff[moduleName] = this.moduleInstances[moduleName];
}

return diff;
}
Object.setPrototypeOf(this, null);
};

/**
* Start Editor!
*
* Get list of modules that needs to be prepared and return a sequence (Promise)
* @return {Promise}
*/
async start() {
const modulesToPrepare = ['Tools', 'UI', 'BlockManager', 'Paste'];
fieldsToExport.forEach(field => {
this[field] = editor[field];
});

await modulesToPrepare.reduce(
(promise, module) => promise.then(async () => {
_.log(`Preparing ${module} module`, 'time');
this.destroy = destroy;

try {
await this.moduleInstances[module].prepare();
} catch (e) {
_.log(`Module ${module} was skipped because of %o`, 'warn', e);
}
_.log(`Preparing ${module} module`, 'timeEnd');
}),
Promise.resolve()
);
Object.setPrototypeOf(this, editor.moduleInstances.API.methods);

return this.moduleInstances.Renderer.render(this.config.data.blocks);
delete this['exportAPI'];
}
};
}
Loading