The read-only mode (#1035)

(ノ◕ヮ◕)ノ*:・゚✧
This commit is contained in:
Hugh Boylan 2020-09-29 15:13:43 -05:00 committed by GitHub
parent 83131d6251
commit 8a5adefe80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 1295 additions and 457 deletions

2
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,7 @@
### 2.19
- `New` - Read-only mode 🥳 [#837](https://github.com/codex-team/editor.js/issues/837)
- `New` - RTL mode added [#670](https://github.com/codex-team/editor.js/issues/670)
- `Fix` — Fix problem with types usage [#1183](https://github.com/codex-team/editor.js/issues/1183)
- `Fix` - Fixed issue with Spam clicking the "Click to tune" button duplicates the icons on FireFox. [#1273](https://github.com/codex-team/editor.js/issues/1273)
@ -43,7 +44,6 @@
> *Breaking changes* `blocks.getBlockByIndex` method now returns BlockAPI object. To access old value, use BlockAPI.holder property
### 2.17
- `Improvements` - Editor's [onchange callback](https://editorjs.io/configuration#editor-modifications-callback) now accepts an API as a parameter

View file

@ -120,6 +120,31 @@ body {
text-decoration: none;
}
.ce-example__statusbar {
position: fixed;
bottom: 10px;
right: 10px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
font-size: 12px;
padding: 8px 15px;
}
.ce-example__statusbar-button {
display: inline-flex;
margin-left: 10px;
background: #4A9DF8;
padding: 6px 12px;
box-shadow: 0 7px 8px -4px rgba(137, 207, 255, 0.77);
transition: all 150ms ease;
cursor: pointer;
border-radius: 31px;
color: #fff;
font-family: 'PT Mono', Menlo, Monaco, Consolas, Courier New, monospace;
text-align: center;
}
@media all and (max-width: 730px){
.ce-example__header,
.ce-example__content{

View file

@ -34,6 +34,15 @@
<div class="ce-example__button" id="saveButton">
editor.save()
</div>
<div class="ce-example__statusbar">
Readonly:
<b id="readonly-state">
Off
</b>
<div class="ce-example__statusbar-button" id="toggleReadOnlyButton">
toggle
</div>
</div>
</div>
<div class="ce-example__output">
<pre class="ce-example__output-content" id="output"></pre>
@ -74,16 +83,16 @@
<!-- Initialization -->
<script>
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details
*/
var editor = new EditorJS({
/**
* Enable/Disable the read only mode
*/
readOnly: false,
/**
* Wrapper of Editor
*/
@ -279,14 +288,37 @@
},
});
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* Toggle read-only button
*/
const toggleReadOnlyButton = document.getElementById('toggleReadOnlyButton');
const readOnlyIndicator = document.getElementById('readonly-state');
/**
* Saving example
*/
saveButton.addEventListener('click', function () {
editor.save().then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
});
editor.save()
.then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
})
.catch((error) => {
console.error('Saving error', error);
});
});
/**
* Toggle read-only example
*/
toggleReadOnlyButton.addEventListener('click', async () => {
const readOnlyState = await editor.readOnly.toggle();
readOnlyIndicator.textContent = readOnlyState ? 'On' : 'Off';
});
</script>
</body>

@ -1 +1 @@
Subproject commit c4c3f564b30cd1bbdeb0bd75a7bd5e6b7dbef61b
Subproject commit 7589eb5e52895593c2c8066b5999c03e4dc42e73

@ -1 +1 @@
Subproject commit 8b74d1bef0a925587841dc79a644f0fef2341a23
Subproject commit dae258e5eafd155cd9fe079c85bda57b901cc291

@ -1 +1 @@
Subproject commit 44ec669216315cd064629fe0d5cab4773d8bb8eb
Subproject commit ad00bd521c4828e251495431136a3a81bb0c3957

@ -1 +1 @@
Subproject commit 034240d58c3186967a0f5b692d9a52ae9cd584cb
Subproject commit 33cd9812ba7c9e14f8ed399e2fd2510b4c51cc29

@ -1 +1 @@
Subproject commit bd5249244eea80d061773aa45a4de877a7829041
Subproject commit ca0227f1d4040e252405585b03f8f3282ec7fe52

@ -1 +1 @@
Subproject commit 09bb47c4b4f3102cd7ecdba864d723e5142e00f4
Subproject commit 2d0dcf66c15e8fd5009c8472413bec4f382d049e

@ -1 +1 @@
Subproject commit 0a0d73b49c2d8077a3e62096c44741db6295e12f
Subproject commit 37ebe0deebaac85ca90bc2a7e411dab0a98316dd

@ -1 +1 @@
Subproject commit 497c0437c5572cb9010988e2edf0d5548d58c441
Subproject commit 439e5fcea060f549065f02dcdd8dff029658ecb0

@ -1 +1 @@
Subproject commit 119611a4735eba7069776a2b2494d9e70c74e707
Subproject commit 4f7663928b39794e4dfc9bb4071a0112402e54b2

@ -1 +1 @@
Subproject commit 698d1e35a682126befdd381d1963242e6b3b9b82
Subproject commit 84310bb638399dc671636c33bb2bb24c8381bb7f

@ -1 +1 @@
Subproject commit 369b7e7c5a7aaef72751a62702ca4fd1222d6c97
Subproject commit 5c655372455b679f6953c449cc9f99c8fdf6b6e9

@ -1 +1 @@
Subproject commit b666a0dc2ec24e3a25582f4598ca39954d3b3a34
Subproject commit 70f1c1680c90b22d74faf059570a66ab58a90dcd

@ -1 +1 @@
Subproject commit 7a99d65f25c371976266062ff74b5d2d4c648433
Subproject commit 1e493039a62bca7c3a05091eab2def339141e604

View file

@ -2,6 +2,12 @@ import { EditorModules } from '../types-internal/editor-modules';
import { EditorConfig } from '../../types';
import { ModuleConfig } from '../types-internal/module-config';
/**
* The type <T> of the Module generic.
* It describes the structure of nodes used in modules.
*/
export type ModuleNodes = object;
/**
* @abstract
* @class Module
@ -11,7 +17,13 @@ import { ModuleConfig } from '../types-internal/module-config';
* @property {object} config - Editor user settings
* @property {EditorModules} Editor - List of Editor modules
*/
export default class Module {
export default class Module<T extends ModuleNodes = {}> {
/**
* Each module can provide some UI elements that will be stored in this property
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public nodes: T = {} as any;
/**
* Editor modules list
*
@ -26,6 +38,50 @@ export default class Module {
*/
protected config: EditorConfig;
/**
* This object provides methods to push into set of listeners that being dropped when read-only mode is enabled
*/
protected readOnlyMutableListeners = {
/**
* Assigns event listener on DOM element and pushes into special array that might be removed
*
* @param {EventTarget} element - DOM Element
* @param {string} eventType - Event name
* @param {Function} handler - Event handler
* @param {boolean|AddEventListenerOptions} options - Listening options
*/
on: (
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
options: boolean | AddEventListenerOptions = false
): void => {
const { Listeners } = this.Editor;
this.mutableListenerIds.push(
Listeners.on(element, eventType, handler, options)
);
},
/**
* Clears all mutable listeners
*/
clearAll: (): void => {
const { Listeners } = this.Editor;
for (const id of this.mutableListenerIds) {
Listeners.offById(id);
}
this.mutableListenerIds = [];
},
};
/**
* The set of listener identifiers which will be dropped in read-only mode
*/
private mutableListenerIds: string[] = [];
/**
* @class
* @param {EditorConfig} config - Editor's config
@ -47,6 +103,19 @@ export default class Module {
this.Editor = Editor;
}
/**
* Remove memorized nodes
*/
public removeAllNodes(): void {
for (const key in this.nodes) {
const node = this.nodes[key];
if (node instanceof HTMLElement) {
node.remove();
}
}
}
/**
* Returns true if current direction is RTL (Right-To-Left)
*/

View file

@ -10,7 +10,9 @@ import { BlockAPI as BlockAPIInterface } from '../../../types/api';
*
* @param {Block} block - Block to expose
*/
function BlockAPI(block: Block): void {
function BlockAPI(
block: Block
): void {
const blockAPI: BlockAPIInterface = {
/**
* Tool name

View file

@ -13,7 +13,7 @@ import {
import { SavedData } from '../../../types/data-formats';
import $ from '../dom';
import * as _ from '../utils';
import ApiModule from '../modules/api';
import ApiModules from '../modules/api';
import BlockAPI from './api';
import { ToolType } from '../modules/tools';
@ -49,7 +49,12 @@ interface BlockConstructorOptions {
/**
* Editor's API methods
*/
api: ApiModule;
api: ApiModules;
/**
* This flag indicates that the Block should be constructed in the read-only mode.
*/
readOnly: boolean;
}
/**
@ -146,7 +151,7 @@ export default class Block {
/**
* Editor`s API module
*/
private readonly api: ApiModule;
private readonly api: ApiModules;
/**
* Focused input index
@ -197,7 +202,8 @@ export default class Block {
* @param {BlockToolData} options.data - Tool's initial data
* @param {BlockToolConstructable} options.Tool Tool's class
* @param {ToolSettings} options.settings - default tool's config
* @param {ApiModule} options.api - Editor API module for pass it to the Block Tunes
* @param {Module} options.api - Editor API module for pass it to the Block Tunes
* @param {boolean} options.readOnly - Read-Only flag
*/
constructor({
name,
@ -205,6 +211,7 @@ export default class Block {
Tool,
settings,
api,
readOnly,
}: BlockConstructorOptions) {
this.name = name;
this.class = Tool;
@ -220,6 +227,7 @@ export default class Block {
config: this.config,
api: this.api.getMethodsForTool(name, ToolType.Block),
block: this.blockAPI,
readOnly: readOnly,
});
this.holder = this.compose();

View file

@ -1,11 +1,9 @@
import $ from './dom';
// eslint-disable-next-line import/no-duplicates
import * as _ from './utils';
// eslint-disable-next-line import/no-duplicates
import { LogLevels } from './utils';
import { EditorConfig, OutputData, SanitizerConfig } from '../../types';
import { EditorModules } from '../types-internal/editor-modules';
import I18n from './i18n';
import { CriticalError } from './errors/critical';
/**
* @typedef {Core} Core - editor core class
@ -151,7 +149,7 @@ export default class Core {
}
if (!this.config.logLevel) {
this.config.logLevel = LogLevels.VERBOSE;
this.config.logLevel = _.LogLevels.VERBOSE;
}
_.setLogLevel(this.config.logLevel);
@ -208,6 +206,9 @@ export default class Core {
}
}
this.config.readOnly = this.config.readOnly as boolean || false;
this.config.i18n = {};
/**
* Adjust i18n
*/
@ -285,13 +286,17 @@ export default class Core {
public async start(): Promise<void> {
const modulesToPrepare = [
'Tools',
'ReadOnly',
'UI',
'Toolbar',
'InlineToolbar',
'BlockManager',
'Paste',
'DragNDrop',
'ModificationsObserver',
'BlockSelection',
'RectangleSelection',
'CrossBlockSelection',
];
await modulesToPrepare.reduce(
@ -301,6 +306,13 @@ export default class Core {
try {
await this.moduleInstances[module].prepare();
} catch (e) {
/**
* CriticalError's will not be caught
* It is used when Editor is rendering in read-only mode with unsupported plugin
*/
if (e instanceof CriticalError) {
throw new Error(e.message);
}
_.log(`Module ${module} was skipped because of %o`, 'warn', e);
}
// _.log(`Preparing ${module} module`, 'timeEnd');

View file

@ -0,0 +1,5 @@
/**
* This type of exception will destroy the Editor! Be careful when using it
*/
export class CriticalError extends Error {
}

View file

@ -1,9 +1,8 @@
import Module from '../../__module';
import { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
import * as _ from './../../utils';
import BlockAPI from '../../block/api';
import Module from '../../__module';
/**
* @class BlocksAPI

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Caret } from '../../../../types/api';
import Module from '../../__module';
/**
* @class CaretAPI

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Events } from '../../../../types/api';
import Module from '../../__module';
/**
* @class EventsAPI

View file

@ -1,8 +1,8 @@
import Module from '../../__module';
import { I18n } from '../../../../types/api';
import I18nInternal from '../../i18n';
import { ToolType } from '../tools';
import { logLabeled } from '../../utils';
import Module from '../../__module';
/**
* Provides methods for working with i18n

View file

@ -31,7 +31,8 @@ export default class API extends Module {
inlineToolbar: this.Editor.InlineToolbarAPI.methods,
tooltip: this.Editor.TooltipAPI.methods,
i18n: this.Editor.I18nAPI.methods,
} as APIInterfaces;
readOnly: this.Editor.ReadOnlyAPI.methods,
};
}
/**

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { InlineToolbar } from '../../../../types/api/inline-toolbar';
import Module from '../../__module';
/**
* @class InlineToolbarAPI

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Listeners } from '../../../../types/api';
import Module from '../../__module';
/**
* @class ListenersAPI

View file

@ -1,6 +1,6 @@
import Module from '../../__module';
import { Notifier } from '../../../../types/api';
import { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';
import Module from '../../__module';
/**
*

View file

@ -0,0 +1,28 @@
import { ReadOnly } from '../../../../types/api';
import Module from '../../__module';
/**
* @class ReadOnlyAPI
* @classdesc ReadOnly API
*/
export default class ReadOnlyAPI extends Module {
/**
* Available methods
*/
public get methods(): ReadOnly {
return {
toggle: (state): Promise<boolean> => this.toggle(state),
};
}
/**
* Set or toggle read-only state
*
* @param {boolean|undefined} state - set or toggle state
*
* @returns {boolean} current value
*/
public toggle(state?: boolean): Promise<boolean> {
return this.Editor.ReadOnly.toggle(state);
}
}

View file

@ -1,6 +1,6 @@
import Module from '../../__module';
import { Sanitizer } from '../../../../types/api';
import { SanitizerConfig } from '../../../../types/configs';
import Module from '../../__module';
/**
* @class SanitizerAPI

View file

@ -1,6 +1,7 @@
import Module from '../../__module';
import { Saver } from '../../../../types/api';
import { OutputData } from '../../../../types';
import * as _ from '../../utils';
import Module from '../../__module';
/**
* @class SaverAPI
@ -20,8 +21,18 @@ export default class SaverAPI extends Module {
/**
* Return Editor's data
*
* @returns {OutputData}
*/
public save(): Promise<OutputData> {
const errorText = 'Editor\'s content can not be saved in read-only mode';
if (this.Editor.ReadOnly.isEnabled) {
_.logLabeled(errorText, 'warn');
return Promise.reject(new Error(errorText));
}
return this.Editor.Saver.save();
}
}

View file

@ -1,6 +1,6 @@
import Module from '../../__module';
import SelectionUtils from '../../selection';
import { Selection as SelectionAPIInterface } from '../../../../types/api';
import Module from '../../__module';
/**
* @class SelectionAPI

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Styles } from '../../../../types/api';
import Module from '../../__module';
/**
*

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Toolbar } from '../../../../types/api';
import Module from '../../__module';
/**
* @class ToolbarAPI

View file

@ -1,6 +1,6 @@
import Module from '../../__module';
import { Tooltip } from '../../../../types/api';
import { TooltipContent, TooltipOptions } from 'codex-tooltip';
import Module from '../../__module';
/**
* @class TooltipAPI

View file

@ -93,7 +93,7 @@ export default class BlockEvents extends Module {
*
* @param {KeyboardEvent} event - keyup event
*/
public keyup(event): void {
public keyup(event: KeyboardEvent): void {
/**
* If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)
*/
@ -107,21 +107,6 @@ export default class BlockEvents extends Module {
this.Editor.UI.checkEmptiness();
}
/**
* Set up mouse selection handlers
*
* @param {MouseEvent} event - mouse down event
*/
public mouseDown(event: MouseEvent): void {
/**
* Each mouse down on Block must disable selectAll state
*/
if (!SelectionUtils.isCollapsed) {
this.Editor.BlockSelection.clearSelection(event);
}
this.Editor.CrossBlockSelection.watchSelection(event);
}
/**
* Open Toolbox to leaf Tools
*
@ -157,10 +142,10 @@ export default class BlockEvents extends Module {
/**
* Add drop target styles
*
* @param {DragEvent} e - drag over event
* @param {DragEvent} event - drag over event
*/
public dragOver(e: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
public dragOver(event: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node);
block.dropTarget = true;
}
@ -168,10 +153,10 @@ export default class BlockEvents extends Module {
/**
* Remove drop target style
*
* @param {DragEvent} e - drag leave event
* @param {DragEvent} event - drag leave event
*/
public dragLeave(e: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
public dragLeave(event: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node);
block.dropTarget = false;
}

View file

@ -164,8 +164,8 @@ export default class BlockManager extends Module {
* @returns {Promise}
*/
public async prepare(): Promise<void> {
const { Listeners, BlockEvents } = this.Editor;
const blocks = new Blocks(this.Editor.UI.nodes.redactor);
const { BlockEvents, Listeners } = this.Editor;
/**
* We need to use Proxy to overload set/get [] operator.
@ -192,13 +192,25 @@ export default class BlockManager extends Module {
'copy',
(e: ClipboardEvent) => BlockEvents.handleCommandC(e)
);
}
/** Copy and cut */
Listeners.on(
document,
'cut',
(e: ClipboardEvent) => BlockEvents.handleCommandX(e)
);
/**
* Toggle read-only state
*
* If readOnly is true:
* - Unbind event handlers from created Blocks
*
* if readOnly is false:
* - Bind event handlers to all existing Blocks
*
* @param {boolean} readOnlyEnabled - "read only" state
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
this.enableModuleBindings();
} else {
this.disableModuleBindings();
}
}
/**
@ -211,6 +223,7 @@ export default class BlockManager extends Module {
* @returns {Block}
*/
public composeBlock({ tool, data = {} }: {tool: string; data?: BlockToolData}): Block {
const readOnly = this.Editor.ReadOnly.isEnabled;
const settings = this.Editor.Tools.getToolSettings(tool);
const Tool = this.Editor.Tools.available[tool] as BlockToolConstructable;
const block = new Block({
@ -219,9 +232,12 @@ export default class BlockManager extends Module {
Tool,
settings,
api: this.Editor.API,
readOnly,
});
this.bindEvents(block);
if (!readOnly) {
this.bindBlockEvents(block);
}
return block;
}
@ -680,18 +696,51 @@ export default class BlockManager extends Module {
}
/**
* Bind Events
* Bind Block events
*
* @param {Block} block - Block to which event should be bound
*/
private bindEvents(block: Block): void {
const { BlockEvents, Listeners } = this.Editor;
private bindBlockEvents(block: Block): void {
const { BlockEvents } = this.Editor;
Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), false);
Listeners.on(block.holder, 'mousedown', (event: MouseEvent) => BlockEvents.mouseDown(event));
Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));
Listeners.on(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));
this.readOnlyMutableListeners.on(block.holder, 'keydown', (event: KeyboardEvent) => {
BlockEvents.keydown(event);
}, true);
this.readOnlyMutableListeners.on(block.holder, 'keyup', (event: KeyboardEvent) => {
BlockEvents.keyup(event);
});
this.readOnlyMutableListeners.on(block.holder, 'dragover', (event: DragEvent) => {
BlockEvents.dragOver(event);
});
this.readOnlyMutableListeners.on(block.holder, 'dragleave', (event: DragEvent) => {
BlockEvents.dragLeave(event);
});
}
/**
* Disable mutable handlers and bindings
*/
private disableModuleBindings(): void {
this.readOnlyMutableListeners.clearAll();
}
/**
* Enables all module handlers and bindings for all Blocks
*/
private enableModuleBindings(): void {
/** Copy and cut */
this.readOnlyMutableListeners.on(
document,
'cut',
(e: ClipboardEvent) => this.Editor.BlockEvents.handleCommandX(e)
);
this.blocks.forEach((block: Block) => {
this.bindBlockEvents(block);
});
}
/**

View file

@ -17,6 +17,16 @@ import { SanitizerConfig } from '../../../types/configs';
*
*/
export default class BlockSelection extends Module {
/**
* Sometimes .anyBlockSelected can be called frequently,
* for example at ui@selectionChange (to clear native browser selection in CBS)
* We use cache to prevent multiple iterations through all the blocks
*
* @private
*/
private anyBlockSelectedCache: boolean | null = null;
/**
* Sanitizer Config
*
@ -71,6 +81,8 @@ export default class BlockSelection extends Module {
BlockManager.blocks.forEach((block) => {
block.selected = state;
});
this.clearCache();
}
/**
@ -81,7 +93,11 @@ export default class BlockSelection extends Module {
public get anyBlockSelected(): boolean {
const { BlockManager } = this.Editor;
return BlockManager.blocks.some((block) => block.selected === true);
if (this.anyBlockSelectedCache === null) {
this.anyBlockSelectedCache = BlockManager.blocks.some((block) => block.selected === true);
}
return this.anyBlockSelectedCache;
}
/**
@ -132,15 +148,30 @@ export default class BlockSelection extends Module {
public prepare(): void {
const { Shortcuts } = this.Editor;
/** Selection shortcut */
this.selection = new SelectionUtils();
/**
* CMD/CTRL+A selection shortcut
*/
Shortcuts.add({
name: 'CMD+A',
handler: (event) => {
const { BlockManager } = this.Editor;
const { BlockManager, ReadOnly } = this.Editor;
/**
* We use Editor's Block selection on CMD+A ShortCut instead of Browsers
*/
if (ReadOnly.isEnabled) {
event.preventDefault();
this.selectAllBlocks();
return;
}
/**
* When one page consist of two or more EditorJS instances
* Shortcut module tries to handle all events. Thats why Editor's selection works inside the target Editor, but
* Shortcut module tries to handle all events.
* Thats why Editor's selection works inside the target Editor, but
* for others error occurs because nothing to select.
*
* Prevent such actions if focus is not inside the Editor
@ -152,8 +183,21 @@ export default class BlockSelection extends Module {
this.handleCommandA(event);
},
});
}
this.selection = new SelectionUtils();
/**
* Toggle read-only state
*
* - Remove all ranges
* - Unselect all Blocks
*
* @param {boolean} readOnlyEnabled - "read only" state
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
SelectionUtils.get()
.removeAllRanges();
this.allBlocksSelected = false;
}
/**
@ -173,6 +217,8 @@ export default class BlockSelection extends Module {
}
block.selected = false;
this.clearCache();
}
/**
@ -290,10 +336,19 @@ export default class BlockSelection extends Module {
block.selected = true;
this.clearCache();
/** close InlineToolbar when we selected any Block */
this.Editor.InlineToolbar.close();
}
/**
* Clear anyBlockSelected cache
*/
public clearCache(): void {
this.anyBlockSelectedCache = null;
}
/**
* Module destruction
* De-registers Shortcut CMD+A

View file

@ -17,6 +17,19 @@ export default class CrossBlockSelection extends Module {
*/
private lastSelectedBlock: Block;
/**
* Module preparation
*
* @returns {Promise}
*/
public async prepare(): Promise<void> {
const { Listeners } = this.Editor;
Listeners.on(document, 'mousedown', (event: MouseEvent) => {
this.enableCrossBlockSelection(event);
});
}
/**
* Sets up listeners
*
@ -51,7 +64,7 @@ export default class CrossBlockSelection extends Module {
* @param {boolean} next - if true, toggle next block. Previous otherwise
*/
public toggleBlockSelectedState(next = true): void {
const { BlockManager } = this.Editor;
const { BlockManager, BlockSelection } = this.Editor;
if (!this.lastSelectedBlock) {
this.lastSelectedBlock = this.firstSelectedBlock = BlockManager.currentBlock;
@ -59,6 +72,8 @@ export default class CrossBlockSelection extends Module {
if (this.firstSelectedBlock === this.lastSelectedBlock) {
this.firstSelectedBlock.selected = true;
BlockSelection.clearCache();
SelectionUtils.get().removeAllRanges();
}
@ -71,8 +86,12 @@ export default class CrossBlockSelection extends Module {
if (this.lastSelectedBlock.selected !== nextBlock.selected) {
nextBlock.selected = true;
BlockSelection.clearCache();
} else {
this.lastSelectedBlock.selected = false;
BlockSelection.clearCache();
}
this.lastSelectedBlock = nextBlock;
@ -120,6 +139,34 @@ export default class CrossBlockSelection extends Module {
this.firstSelectedBlock = this.lastSelectedBlock = null;
}
/**
* Enables Cross Block Selection
*
* @param {MouseEvent} event - mouse down event
*/
private enableCrossBlockSelection(event: MouseEvent): void {
const { UI } = this.Editor;
/**
* Each mouse down on must disable selectAll state
*/
if (!SelectionUtils.isCollapsed) {
this.Editor.BlockSelection.clearSelection(event);
}
/**
* If mouse down is performed inside the editor, we should watch CBS
*/
if (UI.nodes.redactor.contains(event.target as Node)) {
this.watchSelection(event);
} else {
/**
* Otherwise, clear selection
*/
this.Editor.BlockSelection.clearSelection(event);
}
}
/**
* Mouse up event handler.
* Removes the listeners
@ -138,7 +185,7 @@ export default class CrossBlockSelection extends Module {
* @param {MouseEvent} event - mouse over event
*/
private onMouseOver = (event: MouseEvent): void => {
const { BlockManager } = this.Editor;
const { BlockManager, BlockSelection } = this.Editor;
const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock;
const targetBlock = BlockManager.getBlockByChildNode(event.target as Node);
@ -157,6 +204,8 @@ export default class CrossBlockSelection extends Module {
relatedBlock.selected = true;
targetBlock.selected = true;
BlockSelection.clearCache();
return;
}
@ -164,6 +213,8 @@ export default class CrossBlockSelection extends Module {
relatedBlock.selected = false;
targetBlock.selected = false;
BlockSelection.clearCache();
return;
}
@ -180,7 +231,7 @@ export default class CrossBlockSelection extends Module {
* @param {Block} lastBlock - last block in range
*/
private toggleBlocksSelectedState(firstBlock: Block, lastBlock: Block): void {
const { BlockManager } = this.Editor;
const { BlockManager, BlockSelection } = this.Editor;
const fIndex = BlockManager.blocks.indexOf(firstBlock);
const lIndex = BlockManager.blocks.indexOf(lastBlock);
@ -199,6 +250,8 @@ export default class CrossBlockSelection extends Module {
block !== (shouldntSelectFirstBlock ? firstBlock : lastBlock)
) {
BlockManager.blocks[i].selected = !BlockManager.blocks[i].selected;
BlockSelection.clearCache();
}
}
}

View file

@ -14,30 +14,60 @@ export default class DragNDrop extends Module {
private isStartedAtEditor = false;
/**
* Bind events
* Bind module. Enable all bindings if it is not read-only mode
*/
public prepare(): void {
this.bindEvents();
if (!this.Editor.ReadOnly.isEnabled) {
this.enableModuleBindings();
}
}
/**
* Toggle read-only state
*
* if state is true:
* - disable all drag-n-drop event handlers
*
* if state is false:
* - restore drag-n-drop event handlers
*
* @param {boolean} readOnlyEnabled - "read only" state
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (readOnlyEnabled) {
this.disableModuleBindings();
} else {
this.enableModuleBindings();
}
}
/**
* Add drag events listeners to editor zone
*
* @private
*/
private bindEvents(): void {
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'drop', this.processDrop, true);
private enableModuleBindings(): void {
const { UI } = this.Editor;
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragstart', (dragEvent: DragEvent) => {
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
this.isStartedAtEditor = true;
}
this.readOnlyMutableListeners.on(UI.nodes.holder, 'drop', async (dropEvent: DragEvent) => {
await this.processDrop(dropEvent);
}, true);
this.Editor.InlineToolbar.close();
this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragstart', () => {
this.processDragStart();
});
/* Prevent default browser behavior to allow drop on non-contenteditable elements */
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragover', (e) => e.preventDefault(), true);
/**
* Prevent default browser behavior to allow drop on non-contenteditable elements
*/
this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragover', (dragEvent: DragEvent) => {
this.processDragOver(dragEvent);
}, true);
}
/**
* Unbind drag-n-drop event handlers
*/
private disableModuleBindings(): void {
this.readOnlyMutableListeners.clearAll();
}
/**
@ -45,7 +75,7 @@ export default class DragNDrop extends Module {
*
* @param {DragEvent} dropEvent - drop event
*/
private processDrop = async (dropEvent: DragEvent): Promise<void> => {
private async processDrop(dropEvent: DragEvent): Promise<void> {
const {
BlockManager,
Caret,
@ -78,6 +108,24 @@ export default class DragNDrop extends Module {
this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);
}
Paste.processDataTransfer(dropEvent.dataTransfer, true);
await Paste.processDataTransfer(dropEvent.dataTransfer, true);
}
/**
* Handle drag start event
*/
private processDragStart(): void {
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
this.isStartedAtEditor = true;
}
this.Editor.InlineToolbar.close();
}
/**
* @param {DragEvent} dragEvent - drag event
*/
private processDragOver(dragEvent: DragEvent): void {
dragEvent.preventDefault();
}
}

View file

@ -1,4 +1,5 @@
import Module from '../__module';
import * as _ from '../utils';
/**
* Event listener information
@ -6,6 +7,11 @@ import Module from '../__module';
* @interface ListenerData
*/
export interface ListenerData {
/**
* Listener unique identifier
*/
id: string;
/**
* Element where to listen to dispatched events
*/
@ -53,20 +59,24 @@ export default class Listeners extends Module {
private allListeners: ListenerData[] = [];
/**
* Assigns event listener on element
* Assigns event listener on element and returns unique identifier
*
* @param {EventTarget} element - DOM element that needs to be listened
* @param {string} eventType - event type
* @param {Function} handler - method that will be fired on event
* @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
*
* @returns {string}
*/
public on(
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
options: boolean | AddEventListenerOptions = false
): void {
): string {
const id = _.generateId('l');
const assignedEventData = {
id,
element,
eventType,
handler,
@ -81,6 +91,8 @@ export default class Listeners extends Module {
this.allListeners.push(assignedEventData);
element.addEventListener(eventType, handler, options);
return id;
}
/**
@ -110,6 +122,21 @@ export default class Listeners extends Module {
});
}
/**
* Removes listener by id
*
* @param {string} id - listener identifier
*/
public offById(id: string): void {
const listener = this.findById(id);
if (!listener) {
return;
}
listener.element.removeEventListener(listener.eventType, listener.handler, listener.options);
}
/**
* Finds and returns first listener by passed params
*
@ -211,4 +238,15 @@ export default class Listeners extends Module {
}
});
}
/**
* Returns listener data found by id
*
* @param {string} id - listener identifier
*
* @returns {ListenerData}
*/
private findById(id: string): ListenerData {
return this.allListeners.find((listener) => listener.id === id);
}
}

View file

@ -56,6 +56,7 @@ export default class ModificationsObserver extends Module {
}
this.observer = null;
this.nativeInputs.forEach((input) => this.Editor.Listeners.off(input, 'input', this.mutationDebouncer));
this.mutationDebouncer = null;
}
/**
@ -64,13 +65,22 @@ export default class ModificationsObserver extends Module {
* @returns {Promise<void>}
*/
public async prepare(): Promise<void> {
/**
* wait till Browser render Editor's Blocks
*/
window.setTimeout(() => {
this.setObserver();
this.updateNativeInputs();
}, 1000);
if (!this.Editor.ReadOnly.isEnabled) {
this.enableModule();
}
}
/**
* Set read-only state
*
* @param {boolean} readOnlyEnabled - read only flag value
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (readOnlyEnabled) {
this.disableModule();
} else {
this.enableModule();
}
}
/**
@ -169,4 +179,25 @@ export default class ModificationsObserver extends Module {
this.nativeInputs.forEach((input) => this.Editor.Listeners.on(input, 'input', this.mutationDebouncer));
}
/**
* Sets observer and enables it
*/
private enableModule(): void {
/**
* wait till Browser render Editor's Blocks
*/
window.setTimeout(() => {
this.setObserver();
this.updateNativeInputs();
this.enable();
}, 1000);
}
/**
* Disables observer
*/
private disableModule(): void {
this.disable();
}
}

View file

@ -141,12 +141,25 @@ export default class Paste extends Module {
/**
* Set onPaste callback and collect tools` paste configurations
*
* @public
*/
public async prepare(): Promise<void> {
this.setCallback();
this.processTools();
if (!this.Editor.ReadOnly.isEnabled) {
this.setCallback();
}
}
/**
* Set read-only state
*
* @param {boolean} readOnlyEnabled - read only flag value
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
this.setCallback();
} else {
this.unsetCallback();
}
}
/**
@ -258,6 +271,15 @@ export default class Paste extends Module {
Listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
}
/**
* Unset onPaste callback handler
*/
private unsetCallback(): void {
const { Listeners } = this.Editor;
Listeners.off(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
}
/**
* Get and process tool`s paste configs
*/
@ -276,6 +298,7 @@ export default class Paste extends Module {
api: this.Editor.API.getMethodsForTool(name),
config: {},
data: {},
readOnly: false,
}) as BlockTool;
if (tool.pasteConfig === false) {
@ -427,7 +450,7 @@ export default class Paste extends Module {
}
/**
* If Tools is in list of exceptions, skip processing of paste event
* If Tools is in list of errors, skip processing of paste event
*/
if (BlockManager.currentBlock && this.exceptionList.includes(BlockManager.currentBlock.name)) {
return;

View file

@ -0,0 +1,86 @@
import Module from '../__module';
import { CriticalError } from '../errors/critical';
/**
* @module ReadOnly
*
* Has one important method:
* - {Function} toggleReadonly - Set read-only mode or toggle current state
*
* @version 1.0.0
*
* @typedef {ReadOnly} ReadOnly
* @property {boolean} readOnlyEnabled - read-only state
*/
export default class ReadOnly extends Module {
/**
* Value to track read-only state
*
* @type {boolean}
*/
private readOnlyEnabled = false;
/**
* Returns state of read only mode
*/
public get isEnabled(): boolean {
return this.readOnlyEnabled;
}
/**
* Set initial state
*/
public async prepare(): Promise<void> {
const { Tools } = this.Editor;
const { blockTools } = Tools;
const toolsDontSupportReadOnly: string[] = [];
Object.entries(blockTools).forEach(([name, tool]) => {
if (!Tools.isReadOnlySupported(tool)) {
toolsDontSupportReadOnly.push(name);
}
});
if (toolsDontSupportReadOnly.length > 0) {
throw new CriticalError(
`To enable read-only mode all connected tools should support it. Tools ${toolsDontSupportReadOnly.join(', ')} don't support read-only mode.`
);
}
this.readOnlyEnabled = this.config.readOnly;
}
/**
* Set read-only mode or toggle current state
* Call all Modules `toggleReadOnly` method and re-render Editor
*
* @param {boolean} state - (optional) read-only state or toggle
*/
public async toggle(state = !this.readOnlyEnabled): Promise<boolean> {
this.readOnlyEnabled = state;
for (const name in this.Editor) {
/**
* Verify module has method `toggleReadOnly` method
*/
if (!this.Editor[name].toggleReadOnly) {
continue;
}
/**
* set or toggle read-only state
*/
this.Editor[name].toggleReadOnly(state);
}
/**
* Save current Editor Blocks and render again
*/
const savedBlocks = await this.Editor.Saver.save();
await this.Editor.BlockManager.clear();
await this.Editor.Renderer.render(savedBlocks.blocks);
return this.readOnlyEnabled;
}
}

View file

@ -96,38 +96,17 @@ export default class RectangleSelection extends Module {
*/
private overlayRectangle: HTMLDivElement;
/**
* Listener identifiers
*/
private listenerIds: string[] = [];
/**
* Module Preparation
* Creating rect and hang handlers
*/
public prepare(): void {
const { Listeners } = this.Editor;
const { container } = this.genHTML();
Listeners.on(container, 'mousedown', (event: MouseEvent) => {
if (event.button !== this.MAIN_MOUSE_BUTTON) {
return;
}
this.startSelection(event.pageX, event.pageY);
}, false);
Listeners.on(document.body, 'mousemove', (event: MouseEvent) => {
this.changingRectangle(event);
this.scrollByZones(event.clientY);
}, false);
Listeners.on(document.body, 'mouseleave', () => {
this.clearSelection();
this.endSelection();
});
Listeners.on(window, 'scroll', (event) => {
this.changingRectangle(event);
}, false);
Listeners.on(document.body, 'mouseup', () => {
this.endSelection();
}, false);
this.enableModuleBindings();
}
/**
@ -196,6 +175,78 @@ export default class RectangleSelection extends Module {
this.isRectSelectionActivated = false;
}
/**
* Sets Module necessary event handlers
*/
private enableModuleBindings(): void {
const { Listeners } = this.Editor;
const { container } = this.genHTML();
Listeners.on(container, 'mousedown', (mouseEvent: MouseEvent) => {
this.processMouseDown(mouseEvent);
}, false);
Listeners.on(document.body, 'mousemove', (mouseEvent: MouseEvent) => {
this.processMouseMove(mouseEvent);
}, false);
Listeners.on(document.body, 'mouseleave', () => {
this.processMouseLeave();
});
Listeners.on(window, 'scroll', (mouseEvent: MouseEvent) => {
this.processScroll(mouseEvent);
}, false);
Listeners.on(document.body, 'mouseup', () => {
this.processMouseUp();
}, false);
}
/**
* Handle mouse down events
*
* @param {MouseEvent} mouseEvent - mouse event payload
*/
private processMouseDown(mouseEvent: MouseEvent): void {
if (mouseEvent.button !== this.MAIN_MOUSE_BUTTON) {
return;
}
this.startSelection(mouseEvent.pageX, mouseEvent.pageY);
}
/**
* Handle mouse move events
*
* @param {MouseEvent} mouseEvent - mouse event payload
*/
private processMouseMove(mouseEvent: MouseEvent): void {
this.changingRectangle(mouseEvent);
this.scrollByZones(mouseEvent.clientY);
}
/**
* Handle mouse leave
*/
private processMouseLeave(): void {
this.clearSelection();
this.endSelection();
}
/**
* @param {MouseEvent} mouseEvent - mouse event payload
*/
private processScroll(mouseEvent: MouseEvent): void {
this.changingRectangle(mouseEvent);
}
/**
* Handle mouse up
*/
private processMouseUp(): void {
this.endSelection();
}
/**
* Scroll If mouse in scroll zone
*
@ -270,7 +321,7 @@ export default class RectangleSelection extends Module {
*
* @param {MouseEvent} event - mouse event
*/
private changingRectangle(event): void {
private changingRectangle(event: MouseEvent): void {
if (!this.mousedown) {
return;
}

View file

@ -1,7 +1,5 @@
import Module from '../__module';
/* eslint-disable import/no-duplicates */
import * as _ from '../utils';
import { ChainData } from '../utils';
import { BlockToolConstructable, OutputBlockData } from '../../../types';
/**
@ -47,7 +45,7 @@ export default class Renderer extends Module {
public async render(blocks: OutputBlockData[]): Promise<void> {
const chainData = blocks.map((block) => ({ function: (): Promise<void> => this.insertBlock(block) }));
const sequence = await _.sequence(chainData as ChainData[]);
const sequence = await _.sequence(chainData as _.ChainData[]);
this.Editor.UI.checkEmptiness();
@ -60,6 +58,7 @@ export default class Renderer extends Module {
* Insert block to working zone
*
* @param {object} item - Block data to insert
*
* @returns {Promise<void>}
*/
public async insertBlock(item: OutputBlockData): Promise<void> {

View file

@ -64,6 +64,10 @@ export default class Shortcuts extends Module {
public remove(shortcut: string): void {
const index = this.registeredShortcuts.findIndex((shc) => shc.name === shortcut);
if (index === -1 || !this.registeredShortcuts[index]) {
return;
}
this.registeredShortcuts[index].remove();
this.registeredShortcuts.splice(index, 1);
}

View file

@ -4,6 +4,15 @@ import Flipper, { FlipperOptions } from '../../flipper';
import * as _ from '../../utils';
import SelectionUtils from '../../selection';
/**
* HTML Elements that used for BlockSettings
*/
interface BlockSettingsNodes {
wrapper: HTMLElement;
toolSettings: HTMLElement;
defaultSettings: HTMLElement;
}
/**
* Block Settings
*
@ -15,7 +24,7 @@ import SelectionUtils from '../../selection';
* | ...................... |
* |________________________|
*/
export default class BlockSettings extends Module {
export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
* Module Events
*
@ -57,15 +66,6 @@ export default class BlockSettings extends Module {
return this.nodes.wrapper.classList.contains(this.CSS.wrapperOpened);
}
/**
* Block settings UI HTML elements
*/
public nodes: {[key: string]: HTMLElement} = {
wrapper: null,
toolSettings: null,
defaultSettings: null,
};
/**
* List of buttons
*/
@ -103,6 +103,15 @@ export default class BlockSettings extends Module {
this.enableFlipper();
}
/**
* Destroys module
*/
public destroy(): void {
this.flipper.deactivate();
this.flipper = null;
this.removeAllNodes();
}
/**
* Open Block Settings pane
*/
@ -119,6 +128,7 @@ export default class BlockSettings extends Module {
* Highlight content of a Block we are working with
*/
this.Editor.BlockManager.currentBlock.selected = true;
this.Editor.BlockSelection.clearCache();
/**
* Fill Tool's settings

View file

@ -7,10 +7,18 @@ import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* HTML Elements used for ConversionToolbar
*/
interface ConversionToolbarNodes {
wrapper: HTMLElement;
tools: HTMLElement;
}
/**
* Block Converter
*/
export default class ConversionToolbar extends Module {
export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
/**
* CSS getter
*/
@ -29,14 +37,6 @@ export default class ConversionToolbar extends Module {
};
}
/**
* HTML Elements used for UI
*/
public nodes: { [key: string]: HTMLElement } = {
wrapper: null,
tools: null,
};
/**
* Conversion Toolbar open/close state
*
@ -91,6 +91,15 @@ export default class ConversionToolbar extends Module {
return this.nodes.wrapper;
}
/**
* Deactivates flipper and removes all nodes
*/
public destroy(): void {
this.flipper.deactivate();
this.flipper = null;
this.removeAllNodes();
}
/**
* Toggle conversion dropdown visibility
*

View file

@ -4,6 +4,21 @@ import * as _ from '../../utils';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* HTML Elements used for Toolbar UI
*/
interface ToolbarNodes {
wrapper: HTMLElement;
content: HTMLElement;
actions: HTMLElement;
// Content Zone
plusButton: HTMLElement;
// Actions Zone
blockActionsButtons: HTMLElement;
settingsToggler: HTMLElement;
}
/**
*
* «Toolbar» is the node that moves up/down over current block
@ -56,23 +71,7 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
* @property {Element} nodes.pluginSettings - Plugin Settings section of Settings Panel
* @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel
*/
export default class Toolbar extends Module {
/**
* HTML Elements used for Toolbar UI
*/
public nodes: {[key: string]: HTMLElement} = {
wrapper: null,
content: null,
actions: null,
// Content Zone
plusButton: null,
// Actions Zone
blockActionsButtons: null,
settingsToggler: null,
};
export default class Toolbar extends Module<ToolbarNodes> {
/**
* CSS styles
*
@ -99,89 +98,76 @@ export default class Toolbar extends Module {
}
/**
* Makes toolbar
* Returns the Toolbar opening state
*
* @returns {boolean}
*/
public make(): void {
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
public get opened(): boolean {
return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);
}
/**
* Make Content Zone and Actions Zone
*/
['content', 'actions'].forEach((el) => {
this.nodes[el] = $.make('div', this.CSS[el]);
});
/**
* Plus Button public methods
*
* @returns {{hide: function(): void, show: function(): void}}
*/
public get plusButton(): {hide: () => void; show: () => void} {
return {
hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: (): void => {
if (this.Editor.Toolbox.isEmpty) {
return;
}
this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden);
},
};
}
/**
* Actions will be included to the toolbar content so we can align in to the right of the content
*/
$.append(this.nodes.wrapper, this.nodes.content);
$.append(this.nodes.content, this.nodes.actions);
/**
* Fill Content Zone:
* - Plus Button
* - Toolbox
*/
this.nodes.plusButton = $.make('div', this.CSS.plusButton);
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.append(this.nodes.content, this.nodes.plusButton);
this.Editor.Listeners.on(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false);
/**
* Add events to show/hide tooltip for plus button
*/
const tooltipContent = $.make('div');
tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add')));
tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: '⇥ Tab',
}));
this.Editor.Tooltip.onHover(this.nodes.plusButton, tooltipContent);
/**
* Make a Toolbox
*/
this.Editor.Toolbox.make();
/**
* Fill Actions Zone:
* - Settings Toggler
* - Remove Block Button
* - Settings Panel
*/
this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
const settingsIcon = $.svg('dots', 8, 8);
$.append(this.nodes.settingsToggler, settingsIcon);
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(
this.nodes.settingsToggler,
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
placement: 'top',
}
);
/**
* Make and append Settings Panel
*/
this.Editor.BlockSettings.make();
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
/**
* Append toolbar to the Editor
*/
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
/**
* Block actions appearance manipulations
*
* @returns {{hide: function(): void, show: function(): void}}
*/
private get blockActions(): {hide: () => void; show: () => void} {
return {
hide: (): void => {
this.nodes.actions.classList.remove(this.CSS.actionsOpened);
},
show: (): void => {
this.nodes.actions.classList.add(this.CSS.actionsOpened);
},
};
}
/**
* Module preparation method
* Steps:
* - Make Toolbar dependent components like BlockSettings, Toolbox and so on
* - Make itself and append dependent nodes to itself
*/
public async prepare(): Promise<void> {
/**
* Bind events on the Toolbar elements
*/
this.bindEvents();
if (!this.Editor.ReadOnly.isEnabled) {
this.drawUI();
this.enableModuleBindings();
}
}
/**
* Toggles read-only mode
*
* @param {boolean} readOnlyEnabled - read-only mode
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
this.drawUI();
this.enableModuleBindings();
} else {
this.destroy();
this.disableModuleBindings();
}
}
/**
@ -251,15 +237,6 @@ export default class Toolbar extends Module {
}, 50)();
}
/**
* returns toolbar opened state
*
* @returns {boolean}
*/
public get opened(): boolean {
return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);
}
/**
* Close the Toolbar
*/
@ -273,36 +250,81 @@ export default class Toolbar extends Module {
}
/**
* Plus Button public methods
*
* @returns {{hide: function(): void, show: function(): void}}
* Draws Toolbar elements
*/
public get plusButton(): {hide: () => void; show: () => void} {
return {
hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: (): void => {
if (this.Editor.Toolbox.isEmpty) {
return;
}
this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden);
},
};
}
private make(): void {
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
/**
* Block actions appearance manipulations
*
* @returns {{hide: function(): void, show: function(): void}}
*/
private get blockActions(): {hide: () => void; show: () => void} {
return {
hide: (): void => {
this.nodes.actions.classList.remove(this.CSS.actionsOpened);
},
show: (): void => {
this.nodes.actions.classList.add(this.CSS.actionsOpened);
},
};
/**
* Make Content Zone and Actions Zone
*/
['content', 'actions'].forEach((el) => {
this.nodes[el] = $.make('div', this.CSS[el]);
});
/**
* Actions will be included to the toolbar content so we can align in to the right of the content
*/
$.append(this.nodes.wrapper, this.nodes.content);
$.append(this.nodes.content, this.nodes.actions);
/**
* Fill Content Zone:
* - Plus Button
* - Toolbox
*/
this.nodes.plusButton = $.make('div', this.CSS.plusButton);
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.append(this.nodes.content, this.nodes.plusButton);
this.readOnlyMutableListeners.on(this.nodes.plusButton, 'click', () => {
this.plusButtonClicked();
}, false);
/**
* Add events to show/hide tooltip for plus button
*/
const tooltipContent = $.make('div');
tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add')));
tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: '⇥ Tab',
}));
this.Editor.Tooltip.onHover(this.nodes.plusButton, tooltipContent);
/**
* Fill Actions Zone:
* - Settings Toggler
* - Remove Block Button
* - Settings Panel
*/
this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
const settingsIcon = $.svg('dots', 8, 8);
$.append(this.nodes.settingsToggler, settingsIcon);
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(
this.nodes.settingsToggler,
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
placement: 'top',
}
);
/**
* Appending Toolbar components to itself
*/
$.append(this.nodes.content, this.Editor.Toolbox.nodes.toolbox);
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
/**
* Append toolbar to the Editor
*/
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
}
/**
@ -313,14 +335,22 @@ export default class Toolbar extends Module {
}
/**
* Bind events on the Toolbar Elements:
* - Block Settings
* Enable bindings
*/
private bindEvents(): void {
private enableModuleBindings(): void {
/**
* Settings toggler
*/
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());
this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'click', () => {
this.settingsTogglerClicked();
});
}
/**
* Disable bindings
*/
private disableModuleBindings(): void {
this.readOnlyMutableListeners.clearAll();
}
/**
@ -333,4 +363,37 @@ export default class Toolbar extends Module {
this.Editor.BlockSettings.open();
}
}
/**
* Draws Toolbar UI
*
* Toolbar contains BlockSettings and Toolbox.
* Thats why at first we draw its components and then Toolbar itself
*/
private drawUI(): void {
/**
* Make BlockSettings Panel
*/
this.Editor.BlockSettings.make();
/**
* Make Toolbox
*/
this.Editor.Toolbox.make();
/**
* Make Toolbar
*/
this.make();
}
/**
* Removes all created and saved HTMLElements
* It is used in Read-Only mode
*/
private destroy(): void {
this.Editor.Toolbox.destroy();
this.Editor.BlockSettings.destroy();
this.removeAllNodes();
}
}

View file

@ -7,6 +7,21 @@ import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* Inline Toolbar elements
*/
interface InlineToolbarNodes {
wrapper: HTMLElement;
buttons: HTMLElement;
conversionToggler: HTMLElement;
conversionTogglerContent: HTMLElement;
/**
* Zone below the buttons where Tools can create additional actions by 'renderActions()' method
* For example, input for the 'link' tool or textarea for the 'comment' tool
*/
actions: HTMLElement;
}
/**
* Inline toolbar with actions that modifies selected text fragment
*
@ -14,7 +29,7 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
* | B i [link] [mark] |
* |________________________|
*/
export default class InlineToolbar extends Module {
export default class InlineToolbar extends Module<InlineToolbarNodes> {
/**
* CSS styles
*/
@ -42,27 +57,6 @@ export default class InlineToolbar extends Module {
*/
public opened = false;
/**
* Inline Toolbar elements
*/
private nodes: {
wrapper: HTMLElement;
buttons: HTMLElement;
conversionToggler: HTMLElement;
conversionTogglerContent: HTMLElement;
actions: HTMLElement;
} = {
wrapper: null,
buttons: null,
conversionToggler: null,
conversionTogglerContent: null,
/**
* Zone below the buttons where Tools can create additional actions by 'renderActions()' method
* For example, input for the 'link' tool or textarea for the 'comment' tool
*/
actions: null,
};
/**
* Margin above/below the Toolbar
*/
@ -113,59 +107,25 @@ export default class InlineToolbar extends Module {
}
/**
* Making DOM
* Module preparation method
*/
public make(): void {
this.nodes.wrapper = $.make('div', [
this.CSS.inlineToolbar,
...(this.isRtl ? [ this.Editor.UI.CSS.editorRtlFix ] : []),
]);
this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
public async prepare(): Promise<void> {
if (!this.Editor.ReadOnly.isEnabled) {
this.make();
}
}
// To prevent reset of a selection when click on the wrapper
this.Editor.Listeners.on(this.nodes.wrapper, 'mousedown', (event) => {
const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`);
// If click is on actions wrapper,
// do not prevent default behaviour because actions might include interactive elements
if (!isClickedOnActionsWrapper) {
event.preventDefault();
}
});
/**
* Append Inline Toolbar to the Editor
*/
$.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
/**
* Add button that will allow switching block type
*/
this.addConversionToggler();
/**
* Append Inline Toolbar Tools
*/
this.addTools();
/**
* Prepare conversion toolbar.
* If it has any conversion tool then it will be enabled in the Inline Toolbar
*/
this.prepareConversionToolbar();
/**
* Recalculate initial width with all buttons
*/
this.recalculateWidth();
/**
* Allow to leaf buttons by arrows / tab
* Buttons will be filled on opening
*/
this.enableFlipper();
/**
* Toggles read-only mode
*
* @param {boolean} readOnlyEnabled - read-only mode
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
this.make();
} else {
this.destroy();
}
}
/**
@ -246,6 +206,10 @@ export default class InlineToolbar extends Module {
* Hides Inline Toolbar
*/
public close(): void {
if (this.Editor.ReadOnly.isEnabled) {
return;
}
this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);
this.tools.forEach((toolInstance) => {
if (typeof toolInstance.clear === 'function') {
@ -317,6 +281,72 @@ export default class InlineToolbar extends Module {
return this.nodes.wrapper.contains(node);
}
/**
* Removes UI and its components
*/
public destroy(): void {
this.flipper.deactivate();
this.flipper = null;
this.Editor.ConversionToolbar.destroy();
}
/**
* Making DOM
*/
private make(): void {
this.nodes.wrapper = $.make('div', [
this.CSS.inlineToolbar,
...(this.isRtl ? [ this.Editor.UI.CSS.editorRtlFix ] : []),
]);
this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
// To prevent reset of a selection when click on the wrapper
this.Editor.Listeners.on(this.nodes.wrapper, 'mousedown', (event) => {
const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`);
// If click is on actions wrapper,
// do not prevent default behaviour because actions might include interactive elements
if (!isClickedOnActionsWrapper) {
event.preventDefault();
}
});
/**
* Append Inline Toolbar to the Editor
*/
$.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
/**
* Add button that will allow switching block type
*/
this.addConversionToggler();
/**
* Append Inline Toolbar Tools
*/
this.addTools();
/**
* Prepare conversion toolbar.
* If it has any conversion tool then it will be enabled in the Inline Toolbar
*/
this.prepareConversionToolbar();
/**
* Recalculate initial width with all buttons
*/
this.recalculateWidth();
/**
* Allow to leaf buttons by arrows / tab
* Buttons will be filled on opening
*/
this.enableFlipper();
}
/**
* Need to show Inline Toolbar or not
*/

View file

@ -7,6 +7,14 @@ import { BlockToolAPI } from '../../block';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* HTMLElements used for Toolbox UI
*/
interface ToolboxNodes {
toolbox: HTMLElement;
buttons: HTMLElement[];
}
/**
* @class Toolbox
* @classdesc Holder for Tools
@ -17,7 +25,15 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
* @property {object} CSS - CSS class names
*
*/
export default class Toolbox extends Module {
export default class Toolbox extends Module<ToolboxNodes> {
/**
* Current module HTML Elements
*/
public nodes = {
toolbox: null,
buttons: [],
}
/**
* CSS styles
*
@ -52,17 +68,6 @@ export default class Toolbox extends Module {
*/
public opened = false;
/**
* HTMLElements used for Toolbox UI
*/
public nodes: {
toolbox: HTMLElement;
buttons: HTMLElement[];
} = {
toolbox: null,
buttons: [],
};
/**
* How many tools displayed in Toolbox
*
@ -82,12 +87,20 @@ export default class Toolbox extends Module {
*/
public make(): void {
this.nodes.toolbox = $.make('div', this.CSS.toolbox);
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools();
this.enableFlipper();
}
/**
* Destroy Module
*/
public destroy(): void {
this.flipper.deactivate();
this.flipper = null;
this.removeAllNodes();
}
/**
* Toolbox Tool's button click handler
*

View file

@ -143,6 +143,7 @@ export default class Tools extends Module {
TOOLBOX: 'toolbox',
SANITIZE_CONFIG: 'sanitize',
CONVERSION_CONFIG: 'conversionConfig',
IS_READ_ONLY_SUPPORTED: 'isReadOnlySupported',
};
}
@ -334,7 +335,11 @@ export default class Tools extends Module {
*
* @returns {InlineTool} instance
*/
public constructInline(tool: InlineToolConstructable, name: string, toolSettings: ToolSettings = {} as ToolSettings): InlineTool {
public constructInline(
tool: InlineToolConstructable,
name: string,
toolSettings: ToolSettings = {} as ToolSettings
): InlineTool {
const constructorOptions = {
api: this.Editor.API.getMethodsForTool(name),
config: (toolSettings[this.USER_SETTINGS.CONFIG] || {}) as ToolSettings,
@ -392,6 +397,15 @@ export default class Tools extends Module {
};
}
/**
* Returns true if tool supports read-only mode
*
* @param tool - tool to check
*/
public isReadOnlySupported(tool: BlockToolConstructable): boolean {
return tool[this.INTERNAL_SETTINGS.IS_READ_ONLY_SUPPORTED] === true;
}
/**
* Calls each Tool reset method to clean up anything set by Tool
*/

View file

@ -17,6 +17,16 @@ import Selection from '../selection';
import Block from '../block';
import Flipper from '../flipper';
/**
* HTML Elements used for UI
*/
interface UINodes {
holder: HTMLElement;
wrapper: HTMLElement;
redactor: HTMLElement;
loader: HTMLElement;
}
/**
* @class
*
@ -35,7 +45,7 @@ import Flipper from '../flipper';
* @property {Element} nodes.wrapper - <codex-editor>
* @property {Element} nodes.redactor - <ce-redactor>
*/
export default class UI extends Module {
export default class UI extends Module<UINodes> {
/**
* Editor.js UI CSS class names
*
@ -91,15 +101,6 @@ export default class UI extends Module {
*/
public isMobile = false;
/**
* HTML Elements used for UI
*/
public nodes: { [key: string]: HTMLElement } = {
holder: null,
wrapper: null,
redactor: null,
};
/**
* Cache for center column rectangle info
* Invalidates on window resize
@ -146,7 +147,7 @@ export default class UI extends Module {
/**
* Make main UI elements
*/
await this.make();
this.make();
/**
* Loader for rendering process
@ -156,27 +157,47 @@ export default class UI extends Module {
/**
* Append SVG sprite
*/
await this.appendSVGSprite();
/**
* Make toolbar
*/
await this.Editor.Toolbar.make();
/**
* Make the Inline toolbar
*/
await this.Editor.InlineToolbar.make();
this.appendSVGSprite();
/**
* Load and append CSS
*/
await this.loadStyles();
this.loadStyles();
/**
* Bind events for the UI elements
* Prepare with read-only state from config
*/
await this.bindEvents();
if (!this.Editor.ReadOnly.isEnabled) {
this.enableModuleBindings();
}
}
/**
* Toggle read-only state
*
* If readOnly is true:
* - removes all listeners from main UI module elements
*
* if readOnly is false:
* - enables all listeners to UI module elements
*
* @param {boolean} readOnlyEnabled - "read only" state
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
/**
* Prepare components based on read-only state
*/
if (!readOnlyEnabled) {
/**
* Unbind all events
*/
this.enableModuleBindings();
} else {
/**
* Bind events for the UI elements
*/
this.disableModuleBindings();
}
}
/**
@ -240,10 +261,8 @@ export default class UI extends Module {
/**
* Makes Editor.js interface
*
* @returns {Promise<void>}
*/
private async make(): Promise<void> {
private make(): void {
/**
* Element where we need to append Editor.js
*
@ -311,41 +330,48 @@ export default class UI extends Module {
/**
* Bind events on the Editor.js interface
*/
private bindEvents(): void {
this.Editor.Listeners.on(
this.nodes.redactor,
'click',
(event) => this.redactorClicked(event as MouseEvent),
false
);
this.Editor.Listeners.on(this.nodes.redactor,
'mousedown',
(event) => this.documentTouched(event as MouseEvent),
true
);
this.Editor.Listeners.on(this.nodes.redactor,
'touchstart',
(event) => this.documentTouched(event as MouseEvent),
true
);
private enableModuleBindings(): void {
this.readOnlyMutableListeners.on(this.nodes.redactor, 'click', (event: MouseEvent) => {
this.redactorClicked(event);
}, false);
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true);
this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), true);
this.readOnlyMutableListeners.on(this.nodes.redactor, 'mousedown', (event: MouseEvent | TouchEvent) => {
this.documentTouched(event);
}, true);
this.readOnlyMutableListeners.on(this.nodes.redactor, 'touchstart', (event: MouseEvent | TouchEvent) => {
this.documentTouched(event);
}, true);
this.readOnlyMutableListeners.on(document, 'keydown', (event: KeyboardEvent) => {
this.documentKeydown(event);
}, true);
this.readOnlyMutableListeners.on(document, 'click', (event: MouseEvent) => {
this.documentClicked(event);
}, true);
/**
* Handle selection change to manipulate Inline Toolbar appearance
*/
this.Editor.Listeners.on(document, 'selectionchange', (event: Event) => {
this.readOnlyMutableListeners.on(document, 'selectionchange', (event: Event) => {
this.selectionChanged(event);
}, true);
this.Editor.Listeners.on(window, 'resize', () => {
this.readOnlyMutableListeners.on(window, 'resize', () => {
this.resizeDebouncer();
}, {
passive: true,
});
}
/**
* Unbind events on the Editor.js interface
*/
private disableModuleBindings(): void {
this.readOnlyMutableListeners.clearAll();
}
/**
* Resize window handler
*/
@ -570,13 +596,6 @@ export default class UI extends Module {
if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted) {
this.Editor.BlockSelection.clearSelection(event);
}
/**
* Clear Selection if user clicked somewhere
*/
if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted) {
this.Editor.BlockSelection.clearSelection(event);
}
}
/**
@ -708,8 +727,16 @@ export default class UI extends Module {
* @param {Event} event - selection event
*/
private selectionChanged(event: Event): void {
const { CrossBlockSelection, BlockSelection } = this.Editor;
const focusedElement = Selection.anchorElement as Element;
if (CrossBlockSelection.isCrossBlockSelectionStarted) {
// Removes all ranges when any Block is selected
if (BlockSelection.anyBlockSelected) {
Selection.get().removeAllRanges();
}
}
/**
* Event can be fired on clicks at the Editor elements, for example, at the Inline Toolbar
* We need to skip such firings

@ -1 +1 @@
Subproject commit 2fee2bbd157afdfaf175c056b314f7fca6d66fbc
Subproject commit 43e5ba9f41c424eaf4809cd1c89e29d8a7810ace

View file

@ -1,7 +1,7 @@
import $ from '../../dom';
import { API, BlockTool, BlockToolData, BlockToolConstructorOptions } from '../../../../types';
export interface StubData extends BlockToolData{
export interface StubData extends BlockToolData {
title: string;
savedData: BlockToolData;
}
@ -11,6 +11,11 @@ export interface StubData extends BlockToolData{
* It will store its data inside and pass it back with article saving
*/
export default class Stub implements BlockTool {
/**
* Notify core that tool supports read-only mode
*/
public static isReadOnlySupported = true;
/**
* Stub styles
*

View file

@ -558,6 +558,18 @@ export function openTab(url: string): void {
window.open(url, '_blank');
}
/**
* Returns random generated identifier
*
* @param {string} prefix - identifier prefix
*
* @returns {string}
*/
export function generateId(prefix = ''): string {
// tslint:disable-next-line:no-bitwise
return `${prefix}${(Math.floor(Math.random() * 1e8)).toString(16)}`;
}
/**
* Common method for printing a warning about the usage of deprecated property or method.
*

View file

@ -35,6 +35,8 @@ import InlineToolbarAPI from '../components/modules/api/inlineToolbar';
import CrossBlockSelection from '../components/modules/crossBlockSelection';
import ConversionToolbar from '../components/modules/toolbar/conversion';
import TooltipAPI from '../components/modules/api/tooltip';
import ReadOnly from '../components/modules/readonly';
import ReadOnlyAPI from '../components/modules/api/readonly';
import I18nAPI from '../components/modules/api/i18n';
export interface EditorModules {
@ -75,5 +77,7 @@ export interface EditorModules {
CrossBlockSelection: CrossBlockSelection;
NotifierAPI: NotifierAPI;
TooltipAPI: TooltipAPI;
ReadOnly: ReadOnly;
ReadOnlyAPI: ReadOnlyAPI;
I18nAPI: I18nAPI;
}

View file

@ -13,10 +13,13 @@ export interface Blocks {
/**
* Render passed data
* @param {OutputData} data
* @return {Promise<void>}
*
* @param {OutputData} data - saved Block data
* @param {boolean} readOnly - the flag that should be used to render a block in the read-only mode
*
* @returns {Promise<void>}
*/
render(data: OutputData): Promise<void>;
render(data: OutputData, readOnly: boolean): Promise<void>;
/**
* Render passed HTML string

View file

@ -11,4 +11,5 @@ export * from './notifier';
export * from './tooltip';
export * from './inline-toolbar';
export * from './block';
export * from './readonly';
export * from './i18n';

12
types/api/readonly.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
/**
* ReadOnly API
*/
export interface ReadOnly {
/**
* Set or toggle read-only state
*
* @param {Boolean|undefined} state - set or toggle state
* @returns {Promise<boolean>} current value
*/
toggle: (state?: boolean) => Promise<boolean>;
}

View file

@ -29,7 +29,7 @@ export interface EditorConfig {
/**
* @deprecated
* This property will be deprecated in the next major release.
* This property will be deprecated in the next major release.
* Use the 'defaultBlock' property instead.
*/
initialBlock?: string;
@ -70,6 +70,11 @@ export interface EditorConfig {
*/
logLevel?: LogLevels;
/**
* Enable read-only mode
*/
readOnly?: boolean;
/**
* Internalization config
*/

9
types/index.d.ts vendored
View file

@ -5,12 +5,13 @@
*/
import {
EditorConfig,
I18nDictionary,
Dictionary,
DictValue,
EditorConfig,
I18nConfig,
I18nDictionary,
} from './configs';
import {
Blocks,
Caret,
@ -18,6 +19,7 @@ import {
InlineToolbar,
Listeners,
Notifier,
ReadOnly,
Sanitizer,
Saver,
Selection,
@ -26,6 +28,7 @@ import {
Tooltip,
I18n,
} from './api';
import { OutputData } from './data-formats';
/**
@ -88,6 +91,7 @@ export interface API {
inlineToolbar: InlineToolbar;
tooltip: Tooltip;
i18n: I18n;
readOnly: ReadOnly;
}
/**
@ -108,6 +112,7 @@ declare class EditorJS {
public styles: Styles;
public toolbar: Toolbar;
public inlineToolbar: InlineToolbar;
public readOnly: ReadOnly;
constructor(configuration?: EditorConfig|string);
/**

View file

@ -16,6 +16,11 @@ export interface BlockTool extends BaseTool {
*/
sanitize?: SanitizerConfig;
/**
* @param {boolean} readOnly - render HTML on readonly mode
*/
render(readOnly?: boolean): HTMLElement;
/**
* Process Tool's element in DOM and return raw data
* @param {HTMLElement} block - element created by {@link BlockTool#render} function
@ -88,6 +93,7 @@ export interface BlockToolConstructorOptions<D extends object = any, C extends o
data: BlockToolData<D>;
config?: ToolConfig<C>;
block?: BlockAPI;
readOnly: boolean;
}
export interface BlockToolConstructable extends BaseToolConstructable {
@ -116,6 +122,11 @@ export interface BlockToolConstructable extends BaseToolConstructable {
*/
conversionConfig?: ConversionConfig;
/**
* Is Tool supports read-only mode, this property should return true
*/
isReadOnlySupported?: boolean;
/**
* @constructor
*