Skip to content
This repository was archived by the owner on May 5, 2021. It is now read-only.

Commit 544b96f

Browse files
Zyntondmo-odoo
authored andcommitted
[FIX] AbstractNode: check contenteditable attribute on formats too
```html <p contenteditable> <a contenteditable="false">text</a> </p> ``` Prior to this fix, the `contenteditable="false"` was being ignored by `DomLayout` and `FontAwesomeDomObjectRenderer` because it was on a format and not a node.
1 parent 9245883 commit 544b96f

File tree

6 files changed

+102
-21
lines changed

6 files changed

+102
-21
lines changed

packages/core/src/JWEditor.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ import { Core } from './Core';
44
import { ContextManager, Context } from './ContextManager';
55
import { VSelection } from './VSelection';
66
import { VRange } from './VRange';
7-
import { isConstructor } from '../../utils/src/utils';
7+
import { isConstructor, isContentEditable } from '../../utils/src/utils';
88
import { Keymap } from '../../plugin-keymap/src/Keymap';
99
import { StageError } from '../../utils/src/errors';
1010
import { ContainerNode } from './VNodes/ContainerNode';
1111
import { AtomicNode } from './VNodes/AtomicNode';
1212
import { SeparatorNode } from './VNodes/SeparatorNode';
13-
import { ModeIdentifier, ModeDefinition, Mode } from './Mode';
13+
import { ModeIdentifier, ModeDefinition, Mode, RuleProperty } from './Mode';
1414
import { Memory, ChangesLocations } from './Memory/Memory';
1515
import { makeVersionable } from './Memory/Versionable';
1616
import { VersionableArray } from './Memory/VersionableArray';
17-
import { Point } from './VNodes/VNode';
17+
import { Point, VNode } from './VNodes/VNode';
1818

1919
export enum EditorStage {
2020
CONFIGURATION = 'configuration',
@@ -179,6 +179,14 @@ export class JWEditor {
179179
// Public
180180
//--------------------------------------------------------------------------
181181

182+
/**
183+
* Return true if the target node is within an editable context.
184+
*
185+
* @param target
186+
*/
187+
isInEditable(node: VNode): boolean {
188+
return isContentEditable(node) || this.mode.is(node, RuleProperty.EDITABLE);
189+
}
182190
/**
183191
* Load the given plugin with given configuration.
184192
*

packages/core/src/VNodes/AbstractNode.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ export abstract class AbstractNode extends EventMixin {
2828
*/
2929
get editable(): boolean {
3030
if (this._editable === false) return false;
31+
const modifiers = this.modifiers.filter(mod => 'modifiers' in mod);
32+
const lastModifierWithContentEditable = modifiers.reverse().find(modifier => {
33+
return hasContentEditable(modifier);
34+
});
35+
if (lastModifierWithContentEditable) {
36+
return isContentEditable(lastModifierWithContentEditable);
37+
}
3138
return (
3239
!this.parent ||
3340
(hasContentEditable(this.parent)

packages/plugin-dom-layout/src/DomLayout.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import { ZoneXmlDomParser } from './ZoneXmlDomParser';
1515
import { LayoutContainerDomObjectRenderer } from './LayoutContainerDomObjectRenderer';
1616
import { ZoneIdentifier, ZoneNode } from '../../plugin-layout/src/ZoneNode';
1717
import { Keymap } from '../../plugin-keymap/src/Keymap';
18-
import { CommandIdentifier, CommandParams } from '../../core/src/Dispatcher';
18+
import { CommandIdentifier } from '../../core/src/Dispatcher';
1919
import { ActionableDomObjectRenderer } from './ActionableDomObjectRenderer';
2020
import { ActionableGroupDomObjectRenderer } from './ActionableGroupDomObjectRenderer';
2121
import { ActionableGroupSelectItemDomObjectRenderer } from './ActionableGroupSelectItemDomObjectRenderer';
2222
import { LabelDomObjectRenderer } from './LabelDomObjectRenderer';
2323
import { SeparatorDomObjectRenderer } from './SeparatorDomObjectRenderer';
2424
import { RuleProperty } from '../../core/src/Mode';
2525
import { isContentEditable, nodeName, isInstanceOf } from '../../utils/src/utils';
26+
import { VNode } from '../../core/src/VNodes/VNode';
2627

2728
const FocusAndBlurEvents = ['selectionchange', 'blur', 'focus', 'mousedown', 'touchstart'];
2829

@@ -163,9 +164,10 @@ export class DomLayout<T extends DomLayoutConfig = DomLayoutConfig> extends JWPl
163164
}
164165

165166
/**
166-
* Return true if the target node is inside Jabberwock's main editable Zone.
167+
* Return true if the target node is inside Jabberwock's main editable Zone
168+
* and within an editable context.
167169
*
168-
* @param target: Node
170+
* @param target
169171
*/
170172
isInEditable(target: Node): boolean {
171173
const layout = this.dependencies.get(Layout);
@@ -186,7 +188,7 @@ export class DomLayout<T extends DomLayoutConfig = DomLayoutConfig> extends JWPl
186188
// the editable part of Jabberwock.
187189
return (
188190
node &&
189-
(isContentEditable(node) || this.editor.mode.is(node, RuleProperty.EDITABLE)) &&
191+
this.editor.isInEditable(node) &&
190192
!!node.ancestor(node => node instanceof ZoneNode && node.managedZones.includes('main'))
191193
);
192194
}

packages/plugin-fontawesome/src/FontAwesomeDomObjectRenderer.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import {
88
DomObjectElement,
99
} from '../../plugin-renderer-dom-object/src/DomObjectRenderingEngine';
1010
import { RenderingEngineWorker } from '../../plugin-renderer/src/RenderingEngineCache';
11-
import { RuleProperty } from '../../core/src/Mode';
12-
import { CharNode } from '../../plugin-char/src/CharNode';
1311

1412
const zeroWidthSpace = '\u200b';
1513

@@ -98,8 +96,8 @@ export class FontAwesomeDomObjectRenderer extends NodeRenderer<DomObject> {
9896
*/
9997
shouldAddNavigationHelpers(node: FontAwesomeNode): boolean {
10098
const range = this.engine.editor.selection.range;
101-
// Is parent editable.
102-
if (node.parent && !range.mode.is(node.parent, RuleProperty.EDITABLE)) {
99+
100+
if (!this.engine.editor.isInEditable(node)) {
103101
return false;
104102
}
105103
// Is node next to range.

packages/plugin-fontawesome/test/FontAwesome.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,66 @@ describePlugin(FontAwesome, testEditor => {
101101
contentAfter: '<p>a[b]c<i class="fa fa-pastafarianism"></i></p>',
102102
});
103103
});
104+
it('should insert navigation helpers when before a fontawesome, in an editable', async () => {
105+
await testEditor(BasicEditor, {
106+
contentBefore: '<p>abc[]<i class="fa fa-pastafarianism"></i></p>',
107+
contentAfter: '<p>abc[]\u200B<i class="fa fa-pastafarianism"></i>\u200B</p>',
108+
});
109+
await testEditor(BasicEditor, {
110+
contentBefore: '<p>[]<i class="fa fa-pastafarianism"></i></p>',
111+
contentAfter: '<p>\u200B[]<i class="fa fa-pastafarianism"></i>\u200B</p>',
112+
});
113+
});
114+
it('should insert navigation helpers when after a fontawesome, in an editable', async () => {
115+
await testEditor(BasicEditor, {
116+
contentBefore: '<p><i class="fa fa-pastafarianism"></i>[]abc</p>',
117+
contentAfter: '<p>\u200B<i class="fa fa-pastafarianism"></i>\u200B[]abc</p>',
118+
});
119+
await testEditor(BasicEditor, {
120+
contentBefore: '<p><i class="fa fa-pastafarianism"></i>[]</p>',
121+
contentAfter: '<p>\u200B<i class="fa fa-pastafarianism"></i>\u200B[]</p>',
122+
});
123+
});
124+
it('should not insert navigation helpers when not adjacent to a fontawesome, in an editable', async () => {
125+
await testEditor(BasicEditor, {
126+
contentBefore: '<p>ab[]c<i class="fa fa-pastafarianism"></i></p>',
127+
contentAfter: '<p>ab[]c<i class="fa fa-pastafarianism"></i></p>',
128+
});
129+
await testEditor(BasicEditor, {
130+
contentBefore: '<p><i class="fa fa-pastafarianism"></i>a[]bc</p>',
131+
contentAfter: '<p><i class="fa fa-pastafarianism"></i>a[]bc</p>',
132+
});
133+
});
134+
it('should not insert navigation helpers when adjacent to a fontawesome in contenteditable=false container', async () => {
135+
await testEditor(BasicEditor, {
136+
contentBefore: '<p contenteditable="false">abc[]<i class="fa fa-pastafarianism"></i></p>',
137+
contentAfter: '<p contenteditable="false">abc<i class="fa fa-pastafarianism"></i></p>',
138+
});
139+
await testEditor(BasicEditor, {
140+
contentBefore: '<p contenteditable="false"><i class="fa fa-pastafarianism"></i>[]abc</p>',
141+
contentAfter: '<p contenteditable="false"><i class="fa fa-pastafarianism"></i>abc</p>',
142+
});
143+
});
144+
it('should not insert navigation helpers when adjacent to a fontawesome in contenteditable=false format', async () => {
145+
await testEditor(BasicEditor, {
146+
contentBefore: '<p contenteditable="true"><b contenteditable="false">abc[]<i class="fa fa-pastafarianism"></i></b></p>',
147+
contentAfter: '<p contenteditable="true"><b contenteditable="false">abc<i class="fa fa-pastafarianism"></i></b></p>',
148+
});
149+
await testEditor(BasicEditor, {
150+
contentBefore: '<p contenteditable="true"><b contenteditable="false"><i class="fa fa-pastafarianism"></i>[]abc</b></p>',
151+
contentAfter: '<p contenteditable="true"><b contenteditable="false"><i class="fa fa-pastafarianism"></i>abc</b></p>',
152+
});
153+
});
154+
it('should not insert navigation helpers when adjacent to a fontawesome in contenteditable=false format (nested)', async () => {
155+
await testEditor(BasicEditor, {
156+
contentBefore: '<p contenteditable="true"><a contenteditable="true"><b contenteditable="false">abc[]<i class="fa fa-pastafarianism"></i></b></a></p>',
157+
contentAfter: '<p contenteditable="true"><a contenteditable="true"><b contenteditable="false">abc<i class="fa fa-pastafarianism"></i></b></a></p>',
158+
});
159+
await testEditor(BasicEditor, {
160+
contentBefore: '<p contenteditable="true"><a contenteditable="true"><b contenteditable="false"><i class="fa fa-pastafarianism"></i>[]abc</b></a></p>',
161+
contentAfter: '<p contenteditable="true"><a contenteditable="true"><b contenteditable="false"><i class="fa fa-pastafarianism"></i>abc</b></a></p>',
162+
});
163+
});
104164
});
105165
describe('deleteForward', () => {
106166
describe('Selection collapsed', () => {

packages/utils/src/utils.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AbstractNode } from '../../core/src/VNodes/AbstractNode';
22
import { Attributes } from '../../plugin-xml/src/Attributes';
33
import JWEditor from '../../core/src/JWEditor';
4+
import { Modifier } from '../../core/src/Modifier';
45

56
export type Constructor<T> = new (...args) => T;
67

@@ -14,23 +15,28 @@ export function isConstructor<T extends Function>(constructor, superClass: T): c
1415
return constructor.prototype instanceof superClass || constructor === superClass;
1516
}
1617
/**
17-
* Return true if the node has the `contentEditable` attribute.
18+
* Return true if the node or modifier has a modifier with the `contentEditable`
19+
* attribute.
1820
*
19-
* @param node
21+
* @param nodeOrModifier
2022
*/
21-
export function hasContentEditable(node: AbstractNode): boolean {
22-
return node.modifiers.find(Attributes)?.has('contentEditable') || false;
23+
export function hasContentEditable(nodeOrModifier: AbstractNode | Modifier): boolean {
24+
if ('modifiers' in nodeOrModifier) {
25+
return nodeOrModifier.modifiers.find(Attributes)?.has('contentEditable') || false;
26+
}
2327
}
2428
/**
25-
* Return true if the node has the `contentEditable` attribute set to true. This
26-
* implies that its children are editable but it is not necessarily itself
27-
* editable.
29+
* Return true if the node or modifier has a modifier with the `contentEditable`
30+
* attribute set to true. This implies that its children are editable but it is
31+
* not necessarily itself editable.
2832
*
2933
* TODO: unbind from `Attributes`.
3034
*/
31-
export function isContentEditable(node: AbstractNode): boolean {
32-
const editable = node.modifiers.find(Attributes)?.get('contentEditable');
33-
return editable === '' || editable?.toLowerCase() === 'true' || false;
35+
export function isContentEditable(nodeOrModifier: AbstractNode | Modifier): boolean {
36+
if ('modifiers' in nodeOrModifier) {
37+
const editable = nodeOrModifier.modifiers.find(Attributes)?.get('contentEditable');
38+
return editable === '' || editable?.toLowerCase() === 'true' || false;
39+
}
3440
}
3541
/**
3642
* Return true if object a is deep equal to object b, false otherwise.

0 commit comments

Comments
 (0)