editor.js/src/components/modules/toolbar/conversion.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

312 lines
8.8 KiB
TypeScript

import Module from '../../__module';
import $ from '../../dom';
import {BlockToolConstructable} from '../../../../types';
import * as _ from '../../utils';
import {SavedData} from '../../../types-internal/block-data';
import Block from '../../block';
import Flipper from '../../flipper';
/**
* Block Converter
*/
export default class ConversionToolbar extends Module {
/**
* CSS getter
*/
public static get CSS(): { [key: string]: string } {
return {
conversionToolbarWrapper: 'ce-conversion-toolbar',
conversionToolbarShowed: 'ce-conversion-toolbar--showed',
conversionToolbarTools: 'ce-conversion-toolbar__tools',
conversionToolbarLabel: 'ce-conversion-toolbar__label',
conversionTool: 'ce-conversion-tool',
conversionToolHidden: 'ce-conversion-tool--hidden',
conversionToolIcon: 'ce-conversion-tool__icon',
conversionToolFocused : 'ce-conversion-tool--focused',
conversionToolActive : 'ce-conversion-tool--active',
};
}
/**
* HTML Elements used for UI
*/
public nodes: { [key: string]: HTMLElement } = {
wrapper: null,
tools: null,
};
/**
* Conversion Toolbar open/close state
* @type {boolean}
*/
public opened: boolean = false;
/**
* Available tools
*/
private tools: { [key: string]: HTMLElement } = {};
/**
* Instance of class that responses for leafing buttons by arrows/tab
* @type {Flipper|null}
*/
private flipper: Flipper = null;
/**
* Callback that fill be fired on open/close and accepts an opening state
*/
private togglingCallback = null;
/**
* Create UI of Conversion Toolbar
*/
public make(): HTMLElement {
this.nodes.wrapper = $.make('div', ConversionToolbar.CSS.conversionToolbarWrapper);
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
const label = $.make('div', ConversionToolbar.CSS.conversionToolbarLabel, {
textContent: 'Convert to',
});
/**
* Add Tools that has 'import' method
*/
this.addTools();
/**
* Prepare Flipper to be able to leaf tools by arrows/tab
*/
this.enableFlipper();
$.append(this.nodes.wrapper, label);
$.append(this.nodes.wrapper, this.nodes.tools);
return this.nodes.wrapper;
}
/**
* Toggle conversion dropdown visibility
* @param {function} [togglingCallback] — callback that will accept opening state
*/
public toggle(togglingCallback?: (openedState: boolean) => void): void {
if (!this.opened) {
this.open();
} else {
this.close();
}
if (typeof togglingCallback === 'function') {
this.togglingCallback = togglingCallback;
this.togglingCallback(this.opened);
}
}
/**
* Shows Conversion Toolbar
*/
public open(): void {
this.filterTools();
this.opened = true;
this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed);
/**
* We use timeout to prevent bubbling Enter keydown on first dropdown item
* Conversion flipper will be activated after dropdown will open
*/
setTimeout(() => {
this.flipper.activate(Object.values(this.tools).filter((button) => {
return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden);
}));
this.flipper.focusFirst();
if (typeof this.togglingCallback === 'function') {
this.togglingCallback(true);
}
}, 50);
}
/**
* Closes Conversion Toolbar
*/
public close(): void {
this.opened = false;
this.flipper.deactivate();
this.nodes.wrapper.classList.remove(ConversionToolbar.CSS.conversionToolbarShowed);
if (typeof this.togglingCallback === 'function') {
this.togglingCallback(false);
}
}
/**
* Replaces one Block with another
* For that Tools must provide import/export methods
*
* @param {string} replacingToolName
*/
public async replaceWithBlock(replacingToolName: string): Promise <void> {
/**
* At first, we get current Block data
* @type {BlockToolConstructable}
*/
const currentBlockClass = this.Editor.BlockManager.currentBlock.class;
const currentBlockName = this.Editor.BlockManager.currentBlock.name;
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
const { INTERNAL_SETTINGS } = this.Editor.Tools;
const blockData = savedBlock.data;
/**
* When current Block name is equals to the replacing tool Name,
* than convert this Block back to the initial Block
*/
if (currentBlockName === replacingToolName) {
replacingToolName = this.config.initialBlock;
}
/**
* Getting a class of replacing Tool
* @type {BlockToolConstructable}
*/
const replacingTool = this.Editor.Tools.toolsClasses[replacingToolName] as BlockToolConstructable;
/**
* Export property can be:
* 1) Function — Tool defines which data to return
* 2) String — the name of saved property
*
* In both cases returning value must be a string
*/
let exportData: string = '';
const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export;
if (typeof exportProp === 'function') {
exportData = exportProp(blockData);
} else if (typeof exportProp === 'string') {
exportData = blockData[exportProp];
} else {
_.log('Conversion «export» property must be a string or function. ' +
'String means key of saved data object to export. Function should export processed string to export.');
return;
}
/**
* Clean exported data with replacing sanitizer config
*/
const cleaned: string = this.Editor.Sanitizer.clean(
exportData,
replacingTool.sanitize,
);
/**
* «import» property can be Function or String
* function — accept imported string and compose tool data object
* string — the name of data field to import
*/
let newBlockData = {};
const importProp = replacingTool[INTERNAL_SETTINGS.CONVERSION_CONFIG].import;
if (typeof importProp === 'function') {
newBlockData = importProp(cleaned);
} else if (typeof importProp === 'string') {
newBlockData[importProp] = cleaned;
} else {
_.log('Conversion «import» property must be a string or function. ' +
'String means key of tool data to import. Function accepts a imported string and return composed tool data.');
return;
}
this.Editor.BlockManager.replace(replacingToolName, newBlockData);
this.Editor.BlockSelection.clearSelection();
this.close();
this.Editor.InlineToolbar.close();
_.delay(() => {
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
}, 10)();
}
/**
* Iterates existing Tools and inserts to the ConversionToolbar
* if tools have ability to import
*/
private addTools(): void {
const tools = this.Editor.Tools.blockTools;
for (const toolName in tools) {
if (!tools.hasOwnProperty(toolName)) {
continue;
}
const internalSettings = this.Editor.Tools.INTERNAL_SETTINGS;
const toolClass = tools[toolName] as BlockToolConstructable;
const toolToolboxSettings = toolClass[internalSettings.TOOLBOX];
const conversionConfig = toolClass[internalSettings.CONVERSION_CONFIG];
/**
* Skip tools that don't pass 'toolbox' property
*/
if (_.isEmpty(toolToolboxSettings) || !toolToolboxSettings.icon) {
continue;
}
/**
* Skip tools without «import» rule specified
*/
if (!conversionConfig || !conversionConfig.import) {
continue;
}
this.addTool(toolName, toolToolboxSettings.icon, toolToolboxSettings.title);
}
}
/**
* Add tool to the Conversion Toolbar
*/
private addTool(toolName: string, toolIcon: string, title: string): void {
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]);
tool.dataset.tool = toolName;
icon.innerHTML = toolIcon;
$.append(tool, icon);
$.append(tool, $.text(title || _.capitalize(toolName)));
$.append(this.nodes.tools, tool);
this.tools[toolName] = tool;
this.Editor.Listeners.on(tool, 'click', async () => {
await this.replaceWithBlock(toolName);
});
}
/**
* Hide current Tool and show others
*/
private filterTools(): void {
const { currentBlock } = this.Editor.BlockManager;
/**
* Show previously hided
*/
Object.entries(this.tools).forEach(([name, button]) => {
button.hidden = false;
button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name);
});
}
/**
* Prepare Flipper to be able to leaf tools by arrows/tab
*/
private enableFlipper(): void {
this.flipper = new Flipper({
focusedItemClass: ConversionToolbar.CSS.conversionToolFocused,
});
}
}