editor.js/src/components/core.ts
Peter Savchenko ac93017c70
Release 2.16 (#966)
* 2.16.0

* [Refactor] Separate internal and external settings (#845)

* Enable flipping tools via standalone class (#830)

* Enable flipping tools via standalone class

* use flipper to refactor (#842)

* use flipper to refactor

* save changes

* update

* fix flipper on inline toolbar

* ready for testing

* requested changes

* update doc

* updates

* destroy flippers

* some requested changes

* update

* update

* ready

* update

* last changes

* update docs

* Hghl active button of CT, simplify activate/deactivate

* separate dom iterator

* unhardcode directions

* fixed a link in readme.md (#856)

* Fix Block selection via CMD+A (#829)

* Fix Block selection via CMD+A

* Delete editor.js.map

* update

* update

* Update CHANGELOG.md

* Improve style of selected blocks (#858)

* Cross-block-selection style improved

* Update CHANGELOG.md

* Fix case when property 'observer' in modificationObserver is not defined (#866)

* Bump lodash.template from 4.4.0 to 4.5.0 (#885)

Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

Signed-off-by: dependabot[bot] <support@github.com>

* Bump eslint-utils from 1.3.1 to 1.4.2 (#886)

Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>

* Bump mixin-deep from 1.3.1 to 1.3.2 (#887)

Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>

* update bundle and readme

* Update README.md

* upd codeowners, fix funding

* Minor Docs Fix according to main Readme (#916)

* Inline Toolbar now contains Conversion Toolbar (#932)

* Block lifecycle hooks (#906)

* [Fix] Arrow selection (#964)

* Fix arrow selection

* Add docs

* [issue-926]: fix dom iterator leafing when items are empty (#958)

* [issue-926]: fix dom iterator leafing when items are empty

* update Changelog

* Issue 869 (#963)

* Fix issue 943 (#965)

* [Draft] Feature/tooltip enhancements (#907)

* initial

* update

* make module standalone

* use tooltips as external module

* update

* build via prod mode

* add tooltips as external module

* add declaration file and options param

* add api tooltip

* update

* removed submodule

* removed due to the incorrect setip

* setup tooltips again

* wip

* update tooltip module

* toolbox, inline toolbar

* Tooltips in block tunes not uses shorthand

* shorthand in a plus and block settings

* fix doc

* Update tools-inline.md

* Delete tooltip.css

* Update CHANGELOG.md

* Update codex.tooltips

* Update api.md

* [issue-779]: Grammarly conflicts (#956)

* grammarly conflicts

* update

* upd bundle

* Submodule Header now on master

* Submodule Marker now on master

* Submodule Paragraph now on master

* Submodule InlineCode now on master

* Submodule Simple Image now on master

* [issue-868]: Deleting multiple blocks triggers back button in Firefox (#967)

* Deleting multiple blocks triggers back button in Firefox

@evgenusov

* Update editor.js

* Update CHANGELOG.md

* pass options on removeEventListener (#904)

* pass options on removeEventListener by removeAll

* rebuild

* Merge branch 'release/2.16' into pr/904

* Update CHANGELOG.md

* Update inline.ts

* [Fix] Selection rangecount (#968)

* Fix #952 (#969)

* Update codex.tooltips

* Selection bugfix (#970)

* Selection bugfix

* fix cross block selection

* close inline toolbar when blocks selected via shift

* remove inline toolbar closing on cross block selection mouse up due to the bug (#972)

* [Feature] Log levels (#971)

* Decrease margins (#973)

* Decrease margins

* Update editor.licenses.txt

* Update src/components/domIterator.ts

Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com>

* [Fix] Fix delete blocks api method (#974)

* Update docs/usage.md

Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com>

* rm unused

* Update yarn.lock file

* upd bundle, changelog
2019-11-30 23:42:39 +03:00

353 lines
8.9 KiB
TypeScript

import $ from './dom';
import * as _ from './utils';
import {EditorConfig, OutputData, SanitizerConfig} from '../../types';
import {EditorModules} from '../types-internal/editor-modules';
import {LogLevels} from './utils';
/**
* @typedef {Core} Core - editor core class
*/
/**
* Require Editor modules places in components/modules dir
*/
const contextRequire = require.context('./modules', true);
const modules = [];
contextRequire.keys().forEach((filename) => {
/**
* Include files if:
* - extension is .js or .ts
* - does not starts with _
*/
if (filename.match(/^\.\/[^_][\w/]*\.([tj])s$/)) {
modules.push(contextRequire(filename));
}
});
/**
* @class Core
*
* @classdesc Editor.js core class
*
* @property this.config - all settings
* @property this.moduleInstances - constructed editor components
*
* @type {Core}
*/
export default class Core {
/**
* Editor configuration passed by user to the constructor
*/
public config: EditorConfig;
/**
* Object with core modules instances
*/
public moduleInstances: EditorModules = {} as EditorModules;
/**
* Promise that resolves when all core modules are prepared and UI is rendered on the page
*/
public isReady: Promise<void>;
/**
* @param {EditorConfig} config - user configuration
*
*/
constructor(config?: EditorConfig|string) {
/**
* Ready promise. Resolved if Editor.js is ready to work, rejected otherwise
*/
let onReady, onFail;
this.isReady = new Promise((resolve, reject) => {
onReady = resolve;
onFail = reject;
});
Promise.resolve()
.then(async () => {
this.configuration = config;
await this.validate();
await this.init();
await this.start();
_.logLabeled('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75');
setTimeout(async () => {
await this.render();
if ((this.configuration as EditorConfig).autofocus) {
const {BlockManager, Caret} = this.moduleInstances;
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
}
/**
* Remove loader, show content
*/
this.moduleInstances.UI.removeLoader();
/**
* Resolve this.isReady promise
*/
onReady();
}, 500);
})
.catch((error) => {
_.log(`Editor.js is not ready because of ${error}`, 'error');
/**
* Reject this.isReady promise
*/
onFail(error);
});
}
/**
* Setting for configuration
* @param {EditorConfig|string|undefined} config
*/
set configuration(config: EditorConfig|string) {
/**
* Process zero-configuration or with only holderId
* Make config object
*/
if (typeof config !== 'object') {
config = {
holder: config,
};
}
/**
* If holderId is preset, assign him to holder property and work next only with holder
*/
if (config.holderId && !config.holder) {
config.holder = config.holderId;
config.holderId = null;
_.log('holderId property will deprecated in next major release, use holder property instead.', 'warn');
}
/**
* Place config into the class property
* @type {EditorConfig}
*/
this.config = config;
/**
* If holder is empty then set a default value
*/
if (this.config.holder == null) {
this.config.holder = 'editorjs';
}
if (!this.config.logLevel) {
this.config.logLevel = LogLevels.VERBOSE;
}
_.setLogLevel(this.config.logLevel);
/**
* If initial Block's Tool was not passed, use the Paragraph Tool
*/
this.config.initialBlock = this.config.initialBlock || 'paragraph';
/**
* Height of Editor's bottom area that allows to set focus on the last Block
* @type {number}
*/
this.config.minHeight = this.config.minHeight || 300;
/**
* Initial block type
* Uses in case when there is no blocks passed
* @type {{type: (*), data: {text: null}}}
*/
const initialBlockData = {
type : this.config.initialBlock,
data : {},
};
this.config.placeholder = this.config.placeholder || false;
this.config.sanitizer = this.config.sanitizer || {
p: true,
b: true,
a: true,
} as SanitizerConfig;
this.config.hideToolbar = this.config.hideToolbar ? this.config.hideToolbar : false;
this.config.tools = this.config.tools || {};
this.config.data = this.config.data || {} as OutputData;
this.config.onReady = this.config.onReady || (() => {});
this.config.onChange = this.config.onChange || (() => {});
/**
* Initialize Blocks to pass data to the Renderer
*/
if (_.isEmpty(this.config.data)) {
this.config.data = {} as OutputData;
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(): EditorConfig|string {
return this.config;
}
/**
* Checks for required fields in Editor's config
* @returns {Promise<void>}
*/
public async validate(): Promise<void> {
const { holderId, holder } = this.config;
if (holderId && holder) {
throw Error('«holderId» and «holder» param can\'t assign at the same time.');
}
/**
* Check for a holder element's existence
*/
if (typeof holder === 'string' && !$.get(holder)) {
throw Error(`element with ID «${holder}» is missing. Pass correct holder's ID.`);
}
if (holder && typeof holder === 'object' && !$.isElement(holder)) {
throw Error('holder as HTMLElement if provided must be inherit from Element class.');
}
}
/**
* Initializes modules:
* - make and save instances
* - configure
*/
public init() {
/**
* Make modules instances and save it to the @property this.moduleInstances
*/
this.constructModules();
/**
* Modules configuration
*/
this.configureModules();
}
/**
* Start Editor!
*
* Get list of modules that needs to be prepared and return a sequence (Promise)
* @return {Promise}
*/
public async start() {
const modulesToPrepare = [
'Tools',
'UI',
'BlockManager',
'Paste',
'DragNDrop',
'ModificationsObserver',
'BlockSelection',
'RectangleSelection',
];
await modulesToPrepare.reduce(
(promise, module) => promise.then(async () => {
// _.log(`Preparing ${module} module`, 'time');
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(),
);
}
/**
* 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) => {
/**
* 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;
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 ) {
_.log(`Module ${Module.displayName} skipped because`, 'warn', e);
}
});
}
/**
* Modules instances configuration:
* - pass other modules to the 'state' property
* - ...
*/
private configureModules(): void {
for (const name in this.moduleInstances) {
if (this.moduleInstances.hasOwnProperty(name)) {
/**
* Module does not need self-instance
*/
this.moduleInstances[name].state = this.getModulesDiff(name);
}
}
}
/**
* Return modules without passed name
* @param {string} name - module for witch modules difference should be calculated
*/
private getModulesDiff(name: string): EditorModules {
const diff = {} as EditorModules;
for (const moduleName in this.moduleInstances) {
/**
* Skip module with passed name
*/
if (moduleName === name) {
continue;
}
diff[moduleName] = this.moduleInstances[moduleName];
}
return diff;
}
}