mirror of
https://github.com/codex-team/editor.js
synced 2024-06-29 02:40:23 +02:00
Inline Toolbar Tools base example (#260)
* Inline Toolbar Tools base example * texts fixed * imrpove texts * little fixes
This commit is contained in:
parent
cba999a77d
commit
36f505cb02
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
57
docs/tools-inline.md
Normal file
57
docs/tools-inline.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# Tools for the Inline Toolbar
|
||||||
|
|
||||||
|
Similar with [Tools](tools.md) represented Blocks, you can create Tools for the Inline Toolbar. It will work with
|
||||||
|
selected fragment of text. The simplest example is `bold` or `italic` Tools.
|
||||||
|
|
||||||
|
## Base structure
|
||||||
|
|
||||||
|
Inline Tool should implement next methods
|
||||||
|
|
||||||
|
- `render()` — create a button
|
||||||
|
- `surround()` — works with selected range
|
||||||
|
- `checkState()` — get Tool's activated state by selected range
|
||||||
|
|
||||||
|
### render()
|
||||||
|
|
||||||
|
Method that returns button to append at the Inline Toolbar
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
Method does not accept any parameters
|
||||||
|
|
||||||
|
#### Return value
|
||||||
|
|
||||||
|
type | description |
|
||||||
|
-- | -- |
|
||||||
|
`HTMLElement` | element that will be added to the Inline Toolbar |
|
||||||
|
|
||||||
|
|
||||||
|
### surround(range: Range)
|
||||||
|
|
||||||
|
Method that accepts selected range and wrap it somehow
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
name | type | description |
|
||||||
|
-- |-- | -- |
|
||||||
|
range | Range | first range of current Selection |
|
||||||
|
|
||||||
|
#### Return value
|
||||||
|
|
||||||
|
There is no return value
|
||||||
|
|
||||||
|
### checkState(selection: Selection)
|
||||||
|
|
||||||
|
Get Selection and detect if Tool was applied. For example, after that Tool can highlight button or show some details.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
name | type | description |
|
||||||
|
-- |-- | -- |
|
||||||
|
selection | Selection | current Selection |
|
||||||
|
|
||||||
|
#### Return value
|
||||||
|
|
||||||
|
type | description |
|
||||||
|
-- | -- |
|
||||||
|
`Boolean` | `true` if Tool is active, otherwise `false` |
|
64
src/components/inline-tools/inline-tool-bold.ts
Normal file
64
src/components/inline-tools/inline-tool-bold.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import InlineTool from './inline-tool';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bold Tool
|
||||||
|
*
|
||||||
|
* Inline Toolbar Tool
|
||||||
|
*
|
||||||
|
* Makes selected text bolder
|
||||||
|
*/
|
||||||
|
export default class BoldInlineTool implements InlineTool {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native Document's command that uses for Bold
|
||||||
|
*/
|
||||||
|
private readonly commandName: string = 'bold';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Styles
|
||||||
|
*/
|
||||||
|
private readonly CSS = {
|
||||||
|
button: 'ce-inline-tool',
|
||||||
|
buttonActive: 'ce-inline-tool--active',
|
||||||
|
buttonModifier: 'ce-inline-tool--bold',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elements
|
||||||
|
*/
|
||||||
|
private nodes = {
|
||||||
|
button: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
console.log('Bold Inline Tool is ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create button for Inline Toolbar
|
||||||
|
*/
|
||||||
|
public render(): HTMLElement {
|
||||||
|
this.nodes.button = document.createElement('button');
|
||||||
|
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||||
|
return this.nodes.button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap range with <b> tag
|
||||||
|
* @param {Range} range
|
||||||
|
*/
|
||||||
|
public surround(range: Range): void {
|
||||||
|
document.execCommand(this.commandName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check selection and set activated state to button if there are <b> tag
|
||||||
|
* @param {Selection} selection
|
||||||
|
*/
|
||||||
|
public checkState(selection: Selection): boolean {
|
||||||
|
const isActive = document.queryCommandState(this.commandName);
|
||||||
|
|
||||||
|
this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
}
|
52
src/components/inline-tools/inline-tool.ts
Normal file
52
src/components/inline-tools/inline-tool.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* Base structure for the Inline Toolbar Tool
|
||||||
|
*/
|
||||||
|
export default interface InlineTool {
|
||||||
|
/**
|
||||||
|
* Returns button for the Inline Toolbar
|
||||||
|
*/
|
||||||
|
render(): HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that accepts selected range and wrap it somehow
|
||||||
|
* @param {Range} range - selection's range
|
||||||
|
*/
|
||||||
|
surround(range: Range): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Selection and detect if Tool was applied
|
||||||
|
* For example, after that Tool can highlight button or show some details
|
||||||
|
* @param {Selection} selection - current Selection
|
||||||
|
*/
|
||||||
|
checkState(selection: Selection): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow Tool to determine shortcut that will fire 'surround' method
|
||||||
|
*/
|
||||||
|
shortcut?(): Shortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify Shortcut structure for the Inline Tools
|
||||||
|
*/
|
||||||
|
interface Shortcut {
|
||||||
|
/**
|
||||||
|
* Name like 'CMD+I'
|
||||||
|
*/
|
||||||
|
readonly name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key that activates the Shortcut, for example: 73
|
||||||
|
*/
|
||||||
|
readonly keyCode: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is need to CMD or CTRL should be pressed
|
||||||
|
*/
|
||||||
|
readonly cmdKey: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is need to ALT should be pressed
|
||||||
|
*/
|
||||||
|
readonly altKey: boolean;
|
||||||
|
}
|
|
@ -8,6 +8,8 @@
|
||||||
declare var Module: any;
|
declare var Module: any;
|
||||||
declare var $: any;
|
declare var $: any;
|
||||||
declare var _: any;
|
declare var _: any;
|
||||||
|
import InlineTool from '../inline-tools/inline-tool';
|
||||||
|
import BoldInlineTool from '../inline-tools/inline-tool-bold';
|
||||||
import Selection from '../selection';
|
import Selection from '../selection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,13 +49,23 @@ export default class InlineToolbar extends Module {
|
||||||
*/
|
*/
|
||||||
private readonly toolbarVerticalMargin: number = 20;
|
private readonly toolbarVerticalMargin: number = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available Tools classes
|
||||||
|
*/
|
||||||
|
private tools: InlineTool[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
constructor({config}) {
|
constructor({config}) {
|
||||||
|
super({config});
|
||||||
|
|
||||||
super({config});
|
/**
|
||||||
|
* @todo Merge internal tools with external
|
||||||
|
*/
|
||||||
|
this.tools = [
|
||||||
|
new BoldInlineTool(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,15 +73,28 @@ export default class InlineToolbar extends Module {
|
||||||
*/
|
*/
|
||||||
public make() {
|
public make() {
|
||||||
|
|
||||||
this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
|
this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append Inline Toolbar to the Editor
|
* Append Inline Toolbar to the Editor
|
||||||
*/
|
*/
|
||||||
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
|
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append Inline Toolbar Tools
|
||||||
|
*/
|
||||||
|
this.addTools();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Moving / appearance
|
||||||
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows Inline Toolbar by keyup/mouseup
|
* Shows Inline Toolbar by keyup/mouseup
|
||||||
* @param {KeyboardEvent|MouseEvent} event
|
* @param {KeyboardEvent|MouseEvent} event
|
||||||
|
@ -82,6 +107,9 @@ export default class InlineToolbar extends Module {
|
||||||
|
|
||||||
this.move();
|
this.move();
|
||||||
this.open();
|
this.open();
|
||||||
|
|
||||||
|
/** Check Tools state for selected fragment */
|
||||||
|
this.checkToolsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,4 +192,58 @@ export default class InlineToolbar extends Module {
|
||||||
|
|
||||||
return toolConfig && toolConfig[this.Editor.Tools.apiSettings.IS_ENABLED_INLINE_TOOLBAR];
|
return toolConfig && toolConfig[this.Editor.Tools.apiSettings.IS_ENABLED_INLINE_TOOLBAR];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Working with Tools
|
||||||
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill Inline Toolbar with Tools
|
||||||
|
*/
|
||||||
|
private addTools(): void {
|
||||||
|
this.tools.forEach( (tool) => {
|
||||||
|
this.addTool(tool);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add tool button and activate clicks
|
||||||
|
* @param {InlineTool} tool - Tool's instance
|
||||||
|
*/
|
||||||
|
private addTool(tool: InlineTool): void {
|
||||||
|
const button = tool.render();
|
||||||
|
|
||||||
|
this.nodes.wrapper.appendChild(button);
|
||||||
|
this.Editor.Listeners.on(button, 'click', () => {
|
||||||
|
this.toolClicked(tool);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline Tool button clicks
|
||||||
|
* @param {InlineTool} tool - Tool's instance
|
||||||
|
*/
|
||||||
|
private toolClicked(tool: InlineTool): void {
|
||||||
|
const range = Selection.range;
|
||||||
|
|
||||||
|
if (!range) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tool.surround(range);
|
||||||
|
this.checkToolsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Tools` state by selection
|
||||||
|
*/
|
||||||
|
private checkToolsState(): void {
|
||||||
|
this.tools.forEach( (tool) => {
|
||||||
|
tool.checkState(Selection.get);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default class Selection {
|
||||||
* @return {Node|null}
|
* @return {Node|null}
|
||||||
*/
|
*/
|
||||||
static get anchorNode() {
|
static get anchorNode() {
|
||||||
let selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
|
||||||
return selection ? selection.anchorNode : null;
|
return selection ? selection.anchorNode : null;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ export default class Selection {
|
||||||
* @return {Number|null}
|
* @return {Number|null}
|
||||||
*/
|
*/
|
||||||
static get anchorOffset() {
|
static get anchorOffset() {
|
||||||
let selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
|
||||||
return selection ? selection.anchorOffset : null;
|
return selection ? selection.anchorOffset : null;
|
||||||
}
|
}
|
||||||
|
@ -47,11 +47,21 @@ export default class Selection {
|
||||||
* @return {boolean|null}
|
* @return {boolean|null}
|
||||||
*/
|
*/
|
||||||
static get isCollapsed() {
|
static get isCollapsed() {
|
||||||
let selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
|
||||||
return selection ? selection.isCollapsed : null;
|
return selection ? selection.isCollapsed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return first range
|
||||||
|
* @return {Range|null}
|
||||||
|
*/
|
||||||
|
static get range() {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
|
||||||
|
return selection ? selection.getRangeAt(0) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates position and size of selected text
|
* Calculates position and size of selected text
|
||||||
* @return {{x, y, width, height, top?, left?, bottom?, right?}}
|
* @return {{x, y, width, height, top?, left?, bottom?, right?}}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
.ce-inline-toolbar {
|
.ce-inline-toolbar {
|
||||||
@apply --overlay-pane;
|
@apply --overlay-pane;
|
||||||
|
padding: 6px;
|
||||||
width: 100px;
|
|
||||||
height: 40px;
|
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
@ -10,3 +8,30 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ce-inline-tool {
|
||||||
|
display: inline-block;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
vertical-align: bottom;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: var(--color-active-icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--bold {
|
||||||
|
&::before {
|
||||||
|
font-weight: bold;
|
||||||
|
content: 'B'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,43 +1,45 @@
|
||||||
:root {
|
:root {
|
||||||
|
/**
|
||||||
|
* Toolbar buttons
|
||||||
|
*/
|
||||||
|
--bg-light: #eff2f5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toolbar buttons
|
* All gray texts: placeholders, settings
|
||||||
*/
|
*/
|
||||||
--bg-light: #eff2f5;
|
--grayText: #707684;
|
||||||
|
|
||||||
/**
|
/** Blue icons */
|
||||||
* All gray texts: placeholders, settings
|
--color-active-icon: #388AE5;
|
||||||
*/
|
|
||||||
--grayText: #707684;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block content width
|
* Block content width
|
||||||
*/
|
*/
|
||||||
--content-width: 650px;
|
--content-width: 650px;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toolbar Plus Button and Toolbox buttons height and width
|
* Toolbar Plus Button and Toolbox buttons height and width
|
||||||
*/
|
*/
|
||||||
--toolbar-buttons-size: 34px;
|
--toolbar-buttons-size: 34px;
|
||||||
|
|
||||||
--overlay-pane: {
|
--overlay-pane: {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
box-shadow: 0 8px 23px -6px rgba(21,40,54,0.31), 22px -14px 34px -18px rgba(33,48,73,0.26);
|
box-shadow: 0 8px 23px -6px rgba(21,40,54,0.31), 22px -14px 34px -18px rgba(33,48,73,0.26);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
position: absolute;
|
|
||||||
top: -7px;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -7px;
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -7px;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
background: #fff;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,7 @@ module.exports = {
|
||||||
resolve : {
|
resolve : {
|
||||||
// fallback: path.join(__dirname, 'node_modules'),
|
// fallback: path.join(__dirname, 'node_modules'),
|
||||||
modules : [ path.join(__dirname, "src"), "node_modules"],
|
modules : [ path.join(__dirname, "src"), "node_modules"],
|
||||||
|
extensions: ['.js', '.ts'],
|
||||||
alias: {
|
alias: {
|
||||||
'utils': path.resolve(__dirname + '/src/components/', './utils'),
|
'utils': path.resolve(__dirname + '/src/components/', './utils'),
|
||||||
'dom': path.resolve(__dirname + '/src/components/', './dom'),
|
'dom': path.resolve(__dirname + '/src/components/', './dom'),
|
||||||
|
|
Loading…
Reference in a new issue