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

Commit

Permalink
[IMP] Renderer object, layout to generate the minimum of mutations
Browse files Browse the repository at this point in the history
This renderer returns a structure similar to the dom without creating an element.
The layout create elements or use the available elements already in the document.
This has the advantage:
- optimal diff to carry out the minimum mutation in the dom;
- localization in when rendering;
- add and remove handlers.
  • Loading branch information
Gorash authored and dmo-odoo committed Jun 5, 2020
1 parent 5f82622 commit dba411a
Show file tree
Hide file tree
Showing 57 changed files with 6,104 additions and 1,311 deletions.
5 changes: 2 additions & 3 deletions packages/plugin-char/src/Char.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { JWPlugin, JWPluginConfig } from '../../core/src/JWPlugin';
import { CharNode } from './CharNode';
import { CommandParams } from '../../core/src/Dispatcher';
import { Inline } from '../../plugin-inline/src/Inline';
import { CharFormatHtmlDomRenderer } from './CharFormatHtmlDomRenderer';
import { CharHtmlDomRenderer } from './CharHtmlDomRenderer';
import { CharDomObjectRenderer } from './CharDomObjectRenderer';
import { CharXmlDomParser } from './CharXmlDomParser';
import { Modifiers } from '../../core/src/Modifiers';
import { Loadables } from '../../core/src/JWEditor';
Expand All @@ -20,7 +19,7 @@ export class Char<T extends JWPluginConfig = JWPluginConfig> extends JWPlugin<T>
static dependencies = [Inline];
readonly loadables: Loadables<Parser & Renderer> = {
parsers: [CharXmlDomParser],
renderers: [CharFormatHtmlDomRenderer, CharHtmlDomRenderer],
renderers: [CharDomObjectRenderer],
};
commands = {
insertText: {
Expand Down
64 changes: 48 additions & 16 deletions packages/plugin-char/src/CharDomObjectRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,72 @@
import { AbstractRenderer } from '../../plugin-renderer/src/AbstractRenderer';
import { CharNode } from './CharNode';
import { InlineNode } from '../../plugin-inline/src/InlineNode';
import { HtmlDomRenderingEngine } from '../../plugin-html/src/HtmlDomRenderingEngine';
import { VNode } from '../../core/src/VNodes/VNode';
import { Attributes } from '../../plugin-xml/src/Attributes';
import {
DomObjectRenderingEngine,
DomObject,
DomObjectText,
} from '../../plugin-html/src/DomObjectRenderingEngine';
import { InlineFormatDomObjectRenderer } from '../../plugin-inline/src/InlineFormatDomObjectRenderer';
import { Format } from '../../plugin-inline/src/Format';

export class CharHtmlDomRenderer extends AbstractRenderer<Node[]> {
static id = HtmlDomRenderingEngine.id;
export class CharDomObjectRenderer extends InlineFormatDomObjectRenderer {
static id = DomObjectRenderingEngine.id;
engine: DomObjectRenderingEngine;
predicate = CharNode;

async render(node: CharNode): Promise<Node[]> {
async render(node: CharNode): Promise<DomObject> {
// Consecutive compatible char nodes are rendered as a single text node.
let text = '' + node.char;
const charNodes = [node];
const siblings = node.parent.children();
let index = siblings.indexOf(node) + 1;
let next: VNode;
while ((next = siblings[index]) && node.isSameTextNode(next)) {
charNodes.push(next);
if (next.char === ' ' && text[text.length - 1] === ' ') {
let sibling: VNode;
let index = siblings.indexOf(node) - 1;
while ((sibling = siblings[index]) && node.isSameTextNode(sibling)) {
charNodes.unshift(sibling);
index--;
}
index = siblings.indexOf(node) + 1;
while ((sibling = siblings[index]) && node.isSameTextNode(sibling)) {
charNodes.push(sibling);
index++;
}
let text = '';
for (const charNode of charNodes) {
if (charNode.char === ' ' && text[text.length - 1] === ' ') {
// Browsers don't render consecutive space chars otherwise.
text += '\u00A0';
} else {
text += next.char;
text += charNode.char;
}
index++;
}
// Render block edge spaces as non-breakable space (otherwise browsers
// won't render them).
const previous = node.previousSibling();
if (!previous || !previous.is(InlineNode)) {
text = text.replace(/^ /g, '\u00A0');
}
if (!next || !next.is(InlineNode)) {
if (!sibling || !sibling.is(InlineNode)) {
text = text.replace(/ $/g, '\u00A0');
}
const rendering = Promise.resolve([document.createTextNode(text)]);
return this.engine.rendered(charNodes, [this, rendering]);

const textObject: DomObjectText = { text: text };
this.engine.locate(charNodes, textObject);

// If the node has attributes, wrap it inside a span with those
// attributes.
let domObject: DomObject;
const attributes = node.modifiers.find(Attributes);
if (attributes?.length) {
domObject = {
tag: 'SPAN',
children: [textObject],
};
this.engine.renderAttributes(Attributes, node, domObject);
} else {
domObject = textObject;
}

const rendering = this.renderFormats(node.modifiers.filter(Format), domObject);
return this.engine.rendered(charNodes, this, rendering);
}
}
32 changes: 19 additions & 13 deletions packages/plugin-char/test/CharDomObjectRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { expect } from 'chai';
import JWEditor from '../../core/src/JWEditor';
import { Char } from '../src/Char';
import { CharNode } from '../src/CharNode';
import { Html } from '../../plugin-html/src/Html';
import { Renderer } from '../../plugin-renderer/src/Renderer';
import { ContainerNode } from '../../core/src/VNodes/ContainerNode';
import { DomObject } from '../../plugin-html/src/DomObjectRenderingEngine';
import { DomLayout } from '../../plugin-dom-layout/src/DomLayout';
import { VNode } from '../../core/src/VNodes/VNode';

describe('CharDomRenderer', () => {
describe('CharDomObjectRenderer', () => {
describe('render', () => {
let editor: JWEditor;
beforeEach(async () => {
editor = new JWEditor();
editor.load(Html, { target: document.createElement('p') });
editor.load(DomLayout);
editor.load(Char);
await editor.start();
});
Expand All @@ -20,31 +22,35 @@ describe('CharDomRenderer', () => {
});
it('should insert 1 space and 1 nbsp instead of 2 spaces', async () => {
const root = new ContainerNode();
root.append(new CharNode({ char: 'a' }));
const char = new CharNode({ char: 'a' });
root.append(char);
root.append(new CharNode({ char: ' ' }));
root.append(new CharNode({ char: ' ' }));
root.append(new CharNode({ char: 'b' }));

const renderer = editor.plugins.get(Renderer);
const rendered = await renderer.render<Element[]>('dom/html', root);
if (expect(rendered).to.exist) {
expect(rendered[0].innerHTML).to.equal('a &nbsp;b');
}
const rendered = await renderer.render<DomObject>('dom/object', char);
expect(rendered).to.deep.equal({ text: 'a \u00A0b' });

const locations = renderer.engines['dom/object'].locations as Map<DomObject, VNode[]>;
expect(rendered && locations.get(rendered)).to.deep.equal(root.childVNodes);
});
it('should insert 2 spaces and 2 nbsp instead of 4 spaces', async () => {
const root = new ContainerNode();
root.append(new CharNode({ char: 'a' }));
const char = new CharNode({ char: 'a' });
root.append(char);
root.append(new CharNode({ char: ' ' }));
root.append(new CharNode({ char: ' ' }));
root.append(new CharNode({ char: ' ' }));
root.append(new CharNode({ char: ' ' }));
root.append(new CharNode({ char: 'b' }));

const renderer = editor.plugins.get(Renderer);
const rendered = await renderer.render<Element[]>('dom/html', root);
if (expect(rendered).to.exist) {
expect(rendered[0].innerHTML).to.equal('a &nbsp; &nbsp;b');
}
const rendered = await renderer.render<DomObject>('dom/object', char);
expect(rendered).to.deep.equal({ text: 'a \u00A0 \u00A0b' });

const locations = renderer.engines['dom/object'].locations as Map<DomObject, VNode[]>;
expect(rendered && locations.get(rendered)).to.deep.equal(root.children());
});
});
});
4 changes: 2 additions & 2 deletions packages/plugin-dialog/src/Dialog.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { JWPlugin, JWPluginConfig } from '../../core/src/JWPlugin';
import { Parser } from '../../plugin-parser/src/Parser';
import { Loadables } from '../../core/src/JWEditor';
import { DialogZoneHtmlDomRenderer } from './ui/DialogZoneHtmlDomRenderer';
import { DialogZoneDomObjectRenderer } from './ui/DialogZoneDomObjectRenderer';
import { DialogZoneXmlDomParser } from './ui/DialogZoneXmlDomParser';
import { Renderer } from '../../plugin-renderer/src/Renderer';
import { DomLayout } from '../../plugin-dom-layout/src/DomLayout';
Expand All @@ -10,6 +10,6 @@ export class Dialog<T extends JWPluginConfig = JWPluginConfig> extends JWPlugin<
static dependencies = [Parser, Renderer, DomLayout];
readonly loadables: Loadables<Parser & Renderer> = {
parsers: [DialogZoneXmlDomParser],
renderers: [DialogZoneHtmlDomRenderer],
renderers: [DialogZoneDomObjectRenderer],
};
}
32 changes: 17 additions & 15 deletions packages/plugin-dialog/src/ui/DialogZoneDomObjectRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,55 @@
import { HtmlDomRenderingEngine } from '../../../plugin-html/src/HtmlDomRenderingEngine';
import {
DomObjectRenderingEngine,
DomObject,
} from '../../../plugin-html/src/DomObjectRenderingEngine';
import { AbstractRenderer } from '../../../plugin-renderer/src/AbstractRenderer';
import { DialogZoneNode } from './DialogZoneNode';
import { VNode } from '../../../core/src/VNodes/VNode';
import { ComponentId } from '../../../plugin-layout/src/LayoutEngine';
import { Layout } from '../../../plugin-layout/src/Layout';

import template from './Dialog.xml';
import './Dialog.css';
import { Layout } from '../../../plugin-layout/src/Layout';
import { ComponentId } from '../../../plugin-layout/src/LayoutEngine';
import { MetadataNode } from '../../../plugin-metadata/src/MetadataNode';

const container = document.createElement('jw-container');
container.innerHTML = template;
const dialog = container.firstElementChild;

export class DialogZoneHtmlDomRenderer extends AbstractRenderer<Node[]> {
static id = HtmlDomRenderingEngine.id;
engine: HtmlDomRenderingEngine;
export class DialogZoneDomObjectRenderer extends AbstractRenderer<DomObject> {
static id = DomObjectRenderingEngine.id;
engine: DomObjectRenderingEngine;
predicate = DialogZoneNode;

async render(node: DialogZoneNode): Promise<Node[]> {
async render(node: DialogZoneNode): Promise<DomObject> {
const float = document.createElement('jw-dialog-container');
for (const child of node.childVNodes) {
if (!node.hidden.get(child)) {
if (!node.hidden.get(child) && (child.tangible || child.is(MetadataNode))) {
float.appendChild(await this._renderDialog(child));
}
}
return float.childNodes.length ? [float] : [document.createDocumentFragment()];
return {
dom: float.childNodes.length ? [float] : [],
};
}

private async _renderDialog(node: VNode): Promise<Element> {
const clone = dialog.cloneNode(true) as Element;
const content = clone.querySelector('jw-content');
for (const domNode of await this.engine.render(node)) {
content.appendChild(domNode);
}

content.appendChild(this.engine.renderPlaceholder(node));
let componentId: ComponentId;
const components = this.engine.editor.plugins.get(Layout).engines.dom.components;
for (const [id, nodes] of components) {
if (nodes.includes(node)) {
componentId = id;
}
}

clone.addEventListener('click', (ev: MouseEvent): void => {
const target = ev.target as Element;
if (target.classList.contains('jw-close')) {
this.engine.editor.execCommand('hide', { componentID: componentId });
}
});

return clone;
}
}
4 changes: 4 additions & 0 deletions packages/plugin-dom-editable/src/DomEditable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ export class DomEditable<T extends DomEditableConfig = DomEditableConfig> extend
return this.editor.nextEventMutex(
async (): Promise<void> => {
const batch = await batchPromise;
const domEngine = this.dependencies.get(Layout).engines.dom as DomLayoutEngine;
if (batch.mutatedElements) {
domEngine.markForRedraw(batch.mutatedElements);
}
let processed = false;
if (batch.inferredKeydownEvent) {
const domLayout = this.dependencies.get(DomLayout);
Expand Down
6 changes: 3 additions & 3 deletions packages/plugin-dom-layout/src/DomLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
ComponentId,
} from '../../plugin-layout/src/LayoutEngine';
import { Html } from '../../plugin-html/src/Html';
import { ZoneHtmlDomRenderer } from './ui/ZoneHtmlDomRenderer';
import { ZoneDomObjectRenderer } from './ui/ZoneDomObjectRenderer';
import { ZoneXmlDomParser } from './ui/ZoneXmlDomParser';
import { LayoutContainerHtmlDomRenderer } from './ui/LayoutContainerHtmlDomRenderer';
import { LayoutContainerDomObjectRenderer } from './ui/LayoutContainerDomObjectRenderer';
import { ZoneIdentifier } from '../../plugin-layout/src/ZoneNode';
import { Keymap } from '../../plugin-keymap/src/Keymap';
import { CommandIdentifier } from '../../core/src/Dispatcher';
Expand All @@ -27,7 +27,7 @@ export interface DomLayoutConfig extends JWPluginConfig {
export class DomLayout<T extends DomLayoutConfig = DomLayoutConfig> extends JWPlugin<T> {
static dependencies = [Html, Parser, Renderer, Layout, Keymap];
readonly loadables: Loadables<Parser & Renderer & Layout> = {
renderers: [ZoneHtmlDomRenderer, LayoutContainerHtmlDomRenderer],
renderers: [ZoneDomObjectRenderer, LayoutContainerDomObjectRenderer],
parsers: [ZoneXmlDomParser],
layoutEngines: [],
components: [],
Expand Down
Loading

0 comments on commit dba411a

Please sign in to comment.