Skip to content

Commit 6f36707

Browse files
gohaberegneSpecc
andauthored
Tunes improvements for inline actions (codex-team#1722)
* Add tunes improvements * Allow to show Inline Toolbar at Block Tune Wrapper element * Add fake cursor on block selection * Fix lint * Update types * Fix bugs with selection * Remove selection observer * Update due to comments * Fix tests * Update docs/block-tunes.md Co-authored-by: Peter Savchenko <[email protected]> * Move fake cursor to selection utils * Fix missing range for Safari * Fix data attribute value * Add comment * Update z-index for inline-toolbar * Add changelog * Remove fake cursor visibility for the core Co-authored-by: Peter Savchenko <[email protected]>
1 parent cf494a7 commit 6f36707

21 files changed

+302
-73
lines changed

docs/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
### 2.22.2
4+
5+
- `Improvement` — Inline Toolbar might be used for any contenteditable element inside Editor.js zone
6+
- `Improvement` *Tunes API* - Tunes now can provide sanitize configuration
7+
- `Fix` *Tunes API* - Tune config now passed to constructor under `config` property
8+
- `Fix` *Types* - Add common type for internal and external Tools configuration
9+
310
### 2.22.1
411

512
- `Fix` — I18n for internal Block Tunes [#1661](https://github.com/codex-team/editor.js/issues/1661)

docs/block-tunes.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ At the constructor of Tune's class exemplar you will receive an object with foll
2222
| Parameter | Description |
2323
| --------- | ----------- |
2424
| api | Editor's [API](api.md) obejct |
25-
| settings | Configuration of Block Tool Tune is connected to (might be useful in some cases) |
25+
| config | Configuration of Block Tool Tune is connected to (might be useful in some cases) |
2626
| block | [Block API](api.md#block-api) methods for block Tune is connected to |
2727
| data | Saved Tune data |
2828

@@ -145,7 +145,24 @@ No return value
145145

146146
---
147147

148-
#### Format
148+
### static get sanitize()
149+
150+
If your Tune inserts any HTML markup into Block's content you need to provide sanitize configuration, so your HTML is not trimmed on save.
151+
152+
Please see more information at [sanitizer page](sanitizer.md).
153+
154+
155+
```javascript
156+
class Tune {
157+
static get sanitize() {
158+
return {
159+
sup: true
160+
}
161+
}
162+
}
163+
```
164+
165+
## Format
149166

150167
Tunes data is saved to `tunes` property of output object:
151168

example/example-dev.html

+4-5
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@
114114
* Tools list
115115
*/
116116
tools: {
117-
118117
/**
119118
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
120119
*/
@@ -205,10 +204,10 @@
205204
}
206205
},
207206
{
208-
type : 'paragraph',
209-
id: "b6ji-DvaKb",
210-
data : {
211-
text : 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration.'
207+
"id": "b6ji-DvaKb",
208+
"type": "paragraph",
209+
"data": {
210+
"text": "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration."
212211
}
213212
},
214213
{

src/components/block/index.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,19 @@ export default class Block extends EventsDispatcher<BlockEvents> {
201201
/**
202202
* Is fired when DOM mutation has been happened
203203
*/
204-
private didMutated = _.debounce((): void => {
204+
private didMutated = _.debounce((mutations: MutationRecord[]): void => {
205+
const shouldFireUpdate = !mutations.some(({ addedNodes = [], removedNodes }) => {
206+
return [...Array.from(addedNodes), ...Array.from(removedNodes)]
207+
.some(node => $.isElement(node) && (node as HTMLElement).dataset.mutationFree === 'true');
208+
});
209+
210+
/**
211+
* In case some mutation free elements are added or removed, do not trigger didMutated event
212+
*/
213+
if (!shouldFireUpdate) {
214+
return;
215+
}
216+
205217
/**
206218
* Drop cache
207219
*/
@@ -448,8 +460,12 @@ export default class Block extends EventsDispatcher<BlockEvents> {
448460
public set selected(state: boolean) {
449461
if (state) {
450462
this.holder.classList.add(Block.CSS.selected);
463+
464+
SelectionUtils.addFakeCursor(this.holder);
451465
} else {
452466
this.holder.classList.remove(Block.CSS.selected);
467+
468+
SelectionUtils.removeFakeCursor(this.holder);
453469
}
454470
}
455471

src/components/dom.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export default class Dom {
202202
public static get allInputsSelector(): string {
203203
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
204204

205-
return '[contenteditable], textarea, input:not([type]), ' +
205+
return '[contenteditable=true], textarea, input:not([type]), ' +
206206
allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');
207207
}
208208

src/components/modules/api/readonly.ts

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default class ReadOnlyAPI extends Module {
1212
public get methods(): ReadOnly {
1313
return {
1414
toggle: (state): Promise<boolean> => this.toggle(state),
15+
isEnabled: this.isEnabled,
1516
};
1617
}
1718

@@ -25,4 +26,11 @@ export default class ReadOnlyAPI extends Module {
2526
public toggle(state?: boolean): Promise<boolean> {
2627
return this.Editor.ReadOnly.toggle(state);
2728
}
29+
30+
/**
31+
* Returns current read-only state
32+
*/
33+
public get isEnabled(): boolean {
34+
return this.Editor.ReadOnly.isEnabled;
35+
}
2836
}

src/components/modules/rectangleSelection.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,16 @@ export default class RectangleSelection extends Module {
211211
if (mouseEvent.button !== this.MAIN_MOUSE_BUTTON) {
212212
return;
213213
}
214-
this.startSelection(mouseEvent.pageX, mouseEvent.pageY);
214+
215+
/**
216+
* Do not enable the Rectangle Selection when mouse dragging started some editable input
217+
* Used to prevent Rectangle Selection on Block Tune wrappers' inputs that also can be inside the Block
218+
*/
219+
const startedFromContentEditable = (mouseEvent.target as Element).closest($.allInputsSelector) !== null;
220+
221+
if (!startedFromContentEditable) {
222+
this.startSelection(mouseEvent.pageX, mouseEvent.pageY);
223+
}
215224
}
216225

217226
/**

src/components/modules/toolbar/index.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import I18n from '../../i18n';
55
import { I18nInternalNS } from '../../i18n/namespace-internal';
66
import Tooltip from '../../utils/tooltip';
77
import { ModuleConfig } from '../../../types-internal/module-config';
8-
import EventsDispatcher from '../../utils/events';
98
import { EditorConfig } from '../../../../types';
9+
import SelectionUtils from '../../selection';
1010

1111
/**
1212
* HTML Elements used for Toolbar UI
@@ -348,10 +348,19 @@ export default class Toolbar extends Module<ToolbarNodes> {
348348
private enableModuleBindings(): void {
349349
/**
350350
* Settings toggler
351+
*
352+
* mousedown is used because on click selection is lost in Safari and FF
351353
*/
352-
this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'click', () => {
354+
this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'mousedown', (e) => {
355+
/**
356+
* Stop propagation to prevent block selection clearance
357+
*
358+
* @see UI.documentClicked
359+
*/
360+
e.stopPropagation();
361+
353362
this.settingsTogglerClicked();
354-
});
363+
}, true);
355364
}
356365

357366
/**

src/components/modules/toolbar/inline.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,11 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
134134
/**
135135
* Shows Inline Toolbar if something is selected
136136
*
137-
* @param {boolean} [needToClose] - pass true to close toolbar if it is not allowed.
137+
* @param [needToClose] - pass true to close toolbar if it is not allowed.
138138
* Avoid to use it just for closing IT, better call .close() clearly.
139+
* @param [needToShowConversionToolbar] - pass false to not to show Conversion Toolbar
139140
*/
140-
public tryToShow(needToClose = false): void {
141+
public tryToShow(needToClose = false, needToShowConversionToolbar = true): void {
141142
if (!this.allowedToShow()) {
142143
if (needToClose) {
143144
this.close();
@@ -147,7 +148,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
147148
}
148149

149150
this.move();
150-
this.open();
151+
this.open(needToShowConversionToolbar);
151152
this.Editor.Toolbar.close();
152153
}
153154

@@ -233,8 +234,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
233234

234235
/**
235236
* Shows Inline Toolbar
237+
*
238+
* @param [needToShowConversionToolbar] - pass false to not to show Conversion Toolbar
236239
*/
237-
public open(): void {
240+
public open(needToShowConversionToolbar = true): void {
238241
if (this.opened) {
239242
return;
240243
}
@@ -251,7 +254,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
251254
this.buttonsList = this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`);
252255
this.opened = true;
253256

254-
if (this.Editor.ConversionToolbar.hasTools()) {
257+
if (needToShowConversionToolbar && this.Editor.ConversionToolbar.hasTools()) {
255258
/**
256259
* Change Conversion Dropdown content for current tool
257260
*/

src/components/modules/ui.ts

+36-9
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ export default class UI extends Module<UINodes> {
340340
this.documentKeydown(event);
341341
}, true);
342342

343-
this.readOnlyMutableListeners.on(document, 'click', (event: MouseEvent) => {
343+
this.readOnlyMutableListeners.on(document, 'mousedown', (event: MouseEvent) => {
344344
this.documentClicked(event);
345345
}, true);
346346

@@ -591,9 +591,7 @@ export default class UI extends Module<UINodes> {
591591
/**
592592
* Clear Selection if user clicked somewhere
593593
*/
594-
if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted) {
595-
this.Editor.BlockSelection.clearSelection(event);
596-
}
594+
this.Editor.BlockSelection.clearSelection(event);
597595
}
598596

599597
/**
@@ -754,18 +752,45 @@ export default class UI extends Module<UINodes> {
754752
}
755753

756754
/**
757-
* Event can be fired on clicks at the Editor elements, for example, at the Inline Toolbar
758-
* We need to skip such firings
755+
* Usual clicks on some controls, for example, Block Tunes Toggler
756+
*/
757+
if (!focusedElement) {
758+
/**
759+
* If there is no selected range, close inline toolbar
760+
*
761+
* @todo Make this method more straightforward
762+
*/
763+
if (!Selection.range) {
764+
this.Editor.InlineToolbar.close();
765+
}
766+
767+
return;
768+
}
769+
770+
/**
771+
* Event can be fired on clicks at non-block-content elements,
772+
* for example, at the Inline Toolbar or some Block Tune element
759773
*/
760-
if (!focusedElement || !focusedElement.closest(`.${Block.CSS.content}`)) {
774+
const clickedOutsideBlockContent = focusedElement.closest(`.${Block.CSS.content}`) === null;
775+
776+
if (clickedOutsideBlockContent) {
761777
/**
762778
* If new selection is not on Inline Toolbar, we need to close it
763779
*/
764780
if (!this.Editor.InlineToolbar.containsNode(focusedElement)) {
765781
this.Editor.InlineToolbar.close();
766782
}
767783

768-
return;
784+
/**
785+
* Case when we click on external tool elements,
786+
* for example some Block Tune element.
787+
* If this external content editable element has data-inline-toolbar="true"
788+
*/
789+
const inlineToolbarEnabledForExternalTool = (focusedElement as HTMLElement).dataset.inlineToolbar === 'true';
790+
791+
if (!inlineToolbarEnabledForExternalTool) {
792+
return;
793+
}
769794
}
770795

771796
/**
@@ -775,10 +800,12 @@ export default class UI extends Module<UINodes> {
775800
this.Editor.BlockManager.setCurrentBlockByChildNode(focusedElement);
776801
}
777802

803+
const isNeedToShowConversionToolbar = clickedOutsideBlockContent !== true;
804+
778805
/**
779806
* @todo add debounce
780807
*/
781-
this.Editor.InlineToolbar.tryToShow(true);
808+
this.Editor.InlineToolbar.tryToShow(true, isNeedToShowConversionToolbar);
782809
}
783810

784811
/**

0 commit comments

Comments
 (0)