editor.js/src/components/block.ts
George Berezhnoy 577da8fa22
Drag'n'drop support (#445)
* moving caret initial

* small improvements

* last changes, added docs

* requested changes

* implement getters instead of functions in block cursors

* last requested changes

* caret module docs and last improvements

* update docs

* upgrade request

* update docs

* upd

* todo on delays

* Sanitizer docs

* split func upd

* split blocks update

* up docs

* Listeners Module: initial

* listener module updates

* split is ready

* update

* start to make merge

* upd

* split general upd

split is ready

* ups

* keyboard module update

* BlockManager removed keyboard handler

* commit before merging rewriting2.0

* general upd split

* Documentation upd

* document + listener upd

* upd doc

* documentation upd

* doc upd

* listener upd

* update algh extract fragm

* upd extractRangeContent

* upd dom.js

* keyboard upd (shift + enter & enableLineBreaks)

* upd enter pressed

* keyboard.js upd

* enter pressed upd

* documenation added

* documentation upd

* Toolbar: settings zone added. (#252)

* Toolbar: settings zone added.

* update some comments

* Making a Toolbar

* delete block

* dom improvements and merging blocks

* merge and split improvements

* fix merging

* do not remove block if block contains media

* optimize code

* caret behaviour improved

* up

* up

* merging blocks. Now plugins handles this cases

* mergeable getter

* save

* up

* dom getdeepestnode improvements

* improve getDeepest node method one more time

* upd

* Deal with it

* improve isAtStart

* improve docs

* use smart isAtStart and isAtEnt method in navigateNext/navigatePrevious

* improve docs

* fix bug in $.isEmpty, improve keydown

* fix isAtEnd

* rollback setCaret code duplication

* improve backspace

* Debug tree walker

* fix tree walker

* small caret fix

* queue ordering

* update bundle

* improve first letter checkup

* doc upd

* update current block index setter

* TypeScript support, Webpack 4, Inline Toolbar beginning (#257)

* Create UI

* Support TypeScript Modules

* remove tmp files

* migrate to 2-spaced tabs

* Add TS Linter

* Inline Toolbar moving (#258)

* Inline Toolbar moving

* simplify code

* Check is need to show Inline Toolbar

* remove duplicate from doc

* fix doc

* open/close IT

* Close IT by clicks on Redactor

* @guryn going strange

Co-Authored-By: Taly <vitalik7tv@yandex.ru>

* default settings initial

* add move up button to default tunes area

* need to figure out with assets

* Inline Toolbar Tools base example (#260)

* Inline Toolbar Tools base example

* texts fixed

* imrpove texts

* little fixes

* save

* tunes with interface

* add tool settings

* initial api methods

* api is ready

* started writing docs

* Create svg sprite (#261)

* API

* requested changes

* fix conflicts

* add docs

* doc fixes and interface improvements

* update

* API scopes improved

* Deleting block: Initial

* Delete block with confirmation

* Event subscription&unsubscription

* deletion trigger improvements

* small improvements

* Link Inline Tool (#264)

* Link Inline Tool

* api injected

* text improved

* Clear input on the reselection

* little improvements

* Delete tune fixes

* UI: Block Settings, show Plus after Enter keydown (#265)

* Some UI improvements: icons settigns

* Show plus button after split

* decrease autoprefixer

* rename variable

* Revert "Merge branch 'delete-tune-fixes' into rewriting-version2.0"

This reverts commit 779bf5db9e, reversing
changes made to 972eb87d89.

* Delete Tune improvements

* upd

* upd comments

* actualize API docs

* Allow to connect external Inline Tools (#269)

* Allow to connect external Inline Tools

* unhardcode tool's api settings

* Italic inline tool

* update icon size

* upgrade findParentTag function

* add interface selection

* fix cs

* save marker

* bundles

* add todo

* removing wrapper

* update styles

* market -> term

* add comments

* improve code

* descrease margin

* add text block to example

* add line brakes

* remove space

* fix bugs

* fix bug

* umd as a library target

* background -> background-color

if `background` has only `color` then use `background-color`

* Clear API (#274)

* blockManager.clear

* upd

* api bez ebanoj knopki api

* fix assignment

* insert empty block with clear method

* clear and render methods improved

* open saver.save()

* add comments

* update comments

* fix data returned by editor

* rename plugin name field in data object (#276)

* Text tool refactored (#277)

Now it returns strict data format.

* do not add block if tool is not exist (#278)

* do not add block if tool is not exist

* show warning

* add todo

* update warning message

* put message into variable

* Revert "put message into variable"

This reverts commit c1f63894d7.

* update comment

* Module Keyboard rewrited to BlockEvents (#279)

* Module Keydown rewrited to BlockEvents

* move keyup and mouseup to the Block Events

* Move-up tune (#268)

Co-authored-by: khaydarov <murod.haydarov@gmail.com>

* Move up tune initial
* move up tune initial behaviour
* moving up formula, docs and code cleaned
* do not close the toolbar if moving block up
* move nagivate methods to Caret Module
* navigations returns boolean if caret is set
* code improved
* update comments
* disable tune when block index is zero
* provide API with listener module methods
* caret improvements
* add to docs
* update docs and blocks api methods
* docs changes
* small improvements
* swap instead of moves
* update
* fix swap
* remove unused method
* remove useless return value from setToBlock
* improve caret isAtEnd and isAtStart
* improve swap, fix listeners api@off method

* Eslint --fix for project files (#280)

* Header plugin (#281)

* header initial

* fix styles

* eslint fix

* add appendCallback

* add comments

* update styles

* add svgs

* highlight settings buttons

* do not show text plugin in the toolbar

* remove svg

* Fixing caret behaviour. (#282)

Plugins can change their state so that affect on Block's pluginsContent property which is in memory.

* remove useless code

* fix merge

* "MoveDown" tune (#283)

* move down initial

Swap current block with next block and scroll window screen

* check if block is last

added new method to the blocks API that returns blocks count

* fix comments

* animate tune

* add animation when tune is disabled

* requested changes

* remove unused css

* Fix merge function and rename Block's wrapper (#284)

* Fix merge function and rename wrapper

* update

* renew condition

* update

* upd

* Merging blocks: Restore caret position 🤟🤟💪 (#286)

* Merging blocks: Restore caret position 🤟🤟💪

* requested changes

* update removing shadow caret

* hide toolbar and selection on typing (#289)

closes #288

* Editor Instance config Interface (#285)

* create interface for editor config

* use IEditorConfig

* create some interfaces

* add comments

* editor interface

* updates

* update editor interface (#293)

* При перемещении по стрелочкам убирать выделение блока (#296)

* При перемещении по стрелочкам убирать выделение блока

* Add comments

* update comments

* update comment

* update toolbar design (#301)

* Set caret at the end if clicked outsite the block (#305)

* Set caret at last block or create new block at end

* update comment

* fix comments

* Insert new Block when enter pressed on editor area (#307)

* insert new block when enter pressed on editor zone

* extra conditions. Enter must be handled on editors area

* move at editor condition to the Selection method

* closes can return null

* fixing editor area

* do not create new block

* clean example

* updates due to the requested changes

* Add placeholder to contentEditable elements (#306)

* add placeholder to contentEditable elements

* store selection color in a variable

* add placeholder to header block

* Add placeholder to contenteditable only if attribute data-placeholder exists

* remove tool config

* Close toolbar after block is removed (#314)

* makeSettings -> renderSettings (#315)

* Term: new icon, new style. + margin between settings buttons (#316)

Resolves #309

* Tool's renderToolboxIcon function  (#318)

* create renderToolboxIcon

issue was not done

* remove useless span wrapper

* update linters

* update styles

* process click on svg by closest

* remove commented code

* rename function: svgIcon -> toolboxIcon

* add toolboxIcon to docs

* Paste (#259)

* Paste module

* Rewrite paste logic

* Update due comments

* Docs

* Add all block elements

* Sanitize content on paste

* Remove logs and add header handler

* Add comment to dom.js

* Add comment to tools.js

* Split block if paste not at the end

* Update docs

* Update docs

* pidr

* Take Tool name from config

* Update docs

* Label onPaste handler as private

* Resolve conflict

* Replace current block if it is empty (#320)

* Improve Header line-height (#321)

* Fix typo (#324)

* CodeX Editor 2.0

Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>
Co-authored-by: Petr Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>
Co-authored-by: Taly Guryn <vitalik7tv@yandex.ru>

* Fix typo

* Installation docs (#325)

close #310

* Module Shortcuts (#317)

* import shortcuts

* node modules works

* enable shortcut for inline tools

* check shortcut existance

* enable shortcuts to Block tools

* set caret to block if block replaced

* enable shortcuts on inline-tools

* improve code

* last changes

* update

* fix

* insert returns block so we can set caret

* use Map instead own structure

* disable shortcut if iniline-toolbar disabled

* update code styles

* remove todo

* upd

* update

* remove settings from insert function

* create interface

* code improvements

* use const instead of let

* upd

* Simple Image Tool (#326)

* Simple Image

* fix pattern

* show tunes` state

* update code

* update code

* upd

* Fix toolbox appearance, tools boxed are clickable (#331)

* Remove toolsConfig from Editor's config (#327)

* fix linters

* remove toolsConfig

* update tool's interfaces

* add comments

* bundles

* remove test headers

* restore commented code

* update tool's interface

* toolConfig -> toolSetting

* fix typos

toolSetting -> toolSettings
toolsSettings -> toolSettings
toolClasses -> toolsClasses

* update code comments

* update installation doc

* update docs

* update dev dep packages (#333)

* Check is paste handler a function only if it exists (#328)

* Toolbar with tab (#330)

* toolbar tabs initial

* leaf initial

* save state

* flip back toolbox items

* enter on toolbox item

* update

* requested changes

* new condfition

* update

* improve animation on leaf

* fix shift+tab flip

* up

* update

* updates

* Consecutive blank lines are forbidden

* Correct choosing next toolbox item

* update

* update comment

* Validate editor's config before initing (#341)

* validate editor's config before initing

* update readme

* @

* update comments

* add function _.isClass

* Styles API (#343)

* StylesAPI

* use styles api in plugins

* add inline styles

* List Tool [new] (#344)

* list tool initial

* list class with settings

* make tool reactive

* final List improvements

* reorder

* tmp update

* unhadrcode enter handler

* updates

* enableLineBreaks also checks

* skip empty items

* select LI by CMD+A, fix backspace in the last item

* improve check for emptiness

* Example page improved (#347)

* Update new example

* imrpove example.html

* updates

* improve code

* Header plugin (#348)

* isFunction function

* use header from cdn

* Improve paste behaviour (#346)

* Improve paste behaviour

* Done

* Don't pass empty items

* Update comment

* move public up

* Remove disallow paste option

* Quote Tool (#329)

* Quote Tool

* Add icon

* Upd

* fix ENTER on quote

* Remove useless code

* items -> blocks (#351)

* use SimpleImage from cdn (#355)

* use simpleimage from cdn

* add comments

* fix spaces

* fix comments

* remove comments

* update simple-image script

* Update text on the example.html (#356)

* use Paragraph Tool from CDN (#357)

* use Paragraph Tool from CDN

* add line brakes

* rename block: paragraph -> text

* Remove _callbacks.js (#358)

* Clear unused files (#359)

* TOOLBAR_ICON_CLASS -> TOOLBAR_ICON (#360)

* TOOLBAR_ICON_CLASS -> TOOLBAR_ICON

* remove defaultConfig

* Delimiter tool (#362)

* Delimiter added

* ашч

* use delimiter from cdn

* Enter on editor (#363)

* Enterpress on editor

* use additional property

* check enter on body

* update

* fix toolbar behaviour

* upd

* update bundle

* remove useless ui property

* update comment

* add Element.prepend() function (#365)

* add Element.prepend() function

* allow to pass element of array to prepend() polyfill

* use List Tool from cdn (#366)

* use List Tool from cdn

* add missing </div> in example.html

* Pass "config" from Tool's settings to Tool's constructor (#367)

* pass config from Tool's settings to Tool's constructor

* reorder elements

* add apiSettings.CONFIG property

* use string as a object's key 😔 (#368)

* update placeholder's styles (#369)

* Add shortcuts for internal tools (#370)

* Add shortcuts for internal tools

* upd doc

* remove articles

* ☹️ guryn asked

* use quote from cdn (#371)

* Add cache to the inline tools (#372)

* use Inline Code Tool from cdn (#375)

* Issue 354 inline tools filter (#376)

* Allow to filter inline tools.

* update example

* update header

* fix endless cycle (#378)

* Destructured options for Inline Tools (#379)

* add destructured options for inline-tools

* temporary disable inlineCode

* remove term sources

* Fix toolbar moving after arrow navigation (#380)

* Fix toolbar moving after arrow navigation

Resolves #361

* move before open

* fix error on Enter after block removing

* add example Tools as submodules (#381)

* add destructured options for inline-tools

* temporary disable inlineCode

* remove term sources

* add inline code Tool as a submodule

* update version of inline-code package

* add Tools as submodules

* update Tools and use destructured params object

* Add constructor() docs

* update package Paragraph Tool

* update installation docs

* Input navigation (#339)

* Quote Tool

* Add icon

* Upd

* Initial setup

* Save changes

* Add scroll and fix input focus

* Add comments

* Rebuild bundle

* fix ENTER on quote

* Fix split behaviour

* Fix

* Navigate only to contentful blocks

* add comments

* Fix backspace on last block

* Remove log

* It works

* Resolve comments

* Use constants

* New readme 🦅 (#386)

* New readme 🦅

* upd text

* Issue 374 (#385)

* Fix issue #374

* Set current block index to -1 if there is no blocks left

* Insert new block if first one was removed by default

* Paragraph as a default Tool in editor; Zero-conf (#389)

* git commit -m "Removed submodule Paragraph"

* add paragraph to core

+ zero-config

* update bundle

* update comment

* remove log

* enable minifying (#390)

* Drop current block index only if there is no selection at the Editor (#388)

* Drop current block index only if there is no selection at the Editor

* Set current node if click ended out of editor

* Small backspace behaviour improvement (#391)

* Small backspace behaviour improvement

* fix caret position

* update from base branch

* Update webpack config

* Migrate to Yarn (#393)

* Migrate to yarn

* Update scripts

* Rewrite helpers classes to TypeScript (#396)

* Add docs and isReady promise (#394)

* set default holderId value (#404)

* Destroyer (#392)

* Initial destroy method

* Add destroy method to api docs

* Export isReady promise in CodexEditor constructor

* Add drag'n'drop support

* Add docs

* Small fix

* Continue merging master :|

* Fixes after merge

* Add styles and improve behavior

* Close inline toolbar and remove dragged content

* Some improvements of paste data processing

* Delete selection content only if drag has been started at editor

* Add comments

* Support of pasting from copy-buffer

* Change header level

* Improvements

* Use isDraNDrop flag

* Update example

* Improve d'n'd behaviour

* git pull for simple-image tool
2018-09-27 21:11:30 +03:00

389 lines
9.7 KiB
TypeScript

import IBlockTune, {IBlockTuneConstructor} from './interfaces/block-tune';
import $ from './dom';
import _ from './utils';
type Tool = any;
/**
* @class Block
* @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool
*
* @property {Tool} 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';
/**
* @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(): {wrapper: string, wrapperStretched: string, content: string, selected: string, dropTarget: string} {
return {
wrapper: 'ce-block',
wrapperStretched: 'ce-block--stretched',
content: 'ce-block__content',
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[] {
const content = this.holder;
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
const selector = '[contenteditable], textarea, input, '
+ allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');
const inputs = _.array(content.querySelectorAll(selector));
/**
* 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;
}
return inputs;
}
/**
* Return current Tool`s input
*
* @returns {HTMLElement}
*/
get currentInput(): HTMLElement {
return this.inputs[this.inputIndex];
}
/**
* Set input index to the passed element
*
* @param {HTMLElement} element
*/
set currentInput(element: HTMLElement) {
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 {Node}
*/
get pluginsContent(): Node {
const pluginsContent = this.holder.querySelector(`.${Block.CSS.content}`);
if (pluginsContent && pluginsContent.childNodes.length) {
return pluginsContent.childNodes[0];
}
return null;
}
/**
* Get Block's JSON data
* @return {Object}
*/
get data(): object {
return this.save();
}
/**
* 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 {
/**
* Allow Tool to represent decorative contentless blocks: for example "* * *"-tool
* That Tools are not empty
*/
if (this.class.contentless) {
return false;
}
const emptyText = $.isEmpty(this.pluginsContent),
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 selected state
* @param {Boolean} state - 'true' to select, 'false' to remove selection
*/
set selected(state: boolean) {
/**
* We don't need to mark Block as Selected when it is not empty
*/
if (state === true && !this.isEmpty) {
this.holder.classList.add(Block.CSS.selected);
} else {
this.holder.classList.remove(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);
}
public name: string;
public tool: Tool;
public class: any;
public settings: object;
public holder: HTMLDivElement;
public tunes: IBlockTune[];
private readonly api: object;
private inputIndex = 0;
/**
* @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: Tool, toolClass: object, settings: object, apiMethods: object) {
this.name = toolName;
this.tool = toolInstance;
this.class = toolClass;
this.settings = settings;
this.api = apiMethods;
this.holder = this.compose();
/**
* @type {IBlockTune[]}
*/
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) {
this.tool[methodName].call(this.tool, params);
}
}
/**
* Call plugins merge method
* @param {Object} data
*/
public mergeWith(data: object): Promise<void> {
return Promise.resolve()
.then(() => {
this.tool.merge(data);
});
}
/**
* Extracts data from Block
* Groups Tool's save processing time
* @return {Object}
*/
public save(): Promise<void|{tool: string, data: any, time: number}> {
const extractedBlock = this.tool.save(this.pluginsContent);
/**
* 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(function(error) {
_.log(`Saving proccess for ${this.tool.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 also can return data if it passed the validation
*
* @param {Object} data
* @returns {Boolean|Object} valid
*/
public validateData(data: object): object|false {
let isValid = true;
if (this.tool.validate instanceof Function) {
isValid = this.tool.validate(data);
}
if (!isValid) {
return false;
}
return data;
}
/**
* Make an array with default settings
* Each block has default tune instance that have states
* @return {IBlockTune[]}
*/
public makeTunes(): IBlockTune[] {
const tunesList = [MoveUpTune, DeleteTune, MoveDownTune];
// Pluck tunes list and return tune instances with passed Editor API and settings
return tunesList.map( (tune: IBlockTuneConstructor) => {
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;
}
/**
* Toggle drop target state
* @param {boolean} state
*/
public set dropTarget(state) {
this.holder.classList.toggle(Block.CSS.dropTarget, state);
}
/**
* 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;
}
}