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 000000000..52bc98e0d Binary files /dev/null and b/examples/assets/img/s_quotes_carousel_2.jpg differ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + +