mirror of
https://github.com/codex-team/editor.js
synced 2024-05-19 06:47:16 +02:00
ac93017c70
* 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
572 lines
13 KiB
TypeScript
572 lines
13 KiB
TypeScript
import {
|
|
API,
|
|
BlockTool,
|
|
BlockToolConstructable,
|
|
BlockToolData,
|
|
BlockTune,
|
|
BlockTuneConstructable,
|
|
SanitizerConfig,
|
|
ToolConfig,
|
|
} from '../../types';
|
|
|
|
import {SavedData} from '../types-internal/block-data';
|
|
import $ from './dom';
|
|
import * as _ from './utils';
|
|
|
|
/**
|
|
* @class Block
|
|
* @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool
|
|
*
|
|
* @property {BlockTool} tool — current block tool (Paragraph, for example)
|
|
* @property {Object} CSS — block`s css classes
|
|
*
|
|
*/
|
|
|
|
/** Import default tunes */
|
|
import MoveUpTune from './block-tunes/block-tune-move-up';
|
|
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 {
|
|
/**
|
|
* @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
|
|
*
|
|
* @property tool - Tool instance
|
|
* @property html - Returns HTML content of plugin
|
|
* @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
|
|
*/
|
|
export default class Block {
|
|
|
|
/**
|
|
* CSS classes for the Block
|
|
* @return {{wrapper: string, content: string}}
|
|
*/
|
|
static get CSS() {
|
|
return {
|
|
wrapper: 'ce-block',
|
|
wrapperStretched: 'ce-block--stretched',
|
|
content: 'ce-block__content',
|
|
focused: 'ce-block--focused',
|
|
selected: 'ce-block--selected',
|
|
dropTarget: 'ce-block--drop-target',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Find and return all editable elements (contenteditables and native inputs) in the Tool HTML
|
|
*
|
|
* @returns {HTMLElement[]}
|
|
*/
|
|
get inputs(): HTMLElement[] {
|
|
/**
|
|
* Return from cache if existed
|
|
*/
|
|
if (this.cachedInputs.length !== 0) {
|
|
return this.cachedInputs;
|
|
}
|
|
|
|
const content = this.holder;
|
|
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
|
|
|
|
const selector = '[contenteditable], textarea, input:not([type]), '
|
|
+ allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');
|
|
|
|
let inputs = _.array(content.querySelectorAll(selector));
|
|
|
|
/**
|
|
* If contenteditable element contains block elements, treat them as inputs.
|
|
*/
|
|
inputs = inputs.reduce((result, input) => {
|
|
if ($.isNativeInput(input) || $.containsOnlyInlineElements(input)) {
|
|
return [...result, input];
|
|
}
|
|
|
|
return [...result, ...$.getDeepestBlockElements(input)];
|
|
}, []);
|
|
|
|
/**
|
|
* If inputs amount was changed we need to check if input index is bigger then inputs array length
|
|
*/
|
|
if (this.inputIndex > inputs.length - 1) {
|
|
this.inputIndex = inputs.length - 1;
|
|
}
|
|
|
|
/**
|
|
* Cache inputs
|
|
*/
|
|
this.cachedInputs = inputs;
|
|
|
|
return inputs;
|
|
}
|
|
|
|
/**
|
|
* Return current Tool`s input
|
|
*
|
|
* @returns {HTMLElement}
|
|
*/
|
|
get currentInput(): HTMLElement | Node {
|
|
return this.inputs[this.inputIndex];
|
|
}
|
|
|
|
/**
|
|
* Set input index to the passed element
|
|
*
|
|
* @param {HTMLElement} element
|
|
*/
|
|
set currentInput(element: HTMLElement | Node) {
|
|
const index = this.inputs.findIndex((input) => input === element || input.contains(element));
|
|
|
|
if (index !== -1) {
|
|
this.inputIndex = index;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return first Tool`s input
|
|
*
|
|
* @returns {HTMLElement}
|
|
*/
|
|
get firstInput(): HTMLElement {
|
|
return this.inputs[0];
|
|
}
|
|
|
|
/**
|
|
* Return first Tool`s input
|
|
*
|
|
* @returns {HTMLElement}
|
|
*/
|
|
get lastInput(): HTMLElement {
|
|
const inputs = this.inputs;
|
|
|
|
return inputs[inputs.length - 1];
|
|
}
|
|
|
|
/**
|
|
* Return next Tool`s input or undefined if it doesn't exist
|
|
*
|
|
* @returns {HTMLElement}
|
|
*/
|
|
get nextInput(): HTMLElement {
|
|
return this.inputs[this.inputIndex + 1];
|
|
}
|
|
|
|
/**
|
|
* Return previous Tool`s input or undefined if it doesn't exist
|
|
*
|
|
* @returns {HTMLElement}
|
|
*/
|
|
get previousInput(): HTMLElement {
|
|
return this.inputs[this.inputIndex - 1];
|
|
}
|
|
|
|
/**
|
|
* Returns Plugins content
|
|
* @return {HTMLElement}
|
|
*/
|
|
get pluginsContent(): HTMLElement {
|
|
const blockContentNodes = this.holder.querySelector(`.${Block.CSS.content}`);
|
|
|
|
if (blockContentNodes && blockContentNodes.childNodes.length) {
|
|
/**
|
|
* Editors Block content can contain different Nodes from extensions
|
|
* We use DOM isExtensionNode to ignore such Nodes and return first Block that does not match filtering list
|
|
*/
|
|
for (let child = blockContentNodes.childNodes.length - 1; child >= 0; child--) {
|
|
const contentNode = blockContentNodes.childNodes[child];
|
|
|
|
if (!$.isExtensionNode(contentNode)) {
|
|
return contentNode as HTMLElement;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get Block's JSON data
|
|
* @return {Object}
|
|
*/
|
|
get data(): BlockToolData {
|
|
return this.save().then((savedObject) => {
|
|
if (savedObject && !_.isEmpty(savedObject.data)) {
|
|
return savedObject.data;
|
|
} else {
|
|
return {};
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns tool's sanitizer config
|
|
* @return {object}
|
|
*/
|
|
get sanitize(): SanitizerConfig {
|
|
return this.tool.sanitize;
|
|
}
|
|
|
|
/**
|
|
* is block mergeable
|
|
* We plugin have merge function then we call it mergable
|
|
* @return {boolean}
|
|
*/
|
|
get mergeable(): boolean {
|
|
return typeof this.tool.merge === 'function';
|
|
}
|
|
|
|
/**
|
|
* Check block for emptiness
|
|
* @return {Boolean}
|
|
*/
|
|
get isEmpty(): boolean {
|
|
const emptyText = $.isEmpty(this.pluginsContent);
|
|
const emptyMedia = !this.hasMedia;
|
|
|
|
return emptyText && emptyMedia;
|
|
}
|
|
|
|
/**
|
|
* Check if block has a media content such as images, iframes and other
|
|
* @return {Boolean}
|
|
*/
|
|
get hasMedia(): boolean {
|
|
/**
|
|
* This tags represents media-content
|
|
* @type {string[]}
|
|
*/
|
|
const mediaTags = [
|
|
'img',
|
|
'iframe',
|
|
'video',
|
|
'audio',
|
|
'source',
|
|
'input',
|
|
'textarea',
|
|
'twitterwidget',
|
|
];
|
|
|
|
return !!this.holder.querySelector(mediaTags.join(','));
|
|
}
|
|
|
|
/**
|
|
* Set focused state
|
|
* @param {Boolean} state - 'true' to select, 'false' to remove selection
|
|
*/
|
|
set focused(state: boolean) {
|
|
this.holder.classList.toggle(Block.CSS.focused, state);
|
|
}
|
|
|
|
/**
|
|
* Set selected state
|
|
* We don't need to mark Block as Selected when it is empty
|
|
* @param {Boolean} state - 'true' to select, 'false' to remove selection
|
|
*/
|
|
set selected(state: boolean) {
|
|
if (state) {
|
|
this.holder.classList.add(Block.CSS.selected);
|
|
} else {
|
|
this.holder.classList.remove(Block.CSS.selected);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns True if it is Selected
|
|
* @return {boolean}
|
|
*/
|
|
get selected(): boolean {
|
|
return this.holder.classList.contains(Block.CSS.selected);
|
|
}
|
|
|
|
/**
|
|
* Set stretched state
|
|
* @param {Boolean} state - 'true' to enable, 'false' to disable stretched statte
|
|
*/
|
|
set stretched(state: boolean) {
|
|
this.holder.classList.toggle(Block.CSS.wrapperStretched, state);
|
|
}
|
|
|
|
/**
|
|
* Toggle drop target state
|
|
* @param {boolean} state
|
|
*/
|
|
public set dropTarget(state) {
|
|
this.holder.classList.toggle(Block.CSS.dropTarget, state);
|
|
}
|
|
|
|
/**
|
|
* Block Tool`s name
|
|
*/
|
|
public name: string;
|
|
|
|
/**
|
|
* Instance of the Tool Block represents
|
|
*/
|
|
public tool: BlockTool;
|
|
|
|
/**
|
|
* Class blueprint of the ool Block represents
|
|
*/
|
|
public class: BlockToolConstructable;
|
|
|
|
/**
|
|
* User Tool configuration
|
|
*/
|
|
public settings: ToolConfig;
|
|
|
|
/**
|
|
* Wrapper for Block`s content
|
|
*/
|
|
public holder: HTMLDivElement;
|
|
|
|
/**
|
|
* Tunes used by Tool
|
|
*/
|
|
public tunes: BlockTune[];
|
|
|
|
/**
|
|
* Cached inputs
|
|
* @type {HTMLElement[]}
|
|
*/
|
|
private cachedInputs: HTMLElement[] = [];
|
|
|
|
/**
|
|
* Editor`s API
|
|
*/
|
|
private readonly api: API;
|
|
|
|
/**
|
|
* Focused input index
|
|
* @type {number}
|
|
*/
|
|
private inputIndex = 0;
|
|
|
|
/**
|
|
* Mutation observer to handle DOM mutations
|
|
* @type {MutationObserver}
|
|
*/
|
|
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
|
|
* @param {Object} toolInstance — passed Tool`s instance that rendered the Block
|
|
* @param {Object} toolClass — Tool's class
|
|
* @param {Object} settings - default settings
|
|
* @param {Object} apiMethods - Editor API
|
|
*/
|
|
constructor(
|
|
toolName: string,
|
|
toolInstance: BlockTool,
|
|
toolClass: BlockToolConstructable,
|
|
settings: ToolConfig,
|
|
apiMethods: API,
|
|
) {
|
|
this.name = toolName;
|
|
this.tool = toolInstance;
|
|
this.class = toolClass;
|
|
this.settings = settings;
|
|
this.api = apiMethods;
|
|
this.holder = this.compose();
|
|
|
|
this.mutationObserver = new MutationObserver(this.didMutated);
|
|
|
|
/**
|
|
* @type {BlockTune[]}
|
|
*/
|
|
this.tunes = this.makeTunes();
|
|
}
|
|
|
|
/**
|
|
* Calls Tool's method
|
|
*
|
|
* Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function
|
|
*
|
|
* @param {String} methodName
|
|
* @param {Object} params
|
|
*/
|
|
public call(methodName: string, params?: object) {
|
|
/**
|
|
* call Tool's method with the instance context
|
|
*/
|
|
if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
|
|
try {
|
|
this.tool[methodName].call(this.tool, params);
|
|
} catch (e) {
|
|
_.log(`Error during '${methodName}' call: ${e.message}`, 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call plugins merge method
|
|
* @param {Object} data
|
|
*/
|
|
public async mergeWith(data: BlockToolData): Promise<void> {
|
|
await this.tool.merge(data);
|
|
}
|
|
/**
|
|
* Extracts data from Block
|
|
* Groups Tool's save processing time
|
|
* @return {Object}
|
|
*/
|
|
public async save(): Promise<void|SavedData> {
|
|
const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement);
|
|
|
|
/**
|
|
* Measuring execution time
|
|
*/
|
|
const measuringStart = window.performance.now();
|
|
let measuringEnd;
|
|
|
|
return Promise.resolve(extractedBlock)
|
|
.then((finishedExtraction) => {
|
|
/** measure promise execution */
|
|
measuringEnd = window.performance.now();
|
|
|
|
return {
|
|
tool: this.name,
|
|
data: finishedExtraction,
|
|
time : measuringEnd - measuringStart,
|
|
};
|
|
})
|
|
.catch((error) => {
|
|
_.log(`Saving proccess for ${this.name} tool failed due to the ${error}`, 'log', 'red');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Uses Tool's validation method to check the correctness of output data
|
|
* Tool's validation method is optional
|
|
*
|
|
* @description Method returns true|false whether data passed the validation or not
|
|
*
|
|
* @param {BlockToolData} data
|
|
* @returns {Promise<boolean>} valid
|
|
*/
|
|
public async validate(data: BlockToolData): Promise<boolean> {
|
|
let isValid = true;
|
|
|
|
if (this.tool.validate instanceof Function) {
|
|
isValid = await this.tool.validate(data);
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
/**
|
|
* Make an array with default settings
|
|
* Each block has default tune instance that have states
|
|
* @return {BlockTune[]}
|
|
*/
|
|
public makeTunes(): BlockTune[] {
|
|
const tunesList = [MoveUpTune, DeleteTune, MoveDownTune];
|
|
|
|
// Pluck tunes list and return tune instances with passed Editor API and settings
|
|
return tunesList.map( (tune: BlockTuneConstructable) => {
|
|
return new tune({
|
|
api: this.api,
|
|
settings: this.settings,
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enumerates initialized tunes and returns fragment that can be appended to the toolbars area
|
|
* @return {DocumentFragment}
|
|
*/
|
|
public renderTunes(): DocumentFragment {
|
|
const tunesElement = document.createDocumentFragment();
|
|
|
|
this.tunes.forEach( (tune) => {
|
|
$.append(tunesElement, tune.render());
|
|
});
|
|
|
|
return tunesElement;
|
|
}
|
|
|
|
/**
|
|
* Update current input index with selection anchor node
|
|
*/
|
|
public updateCurrentInput(): void {
|
|
this.currentInput = SelectionUtils.anchorNode;
|
|
}
|
|
|
|
/**
|
|
* Is fired when Block will be selected as current
|
|
*/
|
|
public willSelect(): void {
|
|
/**
|
|
* Observe DOM mutations to update Block inputs
|
|
*/
|
|
this.mutationObserver.observe(
|
|
this.holder.firstElementChild,
|
|
{
|
|
childList: true,
|
|
subtree: true,
|
|
characterData: true,
|
|
attributes: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Is fired when Block will be unselected
|
|
*/
|
|
public willUnselect() {
|
|
this.mutationObserver.disconnect();
|
|
}
|
|
|
|
/**
|
|
* Make default Block wrappers and put Tool`s content there
|
|
* @returns {HTMLDivElement}
|
|
*/
|
|
private compose(): HTMLDivElement {
|
|
const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement,
|
|
contentNode = $.make('div', Block.CSS.content),
|
|
pluginsContent = this.tool.render();
|
|
|
|
contentNode.appendChild(pluginsContent);
|
|
wrapper.appendChild(contentNode);
|
|
return wrapper;
|
|
}
|
|
}
|