From 1fadef6401b2ce959a2cb1e3f0762ddd42588181 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Fri, 5 Jun 2020 14:38:59 +0200 Subject: [PATCH] integration --- .gitignore | 1 + bundles/OdooWebsiteEditor.ts | 238 ++++++++++++ dev/odoo-integration-dev.js | 14 + doc/odoo_integration_workflow.md | 30 ++ examples/assets/img/s_quotes_carousel_2.jpg | Bin 0 -> 11505 bytes examples/demo/demo.css | 4 + examples/demo/demo.xml | 7 +- examples/utils/jabberwocky.xml | 2 +- package.json | 5 +- .../odoo-integration.ts | 22 ++ packages/core/src/Dispatcher.ts | 4 +- packages/core/src/JWEditor.ts | 12 +- packages/core/src/VNodes/VElement.ts | 6 +- packages/plugin-char/src/Char.ts | 60 ++- packages/plugin-char/test/Char.test.ts | 10 +- packages/plugin-devtools/assets/DevTools.css | 19 +- .../plugin-devtools/test/devtools.test.ts | 1 + .../src/EventNormalizer.ts | 23 +- packages/plugin-dom-layout/src/DomLayout.ts | 19 +- .../src/ui/DomLayoutEngine.ts | 1 + packages/plugin-html/src/Html.ts | 2 + packages/plugin-html/src/HtmlNode.ts | 13 + .../plugin-html/src/HtmlNodeDomRenderer.ts | 18 + .../src/ImageDomObjectRenderer.ts | 21 ++ packages/plugin-link/src/Link.ts | 21 +- packages/plugin-link/src/LinkFormat.ts | 2 +- .../plugin-media-dialog/assets/Toolbar.css | 94 +++++ .../plugin-media-dialog/assets/Toolbar.xml | 53 +++ .../plugin-media-dialog/src/MediaDialog.ts | 24 ++ .../plugin-odoo-snippets/src/LinkButton.ts | 5 + .../plugin-odoo-snippets/src/MediaButton.ts | 5 + .../plugin-odoo-snippets/src/OdooBindings.ts | 164 +++++++++ .../src/OdooImageHtmlDomRenderer.ts | 22 ++ .../plugin-odoo-snippets/src/OdooSnippet.ts | 341 ++++++++++++++++++ .../src/OdooStructureNode.ts | 16 + .../src/OdooStructureXmlDomParser.ts | 37 ++ .../plugin-odoo-snippets/src/SaveButton.ts | 5 + .../src/OdooVideoDomObjectRenderer.ts | 25 ++ packages/plugin-toolbar/assets/Toolbar.css | 5 + packages/plugin-toolbar/assets/Toolbar.xml | 2 +- packages/plugin-toolbar/src/Toolbar.ts | 11 - webpack-base.config.js | 140 +++++++ webpack-examples.config.js | 1 + webpack-odoo.config.js | 1 + webpack.config.js | 79 ---- 45 files changed, 1443 insertions(+), 142 deletions(-) create mode 100644 bundles/OdooWebsiteEditor.ts create mode 100644 dev/odoo-integration-dev.js create mode 100644 doc/odoo_integration_workflow.md create mode 100644 examples/assets/img/s_quotes_carousel_2.jpg create mode 100644 packages/build-odoo-integration/odoo-integration.ts create mode 100644 packages/plugin-html/src/HtmlNode.ts create mode 100644 packages/plugin-html/src/HtmlNodeDomRenderer.ts create mode 100644 packages/plugin-media-dialog/assets/Toolbar.css create mode 100644 packages/plugin-media-dialog/assets/Toolbar.xml create mode 100644 packages/plugin-media-dialog/src/MediaDialog.ts create mode 100644 packages/plugin-odoo-snippets/src/LinkButton.ts create mode 100644 packages/plugin-odoo-snippets/src/MediaButton.ts create mode 100644 packages/plugin-odoo-snippets/src/OdooBindings.ts create mode 100644 packages/plugin-odoo-snippets/src/OdooImageHtmlDomRenderer.ts create mode 100644 packages/plugin-odoo-snippets/src/OdooSnippet.ts create mode 100644 packages/plugin-odoo-snippets/src/OdooStructureNode.ts create mode 100644 packages/plugin-odoo-snippets/src/OdooStructureXmlDomParser.ts create mode 100644 packages/plugin-odoo-snippets/src/SaveButton.ts create mode 100644 webpack-base.config.js create mode 100644 webpack-examples.config.js create mode 100644 webpack-odoo.config.js delete mode 100644 webpack.config.js diff --git a/.gitignore b/.gitignore index cacd6356b..a55219300 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ ehthumbs.db Thumbs.db data/ build/ +!src/build/ package-lock.json coverage/ diff --git a/bundles/OdooWebsiteEditor.ts b/bundles/OdooWebsiteEditor.ts new file mode 100644 index 000000000..e164e6b9e --- /dev/null +++ b/bundles/OdooWebsiteEditor.ts @@ -0,0 +1,238 @@ +import JWEditor from '../packages/core/src/JWEditor'; +import { Parser } from '../packages/plugin-parser/src/Parser'; +import { Html } from '../packages/plugin-html/src/Html'; +import { Char } from '../packages/plugin-char/src/Char'; +import { LineBreak } from '../packages/plugin-linebreak/src/LineBreak'; +import { Heading } from '../packages/plugin-heading/src/Heading'; +import { Paragraph } from '../packages/plugin-paragraph/src/Paragraph'; +import { List } from '../packages/plugin-list/src/List'; +import { Indent } from '../packages/plugin-indent/src/Indent'; +import { ParagraphNode } from '../packages/plugin-paragraph/src/ParagraphNode'; +import { LineBreakNode } from '../packages/plugin-linebreak/src/LineBreakNode'; +import { Span } from '../packages/plugin-span/src/Span'; +import { Bold } from '../packages/plugin-bold/src/Bold'; +import { Italic } from '../packages/plugin-italic/src/Italic'; +import { Underline } from '../packages/plugin-underline/src/Underline'; +import { Inline } from '../packages/plugin-inline/src/Inline'; +import { Link } from '../packages/plugin-link/src/Link'; +import { Divider } from '../packages/plugin-divider/src/Divider'; +import { Image } from '../packages/plugin-image/src/Image'; +import { Subscript } from '../packages/plugin-subscript/src/Subscript'; +import { Superscript } from '../packages/plugin-superscript/src/Superscript'; +import { Blockquote } from '../packages/plugin-blockquote/src/Blockquote'; +import { Youtube } from '../packages/plugin-youtube/src/Youtube'; +import { Table } from '../packages/plugin-table/src/Table'; +import { Metadata } from '../packages/plugin-metadata/src/Metadata'; +import { Renderer } from '../packages/plugin-renderer/src/Renderer'; +import { Keymap } from '../packages/plugin-keymap/src/Keymap'; +import { Align } from '../packages/plugin-align/src/Align'; +import { Pre } from '../packages/plugin-pre/src/Pre'; +import { TextColor } from '../packages/plugin-textcolor/src/TextColor'; +import { BackgroundColor } from '../packages/plugin-backgroundcolor/src/BackgroundColor'; +import { Layout } from '../packages/plugin-layout/src/Layout'; +import { DomLayout } from '../packages/plugin-dom-layout/src/DomLayout'; +import { DomEditable } from '../packages/plugin-dom-editable/src/DomEditable'; +import { VNode } from '../packages/core/src/VNodes/VNode'; + +import './basicLayout.css'; +import { OdooSnippet } from '../packages/plugin-odoo-snippets/src/OdooSnippet'; + +import { Toolbar } from '../packages/plugin-toolbar/src/Toolbar'; +import { ParagraphButton } from '../packages/plugin-heading/src/HeadingButtons'; +import { Heading1Button } from '../packages/plugin-heading/src/HeadingButtons'; +import { Heading2Button } from '../packages/plugin-heading/src/HeadingButtons'; +import { Heading3Button } from '../packages/plugin-heading/src/HeadingButtons'; +import { Heading4Button } from '../packages/plugin-heading/src/HeadingButtons'; +import { Heading5Button } from '../packages/plugin-heading/src/HeadingButtons'; +import { Heading6Button } from '../packages/plugin-heading/src/HeadingButtons'; +import { PreButton } from '../packages/plugin-pre/src/PreButtons'; +import { BoldButton } from '../packages/plugin-bold/src/BoldButtons'; +import { ItalicButton } from '../packages/plugin-italic/src/ItalicButtons'; +import { UnderlineButton } from '../packages/plugin-underline/src/UnderlineButtons'; +import { OrderedListButton } from '../packages/plugin-list/src/ListButtons'; +import { UnorderedListButton } from '../packages/plugin-list/src/ListButtons'; +import { IndentButton } from '../packages/plugin-indent/src/IndentButtons'; +import { OutdentButton } from '../packages/plugin-indent/src/IndentButtons'; +import { SaveButton } from '../packages/plugin-odoo-snippets/src/SaveButton'; +import { HtmlNode } from '../packages/plugin-html/src/HtmlNode'; +import { MediaButton } from '../packages/plugin-odoo-snippets/src/MediaButton'; +import { CommandImplementation, CommandIdentifier } from '../packages/core/src/Dispatcher'; +import { JWPlugin } from '../packages/core/src/JWPlugin'; +import { OdooVideo } from '../packages/plugin-odoo-video/src/OdooVideo'; +import { LinkButton } from '../packages/plugin-odoo-snippets/src/LinkButton'; +import { DomZonePosition } from '../packages/plugin-layout/src/LayoutEngine'; +import { HtmlDomRenderingEngine } from '../packages/plugin-html/src/HtmlDomRenderingEngine'; +import { + AlignLeftButton, + AlignCenterButton, + AlignRightButton, + AlignJustifyButton, +} from '../packages/plugin-align/src/AlignButtons'; + +interface OdooWebsiteEditorOption { + source: HTMLElement; + location: [Node, DomZonePosition]; + customCommands: Record; + afterRender?: Function; + snippetMenuElement?: HTMLElement; + snippetManipulators?: HTMLElement; + template?: string; + // todo: Remove when configuring the toolbar in another way. + saveButton?: boolean; +} + +export class OdooWebsiteEditor extends JWEditor { + constructor(options: OdooWebsiteEditorOption) { + super(); + class CustomPlugin extends JWPlugin { + commands = options.customCommands; + } + + this.configure({ + defaults: { + Container: ParagraphNode, + Separator: LineBreakNode, + }, + plugins: [ + [Parser], + [Renderer], + [Layout], + [Keymap], + [Html], + [Inline], + [Char], + [LineBreak], + [Heading], + [Paragraph], + [List], + [Indent], + [Span], + [Bold], + [Italic], + [Underline], + [Link], + [Divider], + [Image], + [Subscript], + [Superscript], + [Blockquote], + [Youtube], + [Table], + [Metadata], + [Align], + [Pre], + [TextColor], + [BackgroundColor], + [OdooSnippet], + [OdooVideo], + [CustomPlugin], + ], + }); + this.configure(Toolbar, { + layout: [ + [ + [ + ParagraphButton, + Heading1Button, + Heading2Button, + Heading3Button, + Heading4Button, + Heading5Button, + Heading6Button, + PreButton, + ], + ], + [BoldButton, ItalicButton, UnderlineButton], + [AlignLeftButton, AlignCenterButton, AlignRightButton, AlignJustifyButton], + [OrderedListButton, UnorderedListButton], + [IndentButton, OutdentButton], + [LinkButton], + [MediaButton], + ...(options.saveButton ? [[SaveButton]] : []), + ], + }); + + const defaultTemplate = ` + + +
+
+ +
+
+ +
+
+ + +
+
+
+
+ +
+
+ `; + this.configure(DomLayout, { + components: [ + { + id: 'main_template', + render(editor: JWEditor): Promise { + return editor.plugins + .get(Parser) + .parse('text/html', options.template || defaultTemplate); + }, + }, + { + id: 'snippet_menu', + render(): Promise { + const node: VNode = options.snippetMenuElement + ? new HtmlNode({ domNode: options.snippetMenuElement }) + : new LineBreakNode(); + return Promise.resolve([node]); + }, + }, + { + id: 'snippetManipulators', + render(): Promise { + const node: VNode = options.snippetMenuElement + ? new HtmlNode({ domNode: options.snippetManipulators }) + : new LineBreakNode(); + return Promise.resolve([node]); + }, + }, + ], + componentZones: [ + ['main_template', 'root'], + ['snippet_menu', 'main_sidebar'], + ['snippetManipulators', 'snippetManipulators'], + ], + location: options.location, + afterRender: options.afterRender, + }); + this.configure(DomEditable, { + autoFocus: true, + source: options.source.firstElementChild as HTMLElement, + }); + } + + /** + * Get the value by rendering the "editable" component of the editor. + */ + async getValue(): Promise { + const renderer = this.plugins.get(Renderer); + const layout = this.plugins.get(Layout); + const domLayout = layout.engines.dom; + const domRenderingEngine = renderer.engines[ + HtmlDomRenderingEngine.id + ] as HtmlDomRenderingEngine; + const editable = domLayout.components.get('editable')[0]; + const nodes = await domRenderingEngine.render(editable); + return nodes[0]; + } + + async render(): Promise { + const domLayout = this.plugins.get(DomLayout); + return domLayout.redraw(); + } +} diff --git a/dev/odoo-integration-dev.js b/dev/odoo-integration-dev.js new file mode 100644 index 000000000..03627aadb --- /dev/null +++ b/dev/odoo-integration-dev.js @@ -0,0 +1,14 @@ +// todo: replace this file with the actual code of the lib. +odoo.define('web_editor.jabberwock', function(require) { + 'use strict'; + + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = 'http://localhost:8095/odoo-integration.js'; + document.getElementsByTagName('head')[0].appendChild(script); + return new Promise(resolve => { + script.onload = () => { + resolve(JWEditor); + }; + }); +}); diff --git a/doc/odoo_integration_workflow.md b/doc/odoo_integration_workflow.md new file mode 100644 index 000000000..3f274f45f --- /dev/null +++ b/doc/odoo_integration_workflow.md @@ -0,0 +1,30 @@ +# Odoo integration + +To develop Jabberwock in Odoo, follow these steps: +1) Use the dev mode for the live reloading feature of Webpack. (optional) +2) Build the source and include it in Odoo. + +## 1) Use the dev mode for the live reloading feature of Webpack. + +Temporarily replace the library with the following script. +```bash +cp dev/odoo-integration-dev.js /addons/web_editor/static/lib/jabberwock/jabberwock.js +``` +`odoo-integration-dev.js` will load the script `build-full.js`. +The default loaded script is `http://localhost:8095/odoo-integration.js`. +You might want to change the port if your development port is not "8095". + +Launch the development server (on port 8095): +```bash +npm run dev -- --port 8095 +``` + +Once finished developing, rebuild the source and put it back in Odoo. + +## 2) Build the source and include it in Odoo. + +```bash +npm run build +npm run build-odoo +cp build/webpack/build/odoo-integration.js /addons/web_editor/static/lib/jabberwock/jabberwock.js +``` diff --git a/examples/assets/img/s_quotes_carousel_2.jpg b/examples/assets/img/s_quotes_carousel_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..52bc98e0d3aa7a19a5dc2f5d6935414fb8a9fbcd GIT binary patch literal 11505 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 literal 0 HcmV?d00001 diff --git a/examples/demo/demo.css b/examples/demo/demo.css index 7812b0241..2a2d11d05 100644 --- a/examples/demo/demo.css +++ b/examples/demo/demo.css @@ -29,3 +29,7 @@ jw-editor table.mondrian { width: 65vh; height: 50vh; } + +.jw_selected_image { + outline: 1px red solid; +} diff --git a/examples/demo/demo.xml b/examples/demo/demo.xml index a7eb03a9b..05114541f 100644 --- a/examples/demo/demo.xml +++ b/examples/demo/demo.xml @@ -1,7 +1,12 @@ + +   + +

doo Jabberwck Dem

-

+ An image:hehe +

"Jabberwocky" is a nonsense poem written by Lewis Carroll about the killing of a creature named "the Jabberwock". It was included in his 1871 novel Through the Looking-Glass, and What Alice Found There, the sequel to diff --git a/examples/utils/jabberwocky.xml b/examples/utils/jabberwocky.xml index 3afe0500b..902f3bac0 100644 --- a/examples/utils/jabberwocky.xml +++ b/examples/utils/jabberwocky.xml @@ -33,4 +33,4 @@ He chortled in his joy.
’Twas brillig, and the slithy toves
Did gyre and gimble in the wabe:
All mimsy were the borogoves,
-And the mome raths outgrabe.

\ No newline at end of file +And the mome raths outgrabe.

diff --git a/package.json b/package.json index d9ba8297a..d260d3a67 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "The best editor in the world", "main": "src/index.ts", "scripts": { - "dev": "webpack-dev-server", - "build": "rm -rf build/examples; mkdir -p build/examples/; cp -r ./examples ./build; webpack", + "dev": "webpack-dev-server --config webpack-examples.config.js", + "build": "rm -rf build/examples; mkdir -p build/examples/; cp -r ./examples ./build; webpack --config webpack-examples.config.js", + "build-odoo": "rm -rf build/examples; mkdir -p build/examples/; cp -r ./examples ./build; webpack --config webpack-odoo.config.js", "perf": "karma start --include-files test/**/*.perf.ts", "coverage": "karma start --coverage", "debug": "karma start --no-browsers --debug", diff --git a/packages/build-odoo-integration/odoo-integration.ts b/packages/build-odoo-integration/odoo-integration.ts new file mode 100644 index 000000000..f23bac222 --- /dev/null +++ b/packages/build-odoo-integration/odoo-integration.ts @@ -0,0 +1,22 @@ +import { BasicEditor } from './../../bundles/BasicEditor'; +import { DevTools } from '../plugin-devtools/src/DevTools'; +import { OdooWebsiteEditor } from '../../bundles/OdooWebsiteEditor'; +import { VRange, withRange } from '../core/src/VRange'; +import { DomLayoutEngine } from '../plugin-dom-layout/src/ui/DomLayoutEngine'; +import { Layout } from '../plugin-layout/src/Layout'; +import { Renderer } from '../plugin-renderer/src/Renderer'; +import { ImageNode } from '../plugin-image/src/ImageNode'; +import { createExecCommandHelpersForOdoo } from '../plugin-odoo-snippets/src/OdooBindings'; + +export { + OdooWebsiteEditor, + BasicEditor, + DevTools, + Layout, + DomLayoutEngine, + Renderer, + ImageNode, + withRange, + VRange, + createExecCommandHelpersForOdoo, +}; diff --git a/packages/core/src/Dispatcher.ts b/packages/core/src/Dispatcher.ts index aa5a29669..fea041e8d 100644 --- a/packages/core/src/Dispatcher.ts +++ b/packages/core/src/Dispatcher.ts @@ -46,9 +46,11 @@ export class Dispatcher { const args = { ...params, context }; // Call command handler. - await command.handler(args); + const result = await command.handler(args); await this._dispatchHooks(commandId, args); + + return result; } } diff --git a/packages/core/src/JWEditor.ts b/packages/core/src/JWEditor.ts index 6854491b2..b38089b71 100644 --- a/packages/core/src/JWEditor.ts +++ b/packages/core/src/JWEditor.ts @@ -60,6 +60,10 @@ export class JWEditor { selection = new VSelection(); loaders: Record = {}; private mutex = Promise.resolve(); + // Use a set so that when asynchronous functions are called we ensure that + // each command batch is waited for. + preventRenders: Set = new Set(); + enableRender = true; constructor() { this.dispatcher = new Dispatcher(this); @@ -267,6 +271,12 @@ export class JWEditor { } } + async execBatch(callback: () => Promise): Promise { + this.preventRenders.add(callback); + await callback(); + this.preventRenders.delete(callback); + } + /** * Execute the given command. * @@ -277,7 +287,7 @@ export class JWEditor { commandName: C, params?: CommandParams, ): Promise { - await this.dispatcher.dispatch(commandName, params); + return await this.dispatcher.dispatch(commandName, params); } /** diff --git a/packages/core/src/VNodes/VElement.ts b/packages/core/src/VNodes/VElement.ts index 8f1f4f4f0..3095f18af 100644 --- a/packages/core/src/VNodes/VElement.ts +++ b/packages/core/src/VNodes/VElement.ts @@ -1,8 +1,12 @@ import { ContainerNode } from './ContainerNode'; +export interface VElementParams { + htmlTag: string; +} + export class VElement extends ContainerNode { htmlTag: string; - constructor(params: { htmlTag: string }) { + constructor(params: VElementParams) { super(); this.htmlTag = params.htmlTag; } diff --git a/packages/plugin-char/src/Char.ts b/packages/plugin-char/src/Char.ts index 4cf974957..e111fada7 100644 --- a/packages/plugin-char/src/Char.ts +++ b/packages/plugin-char/src/Char.ts @@ -10,10 +10,17 @@ import { Parser } from '../../plugin-parser/src/Parser'; import { Renderer } from '../../plugin-renderer/src/Renderer'; import { Attributes } from '../../plugin-xml/src/Attributes'; +import { Point, RelativePosition, VNode } from '../../core/src/VNodes/VNode'; + export interface InsertTextParams extends CommandParams { text: string; + select?: boolean; formats?: Modifiers; } +export interface InsertHtmlParams extends CommandParams { + rangePoint?: Point; + html: string; +} export class Char extends JWPlugin { static dependencies = [Inline]; @@ -25,6 +32,9 @@ export class Char extends JWPlugin insertText: { handler: this.insertText, }, + insertHtml: { + handler: this.insertHtml, + }, }; //-------------------------------------------------------------------------- @@ -57,11 +67,53 @@ export class Char extends JWPlugin } // Split the text into CHAR nodes and insert them at the range. const characters = text.split(''); - characters.forEach(char => { - const vNode = new CharNode({ char: char, modifiers: modifiers.clone() }); - vNode.modifiers.get(Attributes).style = style; - range.start.before(vNode); + const charNodes = characters.map(char => { + return new CharNode({ char: char, modifiers: modifiers.clone() }); + }); + charNodes.forEach(charNode => { + charNode.modifiers.get(Attributes).style = style; + range.start.before(charNode); }); + if (params.select && charNodes.length) { + this.editor.selection.select(charNodes[0], charNodes[charNodes.length - 1]); + } inline.resetCache(); } + async insertHtml(params: InsertHtmlParams): Promise { + const parser = this.editor.plugins.get(Parser); + const domParser = parser && parser.engines['dom/html']; + if (!domParser) { + // TODO: remove this when the editor can be instantiated on + // something else than DOM. + throw new Error(`No DOM parser installed.`); + } + const div = document.createElement('div'); + div.innerHTML = params.html; + const parsedEditable = await domParser.parse(div); + const newNodes = parsedEditable[0].children(); + + // Remove the contents of the range if needed. + // todo: use Point or Range but not both. + const range = params.context.range; + if (!range.isCollapsed()) { + range.empty(); + } + if (params.rangePoint) { + const [node, position] = params.rangePoint; + switch (position) { + case RelativePosition.BEFORE: + newNodes.forEach(node.before.bind(node)); + break; + case RelativePosition.AFTER: + [...newNodes].reverse().forEach(node.after.bind(node)); + break; + case RelativePosition.INSIDE: + node.append(...newNodes); + break; + } + } else { + newNodes.forEach(range.start.before.bind(range.start)); + } + return newNodes; + } } diff --git a/packages/plugin-char/test/Char.test.ts b/packages/plugin-char/test/Char.test.ts index ebfba7175..e40540158 100644 --- a/packages/plugin-char/test/Char.test.ts +++ b/packages/plugin-char/test/Char.test.ts @@ -15,9 +15,10 @@ import { UnderlineFormat } from '../../plugin-underline/src/UnderlineFormat'; import { Modifiers } from '../../core/src/Modifiers'; import { AtomicNode } from '../../core/src/VNodes/AtomicNode'; -const insertText = async function(editor: JWEditor, text: string): Promise { +const insertText = async function(editor: JWEditor, text: string, select = false): Promise { await editor.execCommand('insertText', { text: text, + select, }); }; const toggleFormat = async (editor: JWEditor, FormatClass: Constructor): Promise => { @@ -157,6 +158,13 @@ describePlugin(Char, testEditor => { contentAfter: '

ab[]c

', }); }); + it('should insert char in text in a paragraph and select it', async () => { + await testEditor(BasicEditor, { + contentBefore: '

a[]c

', + stepFunction: (editor: JWEditor) => insertText(editor, 'b', true), + contentAfter: '

a[b]c

', + }); + }); }); describe('bold', () => { describe('Selection collapsed', () => { diff --git a/packages/plugin-devtools/assets/DevTools.css b/packages/plugin-devtools/assets/DevTools.css index 64a625adc..513f15f67 100644 --- a/packages/plugin-devtools/assets/DevTools.css +++ b/packages/plugin-devtools/assets/DevTools.css @@ -1,12 +1,9 @@ /* GLOBAL */ jw-devtools { - position: fixed; bottom: 0; min-height: 30px; max-height: 100%; - left: 0; - right: 0; top: auto; background-color: white; border-top: 1px solid #d0d0d0; @@ -83,7 +80,8 @@ jw-devtools .marker-node { } devtools-panel { - height: 100%; + display: flex; + overflow: auto; } devtools-panel:not(.active) { @@ -144,7 +142,6 @@ devtools-navbar > devtools-button { padding: 0 10px 0 10px; background: none; border: 0; - height: 100%; vertical-align: middle; outline: none; cursor: auto; @@ -163,8 +160,9 @@ devtools-navbar > devtools-button.selected { devtools-contents { display: flex; flex-direction: row; + flex: 1; + overflow: auto; position: relative; - height: 100%; font-family: 'Courier New', Courier, monospace; } @@ -175,10 +173,11 @@ devtools-mainpane { overflow: auto; flex-direction: column; width: 100%; - margin-bottom: 50px; } mainpane-contents { + overflow: auto; + flex: 1; padding: 1em; } @@ -252,7 +251,6 @@ devtools-path { display: block; border-top: 1px solid #d0d0d0; background-color: #dddddd; - position: fixed; bottom: 0; left: 0; right: 0; @@ -277,14 +275,13 @@ devtools-pathnode.selected { devtools-sidepane { position: relative; + overflow: auto; display: block; font-size: 15px; box-sizing: border-box; width: 30%; - height: 100%; background-color: white; border-left: 1px solid #d0d0d0; - height: 100%; } devtools-sidepane devtools-about { @@ -303,7 +300,6 @@ devtools-sidepane devtools-about devtools-type { devtools-sidepane devtools-properties { font-size: 12px; padding: 10px; - height: 100%; overflow: auto; margin-bottom: 30px; } @@ -353,7 +349,6 @@ devtools-sidepane devtools-properties > devtools-table > devtools-tbody > devtoo devtools-info { display: flex; flex-direction: column; - height: 100%; } devtools-info devtools-about devtools-id { diff --git a/packages/plugin-devtools/test/devtools.test.ts b/packages/plugin-devtools/test/devtools.test.ts index 890b0268e..bbe1f21e5 100644 --- a/packages/plugin-devtools/test/devtools.test.ts +++ b/packages/plugin-devtools/test/devtools.test.ts @@ -848,6 +848,7 @@ describe('Plugin: DevTools', () => { 'deleteForward', 'hide', 'insert', + 'insertHtml', 'insertParagraphBreak', 'insertText', 'selectAll', diff --git a/packages/plugin-dom-editable/src/EventNormalizer.ts b/packages/plugin-dom-editable/src/EventNormalizer.ts index 0117140ce..f520e105c 100644 --- a/packages/plugin-dom-editable/src/EventNormalizer.ts +++ b/packages/plugin-dom-editable/src/EventNormalizer.ts @@ -1630,15 +1630,20 @@ export class EventNormalizer { // Don't trigger events on the editable if the click was done outside of // the editable itself or on something else than an element. if (this._mousedownInEditable && ev.target instanceof Element) { - // When the users clicks in the DOM, the range is set in the next - // tick. The observation of the resulting range must thus be delayed - // to the next tick as well. Store the data we have now before it - // gets invalidated by the redrawing of the DOM. - this._initialCaretPosition = this._getEventCaretPosition(ev); - this._pointerSelectionTimeout = new Timeout(() => { - return this._analyzeSelectionChange(ev); - }); - this._triggerEventBatch(this._pointerSelectionTimeout.promise); + try { + // When the users clicks in the DOM, the range is set in the next + // tick. The observation of the resulting range must thus be delayed + // to the next tick as well. Store the data we have now before it + // gets invalidated by the redrawing of the DOM. + this._initialCaretPosition = this._getEventCaretPosition(ev); + this._pointerSelectionTimeout = new Timeout(() => { + return this._analyzeSelectionChange(ev); + }); + this._triggerEventBatch(this._pointerSelectionTimeout.promise); + } catch (e) { + this._mousedownInEditable = false; + this._initialCaretPosition = undefined; + } } } /** diff --git a/packages/plugin-dom-layout/src/DomLayout.ts b/packages/plugin-dom-layout/src/DomLayout.ts index a28783448..2ab937580 100644 --- a/packages/plugin-dom-layout/src/DomLayout.ts +++ b/packages/plugin-dom-layout/src/DomLayout.ts @@ -22,6 +22,7 @@ export interface DomLayoutConfig extends JWPluginConfig { locations?: [ComponentId, DomLayoutLocation][]; components?: ComponentDefinition[]; componentZones?: [ComponentId, ZoneIdentifier][]; + afterRender?: Function; } export class DomLayout extends JWPlugin { @@ -36,7 +37,7 @@ export class DomLayout extends JWPl domLocations: this._loadComponentLocations, }; commandHooks = { - '*': this._redraw, + '*': this.redraw, }; constructor(editor: JWEditor, configuration: T) { @@ -59,6 +60,9 @@ export class DomLayout extends JWPl this._loadComponentLocations(this.configuration.locations || []); domLayoutEngine.location = this.configuration.location; await domLayoutEngine.start(); + if (this.configuration.afterRender) { + await this.configuration.afterRender(); + } window.addEventListener('keydown', this.processKeydown, true); } async stop(): Promise { @@ -103,19 +107,20 @@ export class DomLayout extends JWPl } } - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - private async _redraw(): Promise { + async redraw(): Promise { // TODO update this method to use JSON renderer feature (update also show, hide, add, remove) const layout = this.dependencies.get(Layout); const domLayoutEngine = layout.engines.dom as DomLayoutEngine; const editables = domLayoutEngine.components.get('editable'); if (editables?.length) { - return domLayoutEngine.redraw(editables[0]); + await domLayoutEngine.redraw(editables[0]); + await this.configuration.afterRender?.(); } } + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + private _loadComponentLocations(locations: [ComponentId, DomLayoutLocation][]): void { const layout = this.dependencies.get(Layout); const domLayoutEngine = layout.engines.dom as DomLayoutEngine; diff --git a/packages/plugin-dom-layout/src/ui/DomLayoutEngine.ts b/packages/plugin-dom-layout/src/ui/DomLayoutEngine.ts index ae5084f52..df672a788 100644 --- a/packages/plugin-dom-layout/src/ui/DomLayoutEngine.ts +++ b/packages/plugin-dom-layout/src/ui/DomLayoutEngine.ts @@ -160,6 +160,7 @@ export class DomLayoutEngine extends LayoutEngine { return nodes; } async redraw(...nodes: VNode[]): Promise { + if (this.editor.enableRender && this.editor.preventRenders.size) return; if (this._currentlyRedrawing) { throw new Error('Double redraw detected'); } diff --git a/packages/plugin-html/src/Html.ts b/packages/plugin-html/src/Html.ts index ad73a5cd8..0c1b761fc 100644 --- a/packages/plugin-html/src/Html.ts +++ b/packages/plugin-html/src/Html.ts @@ -7,11 +7,13 @@ import { HtmlDomParsingEngine } from './HtmlDomParsingEngine'; import { Xml } from '../../plugin-xml/src/Xml'; import { DomObjectRenderingEngine } from './DomObjectRenderingEngine'; import { HtmlDomRenderingEngine } from './HtmlDomRenderingEngine'; +import { HtmlHtmlDomRenderer } from './HtmlNodeDomRenderer'; export class Html extends JWPlugin { static dependencies = [Parser, Renderer, Xml]; readonly loadables: Loadables = { parsingEngines: [HtmlDomParsingEngine, HtmlTextParsingEngine], renderingEngines: [DomObjectRenderingEngine, HtmlDomRenderingEngine], + renderers: [HtmlHtmlDomRenderer], }; } diff --git a/packages/plugin-html/src/HtmlNode.ts b/packages/plugin-html/src/HtmlNode.ts new file mode 100644 index 000000000..8bd2cbae5 --- /dev/null +++ b/packages/plugin-html/src/HtmlNode.ts @@ -0,0 +1,13 @@ +import { AtomicNode } from '../../core/src/VNodes/AtomicNode'; + +export interface HtmlNodeParams { + domNode: Node; +} + +export class HtmlNode extends AtomicNode { + domNode: Node; + constructor(params: HtmlNodeParams) { + super(); + this.domNode = params.domNode; + } +} diff --git a/packages/plugin-html/src/HtmlNodeDomRenderer.ts b/packages/plugin-html/src/HtmlNodeDomRenderer.ts new file mode 100644 index 000000000..7108aa18f --- /dev/null +++ b/packages/plugin-html/src/HtmlNodeDomRenderer.ts @@ -0,0 +1,18 @@ +import { AbstractRenderer } from '../../plugin-renderer/src/AbstractRenderer'; +import { HtmlNode } from './HtmlNode'; +import { DomObjectRenderingEngine, DomObject } from './DomObjectRenderingEngine'; + +export class HtmlHtmlDomRenderer extends AbstractRenderer { + static id = DomObjectRenderingEngine.id; + engine: DomObjectRenderingEngine; + predicate = HtmlNode; + + constructor(engine, superRenderer) { + super(engine, superRenderer); + } + + async render(node: HtmlNode): Promise { + const domObject: DomObject = { dom: [node.domNode] }; + return domObject; + } +} diff --git a/packages/plugin-image/src/ImageDomObjectRenderer.ts b/packages/plugin-image/src/ImageDomObjectRenderer.ts index 3bebc4329..962a07e4e 100644 --- a/packages/plugin-image/src/ImageDomObjectRenderer.ts +++ b/packages/plugin-image/src/ImageDomObjectRenderer.ts @@ -12,10 +12,31 @@ export class ImageDomObjectRenderer extends AbstractRenderer { predicate = ImageNode; async render(node: ImageNode): Promise { + const select = (): void => { + this.engine.editor.nextEventMutex(() => { + this.engine.editor.execCustomCommand(async () => { + this.engine.editor.selection.select(node, node); + }); + }); + }; const image: DomObject = { tag: 'IMG', + attach: (el: HTMLElement): void => { + el.addEventListener('click', select); + }, + detach: (el: HTMLElement): void => { + el.removeEventListener('click', select); + }, }; this.engine.renderAttributes(Attributes, node, image); + const isSelected = !!this.engine.editor.selection.range.selectedNodes( + selectedNode => selectedNode === node, + ); + if (isSelected) { + const classlist = (image.attributes?.class || '').split(/\s+/); + classlist.push('jw_selected_image'); + image.attributes.class = classlist.join(''); + } return image; } } diff --git a/packages/plugin-link/src/Link.ts b/packages/plugin-link/src/Link.ts index 0ad60089f..4936ecebc 100644 --- a/packages/plugin-link/src/Link.ts +++ b/packages/plugin-link/src/Link.ts @@ -13,13 +13,17 @@ import { Parser } from '../../plugin-parser/src/Parser'; import { Keymap } from '../../plugin-keymap/src/Keymap'; import { Layout } from '../../plugin-layout/src/Layout'; import linkForm from '../assets/LinkForm.xml'; -import { OwlNode } from '../../plugin-owl/src/ui/OwlNode'; -import { LinkComponent } from './components/LinkComponent'; import { Owl } from '../../plugin-owl/src/Owl'; +import { Attributes } from '../../plugin-xml/src/Attributes'; export interface LinkParams extends CommandParams { label?: string; url?: string; + /** + * The target of an html anchor. + * Could be "_blank", "_self" ,"_parent", "_top" or the framename. + */ + target?: string; } export class Link extends JWPlugin { @@ -55,14 +59,6 @@ export class Link extends JWPlugin commandId: 'unlink', }, ], - components: [ - { - id: 'link', - async render(): Promise { - return [new OwlNode(LinkComponent, {})]; - }, - }, - ], componentZones: [['link', 'float']], owlTemplates: [linkForm], }; @@ -83,9 +79,14 @@ export class Link extends JWPlugin // Otherwise create a link and insert it. const link = new LinkFormat(params.url); + if (params.target) { + link.modifiers.get(Attributes).set('target', params.target); + } return this.editor.execCommand('insertText', { text: params.label || link.url, formats: new Modifiers(link), + select: true, + context: params.context, }); } unlink(params: LinkParams): void { diff --git a/packages/plugin-link/src/LinkFormat.ts b/packages/plugin-link/src/LinkFormat.ts index dcdec0861..65f659508 100644 --- a/packages/plugin-link/src/LinkFormat.ts +++ b/packages/plugin-link/src/LinkFormat.ts @@ -1,7 +1,7 @@ import { Format } from '../../plugin-inline/src/Format'; export class LinkFormat extends Format { - constructor(public url = '#') { + constructor(public url = '#', public target = '') { super('A'); } diff --git a/packages/plugin-media-dialog/assets/Toolbar.css b/packages/plugin-media-dialog/assets/Toolbar.css new file mode 100644 index 000000000..260aabbd9 --- /dev/null +++ b/packages/plugin-media-dialog/assets/Toolbar.css @@ -0,0 +1,94 @@ +jw-toolbar { + display: block; + min-height: 14px; + background-color: #875A7B; + border-bottom: 1px solid #68465f; + color: white; + overflow: hidden; + font-family: "Montserrat", sans-serif; + text-align: center; + width: 100%; +} + +.jw-primary-button { + background-color: #00A09D; + color: white; +} + +jw-toolbar span { + font-weight: 600; + padding: 5px; +} + +jw-toolbar .pressed { + background-color: #9c8897; + color: white; +} + +jw-toolbar jw-separator { + display: inline-block; + background-color: #c1a8ba; + height: 30px; + width: 1px; + margin-left: 2px; + margin-right: 2px; + margin-top: 5px; + margin-bottom: 5px; + vertical-align: bottom; +} + +jw-toolbar toolbar-group jw-separator { + margin-top: 0; + margin-bottom: 0; +} + +jw-toolbar toolbar-group { + display: inline-block; + margin: 5px; +} + +jw-toolbar select, jw-toolbar option { + background-color: white; + border: 1px solid #68465f; + padding: 5px; + height: 30px !important; +} + +jw-toolbar option:disabled { + background-color: #875A7B; + color: white; + font-weight: bold; +} + +jw-toolbar button { + background-color: white; + border: 1px solid #68465f; + padding: 5px; + width: 30px !important; + height: 30px !important; +} + +jw-toolbar .h1 { + font-size: 2em; + font-weight: bold; +} +jw-toolbar .h2 { + font-size: 1.5em; + font-weight: bold; +} +jw-toolbar .h3 { + font-size: 1.17em; + font-weight: bold; +} +jw-toolbar .h4 { + font-size: 1em; + font-weight: bold; +} +jw-toolbar .h5 { + font-size: 0.83em; + font-weight: bold; +} +jw-toolbar .h6 { + font-size: 0.67em; + font-weight: bold; +} diff --git a/packages/plugin-media-dialog/assets/Toolbar.xml b/packages/plugin-media-dialog/assets/Toolbar.xml new file mode 100644 index 000000000..3cb3e1db1 --- /dev/null +++ b/packages/plugin-media-dialog/assets/Toolbar.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + +