mirror of
https://github.com/codex-team/editor.js
synced 2024-06-26 09:20:07 +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 $: any;
|
||||
declare var _: any;
|
||||
import InlineTool from '../inline-tools/inline-tool';
|
||||
import BoldInlineTool from '../inline-tools/inline-tool-bold';
|
||||
import Selection from '../selection';
|
||||
|
||||
/**
|
||||
|
@ -47,13 +49,23 @@ export default class InlineToolbar extends Module {
|
|||
*/
|
||||
private readonly toolbarVerticalMargin: number = 20;
|
||||
|
||||
/**
|
||||
* Available Tools classes
|
||||
*/
|
||||
private tools: InlineTool[] = [];
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
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() {
|
||||
|
||||
this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
|
||||
this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
|
||||
|
||||
/**
|
||||
* Append Inline Toolbar to the Editor
|
||||
*/
|
||||
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
|
||||
/**
|
||||
* Append Inline Toolbar to the Editor
|
||||
*/
|
||||
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
|
||||
|
||||
/**
|
||||
* Append Inline Toolbar Tools
|
||||
*/
|
||||
this.addTools();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Moving / appearance
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Shows Inline Toolbar by keyup/mouseup
|
||||
* @param {KeyboardEvent|MouseEvent} event
|
||||
|
@ -82,6 +107,9 @@ export default class InlineToolbar extends Module {
|
|||
|
||||
this.move();
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* 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}
|
||||
*/
|
||||
static get anchorNode() {
|
||||
let selection = window.getSelection();
|
||||
const selection = window.getSelection();
|
||||
|
||||
return selection ? selection.anchorNode : null;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export default class Selection {
|
|||
* @return {Number|null}
|
||||
*/
|
||||
static get anchorOffset() {
|
||||
let selection = window.getSelection();
|
||||
const selection = window.getSelection();
|
||||
|
||||
return selection ? selection.anchorOffset : null;
|
||||
}
|
||||
|
@ -47,11 +47,21 @@ export default class Selection {
|
|||
* @return {boolean|null}
|
||||
*/
|
||||
static get isCollapsed() {
|
||||
let selection = window.getSelection();
|
||||
const selection = window.getSelection();
|
||||
|
||||
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
|
||||
* @return {{x, y, width, height, top?, left?, bottom?, right?}}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
.ce-inline-toolbar {
|
||||
@apply --overlay-pane;
|
||||
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
padding: 6px;
|
||||
transform: translateX(-50%);
|
||||
display: none;
|
||||
|
||||
|
@ -10,3 +8,30 @@
|
|||
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 {
|
||||
/**
|
||||
* Toolbar buttons
|
||||
*/
|
||||
--bg-light: #eff2f5;
|
||||
|
||||
/**
|
||||
* Toolbar buttons
|
||||
*/
|
||||
--bg-light: #eff2f5;
|
||||
/**
|
||||
* All gray texts: placeholders, settings
|
||||
*/
|
||||
--grayText: #707684;
|
||||
|
||||
/**
|
||||
* All gray texts: placeholders, settings
|
||||
*/
|
||||
--grayText: #707684;
|
||||
/** Blue icons */
|
||||
--color-active-icon: #388AE5;
|
||||
|
||||
/**
|
||||
* Block content width
|
||||
*/
|
||||
--content-width: 650px;
|
||||
/**
|
||||
* Block content width
|
||||
*/
|
||||
--content-width: 650px;
|
||||
|
||||
/**
|
||||
* Toolbar Plus Button and Toolbox buttons height and width
|
||||
*/
|
||||
--toolbar-buttons-size: 34px;
|
||||
/**
|
||||
* Toolbar Plus Button and Toolbox buttons height and width
|
||||
*/
|
||||
--toolbar-buttons-size: 34px;
|
||||
|
||||
--overlay-pane: {
|
||||
position: absolute;
|
||||
background: #FFFFFF;
|
||||
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;
|
||||
z-index: 2;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 50%;
|
||||
margin-left: -7px;
|
||||
transform: rotate(-45deg);
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
--overlay-pane: {
|
||||
position: absolute;
|
||||
background: #FFFFFF;
|
||||
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;
|
||||
z-index: 2;
|
||||
|
||||
&::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 : {
|
||||
// fallback: path.join(__dirname, 'node_modules'),
|
||||
modules : [ path.join(__dirname, "src"), "node_modules"],
|
||||
extensions: ['.js', '.ts'],
|
||||
alias: {
|
||||
'utils': path.resolve(__dirname + '/src/components/', './utils'),
|
||||
'dom': path.resolve(__dirname + '/src/components/', './dom'),
|
||||
|
|
Loading…
Reference in a new issue