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

Commit eee75c5

Browse files
Gorashdmo-odoo
authored andcommitted
[ADD] Shadow: add shadow feature
1 parent f49a2c3 commit eee75c5

File tree

20 files changed

+1704
-124
lines changed

20 files changed

+1704
-124
lines changed

examples/layout/layout.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@ import { OutdentButton } from '../../packages/plugin-indent/src/IndentButtons';
1919
import { DomEditable } from '../../packages/plugin-dom-editable/src/DomEditable';
2020
import { DomLayout } from '../../packages/plugin-dom-layout/src/DomLayout';
2121
import { VNode } from '../../packages/core/src/VNodes/VNode';
22-
import JWEditor from '../../packages/core/src/JWEditor';
22+
import JWEditor, { JWEditorConfig, Loadables } from '../../packages/core/src/JWEditor';
2323
import { Parser } from '../../packages/plugin-parser/src/Parser';
24+
import { Shadow } from '../../packages/plugin-shadow/src/Shadow';
2425

2526
import '../utils/jabberwocky.css';
2627
import headerTemplate from './header.xml';
2728
import mainTemplate from './main.xml';
2829
import otherTemplate from './other.xml';
2930
import footerTemplate from './footer.xml';
31+
import { Layout } from '../../packages/plugin-layout/src/Layout';
32+
import { ShadowNode } from '../../packages/plugin-shadow/src/ShadowNode';
33+
import { MetadataNode } from '../../packages/plugin-metadata/src/MetadataNode';
3034

3135
const target = document.getElementById('contentToEdit');
3236
jabberwocky.init(target);
@@ -70,6 +74,31 @@ editor.configure(DomEditable, {
7074
autoFocus: true,
7175
source: target,
7276
});
77+
editor.load(Shadow);
78+
const config: JWEditorConfig & { loadables: Loadables<Layout> } = {
79+
loadables: {
80+
components: [
81+
{
82+
id: 'editable',
83+
async render(editor: JWEditor): Promise<VNode[]> {
84+
const shadow = new ShadowNode();
85+
const style = new MetadataNode('STYLE');
86+
style.contents = '* {border: 1px #aaa dotted;}';
87+
shadow.append(style);
88+
const domEditable = editor.plugins.get(DomEditable);
89+
const editableComponent = domEditable.loadables.components.find(
90+
component => component.id === 'editable',
91+
);
92+
const nodes = await editableComponent.render(editor);
93+
shadow.append(...nodes);
94+
return [shadow];
95+
},
96+
},
97+
],
98+
},
99+
};
100+
editor.configure(config);
101+
73102
editor.configure(Toolbar, {
74103
layout: [
75104
[

examples/layout/main.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<table width="100%">
44
<tr>
55
<td width="10%">TD cell</td>
6-
<td width="80%"><t t-zone="main"/></td>
6+
<td width="80%" style="border: 1px solid red;"><t t-zone="main"/></td>
77
<td width="10%">TD cell</td>
88
</tr>
99
</table>

packages/plugin-devtools/src/components/InspectorComponent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { OwlComponent } from '../../../plugin-owl/src/ui/OwlComponent';
55
import { VNode } from '../../../core/src/VNodes/VNode';
66
import { Layout } from '../../../plugin-layout/src/Layout';
77
import { DomLayoutEngine } from '../../../plugin-dom-layout/src/ui/DomLayoutEngine';
8-
import { caretPositionFromPoint } from '../../../utils/polyfill';
8+
import { caretPositionFromPoint } from '../../../utils/src/polyfill';
99
import { CharNode } from '../../../plugin-char/src/CharNode';
1010

1111
const hoverStyle = 'box-shadow: inset 0 0 0 100vh rgba(95, 146, 204, 0.5); cursor: pointer;';

packages/plugin-dom-editable/src/EventNormalizer.ts

Lines changed: 107 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Direction } from '../../core/src/VSelection';
22
import { MutationNormalizer } from './MutationNormalizer';
3-
import { caretPositionFromPoint } from '../../utils/polyfill';
3+
import { caretPositionFromPoint, elementFromPoint } from '../../utils/src/polyfill';
44
import { targetDeepest } from '../../utils/src/Dom';
5-
import { nodeName } from '../../utils/src/utils';
5+
import { nodeName, getDocument } from '../../utils/src/utils';
66

77
const navigationKey = new Set([
88
'ArrowUp',
@@ -221,7 +221,7 @@ interface DataTransferDetails {
221221
type EventToProcess = Event | DataTransferDetails;
222222

223223
interface EventListenerDeclaration {
224-
readonly target: EventTarget;
224+
readonly target: Document | ShadowRoot;
225225
readonly type: string;
226226
readonly listener: EventListener;
227227
}
@@ -457,6 +457,12 @@ export class EventNormalizer {
457457
* manually fire its execution when a navigation key is pressed.
458458
*/
459459
_stackTimeout: Timeout<EventBatch>;
460+
/**
461+
* Map of the shadow dom event normalizer.
462+
* If an event is triggered inside a shadow dom, we instanciate a new
463+
* EventNormalizer in the shadow dom.
464+
*/
465+
private _shadowNormalizers = new Map<ShadowRoot, EventNormalizer>();
460466

461467
/**
462468
*
@@ -466,32 +472,35 @@ export class EventNormalizer {
466472
constructor(
467473
private _isInEditable: (node: Node) => boolean,
468474
private _triggerEventBatch: TriggerEventBatchCallback,
475+
root: Document | ShadowRoot = document,
469476
) {
470477
this.initNextObservation();
471478

472479
// const document = this.editable.ownerDocument;
473-
this._bindEventInEditable(document, 'compositionstart', this._registerEvent);
474-
this._bindEventInEditable(document, 'compositionupdate', this._registerEvent);
475-
this._bindEventInEditable(document, 'compositionend', this._registerEvent);
476-
this._bindEventInEditable(document, 'beforeinput', this._registerEvent);
477-
this._bindEventInEditable(document, 'input', this._registerEvent);
480+
this._bindEventInEditable(root, 'compositionstart', this._registerEvent);
481+
this._bindEventInEditable(root, 'compositionupdate', this._registerEvent);
482+
this._bindEventInEditable(root, 'compositionend', this._registerEvent);
483+
this._bindEventInEditable(root, 'beforeinput', this._registerEvent);
484+
this._bindEventInEditable(root, 'input', this._registerEvent);
478485

479486
this._bindEvent(document, 'selectionchange', this._onSelectionChange);
480-
this._bindEventInEditable(document, 'contextmenu', this._onContextMenu);
481-
this._bindEvent(document, 'mousedown', this._onPointerDown);
482-
this._bindEvent(document, 'touchstart', this._onPointerDown);
483-
this._bindEvent(document, 'mouseup', this._onPointerUp);
484-
this._bindEvent(document, 'touchend', this._onPointerUp);
485-
this._bindEventInEditable(document, 'keydown', this._onKeyDownOrKeyPress);
486-
this._bindEventInEditable(document, 'keypress', this._onKeyDownOrKeyPress);
487-
this._bindEvent(document, 'onkeyup', this._updateModifiersKeys);
488-
489-
this._bindEventInEditable(document, 'cut', this._onClipboard);
490-
this._bindEventInEditable(document, 'paste', this._onClipboard);
491-
this._bindEventInEditable(document, 'dragstart', this._onDragStart);
492-
this._bindEventInEditable(document, 'drop', this._onDrop);
493-
494-
this._mutationNormalizer = new MutationNormalizer();
487+
this._bindEventInEditable(root, 'contextmenu', this._onContextMenu);
488+
this._bindEvent(root, 'mousedown', this._onPointerDown);
489+
this._bindEvent(root, 'touchstart', this._onPointerDown);
490+
this._bindEvent(root, 'mouseup', this._onPointerUp);
491+
this._bindEvent(root, 'touchend', this._onPointerUp);
492+
this._bindEventInEditable(root, 'keydown', this._onKeyDownOrKeyPress);
493+
this._bindEventInEditable(root, 'keypress', this._onKeyDownOrKeyPress);
494+
this._bindEvent(root, 'onkeyup', this._updateModifiersKeys);
495+
496+
this._bindEventInEditable(root, 'cut', this._onClipboard);
497+
this._bindEventInEditable(root, 'paste', this._onClipboard);
498+
this._bindEventInEditable(root, 'dragstart', this._onDragStart);
499+
this._bindEventInEditable(root, 'drop', this._onDrop);
500+
501+
this._mutationNormalizer = new MutationNormalizer(
502+
root instanceof Document ? root.body : root.lastElementChild,
503+
);
495504
}
496505
/**
497506
* Called when destroy the event normalizer.
@@ -501,6 +510,7 @@ export class EventNormalizer {
501510
destroy(): void {
502511
this._mutationNormalizer.destroy();
503512
this._unbindEvents();
513+
this._shadowNormalizers.forEach(eventNormalizer => eventNormalizer.destroy());
504514
}
505515

506516
//--------------------------------------------------------------------------
@@ -516,14 +526,14 @@ export class EventNormalizer {
516526
* @param type of the event to listen
517527
* @param listener to call when the even occurs on the target
518528
*/
519-
_bindEvent(target: EventTarget, type: string, listener: Function): void {
529+
_bindEvent(target: Document | ShadowRoot, type: string, listener: Function): void {
520530
const boundListener = listener.bind(this);
521531
this._eventListeners.push({
522532
target: target,
523533
type: type,
524534
listener: boundListener,
525535
});
526-
target.addEventListener(type, boundListener, false);
536+
target.addEventListener(type, boundListener, true);
527537
}
528538
/**
529539
* Filter event from editable.
@@ -534,9 +544,20 @@ export class EventNormalizer {
534544
* @param type of the event to listen
535545
* @param listener to call when the even occurs on the target
536546
*/
537-
_bindEventInEditable(target: EventTarget, type: string, listener: Function): void {
547+
_bindEventInEditable(target: Document | ShadowRoot, type: string, listener: Function): void {
538548
const boundListener = (ev: EventToProcess): void => {
539-
if (!('target' in ev) || this._isInEditable(ev.target as Node)) {
549+
const eventTarget = 'target' in ev && (ev.target as Element);
550+
const shadowRoot = eventTarget instanceof Element && eventTarget.shadowRoot;
551+
if (shadowRoot) {
552+
if (!this._shadowNormalizers.get(shadowRoot)) {
553+
const eventNormalizer = new EventNormalizer(
554+
this._isInEditable,
555+
this._triggerEventBatch,
556+
shadowRoot,
557+
);
558+
this._shadowNormalizers.set(shadowRoot, eventNormalizer);
559+
}
560+
} else if (!eventTarget || this._isInEditable(eventTarget)) {
540561
listener.call(this, ev);
541562
}
542563
};
@@ -1306,7 +1327,18 @@ export class EventNormalizer {
13061327
*/
13071328
_getSelection(ev?: MouseEvent | TouchEvent): DomSelectionDescription {
13081329
let selectionDescription: DomSelectionDescription;
1309-
const selection = document.getSelection();
1330+
let target: Node;
1331+
let root: Document | ShadowRoot;
1332+
if (ev) {
1333+
target = this._getEventTarget(ev);
1334+
root = getDocument(target);
1335+
} else if (this._initialCaretPosition?.offsetNode) {
1336+
root = getDocument(this._initialCaretPosition.offsetNode);
1337+
} else {
1338+
root = document;
1339+
}
1340+
1341+
const selection = root.getSelection();
13101342

13111343
let forward: boolean;
13121344
if (!selection || selection.rangeCount === 0) {
@@ -1339,8 +1371,7 @@ export class EventNormalizer {
13391371
// contained in the target of the event, otherwise it means it took no
13401372
// part in it. In this case, consider the caret position instead.
13411373
// This can happen when target is an input or a contenteditable=false.
1342-
if (ev && ev.target instanceof Node) {
1343-
const target = ev.target;
1374+
if (target instanceof Node) {
13441375
if (
13451376
!target.contains(selectionDescription.anchorNode) &&
13461377
!target.contains(selectionDescription.focusNode)
@@ -1369,14 +1400,35 @@ export class EventNormalizer {
13691400
if (selection.direction === Direction.BACKWARD) {
13701401
return false;
13711402
}
1403+
13721404
let startContainer = selection.anchorNode;
13731405
let startOffset = selection.anchorOffset;
13741406
let endContainer = selection.focusNode;
13751407
let endOffset = selection.focusOffset;
1376-
const body = document.body;
1408+
13771409
// The selection might still be on a node which has since been removed.
1378-
const invalidStart = !startContainer || !body.contains(startContainer);
1379-
const invalidEnd = !endContainer || !body.contains(endContainer);
1410+
let invalidStart = true;
1411+
let domNode = startContainer;
1412+
while (domNode && invalidStart) {
1413+
if (domNode instanceof ShadowRoot) {
1414+
domNode = domNode.host;
1415+
} else if (document.body.contains(domNode)) {
1416+
invalidStart = false;
1417+
} else {
1418+
domNode = domNode.parentNode;
1419+
}
1420+
}
1421+
let invalidEnd = true;
1422+
domNode = endContainer;
1423+
while (domNode && invalidEnd) {
1424+
if (domNode instanceof ShadowRoot) {
1425+
domNode = domNode.host;
1426+
} else if (document.body.contains(domNode)) {
1427+
invalidEnd = false;
1428+
} else {
1429+
domNode = domNode.parentNode;
1430+
}
1431+
}
13801432
const invalidSelection = invalidStart || invalidEnd;
13811433

13821434
// The selection might be collapsed in which case there is no selection.
@@ -1473,15 +1525,32 @@ export class EventNormalizer {
14731525
}
14741526
return this._isVisible(el.parentNode, editable);
14751527
}
1528+
/**
1529+
* Return the node and offset targeted by a event, including if the target
1530+
* is inside a shadow element
1531+
*
1532+
* @param ev
1533+
*/
14761534
_getEventCaretPosition(ev: MouseEvent | TouchEvent): CaretPosition {
14771535
const x = ev instanceof MouseEvent ? ev.clientX : ev.touches[0].clientX;
14781536
const y = ev instanceof MouseEvent ? ev.clientY : ev.touches[0].clientY;
14791537
let caretPosition = caretPositionFromPoint(x, y);
1480-
if (!this._isInEditable(caretPosition.offsetNode)) {
1538+
if (!caretPosition || !this._isInEditable(caretPosition.offsetNode)) {
14811539
caretPosition = { offsetNode: ev.target as Node, offset: 0 };
14821540
}
14831541
return caretPosition;
14841542
}
1543+
/**
1544+
* Use the position to get the target from the event (including the target
1545+
* in shadow element)
1546+
*
1547+
* @param ev
1548+
*/
1549+
_getEventTarget(ev: MouseEvent | TouchEvent): Node {
1550+
const x = ev instanceof MouseEvent ? ev.clientX : ev.touches[0].clientX;
1551+
const y = ev instanceof MouseEvent ? ev.clientY : ev.touches[0].clientY;
1552+
return elementFromPoint(x, y) || (ev.target as Node);
1553+
}
14851554

14861555
//--------------------------------------------------------------------------
14871556
// Handlers
@@ -1539,7 +1608,8 @@ export class EventNormalizer {
15391608
_onPointerDown(ev: MouseEvent | TouchEvent): void {
15401609
// Don't trigger events on the editable if the click was done outside of
15411610
// the editable itself or on something else than an element.
1542-
if (ev.target instanceof Element && this._isInEditable(ev.target)) {
1611+
const target = this._getEventTarget(ev);
1612+
if (target && this._isInEditable(target)) {
15431613
this._mousedownInEditable = true;
15441614
this._initialCaretPosition = this._getEventCaretPosition(ev);
15451615
this._selectionHasChanged = false;
@@ -1563,7 +1633,6 @@ export class EventNormalizer {
15631633
// to the next tick as well. Store the data we have now before it
15641634
// gets invalidated by the redrawing of the DOM.
15651635
this._initialCaretPosition = this._getEventCaretPosition(ev);
1566-
15671636
this._pointerSelectionTimeout = new Timeout<EventBatch>(() => {
15681637
return this._analyzeSelectionChange(ev);
15691638
});
@@ -1700,10 +1769,8 @@ export class EventNormalizer {
17001769
*/
17011770
_onSelectionChange(): void {
17021771
if (!this._initialCaretPosition) {
1703-
// TODO: Remove this once the renderer only re-renders what has
1704-
// changed rather than re-rendering everything. Right now it is
1705-
// needed to avoid an infinite loop when selectAll triggers a new
1706-
// setRange, which triggers a new selection, which loops infinitely.
1772+
// Filter the events because we can have some Shadow root and each
1773+
// normaliser bind event on document.
17071774
return;
17081775
}
17091776
const keydownEvent = this.currentStackObservation._eventsMap.keydown;
@@ -1730,7 +1797,6 @@ export class EventNormalizer {
17301797
const modifiedKeyEvent = this._modifierKeys.ctrlKey || this._modifierKeys.metaKey;
17311798
const heuristic = modifiedKeyEvent || this._followsPointerAction;
17321799
const isSelectAll = heuristic && this._isSelectAll(this._getSelection());
1733-
17341800
if (isSelectAll && !this._currentlySelectingAll) {
17351801
if (modifiedKeyEvent) {
17361802
// This select all was triggered from the keyboard. Add a

packages/plugin-dom-editable/src/MutationNormalizer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export class MutationNormalizer {
2121
_listen: boolean;
2222
_mutations: MutationRecord[];
2323

24-
constructor() {
24+
constructor(el: Element = document.body) {
2525
this._observer = new MutationObserver(this._onMutation.bind(this));
26-
this._observer.observe(document, {
26+
this._observer.observe(el, {
2727
characterDataOldValue: true, // add old text value on changes
2828
characterData: true, // monitor text content changes
2929
childList: true, // monitor child nodes addition or removal

0 commit comments

Comments
 (0)