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/BasicEditor.ts b/bundles/BasicEditor.ts index 1faf1d9bd..cd2c22db0 100644 --- a/bundles/BasicEditor.ts +++ b/bundles/BasicEditor.ts @@ -56,6 +56,12 @@ export class BasicEditor extends JWEditor { [Html], [DomLayout], [DomEditable], + // [ + // Dom, + // { + // afterRender: (editable: HTMLElement) => console.log('afterRender'), + // }, + // ], [Inline], [Char], [LineBreak], diff --git a/bundles/OdooWebsiteEditor.ts b/bundles/OdooWebsiteEditor.ts new file mode 100644 index 000000000..033049c54 --- /dev/null +++ b/bundles/OdooWebsiteEditor.ts @@ -0,0 +1,234 @@ +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-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], + // [MediaDialog], + [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]; + } +} diff --git a/dev/odoo-integration.js b/dev/odoo-integration.js new file mode 100644 index 000000000..5889f3d10 --- /dev/null +++ b/dev/odoo-integration.js @@ -0,0 +1,13 @@ +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..0f96a4698 --- /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 doc and include it in Odoo. + +## 1) Use the dev mode for the live reloading feature of Webpack. + +Temporarily replace the library by the following script. +```bash +cp dev/odoo-integration.js /addons/web_editor/static/lib/jabberwock/jabberwock.js +``` +`odoo-integration.js` will load the script `build-full.js`. +The default loaded script is `http://localhost:8080/build-full.js`. +You might want to change the port if your development port is not "8080". + +Launch the development server (on port 8080 by default): +```bash +npm run dev +``` + +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/build-full-odoo.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 fa3ce5c3b..83016ce03 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..b418e225f 100644 --- a/examples/utils/jabberwocky.xml +++ b/examples/utils/jabberwocky.xml @@ -1,36 +1,156 @@ -

Jabberwocky

-

by Lewis Carroll

-

’Twas brillig, and the slithy toves
-Did gyre and gimble in the wabe:
-All mimsy were the borogoves,
-And the mome raths outgrabe.
-
-“Beware the Jabberwock, my son!
-The jaws that bite, the claws that catch!
-Beware the Jubjub bird, and shun
-The frumious Bandersnatch!”
-
-He took his vorpal sword in hand;
-Long time the manxome foe he sought—
-So rested he by the Tumtum tree
-And stood awhile in thought.
-
-And, as in uffish thought he stood,
-The Jabberwock, with eyes of flame,
-Came whiffling through the tulgey wood,
-And burbled as it came!
-
-One, two! One, two! And through and through
-The vorpal blade went snicker-snack!
-He left it dead, and with its head
-He went galumphing back.
-
-“And hast thou slain the Jabberwock?
-Come to my arms, my beamish boy!
-O frabjous day! Callooh! Callay!”
-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 + diff --git a/package.json b/package.json index b8de3b216..a9768b3e9 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..ff57cbce6 100644 --- a/packages/core/src/Dispatcher.ts +++ b/packages/core/src/Dispatcher.ts @@ -10,7 +10,7 @@ export interface CommandImplementation extends Contextual { export interface CommandParams { context?: Context; } -export type CommandHandler = (args) => void; +export type CommandHandler = (args) => any; export type CommandHook = (params: CommandParams, commandId: string) => void; export class Dispatcher { @@ -40,16 +40,18 @@ export class Dispatcher { return; } + let result: any; const [command, context] = this.editor.contextManager.match(commands, params.context); if (command) { // Update command arguments with the computed execution context. const args = { ...params, context }; // Call command handler. - await command.handler(args); + 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..bcd5e6a10 100644 --- a/packages/core/src/JWEditor.ts +++ b/packages/core/src/JWEditor.ts @@ -60,6 +60,9 @@ export class JWEditor { selection = new VSelection(); loaders: Record = {}; private mutex = Promise.resolve(); + // use a set so that when async function are called, wait until every batch + // commands finish. + preventRenders: Set = new Set(); constructor() { this.dispatcher = new Dispatcher(this); @@ -267,6 +270,15 @@ export class JWEditor { } } + async execBatch(callback: () => Promise): Promise { + console.log('this.preventRenders:', this.preventRenders); + this.preventRenders.add(callback); + await callback(); + this.preventRenders.delete(callback); + // const dom = this.plugins.get(Dom); + // return dom._renderInEditable(); + } + /** * Execute the given command. * @@ -277,7 +289,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/core/test/JWeditor.test.ts b/packages/core/test/JWeditor.test.ts index 0aaf8bdc7..826699580 100644 --- a/packages/core/test/JWeditor.test.ts +++ b/packages/core/test/JWeditor.test.ts @@ -4,6 +4,7 @@ import { expect } from 'chai'; import { ModeError } from '../../utils/src/errors'; import { testEditor } from '../../utils/src/testUtils'; import { BasicEditor } from '../../../bundles/BasicEditor'; +import { Layout } from '../../plugin-layout/src/Layout'; describe('core', () => { describe('JWEditor', () => { @@ -234,7 +235,10 @@ describe('core', () => { contentBefore: '
ab[]
', stepFunction: editor => { editor.execCustomCommand(async () => { - editor.vDocument.root + const layout = editor.plugins.get(Layout); + const domEngine = layout.engines.dom; + domEngine.components + .get('editable')[0] .children()[0] .children()[0] .remove(); diff --git a/packages/core/test/VDocument.test.ts b/packages/core/test/VDocument.test.ts index 880ad877c..f339473e3 100644 --- a/packages/core/test/VDocument.test.ts +++ b/packages/core/test/VDocument.test.ts @@ -20,7 +20,7 @@ describe('VDocument', () => { describe('deleteForward', () => { describe('Selection collapsed', () => { describe('Basic', () => { - it('should do nothing', async () => { + it.only('should do nothing', async () => { await testEditor(BasicEditor, { contentBefore: '

[]

', stepFunction: deleteForward, diff --git a/packages/plugin-char/src/Char.ts b/packages/plugin-char/src/Char.ts index 4815bd0a6..6c4d8c185 100644 --- a/packages/plugin-char/src/Char.ts +++ b/packages/plugin-char/src/Char.ts @@ -11,10 +11,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]; @@ -26,6 +33,9 @@ export class Char extends JWPlugin insertText: { handler: this.insertText, }, + insertHtml: { + handler: this.insertHtml.bind(this), + }, }; //-------------------------------------------------------------------------- @@ -58,11 +68,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..a89a267af 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,7 @@ devtools-path { display: block; border-top: 1px solid #d0d0d0; background-color: #dddddd; - position: fixed; + /* position: fixed; */ bottom: 0; left: 0; right: 0; @@ -277,14 +276,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 +301,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 +350,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/DomEditable.ts b/packages/plugin-dom-editable/src/DomEditable.ts index c7ff4929f..022236759 100644 --- a/packages/plugin-dom-editable/src/DomEditable.ts +++ b/packages/plugin-dom-editable/src/DomEditable.ts @@ -190,9 +190,13 @@ export class DomEditable extend } if (!processed) { for (const action of batch.actions) { - const commandSpec = this._matchCommand(action); - if (commandSpec) { - await this.editor.execCommand(...commandSpec); + try { + const commandSpec = this._matchCommand(action); + if (commandSpec) { + await this.editor.execCommand(...commandSpec); + } + } catch (e) { + console.error('impossible to launch action', action); } } } diff --git a/packages/plugin-dom-editable/src/EventNormalizer.ts b/packages/plugin-dom-editable/src/EventNormalizer.ts index 0117140ce..12242c397 100644 --- a/packages/plugin-dom-editable/src/EventNormalizer.ts +++ b/packages/plugin-dom-editable/src/EventNormalizer.ts @@ -1627,18 +1627,23 @@ export class EventNormalizer { * @param ev */ _onPointerUp(ev: MouseEvent): void { - // 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 { + // 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); + } + } 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 013af52f7..53a666f9c 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 { @@ -59,6 +60,7 @@ export class DomLayout extends JWPl this._loadComponentLocations(this.configuration.locations || []); domLayoutEngine.location = this.configuration.location; await domLayoutEngine.start(); + await this.configuration.afterRender?.(); window.addEventListener('keydown', this.processKeydown, true); } async stop(): Promise { @@ -108,13 +110,16 @@ export class DomLayout extends JWPl //-------------------------------------------------------------------------- private async _redraw(): Promise { + // console.log('domlayout redraws'); // 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?.(); } + // console.log('this.configuration.afterRender:', this.configuration.afterRender); } private _loadComponentLocations(locations: [ComponentId, DomLayoutLocation][]): void { const layout = this.dependencies.get(Layout); diff --git a/packages/plugin-dom-layout/src/ui/DomLayoutEngine.ts b/packages/plugin-dom-layout/src/ui/DomLayoutEngine.ts index 632a30003..c115bc40d 100644 --- a/packages/plugin-dom-layout/src/ui/DomLayoutEngine.ts +++ b/packages/plugin-dom-layout/src/ui/DomLayoutEngine.ts @@ -167,9 +167,10 @@ export class DomLayoutEngine extends LayoutEngine { return nodes; } async redraw(node?: VNode): Promise { - if (this._currentlyRedrawing) { - throw new Error('Double redraw detected'); - } + // if (this._currentlyRedrawing) { + // throw new Error('Double redraw detected'); + // } + if (this.editor.preventRenders.size) return; this._currentlyRedrawing = true; // Find the closest node that has already been rendered preciously. const nodeToRedraw = node?.closest(n => { @@ -195,6 +196,7 @@ export class DomLayoutEngine extends LayoutEngine { const domNodes = await this._renderNode(nodeToRedraw); for (const domNode of domNodes) { if (domNode !== domNodeToRedraw) { + if (!parentNode) debugger; parentNode.insertBefore(domNode, domNodeToRedraw); } } @@ -214,7 +216,7 @@ export class DomLayoutEngine extends LayoutEngine { } } this._renderSelection(); - this._currentlyRedrawing = false; + // this._currentlyRedrawing = false; } async parseElement(element: HTMLElement): Promise { const parser = this.editor.plugins.get(Parser); diff --git a/packages/plugin-html/src/Html.ts b/packages/plugin-html/src/Html.ts index 125afc1aa..b13783823 100644 --- a/packages/plugin-html/src/Html.ts +++ b/packages/plugin-html/src/Html.ts @@ -6,11 +6,13 @@ import { HtmlTextParsingEngine } from './HtmlTextParsingEngine'; import { HtmlDomParsingEngine } from './HtmlDomParsingEngine'; import { HtmlDomRenderingEngine } from './HtmlDomRenderingEngine'; import { Xml } from '../../plugin-xml/src/Xml'; +import { HtmlHtmlDomRenderer } from './HtmlNodeDomRenderer'; export class Html extends JWPlugin { static dependencies = [Parser, Renderer, Xml]; readonly loadables: Loadables = { parsingEngines: [HtmlDomParsingEngine, HtmlTextParsingEngine], renderingEngines: [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..5a1256fde --- /dev/null +++ b/packages/plugin-html/src/HtmlNodeDomRenderer.ts @@ -0,0 +1,17 @@ +import { AbstractRenderer } from '../../plugin-renderer/src/AbstractRenderer'; +import { HtmlDomRenderingEngine } from './HtmlDomRenderingEngine'; +import { HtmlNode } from './HtmlNode'; + +export class HtmlHtmlDomRenderer extends AbstractRenderer { + static id = HtmlDomRenderingEngine.id; + engine: HtmlDomRenderingEngine; + predicate = HtmlNode; + + constructor(engine, superRenderer) { + super(engine, superRenderer); + } + + async render(node: HtmlNode): Promise { + return [node.domNode]; + } +} diff --git a/packages/plugin-image/src/ImageHtmlDomRenderer.ts b/packages/plugin-image/src/ImageHtmlDomRenderer.ts index 627cd5469..f6a66dde4 100644 --- a/packages/plugin-image/src/ImageHtmlDomRenderer.ts +++ b/packages/plugin-image/src/ImageHtmlDomRenderer.ts @@ -2,14 +2,29 @@ import { AbstractRenderer } from '../../plugin-renderer/src/AbstractRenderer'; import { ImageNode } from './ImageNode'; import { HtmlDomRenderingEngine } from '../../plugin-html/src/HtmlDomRenderingEngine'; import { Attributes } from '../../plugin-xml/src/Attributes'; +import { Youtube } from '../../plugin-youtube/src/Youtube'; export class ImageHtmlDomRenderer extends AbstractRenderer { static id = HtmlDomRenderingEngine.id; engine: HtmlDomRenderingEngine; - predicate = ImageNode; + predicate = (node): boolean => + !![ImageNode, Youtube].find(NodeClass => node instanceof NodeClass); async render(node: ImageNode): Promise { const image = document.createElement('img'); + const isSelected = this.engine.editor.selection.range.selectedNodes( + selectedNode => selectedNode === node, + ); + if (isSelected.length === 1) { + image.classList.add('jw_selected_image'); + } + image.addEventListener('click', e => { + this.engine.editor.nextEventMutex(() => { + this.engine.editor.execCustomCommand(async () => { + this.engine.editor.selection.select(node, node); + }); + }); + }); this.engine.renderAttributes(Attributes, node, image); return [image]; } diff --git a/packages/plugin-link/src/Link.ts b/packages/plugin-link/src/Link.ts index 0ad60089f..e7f4a9608 100644 --- a/packages/plugin-link/src/Link.ts +++ b/packages/plugin-link/src/Link.ts @@ -72,21 +72,30 @@ export class Link extends JWPlugin //-------------------------------------------------------------------------- async link(params: LinkParams): Promise { - // If the url is undefined, ask the user to provide one. - if (!params.url) { - const layout = this.editor.plugins.get(Layout); - await layout.remove('link'); - await layout.add('link'); + // // If the url is undefined, ask the user to provide one. + // if (!params.url) { + // const layout = this.editor.plugins.get(Layout); + // await layout.remove('link'); + // await layout.add('link'); - return this.editor.execCommand('show', { componentID: 'link' }); - } + // return this.editor.execCommand('show', { componentID: 'link' }); + // } - // Otherwise create a link and insert it. const link = new LinkFormat(params.url); - return this.editor.execCommand('insertText', { + // // TODO: modal re-using url + // if (!link) return; + + // if (range.isCollapsed()) { + this.editor.execCommand('insertText', { text: params.label || link.url, formats: new Modifiers(link), + select: true, }); + // } else { + // for (const inline of selectedInlines) { + // inline.formats.replace(LinkFormat, link); + // } + // } } unlink(params: LinkParams): void { const range = params.context.range; 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 @@ + + + + + + + + + + + + + + + + + + + + + + + +