From 1bc876ed5ec93d5bcb0534fba2bb3d4dfbe4b075 Mon Sep 17 00:00:00 2001 From: Dariusz Szut Date: Thu, 13 Jun 2019 12:02:03 +0200 Subject: [PATCH 01/25] Moved all Online Editor front-end code from AdminUI --- .eslintrc.json | 33 + .prettierrc | 10 + src/bundle/Resources/encore/ez.config.js | 83 ++ .../Resources/public/fonts/alloyeditor-ez.eot | Bin 0 -> 7044 bytes .../Resources/public/fonts/alloyeditor-ez.svg | 126 ++ .../Resources/public/fonts/alloyeditor-ez.ttf | Bin 0 -> 6852 bytes .../public/fonts/alloyeditor-ez.woff | Bin 0 -> 3420 bytes .../public/fonts/alloyeditor-ez.woff2 | Bin 0 -> 2280 bytes .../buttons/base/ez-blocktextalign.js | 79 ++ .../js/OnlineEditor/buttons/base/ez-button.js | 30 + .../buttons/base/ez-embedalign.js | 62 + .../buttons/base/ez-embeddiscovercontent.js | 72 + .../buttons/base/ez-embedimage.js | 36 + .../buttons/base/ez-widgetbutton.js | 18 + .../js/OnlineEditor/buttons/ez-btn-anchor.js | 50 + .../OnlineEditor/buttons/ez-btn-anchoredit.js | 228 +++ .../buttons/ez-btn-attributes-edit.js | 255 ++++ .../buttons/ez-btn-attributes-update.js | 227 +++ .../buttons/ez-btn-blocktextaligncenter.js | 24 + .../buttons/ez-btn-blocktextalignjustify.js | 24 + .../buttons/ez-btn-blocktextalignleft.js | 24 + .../buttons/ez-btn-blocktextalignright.js | 24 + .../js/OnlineEditor/buttons/ez-btn-bold.js | 41 + .../buttons/ez-btn-customtag-edit.js | 66 + .../buttons/ez-btn-customtag-update.js | 273 ++++ .../OnlineEditor/buttons/ez-btn-customtag.js | 46 + .../js/OnlineEditor/buttons/ez-btn-embed.js | 89 ++ .../buttons/ez-btn-embedaligncenter.js | 24 + .../buttons/ez-btn-embedalignleft.js | 24 + .../buttons/ez-btn-embedalignright.js | 24 + .../buttons/ez-btn-embedinline.js | 44 + .../buttons/ez-btn-embedupdate.js | 61 + .../js/OnlineEditor/buttons/ez-btn-heading.js | 57 + .../js/OnlineEditor/buttons/ez-btn-image.js | 92 ++ .../OnlineEditor/buttons/ez-btn-imagelink.js | 66 + .../buttons/ez-btn-imagelinkedit.js | 94 ++ .../buttons/ez-btn-imageupdate.js | 62 + .../buttons/ez-btn-imagevariation.js | 62 + .../buttons/ez-btn-inlinecustomtag-edit.js | 19 + .../buttons/ez-btn-inlinecustomtag-update.js | 58 + .../buttons/ez-btn-inlinecustomtag.js | 19 + .../js/OnlineEditor/buttons/ez-btn-italic.js | 41 + .../js/OnlineEditor/buttons/ez-btn-link.js | 46 + .../OnlineEditor/buttons/ez-btn-linkedit.js | 503 +++++++ .../OnlineEditor/buttons/ez-btn-movedown.js | 48 + .../js/OnlineEditor/buttons/ez-btn-moveup.js | 48 + .../buttons/ez-btn-orderedlist.js | 60 + .../OnlineEditor/buttons/ez-btn-paragraph.js | 58 + .../js/OnlineEditor/buttons/ez-btn-quote.js | 41 + .../buttons/ez-btn-removeblock.js | 59 + .../js/OnlineEditor/buttons/ez-btn-strike.js | 41 + .../OnlineEditor/buttons/ez-btn-subscript.js | 41 + .../buttons/ez-btn-superscript.js | 41 + .../js/OnlineEditor/buttons/ez-btn-table.js | 45 + .../OnlineEditor/buttons/ez-btn-tablecell.js | 51 + .../buttons/ez-btn-tablecolumn.js | 52 + .../buttons/ez-btn-tableremove.js | 32 + .../OnlineEditor/buttons/ez-btn-tablerow.js | 52 + .../OnlineEditor/buttons/ez-btn-underline.js | 41 + .../buttons/ez-btn-unorderedlist.js | 61 + .../js/OnlineEditor/core/base-richtext.js | 383 +++++ .../js/OnlineEditor/core/ez-attributes.js | 43 + .../js/OnlineEditor/core/ez-custom-tags.js | 68 + .../public/js/OnlineEditor/core/table.js | 20 + .../plugins/base/ez-custom-tag-base.js | 577 ++++++++ .../plugins/base/ez-embed-base.js | 744 ++++++++++ .../js/OnlineEditor/plugins/ez-add-content.js | 107 ++ .../js/OnlineEditor/plugins/ez-caret.js | 68 + .../js/OnlineEditor/plugins/ez-custom-tag.js | 96 ++ .../OnlineEditor/plugins/ez-elements-path.js | 96 ++ .../js/OnlineEditor/plugins/ez-embed.js | 79 ++ .../js/OnlineEditor/plugins/ez-focus-block.js | 110 ++ .../OnlineEditor/plugins/ez-move-element.js | 101 ++ .../OnlineEditor/plugins/ez-remove-block.js | 123 ++ .../toolbars/config/base-fixed.js | 51 + .../OnlineEditor/toolbars/config/base-list.js | 16 + .../toolbars/config/base-table.js | 30 + .../js/OnlineEditor/toolbars/config/base.js | 162 +++ .../toolbars/config/ez-custom-style.js | 69 + .../toolbars/config/ez-custom-tag.js | 51 + .../toolbars/config/ez-embed-inline.js | 44 + .../OnlineEditor/toolbars/config/ez-embed.js | 52 + .../toolbars/config/ez-formatted.js | 44 + .../toolbars/config/ez-heading.js | 49 + .../toolbars/config/ez-image-link.js | 37 + .../OnlineEditor/toolbars/config/ez-image.js | 54 + .../toolbars/config/ez-inline-custom-tag.js | 46 + .../OnlineEditor/toolbars/config/ez-link.js | 52 + .../toolbars/config/ez-list-item.js | 16 + .../toolbars/config/ez-list-ordered.js | 16 + .../toolbars/config/ez-list-unordered.js | 16 + .../toolbars/config/ez-paragraph.js | 49 + .../toolbars/config/ez-table-cell.js | 17 + .../toolbars/config/ez-table-row.js | 17 + .../OnlineEditor/toolbars/config/ez-table.js | 20 + .../OnlineEditor/toolbars/config/ez-text.js | 37 + .../public/js/OnlineEditor/toolbars/ez-add.js | 94 ++ .../public/scss/_alloyeditor-ez.scss | 1245 +++++++++++++++++ .../Resources/public/scss/_anchor-edit.scss | 59 + .../Resources/public/scss/_attributes.scss | 21 + .../Resources/public/scss/_buttons.scss | 62 + .../public/scss/_character-counter.scss | 12 + .../Resources/public/scss/_custom-styles.scss | 22 + .../Resources/public/scss/_custom-tag.scss | 99 ++ .../Resources/public/scss/_elements-path.scss | 12 + .../Resources/public/scss/_link-edit.scss | 139 ++ src/bundle/Resources/public/scss/_tools.scss | 8 + .../Resources/public/scss/alloyeditor.scss | 13 + .../public/scss/functions/calculate.rem.scss | 5 + .../public/scss/variables/colors.scss | 43 + 110 files changed, 9381 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .prettierrc create mode 100644 src/bundle/Resources/encore/ez.config.js create mode 100644 src/bundle/Resources/public/fonts/alloyeditor-ez.eot create mode 100644 src/bundle/Resources/public/fonts/alloyeditor-ez.svg create mode 100644 src/bundle/Resources/public/fonts/alloyeditor-ez.ttf create mode 100644 src/bundle/Resources/public/fonts/alloyeditor-ez.woff create mode 100644 src/bundle/Resources/public/fonts/alloyeditor-ez.woff2 create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-blocktextalign.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-button.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embedalign.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embedimage.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-widgetbutton.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-anchor.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-anchoredit.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-attributes-edit.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-attributes-update.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextaligncenter.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignjustify.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignleft.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignright.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-bold.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag-edit.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag-update.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embed.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedaligncenter.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedalignleft.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedalignright.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedinline.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedupdate.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-heading.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-image.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagelink.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagelinkedit.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imageupdate.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagevariation.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-edit.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-update.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-italic.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-link.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-linkedit.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-movedown.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-moveup.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-orderedlist.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-paragraph.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-quote.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-removeblock.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-strike.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-subscript.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-superscript.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-table.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablecell.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablecolumn.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tableremove.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablerow.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-underline.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-unorderedlist.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/core/base-richtext.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/core/ez-attributes.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/core/ez-custom-tags.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/core/table.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-custom-tag-base.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-embed-base.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/ez-add-content.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/ez-caret.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/ez-custom-tag.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/ez-elements-path.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/ez-embed.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/ez-focus-block.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/ez-move-element.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/plugins/ez-remove-block.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-fixed.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-list.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-table.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-custom-style.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-custom-tag.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-embed-inline.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-embed.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-formatted.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-heading.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-image-link.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-image.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-inline-custom-tag.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-link.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-item.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-ordered.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-unordered.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-paragraph.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-cell.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-row.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-text.js create mode 100644 src/bundle/Resources/public/js/OnlineEditor/toolbars/ez-add.js create mode 100644 src/bundle/Resources/public/scss/_alloyeditor-ez.scss create mode 100644 src/bundle/Resources/public/scss/_anchor-edit.scss create mode 100644 src/bundle/Resources/public/scss/_attributes.scss create mode 100644 src/bundle/Resources/public/scss/_buttons.scss create mode 100644 src/bundle/Resources/public/scss/_character-counter.scss create mode 100644 src/bundle/Resources/public/scss/_custom-styles.scss create mode 100644 src/bundle/Resources/public/scss/_custom-tag.scss create mode 100644 src/bundle/Resources/public/scss/_elements-path.scss create mode 100644 src/bundle/Resources/public/scss/_link-edit.scss create mode 100644 src/bundle/Resources/public/scss/_tools.scss create mode 100644 src/bundle/Resources/public/scss/alloyeditor.scss create mode 100644 src/bundle/Resources/public/scss/functions/calculate.rem.scss create mode 100644 src/bundle/Resources/public/scss/variables/colors.scss diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..d269c3e5 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "plugins": ["react"], + "extends": ["eslint:recommended", "plugin:react/recommended"], + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "node": true + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "rules": { + "no-const-assign": "warn", + "no-this-before-super": "warn", + "no-undef": "warn", + "no-unreachable": "warn", + "no-unused-vars": "warn", + "constructor-super": "warn", + "valid-typeof": "warn", + "no-extra-semi": "error", + "no-extra-boolean-cast": "off", + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "jsx-quotes": ["error", "prefer-double"], + "quotes": ["error", "single", { "allowTemplateLiterals": true }], + "eqeqeq": ["error", "always"], + "indent": ["error", 4] + } +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..1bce2829 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 140, + "tabWidth": 4, + "semi": true, + "bracketSpacing": true, + "jsxBracketSameLine": true, + "arrowParens": "always", + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/src/bundle/Resources/encore/ez.config.js b/src/bundle/Resources/encore/ez.config.js new file mode 100644 index 00000000..b58a8186 --- /dev/null +++ b/src/bundle/Resources/encore/ez.config.js @@ -0,0 +1,83 @@ +const path = require('path'); + +module.exports = (Encore) => { + Encore.addEntry('ezplatform-richtext-onlineeditor-js', [ + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-anchor.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-anchoredit.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-paragraph.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-heading.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-movedown.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-moveup.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-blocktextaligncenter.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-blocktextalignjustify.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-blocktextalignleft.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-blocktextalignright.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-removeblock.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-unorderedlist.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-orderedlist.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-table.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-tablecell.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-tablerow.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-tablecolumn.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-tableremove.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-bold.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-italic.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-underline.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-subscript.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-superscript.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-quote.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-strike.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-link.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-linkedit.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-image.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-imageupdate.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-imagevariation.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-imagelink.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-imagelinkedit.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-embed.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-embedinline.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-embedupdate.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-embedaligncenter.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-embedalignleft.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-embedalignright.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-customtag.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-customtag-edit.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-customtag-update.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-edit.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-update.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-attributes-edit.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/buttons/ez-btn-attributes-update.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/ez-add.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/plugins/ez-add-content.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/plugins/ez-move-element.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/plugins/ez-caret.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/plugins/ez-remove-block.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/plugins/ez-embed.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/plugins/ez-focus-block.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/plugins/ez-custom-tag.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/plugins/ez-elements-path.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-paragraph.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-formatted.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-text.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-list-ordered.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-list-unordered.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-list-item.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-table.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-table-row.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-table-cell.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-link.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-heading.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-embed.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-embed-inline.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-image.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-image-link.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-custom-tag.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-inline-custom-tag.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/toolbars/config/ez-custom-style.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/core/table.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/core/ez-custom-tags.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/core/ez-attributes.js'), + path.resolve(__dirname, '../public/js/OnlineEditor/core/base-richtext.js'), + ]).addStyleEntry('ezplatform-richtext-onlineeditor-css', [path.resolve(__dirname, '../public/scss/alloyeditor.scss')]); +}; diff --git a/src/bundle/Resources/public/fonts/alloyeditor-ez.eot b/src/bundle/Resources/public/fonts/alloyeditor-ez.eot new file mode 100644 index 0000000000000000000000000000000000000000..4881ead49aac3e0649d42d2f7af4ede23639decb GIT binary patch literal 7044 zcmd^EO>85{b$+j^fA~-Ru-TGEqB!hXQeMqa{O9h>jx%b0&2X0z z6^a^r4a4wZ5d=XFdooyuj_nj}F{@{9a z&TZ0w#`v4ihcu*P8qo~=lKA|WpBQ`tqi_H27xr51e|hxh=zkOaCr77)^Rw6g`x$n5 zPNbhd9e;T8Z-4R836TT&r9XZ)92_UUd-(*iKLz!kVSxUCAWwe_y7TOG_Vwhq!4dx$ z&7X`XM}znN^ou_uvVV^Ld#8i1pYsDc!u2)u%V&er;XfQd{s=q#A?Ezb`Q&2uAOHT( zDWdf|kokVJ_D7JkNm@cXS8dcqQ;SA0`h(Lizw_1nTmOT-K9Y++{@!=7&+9Wr5714- zX2j#stI_WfX2YRCsfhn&)Hl~tRHD1Yzy0`gU&Ni(!WABmo(?c(&h4zxTFHITB!b$Sb;4-by zDiz3si}$E|);1pZ1+cuTyHdQE~JMu z@rDo<6cS5@u!WG=B!qQ^#L6KwDkO*xT|=-Fh42d@Sd7A?A=r*WSY1f0AEph#o)ltL zLa;1_SwpZfg*ij8HidaZusenD9U<|daM=*BK;eoZpn}3xL%;`x1w%jzg`Oc`hQgvD zpoc==5O72xqLUC1Md7+3V2eWJ0U@A_LgWP@;ElpfLqHydTZVu^3QLB7MhdqL0hbh( z4FRDP-ZliRQdltr)KVB20)8pHV+cs5@HIogG(5(TpgTJ5(zoazxx{1sCjTY>Ol_#Q z)gP(9Q$JDvr+4&Je_#KXwPt}_-VfV)C_90=vv6Y{lQWlk{{83&q}UBA&cmmBg2yPe(xZWOn;;8tYdA)5i;b83xUZeXC%Yqnd(rsoz+zZ3PhxX-(+F#9f= zU0H@S3Eos{Q+nncfU~4)@fx!hPICoarL6U6SQ=4e)I@8{!(MxmIr`m3w7Rxekktm> z^=hNl-{{xoec#AelhK-#S)96YW|o15EClL{*T|f{;!^Qy*~CSoKeM(+SR1q>>XH?T zcKm2XbM+|w5cD}O0xFSUKCae#S><1NRi>uO3DKxc5GX{p?Q;St(5KBt6AZ&IOWg89Jj=U91z#_TfOd{3Ov8n?(cJ_y31Zg+m_PuILp@Fs+M)9;n9kf zrPDniR5t0jw&vXx2m-GwxN2*O-;xW6y7<$`18KzSRf(s4tXxQTv3Sm;Qu+1u{6gDF zCG12*F02##;RCEcL1h%k%}7hFUI#|#2Xz3mlIO@obKP~SZl%theQwDfxgT`fo||y- zUr9qbIcy^h@TfXAv~IbUg)3-Q9cRmSoTTIAw54ptB@rsIx(xvb3Wb!fC5Pd76YE*r z-`sv;CokysL9klai+3xak)5|?DXrB^CZj>6Y&N?qr{3=Fb~3q^#E~(S$7znk7OaO% z*xzH|6i9XbzTasfvC#J2N}<>~ z;9kdVwEZH?8`SzO*f`Hlq3;%)Iv*evl=w}AbgkHR8Sa$i_KV%y2|=SYl`I6(X^0B5 z*vYKH?i%4AEmkA7j)|oO$;-edfP(-TbOm9U9&GMg;|ZEw}WsAjbiD6L-Kil2P|tP~JSKKbkdp4|>Qy#oc8@6;Ldsi3p3O3L#K;2p26 zT(+b6fLiyhP0LQ;`M{^lt}KKW#5vht!7QOzm@mOfDhKfn(ufCeXa^URh4sR6q+I?6!4WAE?HGfBTEsubkI)$RY$ir!k%XX@@1s+J)7KfH=F4_ncdkDZgIcx>) zyVxw+Kn9drf?@EVFsHg1T#1wn2@xU*Fz!W0@XgHt8+w6j&C#Zo^Tl9e}yzH;aWl7rnmVx*NE@be&4ADVM8V zY|r=125W8B37rITWid%uN(up2FUz8M-gmS%QZ3dx1y-ajh%2^XTIJ|O>XSo|iiG3X z+`fv<%cxL46X8-~WJEOqdDj;Qm(0^`Zz(jwfHu~n1KQJe!nPA2+g`Oj$Ftp3LU~F# z(5{opK)o6Y6g`S>NgSd+>Iw-zP30l_Y}rjNrk$lNiSa@b7p`&hF60+6@hN<55l;o+ zY>(p+XuMkSM8Ulm5n}*!g_!yk=OR`t;3mQR83T{J0ZV+^F@vSZ(S;WS32Lx|dCe$w zMFDRJqT5Cj$-Ce2YB}Y#k(Aoaw*Qi`^`#}tMly0F_Y@a9o7oBwz)s4sT`RxXiG5h| zLk6?eFk5XQMPSAPDi$R5ItSijO5Cv>?acp^cEYldaM8#D;%s;6ZsT*)Z|k0+=ED0Ysfw=+NaOrM>XIq$MZN8_;E)np~awt z9W6+?3sUVEn)_VmYra#>C8ZFHntMHArSX#zxDkF87ZU0 zJY4eovPLU|C}g9~vn@XgsMiB2T}BMB5m+QrffXjI8n618-^9=c;*F_8B*?&ez5$50 zRLWX#PHm?fmUa8~dOEGtRZP8cPhaiHJ9jpfm9SJoN1m(m*6uRu0GL;yV8K)<=dIQFd41~R zR}^^@0Z=4K)g**Ak)yN(RD^IrO0$GlrE&rp2elTeE#xhn#V83YPGX65>0?sqBebL; zEo%^I2+gtCief(AtUJAJj*??FZ)?1XuWLKG3nh8LdNB&Fcz^l(7||@h2F1JrwsJAA z!Fe%nfe&KdMm^Guc?aWf#e9l(5d(~uq)`Q3LYH`p!^kH6VazMY|5?myjQ@4aTi|~b z^EQ4x3S-`(O+3&%Cq++qGv?D&<>_ENo_shw9?d4xd&6&he)#lqJh(CVqR5ls^kOtQ zD>v(n7caa!JR432v*B_1@Wb-O^QW!Z?4*1$ot%~*PtImT%u_y}PTn6L&Far)v-A6V zdnfUD{b+Lf%FhoketLM-hl*ESd->;zSKaY0K5(2xA2|m293r1Z%5;biBxQW`c#e-D zEsW0S1pO0y2$|3+xX01Q5Ro>YQ_6H6U3(uRN06@L%Y@u@4xH`b@8pJj9a8e7d90Ec z^75l5?+f_J!)6ERK7q)d!p|ZZMCBZuVTqS{g;%-29)9EU@piG!8+;3~S$;Lx<}%;L z&rAW|;n%o|-&b}>4~~yB1AIa_yLU8%#in`FdH?caHaZy%k5i^U9-hq7hMkU{KAYKt z>2&fndvJL?n&`vPnRPfBA7>9Q$KxT;aBn=im^nxIs4^W`M}UESG@e`xt)t2D&^eln zFHg_x;pyS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/bundle/Resources/public/fonts/alloyeditor-ez.ttf b/src/bundle/Resources/public/fonts/alloyeditor-ez.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a427ac12dde0046445ca46cf06089e433a0af20a GIT binary patch literal 6852 zcmd^EO>85{b$+j^fA~-Ru-TGEqB!hXQeMqa{O9h>jx=6mXAL8O7q&CG1PLgKn&B=b zDik&L8iwJ65d=XFdoiL<@WC()BL}061jr#d1#!OD)s#5e z+08XTT6)#5s$N&UdjH)dCZaTbl{7Wp{@fqE`Mv-C!M_smYZ!g=GoRaQwSV^LyXb!z z{U=AKgY&aj|NR+}Iw#W4pN`)@`PJWk?S#ky{nB4Q8xD>W-?)4N+Ft{D&oDrLL7=A} z0`5FJoqaL+Rba$FNAox1$G|Yh_Pc-iX^Lq54rsnb!uunD9kqhX$l_ItMlkyKr+@O+pXJ~951jRp5d8d` z-@rMq?-)HmH$h59amh+G`eVXwxMVaH;lGdiW&sGQ&~?vOghEgDBOmW+qh-$V8xON+jdrvj~o6&-RRy(FGX zQ--pXqdX+IOe?fX1@fp!KCRI@ZO|>+q%G)eo62;XDiqKidWEW3QuOlw{mO^{_!qGl zAeB%gFItg1yg)#_A%q46M3W(OAs{*lpX+HbP|9?QMhgZwngEF0azJ@w+z7EDBLsv%cF410Bn%L zk^xvFh1&*TmlT!_z(Og!Z2-1PVZ{KfmcqaQ?3cnj24Kk)zG4724UaJ()*W4U=_~Zl zT;ef*ng5P|qc+r=>aWy~)GyV4=^Z`Q-_rkPtyy2OKCpgkm+g-Ib^9mIl5^iVaenQV z-TUqn_XGFG?tdogiN}$r5@P0ooT~qTy=`tEa93cD1A#ny0>0L(%qiv~<3bao>o@vl zD)MWh*Qf{W-KHje601ASpi=F3JG}?oC~k4Vt;oPbHUqxr)Ec|oz(AwdY`2O{&n=jK zC+cr;pLbbd_gyr*!iF>n-c)K+dgdMgv!rYB8nYHma|Kq$~0}WvW;){7?PG4cEm|He+(a6v6 z_6WQ|IwCINP^9BW9L>eI#Uib_I9rgfe^a~x`yvfAd9DdoQ%T+y1Wo!{qIDyzEG7(V zFJLi`A0@xJG9&pdYCCX)M%9Nro9#&Z!hLrx@1~Jo^ICq`xdt`wsDvsjRc4oYO{q1` zUdd?fN^aK@TsOjv;v6`9m0A+P`VGHdbA4W3#>rP!7&8JyuS5Ir$bL|9wzw)yiCevg z-1ef5U)+wmooZ);wQ_8>l8z^LX2-VGYnnIN)=Jr~y_yva%qiC{v4qqpLlSkcOA_(v z7HkY&-Y*oPKI{?rVq~H+NFne7uibUqUQh#MExo3#s-&OW!~ zjNA{pZO=`(_^+fPoE(mkhViI64zzB$mW3HKtB$i}J5JJZa@tb1;*t=RSltGJ1A&6d z*OJ3=c&j`hCCOm4eLUw%t~5{j2k>M( zTXQ#5GVLrcJDK<%J-xi*_*lhRS&=&H_T37UdceJo+i3fu-k{cRLC1M^3VpZW)cF9Zpv12uq-({l%W$V8w_ohuPKY&1 zQ^|rLod&5Oi=E6G^sW*9(SjSPbxbTRNM43*!f+5EgRWQ@wg=k3&-+MwqSS=TPQr0D zY@gXJ!6CGhNcEQB=C-$LVN|nP36xf^ZpCXKz*Y)~B_Ce9fVJB}r+1*>@|`+kKNWQL zRY`e%0l4F}mCJUtAFS4WW7Dz|SReK&yDJN!1#wQ!S1>FT3v(8%q;e4NK#h0+hjuWb zEbt50PO;R~ay^2LB;cWv-y6+t4apo4_#xLS%{*^$ZSHBc6<${RMOMWeoZoh=Jg+X} zuw}^(w-74eAoG}Z9~w@>=L2NTPsyppS>dKmq3AE-a9ZQCooa0Xhb3%_L(80tHnNI6 z1mK+$NF_El_NMuqx`$Sx&DMo<$Y@A~54l6kuAErmu9z{YxXL3-Lw*meS7+pD(c zc($8LC{HN|(sfc9h*v{^B1iEpi9^&!T_NG8sXRoVExXCZw6nA&F3Dg54yTKVi%xZ;U0r?U=z*Z@oG8cwULzC&9?uNvGt`T%SJMCB=-~-JDb@GEP$Pq zW4l&U9ph#gw>XJKCB5C+&n~A>pDC2I6dZYmsD) zV**f>V*+Huc>iwXBjWKA;etMDImx*`k`?$tvU2_O+FCV>w4N!k?I2R3N&*8*_A6Uk z&hEUcZK)mp*W4qy)rGl6xfJ_d=hS>=hE2`5^hau=xDv1#seov-muHJ6k^ENbrc1eL~5O5jHLRa{({ze0=fWF_;= z-qm0$dQs6xWRm#+IjJNiEJ!0&W;_l%6*Rs%6AyZt%$l0(Wj1FW&%6vzjhO~ zQm>C#Rl>WRYy=jGR6qrZs>Z86_BS!Kfp}x; z5D7BSo^K4qTPkHOIH$JL4a>THdp(_2>MEw*c&0DUz`W918G8fK#pcvqdB}|(!d~)C z;iKv@@eHY4Of>uW;%pm5%!jJ8&16!)q(C$0liGAA@^vRbnc%pPq32hsHAvD&tS=PX zsJ7dv=8JH~hurJ8A4nF~mX=JCv5=)Ss%MLlr4!0s%}FR~M5<(Xq;|w#@VezN3UVu% zOW=W6PEyxv`4$m*9$uSsT&ZJ^z$=tgYhOL&+?_j{%1T%&p(D@Ld24qWbpX_>P_SSt zl=IeVyk4LB_=+NLA}|z5QZ)&oP2?yo0Tm%!ETvh(t5P|EjDuPW)fVy=?xK{06(_O8 zy7aNB^buOppcWp48bov3Y(+62Z`PgOHb=>^nzuFH#MiZ*+=Y-l*m^Mvu6Td>$JnA- zz6Qm(f^FquTm$oB+yWlNxQ%+G8RHJd--z)P?IH#kFG-^cx`Zt87Kf2d`pXzsp#M&c zYm9$C#x3AKj&U1bkHQ#tXcG&XbyD<%H)A|aRh|yUQXNONO$AcS# zFOobNPA^82vvRZEcyZ$G;n{FHm<^B1hwqm!oEyKhcycxyVxRK)bn@=- zXjXqVo1Nd^+dGNJ>qnDQ8sMwzm?rc-4e6LhGy^`x+trZ1M4v_b6tWnDYWlI1AGgXA ztTLqwtUaMKDntHtYJBW!Z{vxx=&1qt5x&ZF2oB3w(#I)|O@;qT-IeH~P?(mYm440-ugllKLD^03)Kx=$dor|?-M zgQ%Q?Gc55kukb1t*uyt2A8!}yyur5+o8_y)HkbJ}J~IV;hhO0;zOU?%9vmNM2KWi# z?B3B38k^=#=iSST+2~|6JWiSZcz7~P8+1B)`fO$orqjt6?7`*nXrd2CXV&3le4IVJ z9FK>vhI`}D#mqUvk1ErFbp$i8kH(XWp>;Gl9y&*p@#X26Jv==e9;;_f^{i!&P6tni znbFxXSh#nHA8X!oMl?`6*}K|76?Rg?F!lgrsPsr~1dliARj4o@e~hZ%D_ znM_XyGYuA#7sGQHH%#5Rm`z9T4U-p_hZjfF(fKTWad{5Ij&OT6I2;e%*%wAL2qAe1 UR;Ji{=vZ+zeT&v&15o#(l(bKlSPocpgU$n5%c00dBr)ec}jJq5e|$4~G7UpFjn z>jD6XfvUo&(5?Af|I^Ikf(li;Nad$Mz&_~|BfXt5R4tgwR{?gj$2V}FiKd3P!aTw9Px&d#3?-3%E(L(iGz4#F^u`OnmmSMj|x zoLpkxA+tKU52Zj!j!c2aMW4ktpY3dileI2$H;Cl#YrTz~3>5i2Yd8`Y=yEMy@xu-& zGEYpR$2dm?IUFq|Kj}4Hha8ELkQugX_Fo!~kulM%Ts*00-~g!e1RWpD2>nV-19@1N$3d+t#+B0#%aRFTV5)%+;cCJ*RvN| z?J;kPdk-7`@bWCnF1k!c?~>4Jw|RA32y7hpQk!Mhz3lPWL!idSmJCq$6l+BZUwp^4 z_uT^-(4_22BN?3h36&4@1moQM_Nw=S&XQYCUSoY-k@{*sJM;zZ6&|~_wM$Vl#MPMIED%>TCDFlpD5XyNn4moz36yiXdYaO3jr-`QGy zT{tkF>&Lp_7NhtpAjGcaIaY7wnoAo=$F9$&n>R%4B20O`^~njW6|671v&@=w=l!dF zVCG47s!|!o2DUHdALTU$41JF6ZEMCf-S*iiNEg%lnA4RCHccFSdmI4Z=jE{qaJr zG^}`Bp)4`;Cax7dEnYi2884JUK;McoXqHm3P4><3F)$Kqm{?M@ARAeK5bei#2K9hS zX-U%*S}00G{`p0mNqy#(9=RBvI!U7>BA-F_>%c6mVWXP*a2`>^n!WO;t&lu6tY>6<-nm(JZgz;00^4Ke6v7oYLvCjO_-n15s9>;v?H&Q8mRVVZ(EWYiaL`pD zcawupLH``|tk~7IqFP&`n`H2N7@;zo(^O@nl`Czhl3$mCywF`v#wLeNT`w?=P+N1H zh)=B!v#Rv*+hO?|wmD2;B}ErETs~X)NG_CC91-R(r#ZM$^X|^c7}@22sB>I(l$-g{AHtRkc*#ji5=9 zHQD|FCOTXBrz_bkf^B{smFvAXc?>eIHO+m-1k8J7Ur}QHmAp#45zw=6){H;&GAD+q zXtu%djqp8j?gf$6bHqdjapC8Xiwh*Jg-gtT^VC0CS=XT|za)slJ6|T|O@6p^CYR?e z^f3$R@F4Z)v3A?k>gR%W+#1~ah)p=P)e$S*WW#o;=bDF=`Kr7y`~5pbl_Z7gy-~jx z#&$E)7m4x@C!)jSjh;nUO66GSbkA*k7(p+4OmyoDxTGeonZjd9QZ0h>36o~sqhd=t z%V%5>Dg?~MzE!OPjy>!0HmM~cc~vbaF7k^0Qcx;QPK#-eVckCGYh(Q_6lwi7>s5MAmg)_eX{myFaPQ98sxAriJ;4Hi^6S&FJzXXIkqK|C?-j_PT<Fv}XZsX%szmo86l0xJ8Td>+FXYLSqxBtV3Yu`P?GG$GrUN-2X z+?TDL_g%ZhqBbXo^c@titO5hUyK4sqUM9PJYkN}X&?qCTCE72m3Tz5i@LoSGcoFYf z-@Ntl+FE`*X`a%!_Yq~`PyZmdE-GMIHn~OT_T*Q$mGJcE<871LNt4KeoU3!G7r4Y& zx;!*FUFExHBEkZ~``)Bm)lg;QUu?c39qJv#0+)PVZM0n*oX2 zAga)EJ!vPVcE+g?9{prt8rHHgf8Pp=&q*Ca>{e@edwmJzG#!LSS&4yhy?xfE>o#@E znv&XRiI0Co(2laSc9w@+te5RCdK8GkHeEnJN`MC?%}VmEJ@G~ z?dm-VK0#r3zo(Qccxz%A?X`;6KgxwRR-h3D=}^b-o$vWE;wu$jQtbk==+~RI<_qjT zHZ4m!>D)Wel6HseE56R^`q4-xa@T!qNUR@nvuky4O+L;dS!Fw_T_Hg>w-c-pTKAVD zy4mm=h+nsv9}d=5_gdO2r^!W6jrDCe7#29&(QE!@)7I&%t+@;Ce>5mK7rv&~q|rXC zH{GEV>du<(TJv4b`t|q%qMSmp5nviS91q&GJYpmB4oig5Y`!Yr&T_2VkUG*l$$0ja zHN?YrZ?k_!Y(tbtTG0*UcbJ-%2s`Erw0y0MlK?_XJCAvJMRwt|j`@Ltr;VyJa zwUC=PJQFW$<&JmI>l#MxGGfk=b+c5 zccUjmL?N0G1SB0Y4B2B)WVrsXulZeuyJQytBrUoEu0M(S!vgRQfnHFpg0W%gA3X>< zJh5g+@~Y-c#r7y=^Y0}B5!uAXXe0ia8wF@dH-ZM!7UQ94ShV)lXJP5u*UT0_eFIZg1M=$o#5R{HfxT-$6)xM@H0??x4p2alJPg=(78{gEe;}e2ZpWd;@fWl3kv#i)$;r z6da+y(8qV_zRZx#)KPMZV(6y_o_w{o>*a#W=0_(SG5JCkldYtra;tz?x`HIx8*S*T z6JpLUMv*lVCaXIGVZ*o2t%Y~h#BufW zI~0bCdoSz@K?-spVUs6GsN-3_`J0lXa(g~!Fwp9rNqe58$=?^yTdsX#=1vgYIc0(X zx8-yv4@CnnmWoN?#xAmaYsO1eVIB+3FS6|i<0YQ|O5tyHiEEO4YHL=cYNT$poI?my zdQpq4S1P&@)6_uT5{V_v&l42fs##j%c$8-WHUcCAfdmUg00bZfgkA@NV;kQRBN675v5@R94(v7`dvM{Z z)94$v?Asr}{Mqai>*4oq!8UPspWC4%^fLs9N0U#d$1pp`nKoJ0n0iZYlN&uiF z07?O%Gyuu~pez8&0iZkpDgdA&04f2X4gk~%fVu!sHvsAZK)nE{OacK^yqnAh4uBa8 zriCgOkDw5vuP1063_gOwbxefzgj0fR!CWD65y*Dcok}H@b82x?erT9s)4nN>dmant z!d$-}+T21ZROLjG=a{-r%=e8KWs|4S>uyl}hJ(Zu;_)0!RpFe5KxcyYGQkY@<6dM+ zTLM#6O}#@LMOMknE|N$pj3GvGrhugr3#E$K&Kd$+tfRQKJvwr^k)-!yyPj~Pi2*F; zC1T^jMTN%7r!3V}6dPu$b7Uc>gH7U4ZY6DY?M|;sV>Z`hB`@6RD*eKOai^gUEh1P` zNN}ReT)*kN^!I`kFWIC8;xLDj>1bP0?HbcJLS2J$8^Wn%G7AdX3Q5IT$+XiG?}ImO zI3u75h1)w}>Xz2XI<}NCG zrsWo9=b~Tq@r^?0aPQX$$GGOQ z6(%{yzN@K>?!G=K&xNel zEWUg9hpd59@@x8m(z0!4DDs0(u#lQen`uZ+!G`tHomD(~eP9#>> z9vF~9{KosHawW%!XG=!| zj}1T8xAhXQaqP%u*?P$_&i$`OM?OQJet?OLHxq}`kc}`OC)xm}ASX%LC?ls6HK`3b z136P>53OtJHh^ZEa-#5!FQUfv<@X^c){!-nJl-$kkI^4HpRB!~`%B#S9>29NG4)qZ z|D(lx&TFrjd}cnLz(XyB#5VXxmpyaoD_KeVhaDlKxcF=W02B~hivCBHCwY6^g@ACzT-&lj3PHenO{kMRe|fs;9G_leJYL4sI?H`UwZ zM{`uIO)VVFnUkX2?M*9ugVXI*wLR^2^=8P{aqL*qf8+dy56ZHE&lj3PR!VJgy86TJ zThD=$aj>;7%`PI<2(0FrT(5c?`OzGyR<&_L;V8wqWxJ%?W?;1G;SEj%Th|(4d$Q9} zb=hFcwQ>YK)XgiEXfa~N5m!9%|7MP4Br%CgLXuO{GqZE^3yVuTcJA7}XYcaLs{RJ= z7nVYkHV&_$K-O26xRtD-4)C*eE1}l;dA)EyR~=drehw-ozpM%oUihNMB88#w6V*(0fls5rV=Yi{ag3=KKw7N7zrXju<*Oq zEYWJe&sU#jv{tOLjIv6&W135qorG5BxwMdrZm6~*jP)7xrn*c^@{pctFe<6TyiNzK C+ehO7 literal 0 HcmV?d00001 diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-blocktextalign.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-blocktextalign.js new file mode 100644 index 00000000..cc8a8a0d --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-blocktextalign.js @@ -0,0 +1,79 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBlockTextAlign extends Component { + constructor(props) { + super(props); + + this.getStateClasses = AlloyEditor.ButtonStateClasses.getStateClasses; + } + + /** + * Finds active block. + * + * @method findBlock + * @return {Object} + */ + findBlock() { + return this.props.editor.get('nativeEditor').elementPath().block; + } + + /** + * Checks whether the element holding the caret already has the current + * text align style + * + * @method isActive + * @return {Boolean} + */ + isActive() { + return this.findBlock().getStyle('textAlign') === this.props.textAlign; + } + + /** + * Applies or removes the text align style + * + * @method applyStyle + */ + applyStyle() { + const block = this.findBlock(); + const editor = this.props.editor.get('nativeEditor'); + + if (this.isActive()) { + block.removeStyle('text-align'); + } else { + block.setStyle('text-align', this.props.textAlign); + } + + editor.fire('actionPerformed', this); + editor.fire('customUpdate'); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ez-btn-ae--' + this.props.cssClassSuffix + ' ' + this.getStateClasses(); + const icon = '/bundles/ezplatformadminui/img/ez-icons.svg#' + this.props.iconName; + + return ( + + ); + } +} + +EzBlockTextAlign.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, + textAlign: PropTypes.string.isRequired, + iconName: PropTypes.string.isRequired, + cssClassSuffix: PropTypes.string.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-button.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-button.js new file mode 100644 index 00000000..10bc3bbf --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-button.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzButton extends Component { + constructor(props) { + super(props); + + this.getStateClasses = AlloyEditor.ButtonStateClasses.getStateClasses; + this.execCommand = AlloyEditor.ButtonCommand.execCommand.bind(this); + } + + findSelectedBlock() { + const nativeEditor = this.props.editor.get('nativeEditor'); + const path = nativeEditor.elementPath(); + let block = path.lastElement; + + if (block.hasClass('cke_widget_wrapper')) { + block = nativeEditor.widgets.getByElement(block).element; + } + + if (this.block) { + return this.block; + } + + this.block = block; + + return block; + } +} diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embedalign.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embedalign.js new file mode 100644 index 00000000..cc96a16f --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embedalign.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import WidgetButton from './ez-widgetbutton'; + +export default class EzEmbedAlign extends WidgetButton { + /** + * Checks if the configured alignment is active on the focused embed + * element. + * + * @method isActive + * @return {Boolean} + */ + isActive() { + return this.getWidget().isAligned(this.props.alignment); + } + + /** + * Applies or un-applies the alignment on the currently focused embed + * element. + * + * @method applyStyle + */ + applyStyle() { + const widget = this.getWidget(); + + if (this.isActive()) { + widget.unsetAlignment(); + } else { + widget.setAlignment(this.props.alignment); + } + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ez-btn-ae--' + this.props.cssClassSuffix + ' ' + this.getStateClasses(); + const icon = '/bundles/ezplatformadminui/img/ez-icons.svg#' + this.props.iconName; + + return ( + + ); + } +} + +EzEmbedAlign.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, + alignment: PropTypes.string.isRequired, + iconName: PropTypes.string.isRequired, + cssClassSuffix: PropTypes.string.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js new file mode 100644 index 00000000..79a8289c --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js @@ -0,0 +1,72 @@ +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import EzWidgetButton from './ez-widgetbutton'; + +export default class EzEmbedDiscoverContentButton extends EzWidgetButton { + constructor(props) { + super(props); + + this.confirmHandler = this.confirmHandler.bind(this); + this.cancelHandler = this.cancelHandler.bind(this); + } + + confirmHandler() { + const { editor, udwContentDiscoveredMethod } = this.props; + + editor.get('nativeEditor').unlockSelection(true); + + this[udwContentDiscoveredMethod].apply(this, arguments); + } + + cancelHandler() { + this.props.editor.get('nativeEditor').unlockSelection(true); + } + + /** + * Triggers the UDW to choose the content to embed. + * + * @method chooseContent + */ + chooseContent() { + const { udwIsSelectableMethod, udwConfigName, udwTitle, editor } = this.props; + const selectable = udwIsSelectableMethod ? this[udwIsSelectableMethod] : (item, callback) => callback(true); + const token = document.querySelector('meta[name="CSRF-Token"]').content; + const siteaccess = document.querySelector('meta[name="SiteAccess"]').content; + const languageCode = document.querySelector('meta[name="LanguageCode"]').content; + const config = JSON.parse(document.querySelector(`[data-udw-config-name="${udwConfigName}"]`).dataset.udwConfig); + const selectContent = eZ.alloyEditor.callbacks.selectContent; + const mergedConfig = Object.assign( + { + onConfirm: this.confirmHandler, + onCancel: this.cancelHandler, + title: udwTitle, + multiple: false, + startingLocationId: window.eZ.adminUiConfig.universalDiscoveryWidget.startingLocationId, + restInfo: { token, siteaccess }, + canSelectContent: selectable, + cotfAllowedLanguages: [languageCode], + }, + config + ); + + editor.get('nativeEditor').lockSelection(); + + if (typeof selectContent === 'function') { + selectContent(mergedConfig); + } + } + + /** + * Sets the href of the ezembed widget based on the given content info + * + * @method setContentInfo + * @param {eZ.ContentInfo} contentInfo + */ + setContentInfo(contentId) { + const embedWidget = this.getWidget(); + + embedWidget.setHref('ezcontent://' + contentId); + embedWidget.focus(); + } +} diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embedimage.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embedimage.js new file mode 100644 index 00000000..ba08b5d3 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embedimage.js @@ -0,0 +1,36 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import EzEmbedDiscoverContentButton from './ez-embeddiscovercontent'; + +export default class EzEmbedImageButton extends EzEmbedDiscoverContentButton { + /** + * Checks whether the current selection can be considered as an image. + * This is the case if the content type has an ezimage field definition + * and if the corresponding field is not empty. This method is meant to + * be used as a `isSelectable` function implementation for the UDW. + * + * @method isImage + * @param {Object} selection the UDW potential selection + * @param {Function} callback + */ + isImage(selection, callback) { + console.warn('[DEPRECATED] isImage method is deprecated'); + console.warn('[DEPRECATED] it will be removed from ezplatform-admin-ui 2.0'); + console.warn('[DEPRECATED] please use richtext_embed_image.content_on_the_fly.allowed_content_types in the UDW config'); + + const siteaccess = document.querySelector('meta[name="SiteAccess"]').content; + const request = new Request(selection.item.ContentInfo.Content.ContentType._href, { + method: 'GET', + headers: { + 'Accept': 'application/vnd.ez.api.ContentType+json', + 'X-Siteaccess': siteaccess + }, + mode: 'same-origin', + credentials: 'same-origin' + }); + + fetch(request) + .then(response => response.json()) + .then(data => callback(!!data.ContentType.FieldDefinitions.FieldDefinition.find(field => field.fieldType === 'ezimage'))); + } +} diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-widgetbutton.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-widgetbutton.js new file mode 100644 index 00000000..de302b17 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-widgetbutton.js @@ -0,0 +1,18 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import EzButton from './ez-button'; + +export default class EzWidgetButton extends EzButton { + /** + * Returns the ezembed widget instance for the current selection. + * + * @method getWidget + * @return CKEDITOR.plugins.widget + */ + getWidget() { + const editor = this.props.editor.get('nativeEditor'); + const wrapper = editor.getSelection().getStartElement(); + + return editor.widgets.getByElement(wrapper); + } +} diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-anchor.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-anchor.js new file mode 100644 index 00000000..1979fbee --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-anchor.js @@ -0,0 +1,50 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnAnchor extends Component { + constructor(props) { + super(props); + + this.getStateClasses = AlloyEditor.ButtonStateClasses.getStateClasses; + } + + static get key() { + return 'ezanchor'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + if (this.props.renderExclusive) { + return ; + } + + const cssClass = `ae-button ez-btn-ae--anchor ez-btn-ae ${this.getStateClasses()}`; + const label = Translator.trans(/*@Desc("Anchor")*/ 'anchor_btn.label', {}, 'alloy_editor'); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnAnchor.key] = AlloyEditor.EzBtnAnchor = EzBtnAnchor; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnAnchor = EzBtnAnchor; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-anchoredit.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-anchoredit.js new file mode 100644 index 00000000..f3c412a2 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-anchoredit.js @@ -0,0 +1,228 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +const CLASS_HAS_ANCHOR = 'ez-has-anchor'; +const CLASS_ICON_ANCHOR = 'ez-icon--anchor'; + +export default class EzBtnAnchorEdit extends Component { + constructor(props) { + super(props); + + this.updateValue = this.updateValue.bind(this); + this.saveAnchor = this.saveAnchor.bind(this); + this.removeAnchor = this.removeAnchor.bind(this); + this.fireCustomUpdateEvent = this.fireCustomUpdateEvent.bind(this); + + this.getStateClasses = AlloyEditor.ButtonStateClasses.getStateClasses; + + this.block = null; + + this.state = { + value: '', + isValueUnique: false, + }; + } + + componentDidMount() { + const block = this.findBlock(); + const value = block.getId(); + const isValueUnique = this.isValueUnique(value); + + this.setState(() => ({ value, isValueUnique })); + } + + static get key() { + return 'ezanchoredit'; + } + + findBlock() { + const nativeEditor = this.props.editor.get('nativeEditor'); + const selected = nativeEditor.widgets.selected[0]; + const path = nativeEditor.elementPath(); + let block = path.block; + + if (this.block) { + return this.block; + } + + if (!block && selected) { + block = selected.element; + } + + if (block && block.is('li')) { + block = block.getParent(); + } + + if (!block && path.contains('table')) { + block = path.elements.find((element) => element.is('table')); + } + + this.block = block; + + return block; + } + + findIcon() { + const block = this.findBlock(); + + return [...block.getChildren().$].find((child) => child.classList && child.classList.contains(CLASS_ICON_ANCHOR)); + } + + updateValue({ nativeEvent }) { + const value = nativeEvent.target.value; + const isValueUnique = this.isValueUnique(value); + + this.setState(() => ({ value, isValueUnique })); + } + + isValueUnique(value) { + const block = this.findBlock(); + + return Object.values(CKEDITOR.instances).every((editor) => { + const data = editor.getData(); + const container = document.createElement('div'); + + container.insertAdjacentHTML('afterbegin', data); + + return ( + value && + [...container.querySelectorAll(`#${value}`)].every((element) => { + const ckeditorElement = new CKEDITOR.dom.element(element); + + block.removeClass('is-block-focused'); + + return ckeditorElement.isIdentical(block); + }) + ); + }); + } + + fireCustomUpdateEvent() { + const nativeEditor = this.props.editor.get('nativeEditor'); + + nativeEditor.fire('customUpdate'); + } + + removeAnchor() { + const block = this.findBlock(); + const icon = this.findIcon(); + + block.removeAttribute('id'); + block.removeClass(CLASS_HAS_ANCHOR); + + if (icon) { + icon.remove(); + } + + this.props.cancelExclusive(); + + block.focus(); + + this.fireCustomUpdateEvent(); + } + + saveAnchor() { + const { value } = this.state; + const block = this.findBlock(); + const icon = this.findIcon(); + + block.setAttribute('id', value); + block.addClass(CLASS_HAS_ANCHOR); + + if (!icon) { + this.renderIcon(); + } + + this.props.cancelExclusive(); + + block.focus(); + + this.fireCustomUpdateEvent(); + } + + renderIcon() { + const block = this.findBlock(); + const container = document.createElement('div'); + const icon = ` + + + `; + + container.insertAdjacentHTML('afterbegin', icon); + + const svg = new CKEDITOR.dom.element(container.querySelector('svg')); + + block.append(svg, true); + } + + renderError() { + const { value, isValueUnique } = this.state; + + if (!value || isValueUnique) { + return null; + } + + const errorMessage = Translator.trans( + /*@Desc("This anchor already exists on the page. Anchor name must be unique.")*/ 'anchor_btn.error.unique', + {}, + 'alloy_editor' + ); + + return {errorMessage}; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const nameLabel = Translator.trans(/*@Desc("Name:")*/ 'anchor_edit.input.label', {}, 'alloy_editor'); + const removeBtnTitle = Translator.trans(/*@Desc("Remove")*/ 'anchor_edit.btn.remove.title', {}, 'alloy_editor'); + const saveBtnTitle = Translator.trans(/*@Desc("Save")*/ 'anchor_edit.btn.save.title', {}, 'alloy_editor'); + const { value, isValueUnique } = this.state; + const isRemoveBtnDisabled = !value; + const isSaveBtnDisabled = !value || !isValueUnique; + + return ( +
+
+ + +
+
+ + +
+ {this.renderError()} +
+ ); + } +} + +AlloyEditor.Buttons[EzBtnAnchorEdit.key] = AlloyEditor.EzBtnAnchorEdit = EzBtnAnchorEdit; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnAnchorEdit = EzBtnAnchorEdit; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-attributes-edit.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-attributes-edit.js new file mode 100644 index 00000000..76ebab88 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-attributes-edit.js @@ -0,0 +1,255 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzWidgetButton from './base/ez-widgetbutton'; + +export default class EzBtnAttributesEdit extends EzWidgetButton { + constructor(props) { + super(props); + + this.setDefaultAttributesMethods = { + tr: this.setDefaultAttributesOnTableRows, + td: this.setDefaultAttributesOnTableCells, + li: this.setDefaultAttributesOnListItems, + }; + + this.setDefaultClassesMethods = { + tr: this.setDefaultClassesOnTableRows, + td: this.setDefaultClassesOnTableCells, + li: this.setDefaultClassesOnListItems, + }; + } + + componentDidMount() { + const block = this.findSelectedBlock(); + + if (!block.$.getAttribute('data-ez-node-initialized')) { + this.removeClasses(block); + this.removeAttributes(block); + } + + this.setDefaultClasses(block); + this.setDefaultAttributes(block); + + block.$.setAttribute('data-ez-node-initialized', true); + + this.beforeCommandExecHandler = this.props.editor + .get('nativeEditor') + .on('beforeCommandExec', this.toggleNodeInitialized.bind(this, block, false)); + this.afterCommandExecHandler = this.props.editor.get('nativeEditor').on('afterCommandExec', (event) => { + let add = true; + + if (event.data.name === 'removeFormat') { + this.toggleNodeInitialized(block, add); + + add = false; + this.block = null; + } + + this.toggleNodeInitialized(this.findSelectedBlock(), add); + }); + } + + toggleNodeInitialized(block, add) { + const methodName = add ? 'setAttribute' : 'removeAttribute'; + + block.$[methodName]('data-ez-node-initialized', true); + } + + componentDidUpdate() { + this.block = null; + + const block = this.findSelectedBlock(); + + if (!block.$.getAttribute('data-ez-node-initialized')) { + this.removeClasses(block); + this.removeAttributes(block); + this.setDefaultClasses(block); + this.setDefaultAttributes(block); + + block.$.setAttribute('data-ez-node-initialized', true); + } + } + + componentWillUnmount() { + this.beforeCommandExecHandler.removeListener(); + this.afterCommandExecHandler.removeListener(); + } + + removeClasses(block) { + const classes = [...block.$.classList]; + const classesToRemain = ['is-block-focused', 'ez-embed-type-image', 'is-linked']; + + classes.forEach((className) => { + if (!classesToRemain.includes(className)) { + block.$.classList.remove(className); + } + }); + } + + removeAttributes(block) { + Object.values(block.$.attributes).forEach((attribute) => { + if (attribute.name.startsWith('data-ezattribute')) { + block.removeAttribute(attribute.name); + } + }); + } + + setDefaultClasses(block) { + if (!Object.keys(this.classes).length || block.$.classList.contains('ez-classes-added') || !this.classes.defaultValue) { + return; + } + + const defaultValue = this.classes.defaultValue.split(','); + const setDefaultClassesMethod = this.setDefaultClassesMethods[this.toolbarName] + ? this.setDefaultClassesMethods[this.toolbarName] + : this.setDefaultClassesOnBlock; + + setDefaultClassesMethod(block, defaultValue); + } + + setDefaultClassesOnBlock(block, value) { + block.$.classList.add(...value); + } + + setDefaultClassesOnTableRows(block, value) { + const rows = block.$.closest('table').querySelectorAll('tr'); + + rows.forEach((row) => row.classList.add(...value)); + } + + setDefaultClassesOnTableCells(block, value) { + const cells = block.$.closest('table').querySelectorAll('td'); + + cells.forEach((cell) => cell.classList.add(...value)); + } + + setDefaultClassesOnListItems(block, value) { + const list = block.$.closest('ul') || block.$.closest('ol'); + const listItems = list.querySelectorAll('li'); + + listItems.forEach((listItem) => listItem.classList.add(...value)); + } + + setDefaultAttributes(block) { + Object.entries(this.attributes).forEach(([attributeName, config]) => { + const attributeValue = block.getAttribute(`data-ezattribute-${attributeName}`); + + if (attributeValue !== null) { + return; + } + + const defaultValue = config.defaultValue; + + if (defaultValue !== undefined && defaultValue !== null) { + const setDefaultAttributesMethod = this.setDefaultAttributesMethods[this.toolbarName] + ? this.setDefaultAttributesMethods[this.toolbarName] + : this.setDefaultAttributesOnBlock; + + setDefaultAttributesMethod(block, attributeName, defaultValue); + } + }); + } + + setDefaultAttributesOnBlock(block, attributeName, value) { + block.setAttribute(`data-ezattribute-${attributeName}`, value); + } + + setDefaultAttributesOnTableRows(block, attributeName, value) { + const rows = block.$.closest('table').querySelectorAll('tr'); + + rows.forEach((row) => row.setAttribute(`data-ezattribute-${attributeName}`, value)); + } + + setDefaultAttributesOnTableCells(block, attributeName, value) { + const cells = block.$.closest('table').querySelectorAll('td'); + + cells.forEach((cell) => cell.setAttribute(`data-ezattribute-${attributeName}`, value)); + } + + setDefaultAttributesOnListItems(block, attributeName, value) { + const list = block.$.closest('ul') || block.$.closest('ol'); + const listItems = list.querySelectorAll('li'); + + listItems.forEach((listItem) => listItem.setAttribute(`data-ezattribute-${attributeName}`, value)); + } + + getAttributesValues() { + return Object.entries(this.attributes).reduce((total, [attributeName, config]) => { + const block = this.findSelectedBlock(); + const defaultValue = config.defaultValue; + let value = block.getAttribute(`data-ezattribute-${attributeName}`); + const isValueDefined = value !== null; + + if (config.type === 'choice' && !isValueDefined && !config.multiple) { + value = config.choices[0]; + } + + if (!isValueDefined && defaultValue !== undefined && defaultValue !== null) { + value = defaultValue; + } + + if (config.type === 'boolean' && isValueDefined) { + value = value === 'true'; + } + + total[attributeName] = { value }; + + return total; + }, {}); + } + + getClassesValue() { + const block = this.findSelectedBlock(); + let value = block.$.classList.value + .split(' ') + .filter((className) => Array.isArray(this.classes.choices) && this.classes.choices.includes(className)) + .join(); + + if (!value && !this.classes.multiple && Array.isArray(this.classes.choices)) { + value = this.classes.choices[0]; + } + + return value; + } + + getUpdateBtnName() { + return `ezBtn${this.toolbarName.charAt(0).toUpperCase() + this.toolbarName.slice(1)}Update`; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + if (this.props.renderExclusive) { + const buttonName = this.getUpdateBtnName(); + const ButtonComponent = AlloyEditor[buttonName]; + + return ; + } + + const css = `ae-button ez-btn-ae ez-btn-ae--${this.toolbarName}-edit`; + + return ( + + ); + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnAttributesEdit = EzBtnAttributesEdit; + +EzBtnAttributesEdit.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-attributes-update.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-attributes-update.js new file mode 100644 index 00000000..7706ecfb --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-attributes-update.js @@ -0,0 +1,227 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzWidgetButton from './base/ez-widgetbutton'; + +export default class EzBtnAttributesUpdate extends EzWidgetButton { + constructor(props) { + super(props); + + this.updateValue = this.updateValue.bind(this); + this.updateCheckboxValue = this.updateCheckboxValue.bind(this); + this.udpateSelecValue = this.udpateSelecValue.bind(this); + this.renderAttribute = this.renderAttribute.bind(this); + this.saveValues = this.saveValues.bind(this); + this.updateClassesValue = this.updateClassesValue.bind(this); + + this.state = { + attributesValues: props.attributesValues, + classesValue: props.classesValue, + }; + } + + renderString(attrName, config, value) { + return ( +
+ + +
+ ); + } + + renderCheckbox(attrName, config, value) { + return ( +
+ + +
+ ); + } + + renderNumber(attrName, config, value) { + return ( +
+ + +
+ ); + } + + renderSelect(attrName, config, value, updateValue = this.udpateSelecValue) { + return ( +
+ + +
+ ); + } + + renderChoice(choice) { + return ; + } + + renderAttribute([attributeName, attributeConfig]) { + const renderMethods = this.getAttributeRenderMethods(); + const methodName = renderMethods[attributeConfig.type]; + const value = this.state.attributesValues[attributeName].value; + + return
{this[methodName](attributeName, attributeConfig, value)}
; + } + + renderClass() { + if (!Object.keys(this.classes).length) { + return null; + } + + return this.renderSelect('classes', this.classes, this.state.classesValue, this.updateClassesValue); + } + + render() { + const saveLabel = Translator.trans(/*@Desc("Save")*/ 'custom_attributes_update_btn.save_btn.label', {}, 'alloy_editor'); + const isValid = this.isValid(); + + return ( +
+ {this.renderClass()} + {Object.entries(this.attributes).map(this.renderAttribute)} + +
+ ); + } + + isValid() { + return Object.keys(this.attributes).every((attr) => { + return this.attributes[attr].required ? !!this.state.attributesValues[attr].value : true; + }); + } + + clearClasses() { + const block = this.findSelectedBlock(); + + if (!Object.keys(this.classes).length) { + return; + } + + block.$.classList.remove(...this.classes.choices); + } + + saveValues() { + const block = this.findSelectedBlock(); + const { attributesValues, classesValue } = this.state; + const { editor, cancelExclusive } = this.props; + const nativeEditor = editor.get('nativeEditor'); + + Object.entries(attributesValues).forEach(([attribute, attributeData]) => { + block.setAttribute(`data-ezattribute-${attribute}`, attributeData.value); + }); + + this.clearClasses(); + + if (classesValue) { + block.$.classList.add(...classesValue.split(','), 'ez-classes-added'); + } + + nativeEditor.unlockSelection(true); + nativeEditor.fire('customUpdate'); + + cancelExclusive(); + } + + getSelectedOptions(options) { + return options + .filter(({ selected }) => selected) + .map(({ value }) => value) + .join(); + } + + updateClassesValue({ target }) { + const classesValue = this.getSelectedOptions([...target.options]); + + this.setState({ classesValue }); + } + + udpateSelecValue({ target }) { + const selectedValues = this.getSelectedOptions([...target.options]); + + this.setAttributesValues(target.dataset.attrName, selectedValues); + } + + updateCheckboxValue({ target }) { + this.setAttributesValues(target.dataset.attrName, target.checked); + } + + updateValue({ target }) { + this.setAttributesValues(target.dataset.attrName, target.value); + } + + setAttributesValues(attrName, value) { + const attributesValues = Object.assign({}, this.state.attributesValues); + + attributesValues[attrName].value = value; + + this.setState({ + attributesValues: attributesValues, + }); + } + + getAttributeRenderMethods() { + return { + string: 'renderString', + boolean: 'renderCheckbox', + number: 'renderNumber', + choice: 'renderSelect', + }; + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnAttributesUpdate = EzBtnAttributesUpdate; + +EzBtnAttributesUpdate.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextaligncenter.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextaligncenter.js new file mode 100644 index 00000000..cf45a05c --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextaligncenter.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzBlockTextAlign from './base/ez-blocktextalign'; + +export default class EzBtnBlockTextAlignCenter extends EzBlockTextAlign { + static get key() { + return 'ezblocktextaligncenter'; + } +} + +AlloyEditor.Buttons[EzBtnBlockTextAlignCenter.key] = AlloyEditor.EzBtnBlockTextAlignCenter = EzBtnBlockTextAlignCenter; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnBlockTextAlignCenter = EzBtnBlockTextAlignCenter; + +EzBtnBlockTextAlignCenter.defaultProps = { + textAlign: 'center', + iconName: 'align-center', + cssClassSuffix: 'align-center', + label: Translator.trans(/*@Desc("Center")*/ 'block_text_align_center_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignjustify.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignjustify.js new file mode 100644 index 00000000..1336a751 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignjustify.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzBlockTextAlign from './base/ez-blocktextalign'; + +export default class EzBtnBlockTextAlignJustify extends EzBlockTextAlign { + static get key() { + return 'ezblocktextalignjustify'; + } +} + +AlloyEditor.Buttons[EzBtnBlockTextAlignJustify.key] = AlloyEditor.EzBtnBlockTextAlignJustify = EzBtnBlockTextAlignJustify; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnBlockTextAlignJustify = EzBtnBlockTextAlignJustify; + +EzBtnBlockTextAlignJustify.defaultProps = { + textAlign: 'justify', + iconName: 'align-justify', + cssClassSuffix: 'align-justify', + label: Translator.trans(/*@Desc("Justify")*/ 'blocktext_align_justify_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignleft.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignleft.js new file mode 100644 index 00000000..fe59e8a1 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignleft.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzBlockTextAlign from './base/ez-blocktextalign'; + +export default class EzBtnBlockTextAlignLeft extends EzBlockTextAlign { + static get key() { + return 'ezblocktextalignleft'; + } +} + +AlloyEditor.Buttons[EzBtnBlockTextAlignLeft.key] = AlloyEditor.EzBtnBlockTextAlignLeft = EzBtnBlockTextAlignLeft; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnBlockTextAlignLeft = EzBtnBlockTextAlignLeft; + +EzBtnBlockTextAlignLeft.defaultProps = { + textAlign: 'left', + iconName: 'align-left', + cssClassSuffix: 'align-left', + label: Translator.trans(/*@Desc("Left")*/ 'blocktext_align_left_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignright.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignright.js new file mode 100644 index 00000000..89f53dd6 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-blocktextalignright.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzBlockTextAlign from './base/ez-blocktextalign'; + +export default class EzBtnBlockTextAlignRight extends EzBlockTextAlign { + static get key() { + return 'ezblocktextalignright'; + } +} + +AlloyEditor.Buttons[EzBtnBlockTextAlignRight.key] = AlloyEditor.EzBtnBlockTextAlignRight = EzBtnBlockTextAlignRight; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnBlockTextAlignRight = EzBtnBlockTextAlignRight; + +EzBtnBlockTextAlignRight.defaultProps = { + textAlign: 'right', + iconName: 'align-right', + cssClassSuffix: 'align-right', + label: Translator.trans(/*@Desc("Right")*/ 'blocktext_align_right_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-bold.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-bold.js new file mode 100644 index 00000000..89e8a174 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-bold.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnBold extends AlloyEditor.ButtonBold { + static get key() { + return 'ezbold'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnBold.key] = AlloyEditor.EzBtnBold = EzBtnBold; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnBold = EzBtnBold; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag-edit.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag-edit.js new file mode 100644 index 00000000..23fe3089 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag-edit.js @@ -0,0 +1,66 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzWidgetButton from './base/ez-widgetbutton'; + +export default class EzBtnCustomTagEdit extends EzWidgetButton { + /** + * Gets values for the configuration. + * + * @method getValues + * @return {Object} The configuration values. + */ + getValues() { + return Object.keys(this.attributes).reduce((total, attr) => { + let value = this.getWidget().getConfig(attr); + + if (this.attributes[attr].type === 'boolean') { + value = value === 'true'; + } + + total[attr] = { value }; + + return total; + }, {}); + } + + getUpdateBtnName() { + return `ezBtn${this.customTagName.charAt(0).toUpperCase() + this.customTagName.slice(1)}Update`; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + if (this.props.renderExclusive) { + const buttonName = this.getUpdateBtnName(); + const ButtonComponent = AlloyEditor[buttonName]; + + return ; + } + + const css = `ae-button ez-btn-ae ez-btn-ae--${this.customTagName}-edit`; + + return ( + + ); + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnCustomTagEdit = EzBtnCustomTagEdit; + +EzBtnCustomTagEdit.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag-update.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag-update.js new file mode 100644 index 00000000..f852079a --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag-update.js @@ -0,0 +1,273 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzWidgetButton from './base/ez-widgetbutton'; + +export default class EzBtnCustomTagUpdate extends EzWidgetButton { + constructor(props) { + super(props); + + this.widget = this.getWidget(); + + props.editor.get('nativeEditor').lockSelection(); + + this.state = { + values: props.values, + }; + } + + componentDidMount() { + if (!Object.keys(this.attributes).length) { + this.saveCustomTag(); + } + } + + /** + * Renders the text input. + * + * @method renderString + * @param {Object} config + * @param {String} attrName + * @return {Object} The rendered text input. + */ + renderString(config, attrName) { + return ( +
+ + +
+ ); + } + + /** + * Renders the checkbox input. + * + * @method renderCheckbox + * @param {Object} config + * @param {String} attrName + * @return {Object} The rendered checkbox input. + */ + renderCheckbox(config, attrName) { + return ( +
+ + +
+ ); + } + + /** + * Renders the number input. + * + * @method renderNumber + * @param {Object} config + * @param {String} attrName + * @return {Object} The rendered number input. + */ + renderNumber(config, attrName) { + return ( +
+ + +
+ ); + } + + /** + * Renders the select. + * + * @method renderSelect + * @param {Object} config + * @param {String} attrName + * @return {Object} The rendered select. + */ + renderSelect(config, attrName) { + return ( +
+ + +
+ ); + } + + /** + * Renders the option. + * + * @method renderChoice + * @param {String} choice + * @return {Object} The rendered option. + */ + renderChoice(choice) { + return ; + } + + /** + * Renders the attribute. + * + * @method renderAttribute + * @param {Object} attribute + * @return {Object} The rendered attribute. + */ + renderAttribute(attribute) { + const attributeConfig = this.attributes[attribute]; + const renderMethods = this.getAttributeRenderMethods(); + const methodName = renderMethods[attributeConfig.type]; + + return
{this[methodName](attributeConfig, attribute)}
; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const saveLabel = Translator.trans(/*@Desc("Save")*/ 'custom_tag_update_btn.save_btn.label', {}, 'alloy_editor'); + const attrs = Object.keys(this.attributes); + const isValid = this.isValid(); + + return ( +
+ {attrs.map(this.renderAttribute.bind(this))} + +
+ ); + } + + /** + * Checks if values are valid. + * + * @method isValid + * @return {Boolean} are values valid + */ + isValid() { + return Object.keys(this.attributes).every((attr) => { + return this.attributes[attr].required ? !!this.state.values[attr].value : true; + }); + } + + /** + * Creates the custom tag in AlloyEditor. + * + * @method saveCustomTag + */ + saveCustomTag() { + const { createNewTag, editor } = this.props; + + editor.get('nativeEditor').unlockSelection(true); + + if (createNewTag) { + this.execCommand(); + } + + const widget = this.getWidget() || this.widget; + const configValues = Object.assign({}, this.state.values); + + widget.setFocused(true); + widget.setName(this.customTagName); + widget.setWidgetAttributes(this.createAttributes()); + widget.renderHeader(); + widget.clearConfig(); + + Object.keys(this.attributes).forEach((key) => { + widget.setConfig(key, configValues[key].value); + }); + } + + /** + * Creates attributes. + * + * @method createAttributes + * @return {String} the ezattributes + */ + createAttributes() { + return Object.keys(this.attributes).reduce( + (total, attr) => `${total}

${this.attributes[attr].label}: ${this.state.values[attr].value}

`, + '' + ); + } + + /** + * Update values. + * + * @method updateValues + * @param {Object} event + */ + updateValues(event) { + const values = Object.assign({}, this.state.values); + const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value; + + values[event.target.dataset.attrName].value = value; + + this.setState({ + values: values, + }); + } + + /** + * Gets the render method map. + * + * @method getAttributeRenderMethods + * @return {Object} the render method map + */ + getAttributeRenderMethods() { + return { + string: 'renderString', + boolean: 'renderCheckbox', + number: 'renderNumber', + choice: 'renderSelect', + }; + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnCustomTagUpdate = EzBtnCustomTagUpdate; + +EzBtnCustomTagUpdate.defaultProps = { + command: 'ezcustomtag', + modifiesSelection: true, +}; + +EzBtnCustomTagUpdate.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag.js new file mode 100644 index 00000000..dd47f83b --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-customtag.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzWidgetButton from './base/ez-widgetbutton'; + +export default class EzBtnCustomTag extends EzWidgetButton { + getUpdateBtnName() { + return `ezBtn${this.customTagName.charAt(0).toUpperCase() + this.customTagName.slice(1)}Update`; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + if (this.props.renderExclusive) { + const buttonName = this.getUpdateBtnName(); + const ButtonComponent = AlloyEditor[buttonName]; + + return ; + } + + const css = `ae-button ez-btn-ae ez-btn-ae--${this.customTagName}`; + + return ( + + ); + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnCustomTag = EzBtnCustomTag; + +EzBtnCustomTag.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embed.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embed.js new file mode 100644 index 00000000..488c0d2e --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embed.js @@ -0,0 +1,89 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzEmbedDiscoverContentButton from './base/ez-embeddiscovercontent'; + +export default class EzBtnEmbed extends EzEmbedDiscoverContentButton { + static get key() { + return 'ezembed'; + } + + /** + * Checks if the command is disabled in the current selection. + * + * @method isDisabled + * @return {Boolean} True if the command is disabled, false otherwise. + */ + isDisabled() { + return !this.props.editor.get('nativeEditor').ezembed.canBeAdded(); + } + + /** + * Executes the command generated by the ezembed plugin and set the + * correct value based on the choosen content. + * + * @method addEmbed + * @param {Object} items the result of the choice in the UDW + */ + addEmbed(items) { + const contentInfo = items[0].ContentInfo.Content._id; + + if (navigator.userAgent.indexOf('Chrome') > -1) { + const scrollY = window.pageYOffset; + + this.execCommand(); + window.scroll(window.pageXOffset, scrollY); + } else { + this.execCommand(); + } + this.setContentInfo(contentInfo); + + const widget = this.getWidget(); + + widget.setWidgetContent(''); + widget.renderEmbedPreview(items[0].ContentInfo.Content.Name); + widget.setFocused(true); + + ReactDOM.unmountComponentAtNode(document.querySelector('#react-udw')); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const css = 'ae-button ez-btn-ae ez-btn-ae--embed ' + this.getStateClasses(); + const disabled = this.isDisabled(); + const label = Translator.trans(/*@Desc("Embed")*/ 'embed_btn.label', {}, 'alloy_editor'); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnEmbed.key] = AlloyEditor.EzBtnEmbed = EzBtnEmbed; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnEmbed = EzBtnEmbed; + +EzBtnEmbed.defaultProps = { + command: 'ezembed', + modifiesSelection: true, + udwTitle: Translator.trans(/*@Desc("Select a content to embed")*/ 'embed_btn.udw.title', {}, 'alloy_editor'), + udwContentDiscoveredMethod: 'addEmbed', + udwConfigName: 'richtext_embed', +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedaligncenter.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedaligncenter.js new file mode 100644 index 00000000..be570f0c --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedaligncenter.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzEmbedAlign from './base/ez-embedalign'; + +export default class EzEmbedAlignCenter extends EzEmbedAlign { + static get key() { + return 'ezembedcenter'; + } +} + +AlloyEditor.Buttons[EzEmbedAlignCenter.key] = AlloyEditor.EzEmbedAlignCenter = EzEmbedAlignCenter; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezEmbedAlignCenter = EzEmbedAlignCenter; + +EzEmbedAlignCenter.defaultProps = { + alignment: 'center', + iconName: 'image-center', + cssClassSuffix: 'embed-center', + label: Translator.trans(/*@Desc("Center")*/ 'embed_align_center_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedalignleft.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedalignleft.js new file mode 100644 index 00000000..a35fab2d --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedalignleft.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzEmbedAlign from './base/ez-embedalign'; + +export default class EzEmbedAlignLeft extends EzEmbedAlign { + static get key() { + return 'ezembedleft'; + } +} + +AlloyEditor.Buttons[EzEmbedAlignLeft.key] = AlloyEditor.EzEmbedAlignLeft = EzEmbedAlignLeft; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezEmbedAlignLeft = EzEmbedAlignLeft; + +EzEmbedAlignLeft.defaultProps = { + alignment: 'left', + iconName: 'image-left', + cssClassSuffix: 'embed-left', + label: Translator.trans(/*@Desc("Left")*/ 'embed_align_left_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedalignright.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedalignright.js new file mode 100644 index 00000000..34f50080 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedalignright.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzEmbedAlign from './base/ez-embedalign'; + +export default class EzEmbedAlignRight extends EzEmbedAlign { + static get key() { + return 'ezembedright'; + } +} + +AlloyEditor.Buttons[EzEmbedAlignRight.key] = AlloyEditor.EzEmbedAlignRight = EzEmbedAlignRight; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezEmbedAlignRight = EzEmbedAlignRight; + +EzEmbedAlignRight.defaultProps = { + alignment: 'right', + iconName: 'image-right', + cssClassSuffix: 'embed-right', + label: Translator.trans(/*@Desc("Right")*/ 'embed_align_right_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedinline.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedinline.js new file mode 100644 index 00000000..cf3aebd8 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedinline.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzBtnEmbed from './ez-btn-embed'; + +export default class EzBtnEmbedInline extends EzBtnEmbed { + static get key() { + return 'ezembedinline'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const css = 'ae-button ez-btn-ae ez-btn-ae--embed-inline'; + const label = Translator.trans(/*@Desc("Embed")*/ 'embed_btn.label', {}, 'alloy_editor'); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnEmbedInline.key] = AlloyEditor.EzBtnEmbedInline = EzBtnEmbedInline; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnEmbedInline = EzBtnEmbedInline; + +EzBtnEmbedInline.defaultProps = { + command: 'ezembedinline', + modifiesSelection: true, + udwTitle: Translator.trans(/*@Desc("Select a content to embed")*/ 'embed_btn.udw.title', {}, 'alloy_editor'), + udwContentDiscoveredMethod: 'addEmbed', + udwConfigName: 'richtext_embed', +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedupdate.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedupdate.js new file mode 100644 index 00000000..d1ac9a8d --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-embedupdate.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzEmbedDiscoverContentButton from './base/ez-embeddiscovercontent'; + +export default class EzBtnEmbedUpdate extends EzEmbedDiscoverContentButton { + static get key() { + return 'ezembedupdate'; + } + + /** + * Updates the emebed element with the selected content in UDW. + * + * @method updateEmbed + * @param {EventFacade} e the contentDiscovered event facade + * @protected + */ + updateEmbed(items) { + const contentId = items[0].ContentInfo.Content._id; + const widget = this.getWidget(); + + this.setContentInfo(contentId); + widget.focus(); + widget.setWidgetContent(''); + widget.renderEmbedPreview(items[0].ContentInfo.Content.Name); + + ReactDOM.unmountComponentAtNode(document.querySelector('#react-udw')); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const css = 'ae-button ez-btn-ae ez-btn-ae--embedupdate ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnEmbedUpdate.key] = AlloyEditor.EzBtnEmbedUpdate = EzBtnEmbedUpdate; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnEmbedUpdate = EzBtnEmbedUpdate; + +EzBtnEmbedUpdate.defaultProps = { + udwTitle: Translator.trans(/*@Desc("Select a content to embed")*/ 'embed_update_btn.udw.title', {}, 'alloy_editor'), + udwContentDiscoveredMethod: 'updateEmbed', + udwConfigName: 'richtext_embed', + label: Translator.trans(/*@Desc("Select another content item")*/ 'embed_update_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-heading.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-heading.js new file mode 100644 index 00000000..029b73bc --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-heading.js @@ -0,0 +1,57 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzButton from './base/ez-button'; + +export default class EzBtnHeading extends EzButton { + static get key() { + return 'ezheading'; + } + + /** + * Executes the eZAppendContent to add a heading element in the editor. + * + * @method addHeading + */ + addHeading() { + this.execCommand({ + tagName: 'h1', + }); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const css = 'ae-button ez-btn-ae ez-btn-ae--heading ' + this.getStateClasses(); + const label = Translator.trans(/*@Desc("Heading")*/ 'heading_btn.label', {}, 'alloy_editor'); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnHeading.key] = AlloyEditor.EzBtnHeading = EzBtnHeading; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnHeading = EzBtnHeading; + +EzBtnHeading.propTypes = { + command: PropTypes.string, + modifiesSelection: PropTypes.bool, +}; + +EzBtnHeading.defaultProps = { + command: 'eZAddContent', + modifiesSelection: true, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-image.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-image.js new file mode 100644 index 00000000..e027d1ef --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-image.js @@ -0,0 +1,92 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzEmbedImageButton from './base/ez-embedimage'; + +export default class EzBtnImage extends EzEmbedImageButton { + static get key() { + return 'ezimage'; + } + + /** + * Checks if the command is disabled in the current selection. + * + * @method isDisabled + * @return {Boolean} True if the command is disabled, false otherwise. + */ + isDisabled() { + return !this.props.editor.get('nativeEditor').ezembed.canBeAdded(); + } + + /** + * Executes the command generated by the ezembed plugin and set the + * correct value based on the choosen image. + * + * @method addImage + * @param {Array} items the result of the choice in the UDW + */ + addImage(items) { + const content = items[0].ContentInfo.Content; + + if (navigator.userAgent.indexOf('Chrome') > -1) { + const scrollY = window.pageYOffset; + + this.execCommand(); + window.scroll(window.pageXOffset, scrollY); + } else { + this.execCommand(); + } + + this.setContentInfo(content._id); + + const widget = this.getWidget() + .setConfig('size', 'medium') + .setImageType() + .setWidgetContent(''); + widget.loadImagePreviewFromCurrentVersion(content.CurrentVersion._href, content.Name); + widget.setFocused(true); + + ReactDOM.unmountComponentAtNode(document.querySelector('#react-udw')); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const css = 'ae-button ez-btn-ae ez-btn-ae--image ' + this.getStateClasses(), + disabled = this.isDisabled(); + const label = Translator.trans(/*@Desc("Image")*/ 'image_btn.label', {}, 'alloy_editor'); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnImage.key] = AlloyEditor.EzBtnImage = EzBtnImage; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnImage = EzBtnImage; + +EzBtnImage.defaultProps = { + command: 'ezembed', + modifiesSelection: true, + udwTitle: Translator.trans(/*@Desc("Select an image to embed")*/ 'image_btn.udw.label', {}, 'alloy_editor'), + udwContentDiscoveredMethod: 'addImage', + udwConfigName: 'richtext_embed_image', + udwLoadContent: true, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagelink.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagelink.js new file mode 100644 index 00000000..fc6a2299 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagelink.js @@ -0,0 +1,66 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnImageLink extends AlloyEditor.ButtonLink { + constructor(props) { + super(props); + + this.requestExclusive = this.requestExclusive.bind(this); + } + + static get key() { + return 'ezimagelink'; + } + + getWidget() { + const editor = this.props.editor.get('nativeEditor'); + const wrapper = editor.getSelection().getStartElement(); + + return editor.widgets.getByElement(wrapper); + } + + requestExclusive() { + const widget = this.getWidget(); + + widget.setLinkEditState(); + widget.setFocused(true); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + if (this.getWidget().isEditingLink()) { + const props = this.mergeButtonCfgProps(); + + return ; + } + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnImageLink.key] = AlloyEditor.EzBtnImageLink = EzBtnImageLink; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnImageLink = EzBtnImageLink; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagelinkedit.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagelinkedit.js new file mode 100644 index 00000000..a1e4e3c5 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagelinkedit.js @@ -0,0 +1,94 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzBtnLinkEdit from './ez-btn-linkedit'; + +export default class EzBtnImageLinkEdit extends EzBtnLinkEdit { + constructor(props) { + super(props); + + this.widget = this.getWidget(); + } + + static get key() { + return 'ezimagelinkedit'; + } + + componentWillUnmount() { + if (!this.state.discoveringContent && this.state.isTemporary) { + this.removeLink(); + } + + this.widget.removeLinkEditState(); + this.props.cancelExclusive(); + } + + getInitialState() { + const widget = this.getWidget(); + const linkHref = widget.getEzLinkAttribute('href'); + const linkTarget = widget.getEzLinkAttribute('target'); + const linkTitle = widget.getEzLinkAttribute('title'); + const isTemporary = widget.getEzLinkAttribute('data-ez-temporary-link'); + + return { + linkHref: linkHref || '', + linkTarget: linkTarget || '', + linkTitle: linkTitle || '', + isTemporary: isTemporary || false, + }; + } + + getWidget() { + const editor = this.props.editor.get('nativeEditor'); + const wrapper = editor.getSelection().getStartElement(); + + return editor.widgets.getByElement(wrapper); + } + + udwOnConfirm(udwContainer, items) { + this.widget.setEzLinkAttribute('href', 'ezlocation://' + items[0].id); + this.widget.setLinkEditState(); + this.widget.setFocused(true); + + ReactDOM.unmountComponentAtNode(udwContainer); + } + + removeLink() { + const link = this.widget.getEzLinkElement(); + + link.remove(); + + this.widget.removeLinkEditState(); + this.widget.removeIsLinkedState(); + this.widget.setFocused(true); + + this.props.cancelExclusive(); + } + + updateLink() { + const { linkHref, linkTarget, linkTitle } = this.state; + const hrefMethodName = linkHref === '' ? 'removeEzLinkAttribute' : 'setEzLinkAttribute'; + const targetMethodName = linkTarget === '' ? 'removeEzLinkAttribute' : 'setEzLinkAttribute'; + const titleMethodName = linkTitle === '' ? 'removeEzLinkAttribute' : 'setEzLinkAttribute'; + + this.widget[hrefMethodName]('href', linkHref); + this.widget[hrefMethodName]('data-cke-saved-href', linkHref); + this.widget[targetMethodName]('target', linkTarget); + this.widget[titleMethodName]('title', linkTitle); + + this.widget.removeEzLinkAttribute('data-ez-temporary-link'); + this.widget.removeLinkEditState(); + this.widget.setIsLinkedState(); + + this.widget.setFocused(true); + + this.props.cancelExclusive(); + } +} + +AlloyEditor.Buttons[EzBtnImageLinkEdit.key] = AlloyEditor.EzBtnImageLinkEdit = EzBtnImageLinkEdit; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnImageLinkEdit = EzBtnImageLinkEdit; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imageupdate.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imageupdate.js new file mode 100644 index 00000000..c11584c4 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imageupdate.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzEmbedImageButton from './base/ez-embedimage'; + +export default class EzBtnImageUpdate extends EzEmbedImageButton { + static get key() { + return 'ezimageupdate'; + } + + /** + * Updates the image element with the selected content in UDW. + * + * @method updateImage + * @param {Array} items the result of the choice in the UDW + * @protected + */ + updateImage(items) { + const contentId = items[0].ContentInfo.Content._id; + const content = items[0].ContentInfo.Content; + const widget = this.getWidget(); + + this.setContentInfo(contentId); + widget.focus(); + widget.setWidgetContent(''); + widget.loadImagePreviewFromCurrentVersion(content.CurrentVersion._href, content.Name); + + ReactDOM.unmountComponentAtNode(document.querySelector('#react-udw')); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const css = 'ae-button ez-btn-ae ez-btn-ae--imageupdate ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnImageUpdate.key] = AlloyEditor.EzBtnImageUpdate = EzBtnImageUpdate; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnImageUpdate = EzBtnImageUpdate; + +EzBtnImageUpdate.defaultProps = { + udwTitle: Translator.trans(/*@Desc("Select an image to embed")*/ 'image_update_btn.udw.title', {}, 'alloy_editor'), + udwContentDiscoveredMethod: 'updateImage', + udwConfigName: 'richtext_embed_image', + label: Translator.trans(/*@Desc("Select another image item")*/ 'image_update_btn.label', {}, 'alloy_editor'), +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagevariation.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagevariation.js new file mode 100644 index 00000000..2691280b --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-imagevariation.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzEmbedImageButton from './base/ez-embedimage'; + +export default class EzBtnImageVariation extends EzEmbedImageButton { + static get key() { + return 'ezimagevariation'; + } + + /** + * Change event handler. It updates the image in the editor so that the + * newly choosen variation is used. + * + * @method updateImage + * @protected + * @param {Object} event + */ + updateImage(event) { + const widget = this.getWidget(); + const newVariation = event.target.value; + + widget.setConfig('size', newVariation).setWidgetContent(''); + widget.focus(); + widget.loadImageVariation(widget.variations[newVariation].href); + } + + /** + * Returns the options to add to the drop down list. + * + * @method getImageVariationOptions + * @return Array + */ + getImageVariationOptions() { + return Object.keys(eZ.adminUiConfig.imageVariations).map((variation) => ( + + )); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnImageVariation.key] = AlloyEditor.EzBtnImageVariation = EzBtnImageVariation; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnImageVariation = EzBtnImageVariation; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-edit.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-edit.js new file mode 100644 index 00000000..61376ba1 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-edit.js @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; +import EzBtnCustomTagEdit from './ez-btn-customtag-edit'; + +export default class EzBtnInlineCustomTagEdit extends EzBtnCustomTagEdit { + getUpdateBtnName() { + return `ezBtn${this.customTagName.charAt(0).toUpperCase() + this.customTagName.slice(1)}Update`; + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnInlineCustomTagEdit = EzBtnInlineCustomTagEdit; + +EzBtnInlineCustomTagEdit.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-update.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-update.js new file mode 100644 index 00000000..c0d3b8ac --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag-update.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import EzBtnCustomTagUpdate from './ez-btn-customtag-update'; + +export default class EzBtnInlineCustomTagUpdate extends EzBtnCustomTagUpdate { + /** + * Creates the custom tag in AlloyEditor. + * + * @method saveCustomTag + */ + saveCustomTag() { + const { createNewTag, editor } = this.props; + const nativeEditor = editor.get('nativeEditor'); + const selection = nativeEditor.getSelectedHtml(); + + nativeEditor.unlockSelection(true); + + if (createNewTag) { + this.execCommand(); + } + + const widget = this.getWidget() || this.widget; + const configValues = Object.assign({}, this.state.values); + + widget.setFocused(true); + + if (createNewTag) { + const firstChild = selection.getFirst(); + const isNodeElement = firstChild.type === CKEDITOR.NODE_ELEMENT; + const content = isNodeElement && firstChild.is('table') ? selection.$.textContent : selection.getHtml(); + + widget.setName(this.customTagName); + widget.setWidgetContent(content); + widget.renderIcon(); + } + + widget.clearConfig(); + + Object.keys(this.attributes).forEach((key) => { + widget.setConfig(key, configValues[key].value); + }); + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnInlineCustomTagUpdate = EzBtnInlineCustomTagUpdate; + +EzBtnInlineCustomTagUpdate.defaultProps = { + command: 'ezinlinecustomtag', + modifiesSelection: true, +}; + +EzBtnInlineCustomTagUpdate.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag.js new file mode 100644 index 00000000..6ebfa29a --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-inlinecustomtag.js @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; +import EzBtnCustomTag from './ez-btn-customtag'; + +export default class EzBtnInlineCustomTag extends EzBtnCustomTag { + getUpdateBtnName() { + return `ezBtn${this.customTagName.charAt(0).toUpperCase() + this.customTagName.slice(1)}Update`; + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnInlineCustomTag = EzBtnInlineCustomTag; + +EzBtnInlineCustomTag.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-italic.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-italic.js new file mode 100644 index 00000000..8b4e9690 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-italic.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnItalic extends AlloyEditor.ButtonItalic { + static get key() { + return 'ezitalic'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnItalic.key] = AlloyEditor.EzBtnItalic = EzBtnItalic; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnItalic = EzBtnItalic; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-link.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-link.js new file mode 100644 index 00000000..3a0ce700 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-link.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnLink extends AlloyEditor.ButtonLink { + static get key() { + return 'ezlink'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + if (this.props.renderExclusive) { + const props = this.mergeButtonCfgProps(); + + return ; + } + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnLink.key] = AlloyEditor.EzBtnLink = EzBtnLink; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnLink = EzBtnLink; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-linkedit.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-linkedit.js new file mode 100644 index 00000000..af5f24f8 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-linkedit.js @@ -0,0 +1,503 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnLinkEdit extends Component { + constructor(props) { + super(props); + + this.state = this.getInitialState(); + } + + static get key() { + return 'ezlinkedit'; + } + + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState(this.getInitialState()); + } + + componentWillUnmount() { + if (!this.state.discoveringContent && this.state.isTemporary) { + this.removeLink(); + } + } + + /** + * Lifecycle. Invoked once before the component is mounted. + * The return value will be used as the initial value of this.state. + * + * @method getInitialState + */ + getInitialState() { + const linkUtils = new CKEDITOR.Link(this.props.editor.get('nativeEditor'), { appendProtocol: false }); + let link = linkUtils.getFromSelection(); + let href = ''; + let target = ''; + let title = ''; + let isTemporary = false; + + if (link) { + href = link.getAttribute('href'); + target = link.hasAttribute('target') ? link.getAttribute('target') : target; + title = link.getAttribute('title'); + isTemporary = link.hasAttribute('data-ez-temporary-link'); + } else { + linkUtils.create(href, { 'data-ez-temporary-link': true }); + link = linkUtils.getFromSelection(); + isTemporary = true; + } + + return { + element: link, + linkHref: href, + linkTarget: target, + linkTitle: title, + isTemporary: isTemporary, + }; + } + + udwOnConfirm(items) { + console.log(items); + this.state.element.setAttribute('href', 'ezlocation://' + items[0].id); + + this.invokeWithFixedScrollbar(() => { + this.focusEditedLink(); + }); + } + + /** + * Runs the Universal Discovery Widget so that the user can pick a + * Content. + * + * @method selectContent + * @protected + */ + selectContent() { + const openUDW = () => { + const udwContainer = document.querySelector('#react-udw'); + const token = document.querySelector('meta[name="CSRF-Token"]').content; + const siteaccess = document.querySelector('meta[name="SiteAccess"]').content; + const config = JSON.parse(document.querySelector(`[data-udw-config-name="richtext_embed"]`).dataset.udwConfig); + const title = Translator.trans(/*@Desc("Select content")*/ 'link_edit_btn.udw.title', {}, 'alloy_editor'); + const selectContent = eZ.alloyEditor.callbacks.selectContent; + const mergedConfig = Object.assign( + { + onConfirm: this.udwOnConfirm.bind(this), + title, + multiple: false, + startingLocationId: window.eZ.adminUiConfig.universalDiscoveryWidget.startingLocationId, + restInfo: { token, siteaccess }, + }, + config + ); + console.log('dd'); + if (typeof selectContent === 'function') { + selectContent(mergedConfig); + } + }; + + this.setState( + { + discoveringContent: true, + }, + openUDW.bind(this) + ); + } + + /** + * Gives the focus to the edited link by moving the caret in it. + * + * @method focusEditedLink + * @protected + */ + focusEditedLink() { + const editor = this.props.editor.get('nativeEditor'); + + editor.focus(); + editor.eZ.moveCaretToElement(editor, this.state.element); + editor.fire('actionPerformed', this); + + this.showUI(); + } + + /** + * Fires the editorInteraction event so that AlloyEditor editor + * UI remains visible and is updated. + * + * @method showUI + */ + showUI() { + const nativeEditor = this.props.editor.get('nativeEditor'); + + nativeEditor.fire('editorInteraction', { + editor: this.props.editor, + selectionData: { + element: this.state.element, + region: this.getRegion(), + }, + }); + } + + /** + * Returns the element region. + * + * @method getRegion + * @return {Object} + */ + getRegion() { + const scroll = this.state.element.getWindow().getScrollPosition(); + const region = this.state.element.getClientRect(); + + region.top += scroll.y; + region.bottom += scroll.y; + region.left += scroll.x; + region.right += scroll.x; + region.direction = CKEDITOR.SELECTION_TOP_TO_BOTTOM; + + return region; + } + + /** + * Lifecycle. Renders the row of the button. + * + * @method renderUDWRow + * @return {Object} The content which should be rendered. + */ + renderUDWRow() { + const selectContentLabel = Translator.trans( + /*@Desc("Select content")*/ 'link_edit_btn.button_row.select_content', + {}, + 'alloy_editor' + ); + const separatorLabel = Translator.trans(/*@Desc("or")*/ 'link_edit_btn.button_row.separator', {}, 'alloy_editor'); + const linkToLabel = Translator.trans(/*@Desc("Link to:")*/ 'link_edit_btn.button_row.link_to', {}, 'alloy_editor'); + const selectLabel = Translator.trans(/*@Desc("Select:")*/ 'link_edit_btn.button_row.select', {}, 'alloy_editor'); + const blockPlaceholderText = Translator.trans( + /*@Desc("Type or paste link here")*/ 'link_edit_btn.button_row.block.placeholder.text', + {}, + 'alloy_editor' + ); + + return ( +
+
+ + +
+
+ {separatorLabel} +
+
+ + +
+
+ ); + } + + /** + * Lifecycle. Renders the row of the button. + * + * @method renderInfoRow + * @return {Object} The content which should be rendered. + */ + renderInfoRow() { + const target = this.state.linkTarget; + const title = Translator.trans(/*@Desc("Title:")*/ 'link_edit_btn.info_row.title', {}, 'alloy_editor'); + const openInLabel = Translator.trans(/*@Desc("Open in:")*/ 'link_edit_btn.info_row.open_in.label', {}, 'alloy_editor'); + const sameTabLabel = Translator.trans(/*@Desc("Same tab")*/ 'link_edit_btn.info_row.same_tab', {}, 'alloy_editor'); + const newTabLabel = Translator.trans(/*@Desc("New tab")*/ 'link_edit_btn.info_row.new_tab', {}, 'alloy_editor'); + + return ( +
+
+ + +
+
+ {openInLabel} +
+ + +
+
+
+ ); + } + + /** + * Lifecycle. Renders the row of the button. + * + * @method renderActionRow + * @return {Object} The content which should be rendered. + */ + renderActionRow() { + const removeLabel = Translator.trans(/*@Desc("Remove")*/ 'link_edit_btn.action_row.remove', {}, 'alloy_editor'); + const saveLabel = Translator.trans(/*@Desc("Save")*/ 'link_edit_btn.action_row.save', {}, 'alloy_editor'); + + return ( +
+
+ + +
+
+ ); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + let containerClass = 'ez-ae-edit-link'; + + if (this.state.linkHref) { + containerClass += ' is-linked'; + } + + return ( +
+ {this.renderUDWRow()} + {this.renderInfoRow()} + {this.renderActionRow()} +
+ ); + } + + /** + * Clears the link input. This only changes the component internal + * state, but does not affect the link element of the editor. Only the + * removeLink and updateLink methods are translated to the editor + * element. + * + * @method clearLink + */ + clearLink() { + this.setState({ linkHref: '' }); + } + + /** + * Monitors key interaction inside the input element to respond to the + * keys: + * - Enter: Creates/updates the link. + * - Escape: Discards the changes. + * + * @method handleKeyDown + * @param {SyntheticEvent} event The keyboard event. + */ + handleKeyDown(event) { + if (event.keyCode === 13 || event.keyCode === 27) { + event.preventDefault(); + } + + if (event.keyCode === 13 && event.target.value) { + this.saveLink(); + } else if (event.keyCode === 27) { + const editor = this.props.editor.get('nativeEditor'); + new CKEDITOR.Link(editor).advanceSelection(); + + this.invokeWithFixedScrollbar(() => { + editor.fire('actionPerformed', this); + }); + } + } + + /** + * Updates the component state when the link input changes on user + * interaction. + * + * @method setHref + * @param {SyntheticEvent} event The change event. + */ + setHref(event) { + this.setState({ linkHref: event.target.value }); + } + + /** + * Sets the link title + * + * @method setTitle + * @param {SyntheticEvent} event The change event. + */ + setTitle(event) { + this.setState({ linkTitle: event.target.value }); + } + + /** + * Sets the target of the link + * + * @method setTarget + * @param {SyntheticEvent} event The change event. + */ + setTarget(event) { + this.setState({ linkTarget: event.target.value }); + } + + /** + * Removes the link in the editor element. + * + * @method removeLink + */ + removeLink() { + const editor = this.props.editor.get('nativeEditor'); + const linkUtils = new CKEDITOR.Link(editor); + const selection = editor.getSelection(); + const bookmarks = selection.createBookmarks(); + + linkUtils.remove(this.state.element, { advance: true }); + + selection.selectBookmarks(bookmarks); + + this.props.cancelExclusive(); + + this.invokeWithFixedScrollbar(() => { + editor.fire('actionPerformed', this); + }); + } + + /** + * Saves the link with the current href, title and target. + * + * @method saveLink + */ + saveLink() { + this.setState( + { + isTemporary: false, + }, + () => this.updateLink() + ); + } + + /** + * Updates the link in the editor element. If the element didn't exist + * previously, it will create a new element with the href specified + * in the link input. + * + * @method updateLink + */ + updateLink() { + const editor = this.props.editor.get('nativeEditor'); + const linkUtils = new CKEDITOR.Link(editor); + const linkAttrs = { + target: this.state.linkTarget, + title: this.state.linkTitle, + 'data-ez-temporary-link': this.state.isTemporary ? true : null, + }; + const modifySelection = { advance: true }; + + if (this.state.linkHref) { + linkAttrs.href = this.state.linkHref; + linkUtils.update(linkAttrs, this.state.element, modifySelection); + + this.invokeWithFixedScrollbar(() => { + editor.fire('actionPerformed', this); + }); + } + + // We need to cancelExclusive with the bound parameters in case the + // button is used inside another component in exclusive mode (such + // is the case of the link button) + this.props.cancelExclusive(); + this.showUI(); + } + + /** + * Saves current scrollbar position, invokes callback function and scrolls + * to the saved position afterward. + * + * @method invokeWithFixedScrollbar + * @param {Function} callback invoked after saving current scrollbar position + */ + invokeWithFixedScrollbar(callback) { + if (navigator.userAgent.indexOf('Chrome') > -1) { + const scrollY = window.pageYOffset; + + callback(); + window.scroll(window.pageXOffset, scrollY); + } else { + callback(); + } + } +} + +AlloyEditor.Buttons[EzBtnLinkEdit.key] = AlloyEditor.ButtonLinkEdit = EzBtnLinkEdit; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnLinkEdit = EzBtnLinkEdit; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-movedown.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-movedown.js new file mode 100644 index 00000000..aa475eb9 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-movedown.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnMoveDown extends Component { + static get key() { + return 'ezmovedown'; + } + + /** + * Executes the eZMoveDown command. + * + * @method moveDown + */ + moveDown() { + const editor = this.props.editor.get('nativeEditor'); + + editor.execCommand('eZMoveDown'); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const title = Translator.trans(/*@Desc("Move Down")*/ 'move_down_btn.title', {}, 'alloy_editor'); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnMoveDown.key] = AlloyEditor.EzBtnMoveDown = EzBtnMoveDown; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnMoveDown = EzBtnMoveDown; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-moveup.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-moveup.js new file mode 100644 index 00000000..c1e6f5d0 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-moveup.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnMoveUp extends Component { + static get key() { + return 'ezmoveup'; + } + + /** + * Executes the eZMoveUp command. + * + * @method moveUp + */ + moveUp() { + const editor = this.props.editor.get('nativeEditor'); + + editor.execCommand('eZMoveUp'); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const title = Translator.trans(/*@Desc("Move Up")*/ 'move_up_btn.title', {}, 'alloy_editor'); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnMoveUp.key] = AlloyEditor.EzBtnMoveUp = EzBtnMoveUp; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnMoveUp = EzBtnMoveUp; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-orderedlist.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-orderedlist.js new file mode 100644 index 00000000..b0eb6421 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-orderedlist.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzButton from './base/ez-button'; + +export default class EzBtnOrderedList extends EzButton { + static get key() { + return 'ezorderedlist'; + } + + /** + * Executes the eZAppendContent command to add an unordered list containing + * an empty list item. + * + * @method addList + */ + addList() { + this.execCommand({ + tagName: 'ol', + content: '
  • ', + focusElement: 'li', + }); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const label = Translator.trans(/*@Desc("List")*/ 'ordered_list_btn.label', {}, 'alloy_editor'); + const css = 'ae-button ez-btn-ae ez-btn-ae--ordered-list ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnOrderedList.key] = AlloyEditor.EzBtnOrderedList = EzBtnOrderedList; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnOrderedList = EzBtnOrderedList; + +EzBtnOrderedList.propTypes = { + command: PropTypes.string, + modifiesSelection: PropTypes.bool, +}; + +EzBtnOrderedList.defaultProps = { + command: 'eZAddContent', + modifiesSelection: true, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-paragraph.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-paragraph.js new file mode 100644 index 00000000..e6476364 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-paragraph.js @@ -0,0 +1,58 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzButton from './base/ez-button'; + +export default class EzBtnParagraph extends EzButton { + static get key() { + return 'ezparagraph'; + } + + /** + * Executes the eZAppendContent to add a paragraph element in the editor. + * + * @method addParagraph + */ + addParagraph() { + this.execCommand({ + tagName: 'p', + content: '
    ', + }); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const label = Translator.trans(/*@Desc("Paragraph")*/ 'paragraph_btn.label', {}, 'alloy_editor'); + const css = 'ae-button ez-btn-ae ez-btn-ae--paragraph ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnParagraph.key] = AlloyEditor.EzBtnParagraph = EzBtnParagraph; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnParagraph = EzBtnParagraph; + +EzBtnParagraph.propTypes = { + command: PropTypes.string, + modifiesSelection: PropTypes.bool, +}; + +EzBtnParagraph.defaultProps = { + command: 'eZAddContent', + modifiesSelection: true, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-quote.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-quote.js new file mode 100644 index 00000000..b4f665b8 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-quote.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnQuote extends AlloyEditor.ButtonQuote { + static get key() { + return 'ezquote'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnQuote.key] = AlloyEditor.EzBtnQuote = EzBtnQuote; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnQuote = EzBtnQuote; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-removeblock.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-removeblock.js new file mode 100644 index 00000000..63cadb71 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-removeblock.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzButton from './base/ez-button'; + +export default class EzBtnBlockRemove extends EzButton { + static get key() { + return 'ezblockremove'; + } + + /** + * Executes the eZRemoveBlock to remove block. + * + * @method removeBlock + * @protected + */ + removeBlock(data) { + this.execCommand(data); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const title = Translator.trans(/*@Desc("Remove block")*/ 'remove_block_btn.title', {}, 'alloy_editor'); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnBlockRemove.key] = AlloyEditor.EzBtnBlockRemove = EzBtnBlockRemove; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnBlockRemove = EzBtnBlockRemove; + +EzBtnBlockRemove.propTypes = { + command: PropTypes.string, + modifiesSelection: PropTypes.bool, +}; + +EzBtnBlockRemove.defaultProps = { + command: 'eZRemoveBlock', + modifiesSelection: true, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-strike.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-strike.js new file mode 100644 index 00000000..d55f29bd --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-strike.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnStrike extends AlloyEditor.ButtonStrike { + static get key() { + return 'ezstrike'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnStrike.key] = AlloyEditor.EzBtnStrike = EzBtnStrike; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnStrike = EzBtnStrike; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-subscript.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-subscript.js new file mode 100644 index 00000000..8c5f82db --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-subscript.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnSubscript extends AlloyEditor.ButtonSubscript { + static get key() { + return 'ezsubscript'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnSubscript.key] = AlloyEditor.EzBtnSubscript = EzBtnSubscript; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnSubscript = EzBtnSubscript; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-superscript.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-superscript.js new file mode 100644 index 00000000..2c8a29f7 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-superscript.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnSuperscript extends AlloyEditor.ButtonSuperscript { + static get key() { + return 'ezsuperscript'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnSuperscript.key] = AlloyEditor.EzBtnSuperscript = EzBtnSuperscript; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnSuperscript = EzBtnSuperscript; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-table.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-table.js new file mode 100644 index 00000000..db79a96d --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-table.js @@ -0,0 +1,45 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnTable extends Component { + static get key() { + return 'eztable'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + if (this.props.renderExclusive) { + return ; + } + + const label = Translator.trans(/*@Desc("Table")*/ 'table_btn.label', {}, 'alloy_editor'); + const css = 'ae-button ez-btn-ae ez-btn-ae--table'; + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnTable.key] = AlloyEditor.EzBtnTable = EzBtnTable; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnTable = EzBtnTable; + +EzBtnTable.propTypes = { + editor: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + tabIndex: PropTypes.number.isRequired, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablecell.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablecell.js new file mode 100644 index 00000000..22acc69e --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablecell.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnTableCell extends AlloyEditor.ButtonTableCell { + static get key() { + return 'eztablecell'; + } + + render() { + let buttonCommandsList; + let buttonCommandsListId; + + if (this.props.expanded) { + buttonCommandsListId = 'tableCellList'; + buttonCommandsList = ( + + ); + } + + return ( +
    + + {buttonCommandsList} +
    + ); + } +} + +AlloyEditor.Buttons[EzBtnTableCell.key] = AlloyEditor.EzBtnTableCell = EzBtnTableCell; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnTableCell = EzBtnTableCell; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablecolumn.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablecolumn.js new file mode 100644 index 00000000..7e708120 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablecolumn.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnTableColumn extends AlloyEditor.ButtonTableColumn { + static get key() { + return 'eztablecolumn'; + } + + render() { + let buttonCommandsList; + let buttonCommandsListId; + + if (this.props.expanded) { + buttonCommandsListId = 'tableColumnList'; + buttonCommandsList = ( + + ); + } + + return ( +
    + + {buttonCommandsList} +
    + ); + } +} + +AlloyEditor.Buttons[EzBtnTableColumn.key] = AlloyEditor.EzBtnTableColumn = EzBtnTableColumn; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnTableColumn = EzBtnTableColumn; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tableremove.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tableremove.js new file mode 100644 index 00000000..77464ba1 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tableremove.js @@ -0,0 +1,32 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnTableRemove extends AlloyEditor.ButtonTableRemove { + static get key() { + return 'eztableremove'; + } + + render() { + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnTableRemove.key] = AlloyEditor.EzBtnTableRemove = EzBtnTableRemove; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnTableRemove = EzBtnTableRemove; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablerow.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablerow.js new file mode 100644 index 00000000..22d7b925 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-tablerow.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnTableRow extends AlloyEditor.ButtonTableRow { + static get key() { + return 'eztablerow'; + } + + render() { + let buttonCommandsList; + let buttonCommandsListId; + + if (this.props.expanded) { + buttonCommandsListId = 'tableRowList'; + buttonCommandsList = ( + + ); + } + + return ( +
    + + {buttonCommandsList} +
    + ); + } +} + +AlloyEditor.Buttons[EzBtnTableRow.key] = AlloyEditor.EzBtnTableRow = EzBtnTableRow; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnTableRow = EzBtnTableRow; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-underline.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-underline.js new file mode 100644 index 00000000..b6fb6704 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-underline.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; + +export default class EzBtnUnderline extends AlloyEditor.ButtonUnderline { + static get key() { + return 'ezunderline'; + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const cssClass = 'ae-button ez-btn-ae ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnUnderline.key] = AlloyEditor.EzBtnUnderline = EzBtnUnderline; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnUnderline = EzBtnUnderline; diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-unorderedlist.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-unorderedlist.js new file mode 100644 index 00000000..069eeb70 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/ez-btn-unorderedlist.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import AlloyEditor from 'alloyeditor'; +import EzButton from './base/ez-button'; + +export default class EzBtnUnorderedList extends EzButton { + static get key() { + return 'ezunorderedlist'; + } + + /** + * Executes the eZAppendContent command to add an unordered list containing + * an empty list item. + * + * @method addList + * @protected + */ + addList() { + this.execCommand({ + tagName: 'ul', + content: '
  • ', + focusElement: 'li', + }); + } + + /** + * Lifecycle. Renders the UI of the button. + * + * @method render + * @return {Object} The content which should be rendered. + */ + render() { + const label = Translator.trans(/*@Desc("List")*/ 'unordered_list_btn.label', {}, 'alloy_editor'); + const css = 'ae-button ez-btn-ae ez-btn-ae--unordered-list ' + this.getStateClasses(); + + return ( + + ); + } +} + +AlloyEditor.Buttons[EzBtnUnorderedList.key] = AlloyEditor.EzBtnUnorderedList = EzBtnUnorderedList; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezBtnUnorderedList = EzBtnUnorderedList; + +EzBtnUnorderedList.propTypes = { + command: PropTypes.string, + modifiesSelection: PropTypes.bool, +}; + +EzBtnUnorderedList.defaultProps = { + command: 'eZAddContent', + modifiesSelection: true, +}; diff --git a/src/bundle/Resources/public/js/OnlineEditor/core/base-richtext.js b/src/bundle/Resources/public/js/OnlineEditor/core/base-richtext.js new file mode 100644 index 00000000..8c9bbcd8 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/core/base-richtext.js @@ -0,0 +1,383 @@ +(function(global, doc, eZ, CKEDITOR, AlloyEditor) { + const HTML_NODE = 1; + const TEXT_NODE = 3; + + class BaseRichText { + constructor() { + this.ezNamespace = 'http://ez.no/namespaces/ezpublish5/xhtml5/edit'; + this.xhtmlNamespace = 'http://www.w3.org/1999/xhtml'; + this.customTags = Object.keys(eZ.adminUiConfig.richTextCustomTags).filter( + (key) => !eZ.adminUiConfig.richTextCustomTags[key].isInline + ); + this.inlineCustomTags = Object.keys(eZ.adminUiConfig.richTextCustomTags).filter( + (key) => eZ.adminUiConfig.richTextCustomTags[key].isInline + ); + this.alloyEditorExtraButtons = { + ezadd: [], + link: [], + text: [], + table: [], + tr: [], + td: [], + ...eZ.adminUiConfig.alloyEditor.extraButtons, + }; + this.attributes = global.eZ.adminUiConfig.alloyEditor.attributes; + this.classes = global.eZ.adminUiConfig.alloyEditor.classes; + this.customTagsToolbars = this.customTags.map((customTag) => { + const alloyEditorConfig = eZ.adminUiConfig.richTextCustomTags[customTag]; + + return new eZ.ezAlloyEditor.ezCustomTagConfig({ + name: customTag, + alloyEditor: alloyEditorConfig, + extraButtons: this.alloyEditorExtraButtons, + }); + }); + this.inlineCustomTagsToolbars = this.inlineCustomTags.map((customTag) => { + const alloyEditorConfig = eZ.adminUiConfig.richTextCustomTags[customTag]; + + return new eZ.ezAlloyEditor.ezInlineCustomTagConfig({ + name: customTag, + alloyEditor: alloyEditorConfig, + extraButtons: this.alloyEditorExtraButtons, + }); + }); + this.customStylesConfigurations = Object.entries(eZ.adminUiConfig.richTextCustomStyles).map( + ([customStyleName, customStyleConfig]) => { + return { + name: customStyleConfig.adminUiConfig.label, + style: { + element: customStyleConfig.adminUiConfig.inline ? 'span' : 'div', + attributes: { + 'data-ezelement': customStyleConfig.adminUiConfig.inline ? 'eztemplateinline' : 'eztemplate', + 'data-eztype': 'style', + 'data-ezname': customStyleName, + }, + }, + }; + } + ); + this.alloyEditorExtraPlugins = eZ.adminUiConfig.alloyEditor.extraPlugins; + + this.xhtmlify = this.xhtmlify.bind(this); + } + + getHTMLDocumentFragment(data) { + const fragment = doc.createDocumentFragment(); + const root = fragment.ownerDocument.createElement('div'); + const parsedHTML = new DOMParser().parseFromString(data, 'text/xml'); + const importChildNodes = (parent, element, skipElement) => { + let i; + let newElement; + + if (skipElement) { + newElement = parent; + } else { + if (element.nodeType === Node.ELEMENT_NODE) { + newElement = parent.ownerDocument.createElement(element.localName); + + for (i = 0; i !== element.attributes.length; i++) { + importChildNodes(newElement, element.attributes[i], false); + } + + parent.appendChild(newElement); + } else if (element.nodeType === Node.TEXT_NODE) { + parent.appendChild(parent.ownerDocument.createTextNode(element.nodeValue)); + + return; + } else if (element.nodeType === Node.ATTRIBUTE_NODE) { + parent.setAttribute(element.localName, element.value); + + return; + } else { + return; + } + } + + for (i = 0; i !== element.childNodes.length; i++) { + importChildNodes(newElement, element.childNodes[i], false); + } + }; + + if (!parsedHTML || !parsedHTML.documentElement || parsedHTML.querySelector('parsererror')) { + console.warn('Unable to parse the content of the RichText field'); + + return fragment; + } + + fragment.appendChild(root); + + importChildNodes(root, parsedHTML.documentElement, true); + return fragment; + } + + emptyEmbed(embedNode) { + let element = embedNode.firstChild; + let next; + let removeClass = () => {}; + + while (element) { + next = element.nextSibling; + if (!element.getAttribute || !element.getAttribute('data-ezelement')) { + embedNode.removeChild(element); + } + element = next; + } + + embedNode.classList.forEach((cl) => { + let prevRemoveClass = removeClass; + + if (cl.indexOf('is-embed-') === 0) { + removeClass = () => { + embedNode.classList.remove(cl); + prevRemoveClass(); + }; + } + }); + removeClass(); + } + + xhtmlify(data) { + const xmlDocument = doc.implementation.createDocument(this.xhtmlNamespace, 'html', null); + const htmlDoc = doc.implementation.createHTMLDocument(''); + const section = htmlDoc.createElement('section'); + let body = htmlDoc.createElement('body'); + + section.innerHTML = data; + body.appendChild(section); + body = xmlDocument.importNode(body, true); + xmlDocument.documentElement.appendChild(body); + + return body.innerHTML; + } + + clearCustomTag(customTag) { + const attributesNodes = customTag.querySelectorAll('[data-ezelement="ezattributes"]'); + const headers = customTag.querySelectorAll('.ez-custom-tag__header'); + + attributesNodes.forEach((attributesNode) => attributesNode.remove()); + headers.forEach((header) => header.remove()); + } + + clearAnchor(element) { + const icon = element.querySelector('.ez-icon--anchor'); + + if (icon) { + icon.remove(); + } else { + element.classList.remove('ez-has-anchor'); + } + } + + appendAnchorIcon(element) { + const container = doc.createElement('div'); + const icon = ` + + + `; + + container.insertAdjacentHTML('afterbegin', icon); + + const svg = new CKEDITOR.dom.element(container.querySelector('svg')); + const ckeditorElement = new CKEDITOR.dom.element(element); + + ckeditorElement.append(svg, true); + } + + clearInlineCustomTag(inlineCustomTag) { + const icons = inlineCustomTag.querySelectorAll('.ez-custom-tag__icon-wrapper'); + + icons.forEach((icon) => icon.remove()); + } + + init(container) { + const toolbarProps = { extraButtons: this.alloyEditorExtraButtons, attributes: this.attributes, classes: this.classes }; + const alloyEditor = AlloyEditor.editable(container.getAttribute('id'), { + toolbars: { + ezadd: { + buttons: [ + 'ezheading', + 'ezparagraph', + 'ezunorderedlist', + 'ezorderedlist', + 'ezimage', + 'ezembed', + 'eztable', + ...this.customTags, + ...this.alloyEditorExtraButtons['ezadd'], + ], + tabIndex: 2, + }, + styles: { + selections: [ + ...this.customTagsToolbars, + new eZ.ezAlloyEditor.ezLinkConfig(toolbarProps), + new eZ.ezAlloyEditor.ezTextConfig({ + customStyles: this.customStylesConfigurations, + inlineCustomTags: this.inlineCustomTags, + ...toolbarProps, + }), + ...this.inlineCustomTagsToolbars, + new eZ.ezAlloyEditor.ezParagraphConfig({ + customStyles: this.customStylesConfigurations, + ...toolbarProps, + }), + new eZ.ezAlloyEditor.ezFormattedConfig({ + customStyles: this.customStylesConfigurations, + ...toolbarProps, + }), + new eZ.ezAlloyEditor.ezCustomStyleConfig({ + customStyles: this.customStylesConfigurations, + ...toolbarProps, + }), + new eZ.ezAlloyEditor.ezHeadingConfig({ customStyles: this.customStylesConfigurations, ...toolbarProps }), + new eZ.ezAlloyEditor.ezListOrderedConfig({ + customStyles: this.customStylesConfigurations, + ...toolbarProps, + }), + new eZ.ezAlloyEditor.ezListUnorderedConfig({ + customStyles: this.customStylesConfigurations, + ...toolbarProps, + }), + new eZ.ezAlloyEditor.ezListItemConfig({ + customStyles: this.customStylesConfigurations, + ...toolbarProps, + }), + new eZ.ezAlloyEditor.ezEmbedInlineConfig(toolbarProps), + new eZ.ezAlloyEditor.ezTableConfig(toolbarProps), + new eZ.ezAlloyEditor.ezTableRowConfig(toolbarProps), + new eZ.ezAlloyEditor.ezTableCellConfig(toolbarProps), + new eZ.ezAlloyEditor.ezEmbedImageLinkConfig(toolbarProps), + new eZ.ezAlloyEditor.ezEmbedImageConfig(toolbarProps), + new eZ.ezAlloyEditor.ezEmbedConfig(toolbarProps), + ], + tabIndex: 1, + }, + }, + extraPlugins: + AlloyEditor.Core.ATTRS.extraPlugins.value + + ',' + + [ + 'ezaddcontent', + 'ezmoveelement', + 'ezremoveblock', + 'ezembed', + 'ezembedinline', + 'ezfocusblock', + 'ezcustomtag', + 'ezinlinecustomtag', + 'ezelementspath', + ...this.alloyEditorExtraPlugins, + ].join(','), + }); + const wrapper = this.getHTMLDocumentFragment(container.closest('.ez-data-source').querySelector('textarea').value); + const section = wrapper.childNodes[0]; + const nativeEditor = alloyEditor.get('nativeEditor'); + const saveRichText = () => { + const data = alloyEditor.get('nativeEditor').getData(); + const documentFragment = doc.createDocumentFragment(); + const root = doc.createElement('div'); + + root.innerHTML = data; + documentFragment.appendChild(root); + + documentFragment.querySelectorAll('[data-ezelement="ezembed"]').forEach(this.emptyEmbed); + documentFragment.querySelectorAll('[data-ezelement="ezembedinline"]').forEach(this.emptyEmbed); + documentFragment.querySelectorAll('[data-ezelement="eztemplate"]:not([data-eztype="style"])').forEach(this.clearCustomTag); + documentFragment.querySelectorAll('.ez-has-anchor').forEach(this.clearAnchor); + documentFragment + .querySelectorAll('[data-ezelement="eztemplateinline"]:not([data-eztype="style"])') + .forEach(this.clearInlineCustomTag); + + this.iterateThroughChildNodes(documentFragment, this.removeNodeInitializedState); + + container.closest('.ez-data-source').querySelector('textarea').value = this.xhtmlify(root.innerHTML).replace( + this.xhtmlNamespace, + this.ezNamespace + ); + + this.countWordsCharacters(container, documentFragment); + }; + + if (!section.hasChildNodes()) { + section.appendChild(doc.createElement('p')); + } + + nativeEditor.once('dataReady', () => container.querySelectorAll('.ez-has-anchor').forEach(this.appendAnchorIcon)); + + this.iterateThroughChildNodes(section, this.setNodeInitializedState); + this.countWordsCharacters(container, section); + nativeEditor.setData(section.innerHTML); + + nativeEditor.on('blur', saveRichText); + nativeEditor.on('change', saveRichText); + nativeEditor.on('customUpdate', saveRichText); + nativeEditor.on('editorInteraction', saveRichText); + + return alloyEditor; + } + + setNodeInitializedState(node) { + if (node.nodeType === HTML_NODE) { + node.setAttribute('data-ez-node-initialized', true); + } + } + + removeNodeInitializedState(node) { + if (node.nodeType === HTML_NODE) { + node.removeAttribute('data-ez-node-initialized'); + } + } + + countWordsCharacters(container, editorHtml) { + const counterWrapper = container.parentElement.querySelector('.ez-character-counter'); + + if (counterWrapper) { + const wordWrapper = counterWrapper.querySelector('.ez-character-counter__word-count'); + const charactersWrapper = counterWrapper.querySelector('.ez-character-counter__character-count'); + const words = this.getTextNodeValues(editorHtml); + + wordWrapper.innerText = words.length; + charactersWrapper.innerText = words.join(' ').length; + } + } + + getTextNodeValues(node) { + let values = []; + + const pushValue = (node) => { + if (node.nodeType === TEXT_NODE) { + const nodeValue = this.sanitize(node.nodeValue); + + values = values.concat(this.splitIntoWords(nodeValue)); + } + }; + + this.iterateThroughChildNodes(node, pushValue); + + return values; + } + + iterateThroughChildNodes(node, callback) { + if (typeof node.getAttribute === 'function' && node.getAttribute('data-ezelement') === 'ezconfig') { + return; + } + callback(node); + node = node.firstChild; + + while (node) { + this.iterateThroughChildNodes(node, callback); + node = node.nextSibling; + } + } + + sanitize(text) { + return text.replace(/[\u200B-\u200D\uFEFF]/g, ''); + } + + splitIntoWords(text) { + return text.split(' ').filter((word) => word.trim()); + } + } + + eZ.BaseRichText = BaseRichText; +})(window, window.document, window.eZ, window.CKEDITOR, window.AlloyEditor); diff --git a/src/bundle/Resources/public/js/OnlineEditor/core/ez-attributes.js b/src/bundle/Resources/public/js/OnlineEditor/core/ez-attributes.js new file mode 100644 index 00000000..162407d9 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/core/ez-attributes.js @@ -0,0 +1,43 @@ +(function(global, doc, eZ, AlloyEditor) { + const { attributes, classes } = eZ.adminUiConfig.alloyEditor; + const toolbarNames = new Set([...Object.keys(attributes), ...Object.keys(classes)]); + + toolbarNames.forEach((toolbarName) => { + const componentClassName = `ezBtn${toolbarName.charAt(0).toUpperCase() + toolbarName.slice(1)}`; + const editComponentClassName = `${componentClassName}Edit`; + const updateComponentClassName = `${componentClassName}Update`; + const toolbarClasses = classes[toolbarName] || {}; + const toolbarAttributes = attributes[toolbarName]; + + class ButtonAttributesEdit extends eZ.ezAlloyEditor.ezBtnAttributesEdit { + constructor(props) { + super(props); + + this.toolbarName = toolbarName; + this.classes = toolbarClasses; + this.attributes = toolbarAttributes || {}; + } + + static get key() { + return `${toolbarName}edit`; + } + } + + class ButtonAttributesUpdate extends eZ.ezAlloyEditor.ezBtnAttributesUpdate { + constructor(props) { + super(props); + + this.toolbarName = toolbarName; + this.classes = toolbarClasses; + this.attributes = toolbarAttributes || {}; + } + + static get key() { + return `${toolbarName}update`; + } + } + + AlloyEditor.Buttons[ButtonAttributesEdit.key] = AlloyEditor[editComponentClassName] = ButtonAttributesEdit; + AlloyEditor.Buttons[ButtonAttributesUpdate.key] = AlloyEditor[updateComponentClassName] = ButtonAttributesUpdate; + }); +})(window, window.document, window.eZ, window.AlloyEditor); diff --git a/src/bundle/Resources/public/js/OnlineEditor/core/ez-custom-tags.js b/src/bundle/Resources/public/js/OnlineEditor/core/ez-custom-tags.js new file mode 100644 index 00000000..b61b2b6b --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/core/ez-custom-tags.js @@ -0,0 +1,68 @@ +(function(global, doc, eZ, AlloyEditor) { + Object.entries(eZ.adminUiConfig.richTextCustomTags).forEach(([customTag, tagConfig]) => { + const isInline = tagConfig.isInline; + const componentClassName = `ezBtn${customTag.charAt(0).toUpperCase() + customTag.slice(1)}`; + const editComponentClassName = `${componentClassName}Edit`; + const updateComponentClassName = `${componentClassName}Update`; + const buttonCustomTagBaseClass = isInline ? eZ.ezAlloyEditor.ezBtnInlineCustomTag : eZ.ezAlloyEditor.ezBtnCustomTag; + const buttonCustomTagEditBaseClass = isInline ? eZ.ezAlloyEditor.ezBtnInlineCustomTagEdit : eZ.ezAlloyEditor.ezBtnCustomTagEdit; + const buttonCustomTagUpdateBaseClass = isInline + ? eZ.ezAlloyEditor.ezBtnInlineCustomTagUpdate + : eZ.ezAlloyEditor.ezBtnCustomTagUpdate; + + class ButtonCustomTag extends buttonCustomTagBaseClass { + constructor(props) { + super(props); + + const values = {}; + + if (tagConfig.attributes) { + Object.entries(tagConfig.attributes).forEach(([attr, value]) => { + values[attr] = { + value: value.defaultValue, + }; + }); + } + + this.label = tagConfig.label; + this.icon = tagConfig.icon || '/bundles/ezplatformadminui/img/ez-icons.svg#tag'; + this.customTagName = customTag; + this.values = values; + } + + static get key() { + return customTag; + } + } + + class ButtonCustomTagEdit extends buttonCustomTagEditBaseClass { + constructor(props) { + super(props); + + this.customTagName = customTag; + this.attributes = tagConfig.attributes || {}; + } + + static get key() { + return `${customTag}edit`; + } + } + + class ButtonCustomTagUpdate extends buttonCustomTagUpdateBaseClass { + constructor(props) { + super(props); + + this.customTagName = customTag; + this.attributes = tagConfig.attributes || {}; + } + + static get key() { + return `${customTag}update`; + } + } + + AlloyEditor.Buttons[ButtonCustomTag.key] = AlloyEditor[componentClassName] = ButtonCustomTag; + AlloyEditor.Buttons[ButtonCustomTagEdit.key] = AlloyEditor[editComponentClassName] = ButtonCustomTagEdit; + AlloyEditor.Buttons[ButtonCustomTagUpdate.key] = AlloyEditor[updateComponentClassName] = ButtonCustomTagUpdate; + }); +})(window, window.document, window.eZ, window.AlloyEditor); diff --git a/src/bundle/Resources/public/js/OnlineEditor/core/table.js b/src/bundle/Resources/public/js/OnlineEditor/core/table.js new file mode 100644 index 00000000..fd5aeef2 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/core/table.js @@ -0,0 +1,20 @@ +(function(global, doc, CKEDITOR) { + if (!CKEDITOR.Table) { + return; + } + + const originalCreate = CKEDITOR.Table.prototype.create; + + CKEDITOR.Table.prototype.create = function(config) { + if (this._editor.widgets && this._editor.widgets.selected.length) { + this._editor.execCommand('eZAddContent', { + tagName: 'p', + content: '
    ', + }); + + this._editor.selectionChange(true); + } + + return originalCreate.call(this, config); + }; +})(window, window.document, window.CKEDITOR); diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-custom-tag-base.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-custom-tag-base.js new file mode 100644 index 00000000..f85e0849 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-custom-tag-base.js @@ -0,0 +1,577 @@ +const DATA_ALIGNMENT_ATTR = 'ezalign'; + +const customTagBaseDefinition = { + defaults: { + name: 'customtag', + content: '', + }, + draggable: false, + template: + '
    {content}
    ', + requiredContent: 'div', + editables: { + content: { + selector: '[data-ezelement="ezcontent"]', + }, + }, + setNameFireEditorInteractionTimeout: null, + setAlignmentFireEditorInteractionTimeout: null, + unsetAlignmentFireEditorInteractionTimeout: null, + setConfigFireEditorInteractionTimeout: null, + clearConfigFireEditorInteractionTimeout: null, + + upcast: (element) => { + return element.name === 'div' && element.attributes['data-ezelement'] === 'eztemplate' && !element.attributes['data-eztype']; + }, + + insertWrapper: function(wrapper) { + this.editor.eZ.appendElement(wrapper); + }, + + /** + * Insert an `ezembed` widget in the editor. It overrides the + * default implementation to make sure that in the case where an + * embed widget is focused, a new one is added after it. + * + * @method insert + */ + insert: function() { + const element = CKEDITOR.dom.element.createFromHtml(this.template.output(this.defaults)); + const wrapper = this.editor.widgets.wrapElement(element, this.name); + + this.editor.widgets.initOn(element, this.name); + + this.insertWrapper(wrapper); + + const instance = this.editor.widgets.getByElement(wrapper); + instance.ready = true; + instance.fire('ready'); + instance.focus(); + }, + + /** + * It's not possible to *edit* an embed widget in AlloyEditor, + * so `edit` directly calls `insert` instead. This is needed + * because by default, the CKEditor engine calls this method + * when an embed widget has the focus and the `ezcustomtag` command + * is executed. In AlloyEditor, we want to insert a new widget, + * not to `edit` the focused widget as the editing process is + * provided by the style toolbar. + * + * @method edit + */ + edit: function() { + this.insert(); + }, + + init: function() { + this.on('focus', this.fireEditorInteraction); + this.syncAlignment(true); + this.renderAttributes(); + this.renderHeader(); + this.getEzContentElement(); + this.getEzConfigElement(); + this.cancelEditEvents(); + this.toggleState({ + currentTarget: { + dataset: { + target: 'attributes', + }, + }, + }); + }, + + getIdentifier() { + return 'ezcustomtag'; + }, + + /** + * Clear the node. + * + * @method clearNode + * @param {Element} node + */ + clearNode: function(node) { + let element = node.getFirst(); + let next; + + while (element) { + next = element.getNext(); + element.remove(); + element = next; + } + }, + + /** + * Renders the custom tag header. + * + * @method renderHeader + */ + renderHeader: function() { + const customTagConfig = global.eZ.adminUiConfig.richTextCustomTags[this.getName()]; + + if (!customTagConfig) { + return; + } + + const header = this.getHeader(); + const template = ` +
    + ${customTagConfig.label} +
    +
    + + +
    + `; + + this.clearNode(header); + + header.appendHtml(template); + + this.attachButtonsListeners(); + }, + + /** + * Attaches event listeners to toggle state buttons. + * + * @method attachButtonsListeners + */ + attachButtonsListeners: function() { + const header = this.getHeader(); + const attributesBtn = header.findOne('.ez-custom-tag__header-btn--attributes'); + const contentBtn = header.findOne('.ez-custom-tag__header-btn--content'); + + [attributesBtn, contentBtn].forEach((btn) => btn.$.addEventListener('click', this.toggleState.bind(this), false)); + }, + + /** + * Toggles the custom tag state. + * + * @method toggleState + * @param {Event} event + */ + toggleState: function(event) { + const visibleElement = event.currentTarget.dataset.target; + const classes = { + attributes: 'ez-custom-tag--attributes-visible', + content: 'ez-custom-tag--content-visible', + }; + + Object.entries(classes).forEach(([key, className]) => this.element.$.classList.toggle(className, key === visibleElement)); + }, + + /** + * Renders the custom tag attributes. + * + * @method renderAttributes + */ + renderAttributes: function() { + const customTagConfig = global.eZ.adminUiConfig.richTextCustomTags[this.getName()]; + + if (!customTagConfig || !customTagConfig.attributes) { + return; + } + const attributes = Object.keys(customTagConfig.attributes).reduce((total, attr) => { + const value = this.getConfig(attr); + + return `${total}

    ${customTagConfig.attributes[attr].label}: ${value}

    `; + }, ''); + + this.setWidgetAttributes(attributes); + }, + + /** + * Sets the `name` of the custom tag. + * + * @method setName + * @param {String} name + * @return {CKEDITOR.plugins.widget} + */ + setName: function(name) { + this.element.data('ezname', name); + + window.clearTimeout(this.setNameFireEditorInteractionTimeout); + this.setNameFireEditorInteractionTimeout = window.setTimeout(this.fireEditorInteraction.bind(this, 'nameUpdated'), 50); + + return this; + }, + + /** + * Gets the `name` of the custom tag. + * + * @method getName + * @return {CKEDITOR.plugins.widget} + */ + getName: function() { + return this.element.data('ezname'); + }, + + /** + * Cancels the widget events that trigger the `edit` event as + * an embed widget can not be edited in a *CKEditor way*. + * + * @method cancelEditEvents + */ + cancelEditEvents: function() { + const cancel = (event) => event.cancel(); + + this.on('doubleclick', cancel, null, null, 5); + this.on('key', cancel, null, null, 5); + }, + + /** + * Initializes the alignment on the widget wrapper if the widget + * is aligned. + * + * @method syncAlignment + * @param {Boolean} fireEditorInteractionPrevented + */ + syncAlignment: function(fireEditorInteractionPrevented) { + const align = this.element.data(DATA_ALIGNMENT_ATTR); + + if (align) { + this.setAlignment(align, fireEditorInteractionPrevented); + } else { + this.unsetAlignment(fireEditorInteractionPrevented); + } + }, + + /** + * Sets the alignment of the embed widget to `type` and fires + * the corresponding `editorInteraction` event. + * + * @method setAlignment + * @param {String} type + * @param {Boolean} fireEditorInteractionPrevented + */ + setAlignment: function(type, fireEditorInteractionPrevented) { + this.wrapper.data(DATA_ALIGNMENT_ATTR, type); + this.element.data(DATA_ALIGNMENT_ATTR, type); + + if (!fireEditorInteractionPrevented) { + window.clearTimeout(this.setAlignmentFireEditorInteractionTimeout); + this.setAlignmentFireEditorInteractionTimeout = window.setTimeout(this.fireEditorInteraction.bind(this, 'aligmentUpdated'), 50); + } + }, + + /** + * Removes the alignment of the widget and fires the + * corresponding `editorInteraction` event. + * + * @method unsetAlignment + * @param {Boolean} fireEditorInteractionPrevented + */ + unsetAlignment: function(fireEditorInteractionPrevented) { + this.wrapper.data(DATA_ALIGNMENT_ATTR, false); + this.element.data(DATA_ALIGNMENT_ATTR, false); + + if (!fireEditorInteractionPrevented) { + window.clearTimeout(this.unsetAlignmentFireEditorInteractionTimeout); + this.unsetAlignmentFireEditorInteractionTimeout = window.setTimeout( + this.fireEditorInteraction.bind(this, 'aligmentRemoved'), + 50 + ); + } + }, + + /** + * Checks whether the embed is aligned with `type` alignment. + * + * @method isAligned + * @param {String} type + * @return {Boolean} + */ + isAligned: function(type) { + return this.wrapper.data(DATA_ALIGNMENT_ATTR) === type; + }, + + /** + * Sets the widget content. + * + * @method setWidgetContent + * @param {String|CKEDITOR.dom.node} content + * @return {CKEDITOR.plugins.widget} + */ + setWidgetContent: function(content) { + const ezContent = this.getEzContentElement(); + let element = ezContent.getFirst(); + let next; + + while (element) { + next = element.getNext(); + element.remove(); + element = next; + } + + if (content instanceof CKEDITOR.dom.node) { + ezContent.append(content); + } else { + ezContent.appendHtml(content); + } + + return this; + }, + + /** + * Sets a config value under the `key` for the custom tag. + * + * @method setConfig + * @param {String} key + * @param {String} value + * @return {CKEDITOR.plugins.widget} + */ + setConfig: function(key, value) { + let valueElement = this.getValueElement(key); + + if (!valueElement) { + valueElement = new CKEDITOR.dom.element('span'); + valueElement.data('ezelement', 'ezvalue'); + valueElement.data('ezvalue-key', key); + this.getEzConfigElement().append(valueElement); + } + + valueElement.setText(value); + + window.clearTimeout(this.setConfigFireEditorInteractionTimeout); + this.setConfigFireEditorInteractionTimeout = window.setTimeout(this.fireEditorInteraction.bind(this, 'configUpdated'), 50); + + return this; + }, + + /** + * Sets the widget attributes. + * + * @method setWidgetAttributes + * @param {String|CKEDITOR.dom.node} attributes + * @return {CKEDITOR.plugins.widget} + */ + setWidgetAttributes: function(attributes) { + const ezAttributes = this.getEzAttributesElement(); + let element = ezAttributes.getFirst(); + let next; + + while (element) { + next = element.getNext(); + element.remove(); + element = next; + } + + if (attributes instanceof CKEDITOR.dom.node) { + ezAttributes.append(attributes); + } else { + ezAttributes.appendHtml(attributes); + } + + return this; + }, + + /** + * Returns the config value for the `key` or empty string if the + * config key is not found. + * + * @method getConfig + * @return {String} + */ + getConfig: function(key) { + const valueElement = this.getValueElement(key); + + return valueElement ? valueElement.getText() : ''; + }, + + clearConfig: function() { + const config = this.getEzConfigElement(); + + while (config.firstChild) { + config.removeChild(config.firstChild); + } + + window.clearTimeout(this.clearConfigFireEditorInteractionTimeout); + this.clearConfigFireEditorInteractionTimeout = window.setTimeout(this.fireEditorInteraction.bind(this, 'configCleared'), 50); + }, + + /** + * Returns the Element holding the config under `key` + * + * @method getValueElement + * @param {String} key + * @return {CKEDITOR.dom.element} + */ + getValueElement: function(key) { + return this.getEzConfigElement().findOne('[data-ezelement="ezvalue"][data-ezvalue-key="' + key + '"]'); + }, + + /** + * Returns the element used as a container the config values. if + * it does not exist, it is created. + * + * @method getEzConfigElement + * @return {CKEDITOR.dom.element} + */ + getEzConfigElement: function() { + let config = [...this.element.getChildren().$].find((child) => child.dataset && child.dataset.ezelement === 'ezconfig'); + + if (!config) { + config = new CKEDITOR.dom.element('span'); + config.data('ezelement', 'ezconfig'); + this.element.append(config); + } else { + config = new CKEDITOR.dom.element(config); + } + + return config; + }, + + /** + * Returns the element used as a container the content values. if + * it does not exist, it is created. + * + * @method getEzContentElement + * @return {CKEDITOR.dom.element} + */ + getEzContentElement: function() { + let content = [...this.element.getChildren().$].find((child) => child.dataset && child.dataset.ezelement === 'ezcontent'); + + if (!content) { + content = new CKEDITOR.dom.element('div'); + content.data('ezelement', 'ezcontent'); + this.element.append(content); + } else { + content = new CKEDITOR.dom.element(content); + } + + return content; + }, + + /** + * Returns the element used as a container the attributes values. if + * it does not exist, it is created. + * + * @method getEzAttributesElement + * @return {CKEDITOR.dom.element} + */ + getEzAttributesElement: function() { + let attributes = [...this.element.getChildren().$].find((child) => child.dataset && child.dataset.ezelement === 'ezattributes'); + + if (!attributes) { + attributes = new CKEDITOR.dom.element('div'); + attributes.data('ezelement', 'ezattributes'); + this.element.append(attributes, true); + } else { + attributes = new CKEDITOR.dom.element(attributes); + } + + return attributes; + }, + + /** + * Returns the element used as a container the header. if + * it does not exist, it is created. + * + * @method getHeader + * @return {CKEDITOR.dom.element} + */ + getHeader: function() { + let header = [...this.element.getChildren().$].find((child) => child.dataset && child.classList.contains('ez-custom-tag__header')); + + if (!header) { + header = new CKEDITOR.dom.element('div'); + header.addClass('ez-custom-tag__header'); + this.element.append(header, true); + } else { + header = new CKEDITOR.dom.element(header); + } + + return header; + }, + + /** + * Fires the editorInteraction event so that AlloyEditor editor + * UI remains visible and is updated. This method also computes + * `selectionData.region` and the `pageX` and `pageY` properties + * so that the add toolbar is correctly positioned on the + * widget. + * + * @method fireEditorInteraction + * @param {Object|String} evt this initial event info object or + * the event name for which the `editorInteraction` is fired. + */ + fireEditorInteraction: function(evt) { + const wrapperRegion = this.getWrapperRegion(); + const name = evt.name || evt; + const event = { + editor: this.editor, + target: this.element.$, + name: 'widget' + name, + pageX: wrapperRegion.left, + pageY: wrapperRegion.top + wrapperRegion.height, + }; + + this.editor.focus(); + this.focus(); + + this.editor.fire('editorInteraction', { + nativeEvent: event, + selectionData: { + element: this.element, + region: wrapperRegion, + }, + }); + }, + + /** + * Moves the widget after the given element. It also fires the + * `editorInteraction` event so that the UI can respond to that + * change. + * + * @method moveAfter + * @param {CKEDITOR.dom.element} element + */ + moveAfter: function(element) { + this.wrapper.insertAfter(element); + this.fireEditorInteraction('moveAfter'); + }, + + /** + * Moves the widget before the given element. It also fires the + * `editorInteraction` event so that the UI can respond to that + * change. + * + * @method moveAfter + * @param {CKEDITOR.dom.element} element + */ + moveBefore: function(element) { + this.wrapper.insertBefore(element); + this.fireEditorInteraction('moveBefore'); + }, + + /** + * Returns the wrapper element region. + * + * @method getWrapperRegion + * @private + * @return {Object} + */ + getWrapperRegion: function() { + const scroll = this.wrapper.getWindow().getScrollPosition(); + const region = this.wrapper.getClientRect(); + + region.top += scroll.y; + region.bottom += scroll.y; + region.left += scroll.x; + region.right += scroll.x; + region.direction = CKEDITOR.SELECTION_TOP_TO_BOTTOM; + + return region; + }, +}; + +export default customTagBaseDefinition; diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-embed-base.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-embed-base.js new file mode 100644 index 00000000..91e60abf --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-embed-base.js @@ -0,0 +1,744 @@ +const IMAGE_TYPE_CLASS = 'ez-embed-type-image'; +const IS_LINKED_CLASS = 'is-linked'; +const SHOW_EDIT_LINK_TOOLBAR_ATTR = 'data-show-edit-link-toolbar'; +const DATA_ALIGNMENT_ATTR = 'ezalign'; + +const embedBaseDefinition = { + defaults: { + href: 'ezcontent://', + content: 'home', + view: 'embed', + }, + draggable: false, + template: '
    {content}
    ', + requiredContent: 'div', + + upcast: (element) => { + return element.name === 'div' && element.attributes['data-ezelement'] === 'ezembed'; + }, + + insertWrapper: function(wrapper) { + this.editor.eZ.appendElement(wrapper); + }, + + /** + * Insert an `ezembed` widget in the editor. It overrides the + * default implementation to make sure that in the case where an + * embed widget is focused, a new one is added after it. + * + * @method insert + */ + insert: function() { + const element = CKEDITOR.dom.element.createFromHtml(this.template.output(this.defaults)); + const wrapper = this.editor.widgets.wrapElement(element, this.name); + + this.editor.widgets.initOn(element, this.name); + + this.insertWrapper(wrapper); + + const instance = this.editor.widgets.getByElement(wrapper); + instance.ready = true; + instance.fire('ready'); + instance.focus(); + }, + + /** + * It's not possible to *edit* an embed widget in AlloyEditor, + * so `edit` directly calls `insert` instead. This is needed + * because by default, the CKEditor engine calls this method + * when an embed widget has the focus and the `ezembed` command + * is executed. In AlloyEditor, we want to insert a new widget, + * not to `edit` the focused widget as the editing process is + * provided by the style toolbar. + * + * @method edit + */ + edit: function() { + this.insert(); + }, + + init: function() { + this.on('focus', this.fireEditorInteraction); + this.syncAlignment(); + this.getEzConfigElement(); + this.setWidgetContent(''); + this.cancelEditEvents(); + + this.initEditMode(); + }, + + getIdentifier() { + return this.isImage() ? 'ezembedimage' : 'ezembed'; + }, + + /** + * Initializes the edit mode. + * + * @method initEditMode + */ + initEditMode: function() { + const contentId = this.getHref().replace('ezcontent://', ''); + + if (!contentId) { + return; + } + + this.loadContent(contentId); + }, + + /** + * Loads the content info. + * + * @method loadContent + * @param {String} contentId The content id + */ + loadContent: function(contentId) { + const token = document.querySelector('meta[name="CSRF-Token"]').content; + const siteaccess = document.querySelector('meta[name="SiteAccess"]').content; + const body = JSON.stringify({ + ViewInput: { + identifier: `embed-load-content-info-${contentId}`, + public: false, + ContentQuery: { + Criteria: {}, + FacetBuilders: {}, + SortClauses: {}, + Filter: { ContentIdCriterion: `${contentId}` }, + limit: 1, + offset: 0, + }, + }, + }); + const request = new Request('/api/ezp/v2/views', { + method: 'POST', + headers: { + Accept: 'application/vnd.ez.api.View+json; version=1.1', + 'Content-Type': 'application/vnd.ez.api.ViewInput+json; version=1.1', + 'X-Siteaccess': siteaccess, + 'X-CSRF-Token': token, + }, + body, + mode: 'same-origin', + credentials: 'same-origin', + }); + + fetch(request) + .then((response) => response.json()) + .then(this.handleContentLoaded.bind(this)) + .catch((error) => window.eZ.helpers.notification.showErrorNotification(error)); + }, + + /** + * Loads the image variation. + * + * @method loadImageVariation + * @param {String} variationHref The variation href + */ + loadImageVariation: function(variationHref) { + const token = document.querySelector('meta[name="CSRF-Token"]').content; + const siteaccess = document.querySelector('meta[name="SiteAccess"]').content; + const request = new Request(variationHref, { + method: 'GET', + headers: { + Accept: 'application/vnd.ez.api.ContentImageVariation+json', + 'X-Siteaccess': siteaccess, + 'X-CSRF-Token': token, + }, + credentials: 'same-origin', + mode: 'same-origin', + }); + + fetch(request) + .then((response) => response.json()) + .then((imageData) => this.renderEmbedImagePreview(imageData.ContentImageVariation.uri)); + }, + + /** + * Finds the ezimage field. + * + * @method findEzimageField + * @returns {Object} + */ + findEzimageField(fields) { + return fields.find((field) => field.fieldTypeIdentifier === 'ezimage'); + }, + + /** + * Handles loading the content info. + * + * @method handleContentLoaded + * @param {Object} hits The result of content search + */ + handleContentLoaded: function(hits) { + const isEmbedImage = this.element.$.classList.contains(IMAGE_TYPE_CLASS); + const content = hits.View.Result.searchHits.searchHit[0].value.Content; + + if (isEmbedImage) { + const fieldImage = this.findEzimageField(content.CurrentVersion.Version.Fields.field); + + if (!fieldImage || !fieldImage.fieldValue) { + this.renderEmbedPreview(content.Name); + + return; + } + + const size = this.getConfig('size'); + const variationHref = fieldImage.fieldValue.variations[size].href; + + this.variations = fieldImage.fieldValue.variations; + + this.loadImageVariation(variationHref); + } else { + this.renderEmbedPreview(content.Name); + } + }, + + /** + * Loads image preview from current version href + * + * @method loadImagePreviewFromCurrentVersion + * @param {String} currentVersionHref The current version href + * @param {String} contnetName The content name + */ + loadImagePreviewFromCurrentVersion: function(currentVersionHref, contentName) { + const token = document.querySelector('meta[name="CSRF-Token"]').content; + const siteaccess = document.querySelector('meta[name="SiteAccess"]').content; + const request = new Request(currentVersionHref, { + method: 'GET', + headers: { + Accept: 'application/vnd.ez.api.Version+json', + 'X-Siteaccess': siteaccess, + 'X-CSRF-Token': token, + }, + credentials: 'same-origin', + mode: 'same-origin', + }); + + fetch(request) + .then((response) => response.json()) + .then((data) => { + const fieldImage = this.findEzimageField(data.Version.Fields.field); + + if (!fieldImage || !fieldImage.fieldValue) { + contentName = contentName ? contentName : ''; + + this.renderEmbedPreview(contentName); + + return; + } + + const size = this.getConfig('size'); + const variationHref = fieldImage.fieldValue.variations[size].href; + + this.variations = fieldImage.fieldValue.variations; + + this.loadImageVariation(variationHref); + }); + }, + + createEmbedPreviewNode: function() { + return document.createElement('p'); + }, + + createEmbedPreview: function(title) { + return ` + + + + ${title} + `; + }, + + /** + * Renders the embed preview + * + * @method renderEmbedPreview + * @param {String} title The content title + */ + renderEmbedPreview: function(title) { + const elementNode = this.createEmbedPreviewNode(); + const escapedTitle = eZ.helpers.text.escapeHTML(title); + const template = this.createEmbedPreview(escapedTitle); + + elementNode.classList.add('ez-embed-content'); + elementNode.innerHTML = template; + + this.setWidgetContent(elementNode); + }, + + /** + * Renders the embed image preview + * + * @method renderEmbedImagePreview + * @param {String} imageUri The image uri + */ + renderEmbedImagePreview: function(imageUri) { + const elementNode = document.createElement('img'); + + elementNode.setAttribute('src', imageUri); + + this.setWidgetContent(elementNode); + + if (this.element.hasClass(IS_LINKED_CLASS)) { + this.renderLinkedIcon(); + } + }, + + /** + * Cancels the widget events that trigger the `edit` event as + * an embed widget can not be edited in a *CKEditor way*. + * + * @method cancelEditEvents + */ + cancelEditEvents: function() { + const cancel = (event) => event.cancel(); + + this.on('doubleclick', cancel, null, null, 5); + this.on('key', cancel, null, null, 5); + }, + + /** + * Initializes the alignment on the widget wrapper if the widget + * is aligned. + * + * @method syncAlignment + */ + syncAlignment: function() { + const align = this.element.data(DATA_ALIGNMENT_ATTR); + + if (align) { + this._setAlignment(align); + } else { + this._unsetAlignment(); + } + }, + + /** + * Sets the alignment of the embed widget to `type`. The + * alignment is set by adding the `data-ezalign` attribute + * on the widget wrapper and the widget element. + * + * @method _setAlignment + * @param {String} type + */ + _setAlignment: function(type) { + this.wrapper.data(DATA_ALIGNMENT_ATTR, type); + this.element.data(DATA_ALIGNMENT_ATTR, type); + }, + + /** + * Sets the alignment of the embed widget to `type` and fires + * the corresponding `editorInteraction` event. + * + * @method setAlignment + * @param {String} type + */ + setAlignment: function(type) { + this._setAlignment(type); + this.fireEditorInteraction('setAlignment' + type); + }, + + /** + * Removes the alignment of the widget. + * + * @method _unsetAlignment + */ + _unsetAlignment: function() { + this.wrapper.data(DATA_ALIGNMENT_ATTR, false); + this.element.data(DATA_ALIGNMENT_ATTR, false); + }, + + /** + * Removes the alignment of the widget and fires the + * corresponding `editorInteraction` event. + * + * @method unsetAlignment + */ + unsetAlignment: function() { + this._unsetAlignment(); + this.fireEditorInteraction('unsetAlignment'); + }, + + /** + * Checks whether the embed is aligned with `type` alignment. + * + * @method isAligned + * @param {String} type + * @return {Boolean} + */ + isAligned: function(type) { + return this.wrapper.data(DATA_ALIGNMENT_ATTR) === type; + }, + + /** + * Set the embed as an embed representing an image + * + * @method setImageType + * @return {CKEDITOR.plugins.widget} + */ + setImageType: function() { + this.element.addClass(IMAGE_TYPE_CLASS); + + return this; + }, + + /** + * Check whether the embed widget represents an image or not. + * + * @method isImage + * @return {Boolean} + */ + isImage: function() { + return this.element.hasClass(IMAGE_TYPE_CLASS); + }, + + /** + * Sets the `href` of the embed is URI to the embed content or + * location. (ezcontent://32 for instance). + * + * @method setHref + * @param {String} href + * @return {CKEDITOR.plugins.widget} + */ + setHref: function(href) { + this.element.data('href', href); + + return this; + }, + + /** + * Returns the `href`of the embed. + * + * @method getHref + * @return {String} + */ + getHref: function() { + return this.element.data('href'); + }, + + /** + * Sets the widget content. It makes sure the config element is + * not overwritten. + * + * @method setWidgetContent + * @param {String|CKEDITOR.dom.node} content + * @return {CKEDITOR.plugins.widget} + */ + setWidgetContent: function(content) { + let element = this.element.getFirst(); + let next; + + while (element) { + next = element.getNext(); + + const isEzElement = element.data && element.data('ezelement'); + const isAnchorIcon = element.$.classList && element.$.classList.contains('ez-icon--anchor'); + const shouldRemove = !(isEzElement || isAnchorIcon); + + if (shouldRemove) { + element.remove(); + } + + element = next; + } + + if (content instanceof CKEDITOR.dom.node) { + this.element.append(content); + } else { + this.element.appendText(content); + } + + return this; + }, + + /** + * Moves the widget after the given element. It also fires the + * `editorInteraction` event so that the UI can respond to that + * change. + * + * @method moveAfter + * @param {CKEDITOR.dom.element} element + */ + moveAfter: function(element) { + this.wrapper.insertAfter(element); + this.fireEditorInteraction('moveAfter'); + }, + + /** + * Moves the widget before the given element. It also fires the + * `editorInteraction` event so that the UI can respond to that + * change. + * + * @method moveAfter + * @param {CKEDITOR.dom.element} element + */ + moveBefore: function(element) { + this.wrapper.insertBefore(element); + this.fireEditorInteraction('moveBefore'); + }, + + /** + * Sets a config value under the `key` for the embed. + * + * @method setConfig + * @param {String} key + * @param {String} value + * @return {CKEDITOR.plugins.widget} + */ + setConfig: function(key, value) { + let valueElement = this.getValueElement(key); + + if (!valueElement) { + valueElement = new CKEDITOR.dom.element('span'); + valueElement.data('ezelement', 'ezvalue'); + valueElement.data('ezvalue-key', key); + this.getEzConfigElement().append(valueElement); + } + + valueElement.setText(value); + + return this; + }, + + /** + * Returns the config value for the `key` or undefined if the + * config key is not found. + * + * @method getConfig + * @return {String} + */ + getConfig: function(key) { + const valueElement = this.getValueElement(key); + + return valueElement ? valueElement.getText() : undefined; + }, + + /** + * Returns the Element holding the config under `key` + * + * @method getValueElement + * @param {String} key + * @return {CKEDITOR.dom.element} + */ + getValueElement: function(key) { + return this.element.findOne('[data-ezelement="ezvalue"][data-ezvalue-key="' + key + '"]'); + }, + + /** + * Returns the element used as a container the config values. if + * it does not exist, it is created. + * + * @method getEzConfigElement + * @return {CKEDITOR.dom.element} + */ + getEzConfigElement: function() { + let config = this.element.findOne('[data-ezelement="ezconfig"]'); + + if (!config) { + config = new CKEDITOR.dom.element('span'); + config.data('ezelement', 'ezconfig'); + this.element.append(config, true); + } + + return config; + }, + + /** + * Returns the element used as a container the link values. if + * it does not exist, it is created. + * + * @method getEzLinkElement + * @return {CKEDITOR.dom.element} + */ + getEzLinkElement: function() { + let link = this.element.findOne('[data-ezelement="ezlink"]'); + + if (!link) { + link = new CKEDITOR.dom.element('a'); + link.$.innerHTML = ' '; + link.setAttribute('data-ezelement', 'ezlink'); + link.setAttribute('data-ez-temporary-link', true); + this.element.append(link); + } + + return link; + }, + + /** + * Gets the link attribute + * + * @method getEzLinkAttribute + * @param {String} attribute + * @return {String} + */ + getEzLinkAttribute: function(attribute) { + const link = this.getEzLinkElement(); + + return link.getAttribute(attribute); + }, + + /** + * Sets the link attribute + * + * @method getEzLinkAttribute + * @param {String} attribute + * @param {String} value + */ + setEzLinkAttribute: function(attribute, value) { + const link = this.getEzLinkElement(); + + link.setAttribute(attribute, value); + }, + + /** + * Removes the link attribute + * + * @method removeEzLinkAttribute + * @param {String} attribute + */ + removeEzLinkAttribute: function(attribute) { + const link = this.getEzLinkElement(); + + link.removeAttribute(attribute); + }, + + /** + * Sets the link edit state + * + * @method setLinkEditState + */ + setLinkEditState: function() { + this.element.setAttribute(SHOW_EDIT_LINK_TOOLBAR_ATTR, true); + }, + + /** + * Removes the link edit state + * + * @method removeLinkEditState + */ + removeLinkEditState: function() { + this.element.removeAttribute(SHOW_EDIT_LINK_TOOLBAR_ATTR); + }, + + /** + * Checks if widget is in link edit state + * + * @method isEditingLink + * @return {Boolean} + */ + isEditingLink: function() { + return this.element.hasAttribute(SHOW_EDIT_LINK_TOOLBAR_ATTR); + }, + + /** + * Sets the is linked state + * + * @method setIsLinkedState + */ + setIsLinkedState: function() { + this.element.$.classList.add(IS_LINKED_CLASS); + this.renderLinkedIcon(); + }, + + /** + * Removes the is linked state + * + * @method removeIsLinkedState + */ + removeIsLinkedState: function() { + this.element.$.classList.remove(IS_LINKED_CLASS); + this.removeLinkedIcon(); + }, + + /** + * Renders the linked icon + * + * @method renderLinkedIcon + */ + renderLinkedIcon: function() { + const iconWrapper = new CKEDITOR.dom.element('span'); + const icon = ` + + + + `; + + if (this.element.findOne('.ez-embed__icon-wrapper')) { + return; + } + + iconWrapper.$.classList.add('ez-embed__icon-wrapper'); + iconWrapper.$.innerHTML = icon; + + this.element.append(iconWrapper); + }, + + /** + * Removes the linked icon + * + * @method removeLinkedIcon + */ + removeLinkedIcon: function() { + const iconWrapper = this.element.findOne('.ez-embed__icon-wrapper'); + + if (iconWrapper) { + iconWrapper.remove(); + } + }, + + /** + * Fires the editorInteraction event so that AlloyEditor editor + * UI remains visible and is updated. This method also computes + * `selectionData.region` and the `pageX` and `pageY` properties + * so that the add toolbar is correctly positioned on the + * widget. + * + * @method fireEditorInteraction + * @param {Object|String} evt this initial event info object or + * the event name for which the `editorInteraction` is fired. + */ + fireEditorInteraction: function(evt) { + const wrapperRegion = this.getWrapperRegion(); + const name = evt.name || evt; + const event = { + editor: this.editor, + target: this.element.$, + name: 'widget' + name, + pageX: wrapperRegion.left, + pageY: wrapperRegion.top + wrapperRegion.height, + }; + + this.editor.focus(); + this.focus(); + + this.editor.fire('editorInteraction', { + nativeEvent: event, + selectionData: { + element: this.element, + region: wrapperRegion, + }, + }); + }, + + /** + * Returns the wrapper element region. + * + * @method getWrapperRegion + * @private + * @return {Object} + */ + getWrapperRegion: function() { + const scroll = this.wrapper.getWindow().getScrollPosition(); + const region = this.wrapper.getClientRect(); + + region.top += scroll.y; + region.bottom += scroll.y; + region.left += scroll.x; + region.right += scroll.x; + region.direction = CKEDITOR.SELECTION_TOP_TO_BOTTOM; + + return region; + }, +}; + +export default embedBaseDefinition; diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-add-content.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-add-content.js new file mode 100644 index 00000000..b1b7b595 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-add-content.js @@ -0,0 +1,107 @@ +(function(global) { + if (global.CKEDITOR.plugins.get('ezaddcontent')) { + return; + } + + /** + * Creates a given HTMLElement + * + * @method createElement + * @return HTMLElement + */ + const createElement = (doc, tagName, content, attributes) => { + const element = doc.createElement(tagName); + + element.setAttributes(attributes); + element.setHtml(content ? content : '
    '); + + return element; + }; + + /** + * Fires the `editorInteraction` event this is done to make sure the + * AlloyEditor's UI remains visible + * + * @method fireEditorInteractionEvent + */ + const fireEditorInteractionEvent = (editor, element) => { + const event = { + editor: editor, + target: element.$, + name: 'eZAddContentDone', + }; + + editor.fire('editorInteraction', { + nativeEvent: event, + selectionData: editor.getSelectionData(), + }); + }; + + const isCustomTag = (el) => !!el.findOne('[data-ezelement="eztemplate"]'); + + /** + * Appends the element to the editor content. Depending on the editor's + * state, the element is added at a different place: + * + * - if nothing is selected, editor.insertElement is called and the element + * is added at the beginning of the editor + * - if a block element is selected (not a widget), the element is added + * after the element or after the first block in the element path (after + * the ul element if a li has the focus) + * - if a widget has the focus, the element is added right after it + * + * @method appendElement + * @param {CKEDITOR.editor} editor + * @param {CKEDITOR.dom.element} element + */ + const appendElement = (editor, element) => { + const elementPath = editor.elementPath(); + + if (elementPath && elementPath.block) { + const elements = elementPath.elements; + const insertIndex = !elementPath.contains(isCustomTag, true) ? elements.length - 2 : 0; + + element.insertAfter(elements[insertIndex]); + } else if (editor.widgets && editor.widgets.focused) { + element.insertAfter(editor.widgets.focused.wrapper); + } else { + editor.insertElement(element); + } + }; + + const addContentCommand = { + exec: function(editor, data) { + const element = createElement(editor.document, data.tagName, data.content, data.attributes); + let focusElement = element; + + appendElement(editor, focusElement); + + if (data.focusElement) { + focusElement = element.findOne(data.focusElement); + } + + editor.eZ.moveCaretToElement(editor, focusElement); + fireEditorInteractionEvent(editor, focusElement); + }, + }; + + /** + * CKEditor plugin providing the eZAddContent command. This command + * allows to add content to the editor content in the editable region + * pointed by the selector available under `eZ.editableRegion` in the + * configuration. + * + * @class ezaddcontent + * @namespace CKEDITOR.plugins + * @constructor + */ + global.CKEDITOR.plugins.add('ezaddcontent', { + requires: ['ezcaret'], + + init: function(editor) { + editor.eZ = editor.eZ || {}; + editor.eZ.appendElement = appendElement.bind(editor, editor); + editor.addCommand('eZAddContent', addContentCommand); + }, + }); +})(window); diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-caret.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-caret.js new file mode 100644 index 00000000..3a32cac2 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-caret.js @@ -0,0 +1,68 @@ +(function (global) { + if (CKEDITOR.plugins.get('ezcaret')) { + return; + } + + /** + * Moves caret to the element. + * + * @method moveCaretToElement + */ + const moveCaretToElement = (editor, element) => { + const range = editor.createRange(); + + range.moveToPosition(element, CKEDITOR.POSITION_AFTER_START); + editor.getSelection().selectRanges([range]); + } + + /** + * Finds caret element. + * + * @method findCaretElement + * @return HTMLElement + */ + const findCaretElement = (element) => { + const child = element.getChild(0); + + if (child && child.type !== CKEDITOR.NODE_TEXT) { + return findCaretElement(child); + } + + return element; + } + + /** + * CKEDITOR plugin providing an API to handle the caret in the editor. + * + * @class ezcaret + * @namespace CKEDITOR.plugins + * @constructor + */ + CKEDITOR.plugins.add('ezcaret', { + init: function (editor) { + editor.eZ = editor.eZ || {}; + + /** + * Moves the caret in the editor to the given element + * + * @method eZ.moveCaretToElement + * @param {CKEDITOR.editor} editor + * @param {CKEDITOR.dom.element} element + */ + editor.eZ.moveCaretToElement = moveCaretToElement; + + /** + * Finds the "caret element" for the given element. For some elements, + * like ul or table, moving the caret inside them actually means finding + * the first element that can be filled by the user. + * + * @method eZ.findCaretElement + * @protected + * @param {CKEDITOR.dom.element} element + * @return {CKEDITOR.dom.element} + */ + editor.eZ.findCaretElement = findCaretElement; + }, + }); +})(window); + diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-custom-tag.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-custom-tag.js new file mode 100644 index 00000000..a1f5f24c --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-custom-tag.js @@ -0,0 +1,96 @@ +import customTagBaseDefinition from './base/ez-custom-tag-base'; + +const ZERO_WIDTH_SPACE = '​'; + +CKEDITOR.dtd.$editable.span = 1; + +(function(global) { + if (CKEDITOR.plugins.get('ezcustomtag') && CKEDITOR.plugins.get('ezinlinecustomtag')) { + return; + } + + CKEDITOR.plugins.add('ezcustomtag', { + requires: 'widget,ezaddcontent', + + init: function(editor) { + const customTagDefinition = Object.assign({}, customTagBaseDefinition, { editor, global }); + + editor.widgets.add('ezcustomtag', customTagDefinition); + }, + }); + + CKEDITOR.plugins.add('ezinlinecustomtag', { + requires: 'widget,ezaddcontent', + + init: function(editor) { + const inlineCustomTagDefinition = Object.assign({}, customTagBaseDefinition, { + editor, + global, + template: ` + + {content} + `, + requiredContent: 'div', + + upcast: (element) => { + return ( + element.name === 'span' && + element.attributes['data-ezelement'] === 'eztemplateinline' && + !element.attributes['data-eztype'] + ); + }, + + init: function() { + this.on('focus', this.fireEditorInteraction); + this.syncAlignment(true); + this.getEzConfigElement(); + this.cancelEditEvents(); + this.renderIcon(); + }, + + getIdentifier() { + return 'ezinlinecustomtag'; + }, + + insertWrapper: function(wrapper) { + this.editor.insertElement(wrapper); + }, + + renderIcon: function() { + const customTagConfig = global.eZ.adminUiConfig.richTextCustomTags[this.getName()]; + + if (!customTagConfig) { + return; + } + + const iconWrapper = this.getIconWrapper(); + const icon = ` + + + + `; + + iconWrapper.appendHtml(icon); + }, + + getIconWrapper: function() { + let iconWrapper = [...this.element.getChildren().$].find( + (child) => child.dataset && child.classList.contains('ez-custom-tag__icon-wrapper') + ); + + if (!iconWrapper) { + iconWrapper = new CKEDITOR.dom.element('span'); + iconWrapper.addClass('ez-custom-tag__icon-wrapper'); + this.element.append(iconWrapper, true); + } else { + iconWrapper = new CKEDITOR.dom.element(iconWrapper); + } + + return iconWrapper; + }, + }); + + editor.widgets.add('ezinlinecustomtag', inlineCustomTagDefinition); + }, + }); +})(window); diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-elements-path.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-elements-path.js new file mode 100644 index 00000000..3c15e89d --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-elements-path.js @@ -0,0 +1,96 @@ +(function(global) { + if (CKEDITOR.plugins.get('ezelementspath')) { + return; + } + + const elementsLabel = { + ezcustomtag: 'Custom Tag', + ezinlinecustomtag: 'Inline Custom Tag', + ezembed: 'Embed', + ezembedinline: 'Embed Inline', + ezembedimage: 'Embed Image', + }; + const skipElementsSelectors = ['.cke_widget_element', '.cke_widget_editable']; + const getLabel = (elementIdentifier) => (elementsLabel[elementIdentifier] ? elementsLabel[elementIdentifier] : elementIdentifier); + const itemPathTemplate = new CKEDITOR.template('
  • {label}
  • '); + const updatePath = (event) => { + const { editor, data } = event; + const elements = [...data.path.elements] + .reverse() + .slice(1) + .map(removeSkippedElements) + .filter((element) => !!element); + const pathItems = elements.map(createPathItem.bind(this, editor)); + const pathContainer = editor.container.getParent().findOne('.ez-elements-path'); + + if (!pathContainer) { + return; + } + + pathContainer.setHtml(''); + pathItems.forEach((pathItem) => pathContainer.append(pathItem)); + }; + const removeSkippedElements = (element) => { + const container = new CKEDITOR.dom.element('div'); + const containerWrapper = new CKEDITOR.dom.element('div'); + const clonedElement = element.clone(true); + + container.addClass('ez-cloned'); + container.append(clonedElement); + containerWrapper.append(container); + + const shouldBeSkipped = skipElementsSelectors.some((selector) => containerWrapper.findOne(`.ez-cloned > ${selector}`)); + + return shouldBeSkipped ? null : element; + }; + const createPathItem = (editor, element) => { + const label = getElementLabel(editor, element); + const pathItem = CKEDITOR.dom.element.createFromHtml(itemPathTemplate.output({ label })); + + pathItem.on('click', selectElement.bind(this, editor, element)); + + return pathItem; + }; + const selectElement = (editor, element) => { + const selection = editor.getSelection(); + + selection.selectElement(element); + + if (isWidgetElement(editor, element)) { + return; + } + + editor.fire('editorInteraction', { + nativeEvent: { + editor: editor, + target: element.$, + }, + selectionData: editor.getSelectionData(), + }); + }; + const getElementLabel = (editor, element) => { + const widgetIdentifier = getWidgetIdentifier(editor, element); + const label = widgetIdentifier ? getLabel(widgetIdentifier) : getLabel(element.getName()); + + return label; + }; + const getWidgetIdentifier = (editor, element) => { + const widget = editor.widgets.getByElement(element); + const widgetIdentifier = + isWidgetElement(editor, element) && typeof widget.getIdentifier === 'function' ? widget.getIdentifier() : null; + + return widgetIdentifier; + }; + const isWidgetElement = (editor, element) => { + const widget = editor.widgets.getByElement(element); + const elementFirstChild = element.getFirst(); + + return widget && elementFirstChild.type === 1 && widget.element.isIdentical(elementFirstChild); + }; + + CKEDITOR.plugins.add('ezelementspath', { + init: function(editor) { + editor.on('selectionChange', updatePath); + }, + }); +})(window); diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-embed.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-embed.js new file mode 100644 index 00000000..d26fcc0e --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-embed.js @@ -0,0 +1,79 @@ +import embedBaseDefinition from './base/ez-embed-base'; + +const ZERO_WIDTH_SPACE = '​'; + +(function(global) { + if (CKEDITOR.plugins.get('ezembed') && CKEDITOR.plugins.get('ezembedinline')) { + return; + } + + /** + * CKEditor plugin to configure the widget plugin so that it recognizes the + * `div[data-ezelement="embed"]` elements as widget. + * + * @class ezembed + * @namespace CKEDITOR.plugins + * @constructor + */ + CKEDITOR.plugins.add('ezembed', { + requires: 'widget,ezaddcontent', + + init: function(editor) { + editor.ezembed = { + canBeAdded: () => { + const path = editor.elementPath(); + + return !path || path.contains('table', true) === null; + }, + }; + + const embedDefinition = Object.assign({}, embedBaseDefinition, { editor }); + + editor.widgets.add('ezembed', embedDefinition); + }, + }); + + /** + * CKEditor plugin to configure the widget plugin so that it recognizes the + * `div[data-ezelement="embedinline"]` elements as widget. + * + * @class ezembedinline + * @namespace CKEDITOR.plugins + * @constructor + */ + CKEDITOR.plugins.add('ezembedinline', { + requires: 'widget,ezaddcontent', + + init: function(editor) { + const embedInlineDefinition = Object.assign({}, embedBaseDefinition, { + editor, + defaults: { + href: 'ezcontent://', + content: 'home', + view: 'embed-inline', + }, + template: '{content}', + requiredContent: 'span', + + upcast: (element) => { + return element.name === 'span' && element.attributes['data-ezelement'] === 'ezembedinline'; + }, + + getIdentifier() { + return 'ezembedinline'; + }, + + insertWrapper: function(wrapper) { + this.editor.insertHtml(ZERO_WIDTH_SPACE); + this.editor.insertElement(wrapper); + }, + + createEmbedPreviewNode: function() { + return document.createElement('span'); + }, + }); + + editor.widgets.add('ezembedinline', embedInlineDefinition); + }, + }); +})(window); diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-focus-block.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-focus-block.js new file mode 100644 index 00000000..09af737d --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-focus-block.js @@ -0,0 +1,110 @@ +(function(global) { + const FOCUSED_CLASS = 'is-block-focused'; + + if (CKEDITOR.plugins.get('ezfocusblock')) { + return; + } + + /** + * Finds the focused blocks. + * + * @method findFocusedBlock + * @param {Object} editor the CKEditor + */ + const findFocusedBlock = (editor) => editor.element.findOne('.' + FOCUSED_CLASS); + + /** + * Finds blocks to focus. + * + * @method findNewFocusedBlock + * @param {Object} elementPath the element path + */ + const findNewFocusedBlock = (elementPath) => { + const block = elementPath.block; + const elements = elementPath.elements; + + if (!block) { + return null; + } + + return elements[elements.length - 2]; + }; + + /** + * Updates the focused blocks. + * + * @method updateFocusedBlock + * @param {Event} event the event object + */ + const updateFocusedBlock = (event) => { + const block = findNewFocusedBlock(event.data.path); + const oldBlock = findFocusedBlock(event.editor); + + if (oldBlock && (!block || block.$ !== oldBlock.$)) { + oldBlock.removeClass(FOCUSED_CLASS); + } + + if (block) { + block.addClass(FOCUSED_CLASS); + } + }; + + /** + * Clear the focus from block. + * + * @method clearFocusedBlock + * @param {Event} event the event object + */ + const clearFocusedBlock = (event) => { + const oldBlock = findFocusedBlock(event.editor); + + if (oldBlock) { + oldBlock.removeClass(FOCUSED_CLASS); + } + }; + + /** + * Clear the focus blocks from data. + * + * @method clearFocusedBlockFromData + * @param {Event} event the event object + */ + const clearFocusedBlockFromData = (event) => { + const doc = document.createDocumentFragment(); + const root = document.createElement('div'); + let i; + + doc.appendChild(root); + root.innerHTML = event.data.dataValue; + const list = root.querySelectorAll('.' + FOCUSED_CLASS); + + if (list.length) { + for (i = 0; i != list.length; ++i) { + const element = list[i]; + + element.classList.remove(FOCUSED_CLASS); + + if (!element.getAttribute('class')) { + element.removeAttribute('class'); + } + } + event.data.dataValue = root.innerHTML; + } + }; + + /** + * CKEditor plugin to add/remove the focused class on the block holding the + * caret. + * + * @class ezfocusblock + * @namespace CKEDITOR.plugins + * @constructor + */ + CKEDITOR.plugins.add('ezfocusblock', { + init: function(editor) { + editor.on('selectionChange', updateFocusedBlock); + editor.on('blur', clearFocusedBlock); + editor.on('getData', clearFocusedBlockFromData); + }, + }); +})(window); diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-move-element.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-move-element.js new file mode 100644 index 00000000..4882d308 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-move-element.js @@ -0,0 +1,101 @@ +(function(global) { + if (CKEDITOR.plugins.get('ezmoveelement')) { + return; + } + + /** + * Fires the editorInteraction event. + * + * @method fireEditorInteraction + * @param {Object} editor the CKEditor + * @param {Event} evt the event object + * @param {Object} target the target + */ + const fireEditorInteraction = (editor, evt, target) => { + const event = { + editor: editor, + target: target.$, + name: evt, + }; + + editor.fire('editorInteraction', { + nativeEvent: event, + selectionData: editor.getSelectionData(), + }); + }; + + const findElements = (editor) => { + const path = editor.elementPath(); + let focused = path.block; + let widget; + + if (!focused) { + widget = editor.widgets.focused; + focused = widget ? widget.wrapper : null; + } + + if (!focused && path.contains('table')) { + focused = path.elements.find((element) => element.is('table')); + } + + if (focused.is('li')) { + focused = focused.getParent(); + } + + return { + focused, + widget, + }; + }; + + const moveUpCommand = { + exec: function(editor, data) { + const { focused, widget } = findElements(editor); + const previous = focused.getPrevious(); + + if (previous) { + if (widget) { + widget.moveBefore(previous); + } else { + focused.insertBefore(previous); + editor.eZ.moveCaretToElement(editor, editor.eZ.findCaretElement(focused)); + fireEditorInteraction(editor, 'eZMoveUpDone', focused); + } + } + }, + }; + + const moveDownCommand = { + exec: function(editor, data) { + const { focused, widget } = findElements(editor); + const next = focused.getNext(); + + if (next) { + if (widget) { + widget.moveAfter(next); + } else { + focused.insertAfter(next); + editor.eZ.moveCaretToElement(editor, editor.eZ.findCaretElement(focused)); + fireEditorInteraction(editor, 'eZMoveDownDone', focused); + } + } + }, + }; + + /** + * CKEditor plugin providing the eZMoveUp and eZMoveDown commands. These + * commands allow to move the element having the focus in the editor. + * + * @class ezmoveelement + * @namespace CKEDITOR.plugins + * @constructor + */ + CKEDITOR.plugins.add('ezmoveelement', { + requires: ['ezcaret'], + + init: function(editor) { + editor.addCommand('eZMoveUp', moveUpCommand); + editor.addCommand('eZMoveDown', moveDownCommand); + }, + }); +})(window); diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-remove-block.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-remove-block.js new file mode 100644 index 00000000..dae563db --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-remove-block.js @@ -0,0 +1,123 @@ +(function(global) { + if (CKEDITOR.plugins.get('ezremoveblock')) { + return; + } + + const removeBlockCommand = { + /** + * Moves the caret to the element + * + * @method moveCaretToElement + * @param {CKEDITOR.editor} editor + * @param {CKEDITOR.dom.element} element + */ + moveCaretToElement: function(editor, element) { + const caretElement = editor.eZ.findCaretElement(element); + + editor.eZ.moveCaretToElement(editor, caretElement); + this.fireEditorInteraction(editor, caretElement); + }, + + /** + * Fires the editorInteraction event so that AlloyEditor's UI is updated + * for the newly focused element + * + * @method fireEditorInteraction + * @param {CKEDITOR.editor} editor + * @param {CKEDITOR.dom.element} removedElement + * @param {CKEDITOR.dom.element} newFocus + */ + fireEditorInteraction: function(editor, newFocus) { + const event = { + editor: editor, + target: newFocus.$, + name: 'eZRemoveBlockDone', + }; + + editor.fire('editorInteraction', { + nativeEvent: event, + selectionData: editor.getSelectionData(), + }); + }, + + /** + * Changes the focused element in the editor to the given newFocus + * element + * + * @param {CKEDITOR.editor} editor + * @param {CKEDITOR.dom.element} newFocus + * @protected + * @method changeFocus + */ + changeFocus: function(editor, newFocus) { + const widget = editor.widgets.getByElement(newFocus); + + if (widget) { + widget.focus(); + } else { + this.moveCaretToElement(editor, newFocus); + } + }, + + getElementToRemove(editor) { + const path = editor.elementPath(); + let toRemove = editor.widgets.focused ? editor.widgets.focused.wrapper : path.block; + + if (!toRemove) { + toRemove = path.elements.find((element) => element.$.dataset.ezelement === 'eztemplateinline'); + } + + return toRemove.is('li') ? toRemove.getParent() : toRemove; + }, + + getElementToFocus(elementToRemove) { + let elementToFocus = elementToRemove.getNext(); + + if (!elementToFocus || elementToFocus.type === CKEDITOR.NODE_TEXT || elementToFocus.hasAttribute('data-cke-temp')) { + elementToFocus = elementToRemove.getPrevious(); + } + + if (elementToFocus && elementToFocus.type === CKEDITOR.NODE_TEXT) { + elementToFocus = elementToFocus.getParent(); + } + + if (!elementToFocus) { + elementToFocus = elementToRemove.getParent(); + } + + return elementToFocus; + }, + + exec: function(editor) { + const elementToRemove = this.getElementToRemove(editor); + let elementToFocus = this.getElementToFocus(elementToRemove); + + elementToRemove.remove(); + + if (elementToFocus) { + if (elementToFocus.hasClass('ez-data-source__richtext')) { + elementToFocus = new CKEDITOR.dom.element('p'); + + editor.insertElement(elementToFocus); + } + + this.changeFocus(editor, elementToFocus); + } + }, + }; + + /** + * CKEditor plugin providing the eZRemoveBlock command. This command + * allows to remove the block element holding the caret or the focused + * widget + * + * @class ezremoveblock + * @namespace CKEDITOR.plugins + * @constructor + */ + CKEDITOR.plugins.add('ezremoveblock', { + requires: ['widget', 'ezcaret'], + + init: (editor) => editor.addCommand('eZRemoveBlock', removeBlockCommand), + }); +})(window); diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-fixed.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-fixed.js new file mode 100644 index 00000000..5a0c1f3f --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-fixed.js @@ -0,0 +1,51 @@ +import EzConfigBase from './base'; + +const TOOLBAR_OFFSET = 10; +let isScrollEventAdded = false; +let originalComponentWillUnmount = null; + +export default class EzConfgiFixedBase extends EzConfigBase { + static getTopPosition(block, editor) { + const toolbar = document.querySelector('.ae-toolbar-floating'); + const editorRect = editor.element.getClientRect(); + const toolbarHeight = toolbar.getBoundingClientRect().height; + const shouldBeFixed = editorRect.top - toolbarHeight - 2 * TOOLBAR_OFFSET < 0; + const top = shouldBeFixed + ? TOOLBAR_OFFSET + : editorRect.top + editor.element.getWindow().getScrollPosition().y - toolbarHeight - TOOLBAR_OFFSET; + + toolbar.classList.toggle('ae-toolbar-floating--fixed', shouldBeFixed); + + return top; + } + + static componentWillUnmount() { + if (typeof originalComponentWillUnmount === 'function') { + originalComponentWillUnmount(); + } + + isScrollEventAdded = false; + + window.removeEventListener('scroll', this._updatePosition, false); + } + + getArrowBoxClasses() { + return 'ae-toolbar-floating ae-arrow-box ez-ae-arrow-box-left'; + } + + setPosition(payload) { + const editor = payload.editor.get('nativeEditor'); + const block = EzConfgiFixedBase.getBlockElement(payload); + + if (!isScrollEventAdded) { + originalComponentWillUnmount = this.componentWillUnmount.bind(this); + this.componentWillUnmount = EzConfgiFixedBase.componentWillUnmount.bind(this); + + isScrollEventAdded = true; + + window.addEventListener('scroll', this._updatePosition, false); + } + + return EzConfgiFixedBase.setPositionFor.call(this, block, editor, EzConfgiFixedBase.getTopPosition.bind(this)); + } +} diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-list.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-list.js new file mode 100644 index 00000000..74ea5d5b --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-list.js @@ -0,0 +1,16 @@ +import EzConfigBase from './base'; + +export default class EzListBaseConfig extends EzConfigBase { + constructor(config) { + super(config); + + this.name = this.getConfigName(); + this.buttons = ['ezmoveup', 'ezmovedown', this.getEditAttributesButton(config), 'ezembedinline', 'ezanchor', 'ezblockremove']; + + this.addExtraButtons(config.extraButtons); + } + + getConfigName() { + return ''; + } +} diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-table.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-table.js new file mode 100644 index 00000000..27f392c8 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base-table.js @@ -0,0 +1,30 @@ +import AlloyEditor from 'alloyeditor'; + +export default class EzConfigTableBase { + constructor(config) { + this.name = this.getConfigName(); + + const editAttributesButton = config.attributes[this.name] || config.classes[this.name] ? `${this.name}edit` : ''; + + this.buttons = [ + 'ezmoveup', + 'ezmovedown', + editAttributesButton, + 'tableHeading', + 'ezembedinline', + 'ezanchor', + 'eztablerow', + 'eztablecolumn', + 'eztablecell', + 'eztableremove', + ...config.extraButtons[this.name], + ]; + + this.getArrowBoxClasses = AlloyEditor.SelectionGetArrowBoxClasses.table; + this.setPosition = AlloyEditor.SelectionSetPosition.table; + } + + getConfigName() { + return ''; + } +} diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base.js new file mode 100644 index 00000000..1385954b --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/base.js @@ -0,0 +1,162 @@ +import ReactDOM from 'react-dom'; + +export default class EzConfigBase { + static outlineTotalWidth(block) { + let outlineOffset = parseInt(block.getComputedStyle('outline-offset'), 10); + const outlineWidth = parseInt(block.getComputedStyle('outline-width'), 10); + + if (isNaN(outlineOffset)) { + // Edge does not support offset-offset yet + // 1 comes from the stylesheet, see theme/alloyeditor/content.css + outlineOffset = 1; + } + return outlineOffset + outlineWidth; + } + + static isEmpty(block) { + const nodes = [...block.$.childNodes]; + const count = nodes.length; + const areAllTextNodesEmpty = !!count && nodes.every((node) => node.nodeName === '#text' && !node.data.replace(/\u200B/g, '')); + const isOnlyBreakLine = count === 1 && block.$.childNodes.item(0).localName === 'br'; + + return count === 0 || isOnlyBreakLine || areAllTextNodesEmpty; + } + + static setPositionFor(block, editor, getTopPosition) { + const blockRect = block.getClientRect(); + const outlineWidth = EzConfigBase.outlineTotalWidth(block); + const empty = EzConfigBase.isEmpty(block); + let positionReference = block; + let left = 0; + + if (editor.widgets.getByElement(block)) { + left = blockRect.left; + } else { + if (empty) { + block.appendHtml(' '); + positionReference = block.findOne('span'); + } + + const range = document.createRange(); + const scrollLeft = parseInt(block.$.scrollLeft, 10); + range.selectNodeContents(positionReference.$); + left = range.getBoundingClientRect().left + scrollLeft; + + if (empty) { + positionReference.remove(); + } + } + + const topPosition = getTopPosition(block, editor); + + const domElement = new CKEDITOR.dom.element(ReactDOM.findDOMNode(this)); + + domElement.addClass('ae-toolbar-transition'); + domElement.setStyles({ + left: left - outlineWidth + 'px', + top: topPosition + 'px', + }); + + return true; + } + + static getTopPosition(block, editor) { + const blockRect = block.getClientRect(); + const outlineWidth = EzConfigBase.outlineTotalWidth(block); + const xy = this.getWidgetXYPoint( + blockRect.left - outlineWidth, + blockRect.top + block.getWindow().getScrollPosition().y - outlineWidth, + CKEDITOR.SELECTION_BOTTOM_TO_TOP + ); + + return xy[1]; + } + + static getBlockElement(payload) { + const editor = payload.editor.get('nativeEditor'); + const nativeEvent = payload.editorEvent.data.nativeEvent; + const targetElement = nativeEvent ? new CKEDITOR.dom.element(payload.editorEvent.data.nativeEvent.target) : null; + const isWidgetElement = targetElement ? editor.widgets.getByElement(targetElement) : false; + const path = editor.elementPath(); + let block = path.block; + + if (!block || isWidgetElement) { + const inlineCustomTag = path.elements.find((element) => element.$.dataset.ezelement === 'eztemplateinline'); + + block = inlineCustomTag || targetElement; + } + + if (block.is('li')) { + block = block.getParent(); + } + + return block; + } + + getStyles(customStyles = []) { + const headingLabel = Translator.trans(/*@Desc("Heading")*/ 'toolbar_config_base.heading_label', {}, 'alloy_editor'); + const paragraphLabel = Translator.trans(/*@Desc("Paragraph")*/ 'toolbar_config_base.paragraph_label', {}, 'alloy_editor'); + const formattedLabel = Translator.trans(/*@Desc("Formatted")*/ 'toolbar_config_base.formatted_label', {}, 'alloy_editor'); + + return { + name: 'styles', + cfg: { + showRemoveStylesItem: false, + styles: [ + { name: `${headingLabel} 1`, style: { element: 'h1' } }, + { name: `${headingLabel} 2`, style: { element: 'h2' } }, + { name: `${headingLabel} 3`, style: { element: 'h3' } }, + { name: `${headingLabel} 4`, style: { element: 'h4' } }, + { name: `${headingLabel} 5`, style: { element: 'h5' } }, + { name: `${headingLabel} 6`, style: { element: 'h6' } }, + { name: paragraphLabel, style: { element: 'p' } }, + { name: formattedLabel, style: { element: 'pre' } }, + ...customStyles, + ], + }, + }; + } + + getEditAttributesButton(config) { + return config.attributes[this.name] || config.classes[this.name] ? `${this.name}edit` : ''; + } + + addExtraButtons(extraButtons = {}) { + if (extraButtons[this.name]) { + this.buttons = [...this.buttons, ...extraButtons[this.name]]; + } + } + + /** + * Returns the arrow box classes for the toolbar. The toolbar is + * always positioned above its related block and has a special class to + * move its tail on the left. + * + * @method getArrowBoxClasses + * @return {String} + */ + getArrowBoxClasses() { + return 'ae-arrow-box ae-arrow-box-bottom ez-ae-arrow-box-left'; + } + + /** + * Sets the position of the toolbar. It overrides the default styles + * toolbar positioning to position the toolbar just above its related + * block element. The related block element is the block indicated in + * CKEditor's path or the target of the editorEvent event. + * + * @method setPosition + * @param {Object} payload + * @param {AlloyEditor.Core} payload.editor + * @param {Object} payload.selectionData + * @param {Object} payload.editorEvent + * @return {Boolean} true if the method was able to position the + * toolbar + */ + setPosition(payload) { + const editor = payload.editor.get('nativeEditor'); + const block = EzConfigBase.getBlockElement(payload); + + return EzConfigBase.setPositionFor.call(this, block, editor, EzConfigBase.getTopPosition.bind(this)); + } +} diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-custom-style.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-custom-style.js new file mode 100644 index 00000000..b51877c5 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-custom-style.js @@ -0,0 +1,69 @@ +import EzConfigBase from './base'; + +export default class EzCustomStyleConfig extends EzConfigBase { + constructor(config) { + super(config); + + this.name = 'custom-style'; + this.buttons = [ + 'ezmoveup', + 'ezmovedown', + this.getStyles(config.customStyles), + 'ezanchor', + 'ezblocktextalignleft', + 'ezblocktextaligncenter', + 'ezblocktextalignright', + 'ezblocktextalignjustify', + 'ezblockremove', + ]; + + this.addExtraButtons(config.extraButtons); + } + + getStyles(customStyles = []) { + return { + name: 'styles', + cfg: { + showRemoveStylesItem: false, + styles: [...customStyles], + }, + }; + } + + /** + * Tests whether the `custom style` toolbar should be visible. It is + * visible when an existing custom style gets the focus. + * + * @method test + * @param {Object} payload + * @param {AlloyEditor.Core} payload.editor + * @param {Object} payload.data + * @param {Object} payload.data.selectionData + * @param {Event} payload.data.nativeEvent + * @return {Boolean} + */ + test(payload) { + const nativeEditor = payload.editor.get('nativeEditor'); + const path = nativeEditor.elementPath(); + const isInTable = path && path.contains((element) => element.is('table')); + + return ( + nativeEditor.isSelectionEmpty() && + path && + path.contains((element) => { + const ezElement = element.getAttribute('data-ezelement'); + + return ( + (ezElement === 'eztemplate' || ezElement === 'eztemplateinline') && + element.getAttribute('data-eztype') === 'style' && + !isInTable + ); + }) + ); + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezCustomStyleConfig = EzCustomStyleConfig; diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-custom-tag.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-custom-tag.js new file mode 100644 index 00000000..3f1de5d3 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-custom-tag.js @@ -0,0 +1,51 @@ +import EzConfigBase from './base'; + +export default class EzCustomTagConfig extends EzConfigBase { + constructor(config) { + super(config); + + const editButton = !!config.alloyEditor.attributes ? `${config.name}edit` : ''; + const defaultButtons = [ + 'ezmoveup', + 'ezmovedown', + editButton, + 'ezanchor', + 'ezembedleft', + 'ezembedcenter', + 'ezembedright', + 'ezblockremove', + ]; + const customButtons = config.alloyEditor.toolbarButtons; + const buttons = customButtons && customButtons.length ? customButtons : defaultButtons; + + this.name = config.name; + this.buttons = buttons; + + this.addExtraButtons(config.extraButtons); + + this.test = this.test.bind(this); + } + + /** + * Tests whether the `embed` toolbar should be visible, it is visible + * when an ezembed widget gets the focus. + * + * @method test + * @param {Object} payload + * @param {AlloyEditor.Core} payload.editor + * @param {Object} payload.data + * @param {Object} payload.data.selectionData + * @param {Event} payload.data.nativeEvent + * @return {Boolean} + */ + test(payload) { + const element = payload.data.selectionData.element; + + return !!(element && element.$.dataset.ezname == this.name); + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezCustomTagConfig = EzCustomTagConfig; diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-embed-inline.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-embed-inline.js new file mode 100644 index 00000000..6fc22c70 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-embed-inline.js @@ -0,0 +1,44 @@ +import EzConfigBase from './base'; + +const EMBED_INLINE_NAME = 'ezembedinline'; + +export default class EzEmbedInlineConfig extends EzConfigBase { + constructor(config) { + super(config); + + this.name = 'embedinline'; + this.buttons = [this.getEditAttributesButton(config), 'ezembedupdate', 'ezblockremove']; + + this.addExtraButtons(config.extraButtons); + } + + /** + * Tests whether the `embedinline` toolbar should be visible, it is visible + * when an ezembed widget gets the focus. + * + * @method test + * @param {Object} payload + * @param {AlloyEditor.Core} payload.editor + * @param {Object} payload.data + * @param {Object} payload.data.selectionData + * @param {Event} payload.data.nativeEvent + * @return {Boolean} + */ + test(payload) { + const nativeEvent = payload.data.nativeEvent; + + if (!nativeEvent) { + return false; + } + + const target = new CKEDITOR.dom.element(nativeEvent.target); + const widget = payload.editor.get('nativeEditor').widgets.getByElement(target); + + return !!(widget && widget.name === EMBED_INLINE_NAME); + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezEmbedInlineConfig = EzEmbedInlineConfig; diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-embed.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-embed.js new file mode 100644 index 00000000..e7d9bc08 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-embed.js @@ -0,0 +1,52 @@ +import EzConfigBase from './base'; + +export default class EzEmbedConfig extends EzConfigBase { + constructor(config) { + super(config); + + this.name = 'embed'; + this.buttons = [ + 'ezmoveup', + 'ezmovedown', + this.getEditAttributesButton(config), + 'ezembedupdate', + 'ezanchor', + 'ezembedleft', + 'ezembedcenter', + 'ezembedright', + 'ezblockremove', + ]; + + this.addExtraButtons(config.extraButtons); + } + + /** + * Tests whether the `embed` toolbar should be visible, it is visible + * when an ezembed widget gets the focus. + * + * @method test + * @param {Object} payload + * @param {AlloyEditor.Core} payload.editor + * @param {Object} payload.data + * @param {Object} payload.data.selectionData + * @param {Event} payload.data.nativeEvent + * @return {Boolean} + */ + test(payload) { + const nativeEvent = payload.data.nativeEvent; + + if (!nativeEvent) { + return false; + } + + const target = new CKEDITOR.dom.element(nativeEvent.target); + const widget = payload.editor.get('nativeEditor').widgets.getByElement(target); + + return !!(widget && widget.name === 'ezembed'); + } +} + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezEmbedConfig = EzEmbedConfig; diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-formatted.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-formatted.js new file mode 100644 index 00000000..54e971e5 --- /dev/null +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-formatted.js @@ -0,0 +1,44 @@ +import EzConfigBase from './base'; + +export default class EzFormattedConfig extends EzConfigBase { + constructor(config) { + super(config); + + this.name = 'formatted'; + this.buttons = [ + 'ezmoveup', + 'ezmovedown', + this.getEditAttributesButton(config), + this.getStyles(config.customStyles), + 'ezanchor', + 'ezblockremove', + ]; + + this.addExtraButtons(config.extraButtons); + } + + /** + * Tests whether the `pre` toolbar should be visible. It is + * visible when the selection is empty and when the caret is inside a + * formatted tag (
    ).
    +     *
    +     * @method test
    +     * @param {Object} payload
    +     * @param {AlloyEditor.Core} payload.editor
    +     * @param {Object} payload.data
    +     * @param {Object} payload.data.selectionData
    +     * @param {Event} payload.data.nativeEvent
    +     * @return {Boolean}
    +     */
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +
    +        return nativeEditor.isSelectionEmpty() && path && path.contains('pre');
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezFormattedConfig = EzFormattedConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-heading.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-heading.js
    new file mode 100644
    index 00000000..80ed5a50
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-heading.js
    @@ -0,0 +1,49 @@
    +import EzConfgiFixedBase from './base-fixed';
    +
    +export default class EzHeadingConfig extends EzConfgiFixedBase {
    +    constructor(config) {
    +        super(config);
    +
    +        this.name = 'heading';
    +        this.buttons = [
    +            'ezmoveup',
    +            'ezmovedown',
    +            this.getEditAttributesButton(config),
    +            this.getStyles(config.customStyles),
    +            'ezembedinline',
    +            'ezanchor',
    +            'ezblocktextalignleft',
    +            'ezblocktextaligncenter',
    +            'ezblocktextalignright',
    +            'ezblocktextalignjustify',
    +            'ezblockremove',
    +        ];
    +
    +        this.addExtraButtons(config.extraButtons);
    +    }
    +
    +    /**
    +     * Tests whether the `paragraph` toolbar should be visible. It is
    +     * visible when the selection is empty and when the caret is inside a
    +     * paragraph.
    +     *
    +     * @method test
    +     * @param {Object} payload
    +     * @param {AlloyEditor.Core} payload.editor
    +     * @param {Object} payload.data
    +     * @param {Object} payload.data.selectionData
    +     * @param {Event} payload.data.nativeEvent
    +     * @return {Boolean}
    +     */
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +
    +        return nativeEditor.isSelectionEmpty() && path && path.contains(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezHeadingConfig = EzHeadingConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-image-link.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-image-link.js
    new file mode 100644
    index 00000000..ad0c1dcd
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-image-link.js
    @@ -0,0 +1,37 @@
    +import EzConfigBase from './base';
    +
    +export default class EzEmbedImageLinkConfig extends EzConfigBase {
    +    constructor(config) {
    +        super(config);
    +
    +        this.name = 'embedimagelink';
    +        this.buttons = ['ezimagelink'];
    +
    +        this.addExtraButtons(config.extraButtons);
    +    }
    +
    +    /**
    +     * Tests whether the `image` toolbar should be visible, it is visible
    +     * when an ezembed widget containing an  is visible.
    +     *
    +     * @method test
    +     * @param {Object} payload
    +     * @param {AlloyEditor.Core} payload.editor
    +     * @param {Object} payload.data
    +     * @param {Object} payload.data.selectionData
    +     * @param {Event} payload.data.nativeEvent
    +     * @return {Boolean}
    +     */
    +    test(payload) {
    +        const nativeEvent = payload.data.nativeEvent;
    +        const target = new CKEDITOR.dom.element(nativeEvent.target);
    +        const widget = payload.editor.get('nativeEditor').widgets.getByElement(target);
    +
    +        return !!(widget && widget.name === 'ezembed' && widget.isImage() && widget.isEditingLink());
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezEmbedImageLinkConfig = EzEmbedImageLinkConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-image.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-image.js
    new file mode 100644
    index 00000000..9d38ebde
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-image.js
    @@ -0,0 +1,54 @@
    +import EzConfigBase from './base';
    +
    +export default class EzEmbedImageConfig extends EzConfigBase {
    +    constructor(config) {
    +        super(config);
    +
    +        this.name = 'embedimage';
    +        this.buttons = [
    +            'ezmoveup',
    +            'ezmovedown',
    +            this.getEditAttributesButton(config),
    +            'ezimageupdate',
    +            'ezimagevariation',
    +            'ezimagelink',
    +            'ezanchor',
    +            'ezembedleft',
    +            'ezembedcenter',
    +            'ezembedright',
    +            'ezblockremove',
    +        ];
    +
    +        this.addExtraButtons(config.extraButtons);
    +    }
    +
    +    /**
    +     * Tests whether the `image` toolbar should be visible, it is visible
    +     * when an ezembed widget containing an  is visible.
    +     *
    +     * @method test
    +     * @param {Object} payload
    +     * @param {AlloyEditor.Core} payload.editor
    +     * @param {Object} payload.data
    +     * @param {Object} payload.data.selectionData
    +     * @param {Event} payload.data.nativeEvent
    +     * @return {Boolean}
    +     */
    +    test(payload) {
    +        const nativeEvent = payload.data.nativeEvent;
    +
    +        if (!nativeEvent) {
    +            return false;
    +        }
    +
    +        const target = new CKEDITOR.dom.element(nativeEvent.target);
    +        const widget = payload.editor.get('nativeEditor').widgets.getByElement(target);
    +
    +        return !!(widget && widget.name === 'ezembed' && widget.isImage());
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezEmbedImageConfig = EzEmbedImageConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-inline-custom-tag.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-inline-custom-tag.js
    new file mode 100644
    index 00000000..6db5fcef
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-inline-custom-tag.js
    @@ -0,0 +1,46 @@
    +import EzConfigBase from './base';
    +
    +export default class EzInlineCustomTagConfig extends EzConfigBase {
    +    constructor(config) {
    +        super(config);
    +
    +        const editButton = !!config.alloyEditor.attributes ? `${config.name}edit` : '';
    +        const defaultButtons = [editButton, 'ezblockremove'];
    +        const customButtons = config.alloyEditor.toolbarButtons;
    +        const buttons = customButtons && customButtons.length ? customButtons : defaultButtons;
    +
    +        this.name = config.name;
    +        this.buttons = buttons;
    +
    +        this.addExtraButtons(config.extraButtons);
    +
    +        this.test = this.test.bind(this);
    +    }
    +
    +    /**
    +     * Tests whether the `inline custom tag` toolbar should be visible, it is visible
    +     * when an ezinlinecustomtag widget gets the focus.
    +     *
    +     * @method test
    +     * @param {Object} payload
    +     * @param {AlloyEditor.Core} payload.editor
    +     * @param {Object} payload.data
    +     * @param {Object} payload.data.selectionData
    +     * @param {Event} payload.data.nativeEvent
    +     * @return {Boolean}
    +     */
    +    test(payload) {
    +        const element = payload.data.selectionData.element;
    +        const path = payload.editor.get('nativeEditor').elementPath();
    +        const isInlineCustomTag = path.contains(
    +            (element) => element.$.dataset.ezelement === 'eztemplateinline' && element.$.dataset.ezname === this.name
    +        );
    +
    +        return !!((element && element.$.dataset.ezname === this.name) || isInlineCustomTag);
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezInlineCustomTagConfig = EzInlineCustomTagConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-link.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-link.js
    new file mode 100644
    index 00000000..dc4a036c
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-link.js
    @@ -0,0 +1,52 @@
    +import AlloyEditor from 'alloyeditor';
    +
    +export default class EzLinkConfig {
    +    constructor(config) {
    +        this.name = 'link';
    +        this.buttons = ['ezlinkedit', ...config.extraButtons[this.name]];
    +
    +        this.test = AlloyEditor.SelectionTest.link;
    +    }
    +
    +    /**
    +     * Returns the arrow box classes for the toolbar. The toolbar is
    +     * always positioned above its related block and has a special class to
    +     * move its tail on the left.
    +     *
    +     * @method getArrowBoxClasses
    +     * @return {String}
    +     */
    +    getArrowBoxClasses() {
    +        return 'ae-arrow-box ae-arrow-box-bottom';
    +    }
    +
    +    /**
    +     * Sets the position of the toolbar. It overrides the default styles
    +     * toolbar positioning to position the toolbar just above its related
    +     * block element. The related block element is the block indicated in
    +     * CKEditor's path or the target of the editorEvent event.
    +     *
    +     * @method setPosition
    +     * @param {Object} payload
    +     * @param {AlloyEditor.Core} payload.editor
    +     * @param {Object} payload.selectionData
    +     * @param {Object} payload.editorEvent
    +     * @return {Boolean} true if the method was able to position the
    +     * toolbar
    +     */
    +    setPosition(payload) {
    +        const domElement = new CKEDITOR.dom.element(ReactDOM.findDOMNode(this));
    +        const region = payload.selectionData.region;
    +        const xy = this.getWidgetXYPoint(region.left, region.top, CKEDITOR.SELECTION_BOTTOM_TO_TOP);
    +
    +        domElement.addClass('ae-toolbar-transition');
    +        domElement.setStyles({ left: xy[0] + 'px', top: xy[1] + 'px' });
    +
    +        return true;
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezLinkConfig = EzLinkConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-item.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-item.js
    new file mode 100644
    index 00000000..1c87ed9c
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-item.js
    @@ -0,0 +1,16 @@
    +import EzConfigListBase from './base-list';
    +
    +export default class EzListItemConfig extends EzConfigListBase {
    +    getConfigName() {
    +        return 'li';
    +    }
    +
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +
    +        return path && path.lastElement.is('li');
    +    }
    +}
    +
    +eZ.addConfig('ezAlloyEditor.ezListItemConfig', EzListItemConfig);
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-ordered.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-ordered.js
    new file mode 100644
    index 00000000..8d794314
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-ordered.js
    @@ -0,0 +1,16 @@
    +import EzConfigListBase from './base-list';
    +
    +export default class EzListOrderedConfig extends EzConfigListBase {
    +    getConfigName() {
    +        return 'ol';
    +    }
    +
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +
    +        return path && path.lastElement.is('ol');
    +    }
    +}
    +
    +eZ.addConfig('ezAlloyEditor.ezListOrderedConfig', EzListOrderedConfig);
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-unordered.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-unordered.js
    new file mode 100644
    index 00000000..29886559
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-list-unordered.js
    @@ -0,0 +1,16 @@
    +import EzConfigListBase from './base-list';
    +
    +export default class EzListUnorderedConfig extends EzConfigListBase {
    +    getConfigName() {
    +        return 'ul';
    +    }
    +
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +
    +        return path && path.lastElement.is('ul');
    +    }
    +}
    +
    +eZ.addConfig('ezAlloyEditor.ezListUnorderedConfig', EzListUnorderedConfig);
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-paragraph.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-paragraph.js
    new file mode 100644
    index 00000000..21e5f8e2
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-paragraph.js
    @@ -0,0 +1,49 @@
    +import EzConfgiFixedBase from './base-fixed';
    +
    +export default class EzParagraphConfig extends EzConfgiFixedBase {
    +    constructor(config) {
    +        super(config);
    +
    +        this.name = 'paragraph';
    +        this.buttons = [
    +            'ezmoveup',
    +            'ezmovedown',
    +            this.getEditAttributesButton(config),
    +            this.getStyles(config.customStyles),
    +            'ezembedinline',
    +            'ezanchor',
    +            'ezblocktextalignleft',
    +            'ezblocktextaligncenter',
    +            'ezblocktextalignright',
    +            'ezblocktextalignjustify',
    +            'ezblockremove',
    +        ];
    +
    +        this.addExtraButtons(config.extraButtons);
    +    }
    +
    +    /**
    +     * Tests whether the `paragraph` toolbar should be visible. It is
    +     * visible when the selection is empty and when the caret is inside a
    +     * paragraph.
    +     *
    +     * @method test
    +     * @param {Object} payload
    +     * @param {AlloyEditor.Core} payload.editor
    +     * @param {Object} payload.data
    +     * @param {Object} payload.data.selectionData
    +     * @param {Event} payload.data.nativeEvent
    +     * @return {Boolean}
    +     */
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +
    +        return nativeEditor.isSelectionEmpty() && path && path.contains('p');
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezParagraphConfig = EzParagraphConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-cell.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-cell.js
    new file mode 100644
    index 00000000..18f352fc
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-cell.js
    @@ -0,0 +1,17 @@
    +import EzConfigTableBase from './base-table';
    +
    +export default class EzTableCellConfig extends EzConfigTableBase {
    +    getConfigName() {
    +        return 'td';
    +    }
    +
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +        const lastElement = path.lastElement;
    +
    +        return lastElement.is('td');
    +    }
    +}
    +
    +eZ.addConfig('ezAlloyEditor.ezTableCellConfig', EzTableCellConfig);
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-row.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-row.js
    new file mode 100644
    index 00000000..b7a29ee4
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-row.js
    @@ -0,0 +1,17 @@
    +import EzConfigTableBase from './base-table';
    +
    +export default class EzTableRowConfig extends EzConfigTableBase {
    +    getConfigName() {
    +        return 'tr';
    +    }
    +
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +        const lastElement = path.lastElement;
    +
    +        return lastElement.is('tr');
    +    }
    +}
    +
    +eZ.addConfig('ezAlloyEditor.ezTableRowConfig', EzTableRowConfig);
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table.js
    new file mode 100644
    index 00000000..dfdeab3a
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table.js
    @@ -0,0 +1,20 @@
    +import EzConfigTableBase from './base-table';
    +
    +export default class EzTableConfig extends EzConfigTableBase {
    +    getConfigName() {
    +        return 'table';
    +    }
    +
    +    test(payload) {
    +        const nativeEditor = payload.editor.get('nativeEditor');
    +        const path = nativeEditor.elementPath();
    +        const lastElement = path.lastElement;
    +
    +        return lastElement.is('table');
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezTableConfig = EzTableConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-text.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-text.js
    new file mode 100644
    index 00000000..d5465641
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-text.js
    @@ -0,0 +1,37 @@
    +import AlloyEditor from 'alloyeditor';
    +
    +export default class EzTextConfig {
    +    constructor(config) {
    +        this.name = 'text';
    +        this.buttons = [
    +            this.getStyles(config.customStyles),
    +            'ezbold',
    +            'ezitalic',
    +            'ezunderline',
    +            'ezsubscript',
    +            'ezsuperscript',
    +            'ezquote',
    +            'ezstrike',
    +            'ezlink',
    +            ...config.inlineCustomTags,
    +            ...config.extraButtons[this.name],
    +        ];
    +
    +        this.test = AlloyEditor.SelectionTest.text;
    +    }
    +
    +    getStyles(customStyles = []) {
    +        return {
    +            name: 'styles',
    +            cfg: {
    +                showRemoveStylesItem: true,
    +                styles: [...customStyles],
    +            },
    +        };
    +    }
    +}
    +
    +const eZ = (window.eZ = window.eZ || {});
    +
    +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {};
    +eZ.ezAlloyEditor.ezTextConfig = EzTextConfig;
    diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/ez-add.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/ez-add.js
    new file mode 100644
    index 00000000..c11c1220
    --- /dev/null
    +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/ez-add.js
    @@ -0,0 +1,94 @@
    +import React, { Component } from 'react';
    +import PropTypes from 'prop-types';
    +import AlloyEditor from 'alloyeditor';
    +
    +export default class EzToolbarAdd extends AlloyEditor.Toolbars.add {
    +    static get key() {
    +        return 'ezadd';
    +    }
    +
    +    constructor(props) {
    +        super(props);
    +
    +        this.setPosition = this.setPosition.bind(this);
    +    }
    +
    +    setPosition() {
    +        const domNode = ReactDOM.findDOMNode(this);
    +        const rect = this.props.editor.get('nativeEditor').element.$.getBoundingClientRect();
    +
    +        new CKEDITOR.dom.element(domNode).setStyles({ left: `${rect.left}px` });
    +    }
    +
    +    componentDidUpdate(prevProps, prevState) {
    +        const { selectionData } = this.props;
    +
    +        this._updatePosition();
    +
    +        if (selectionData && !selectionData.region.top) {
    +            this.setTopPosition();
    +        }
    +
    +        // In case of exclusive rendering, focus the first descendant (button)
    +        // so the user will be able to start interacting with the buttons immediately.
    +        if (this.props.renderExclusive) {
    +            this.focus();
    +
    +            this._animate(this.setPosition);
    +        }
    +    }
    +
    +    setTopPosition() {
    +        const { editor } = this.props;
    +        const domNode = ReactDOM.findDOMNode(this);
    +        const path = editor.get('nativeEditor').elementPath();
    +        const table = path.elements.find((element) => element.is('table'));
    +
    +        if (!table) {
    +            return;
    +        }
    +
    +        const rect = table.$.getBoundingClientRect();
    +
    +        new CKEDITOR.dom.element(domNode).setStyles({ top: `${rect.top}px` });
    +    }
    +
    +    /**
    +     * Lifecycle. Renders the UI of the button.
    +     *
    +     * @method render
    +     * @return {Object} The content which should be rendered.
    +     */
    +    render() {
    +        const { selectionData, editor } = this.props;
    +        const path = editor.get('nativeEditor').elementPath();
    +        const isInlineCustomTag = path && path.contains((element) => element.$.dataset.ezelement === 'eztemplateinline');
    +
    +        if ((selectionData && selectionData.text) || isInlineCustomTag) {
    +            return null;
    +        }
    +
    +        const buttons = this._getButtons();
    +        const className = this._getToolbarClassName();
    +
    +        return (
    +            
    +
    {buttons}
    +
    + ); + } +} + +AlloyEditor.Toolbars[EzToolbarAdd.key] = EzToolbarAdd; + +const eZ = (window.eZ = window.eZ || {}); + +eZ.ezAlloyEditor = eZ.ezAlloyEditor || {}; +eZ.ezAlloyEditor.ezToolbarAdd = EzToolbarAdd; diff --git a/src/bundle/Resources/public/scss/_alloyeditor-ez.scss b/src/bundle/Resources/public/scss/_alloyeditor-ez.scss new file mode 100644 index 00000000..7f8cc10c --- /dev/null +++ b/src/bundle/Resources/public/scss/_alloyeditor-ez.scss @@ -0,0 +1,1245 @@ +@font-face { + font-family: 'alloyeditor-ez'; + src: url('/bundles/ezplatformrichtext/fonts/alloyeditor-ez.eot'); + src: url('/bundles/ezplatformrichtext/fonts/alloyeditor-ez.eot?#iefix') format('embedded-opentype'), + url('/bundles/ezplatformrichtext/fonts/alloyeditor-ez.woff') format('woff'), + url('/bundles/ezplatformrichtext/fonts/alloyeditor-ez.ttf') format('truetype'), + url('/bundles/ezplatformrichtext/fonts/alloyeditor-ez.svg#alloyeditor-ez') format('svg'); + font-weight: normal; + font-style: normal; +} + +.cke_widget_wrapper:hover > .cke_widget_element[data-ezelement='ezembed'] { + outline: 1px solid $ez-custom-tag-color-border; +} + +.ae-ui .ae-toolbar .ae-container-edit-table .ae-button { + border-radius: 50%; + background-color: $ez-ae-color-positive; + color: $ez-ae-color-primary; + margin-left: 16px; +} + +.ae-container-edit-table .ae-button .ae-icon-ok { + font-size: 30px; +} + +.ae-container-edit-table .ae-container-input { + border: 1px solid $ez-ae-color-ground; +} + +.ae-ui .ae-container-edit-table .ae-container-input .ae-input { + height: 30px; +} + +[class*='ae-icon-'], +[class*=' ae-icon-'] { + display: block; + font-family: 'alloyeditor-ez'; + speak: none; + font-size: 20px; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1.2; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.ae-icon-add:before { + content: '\'; +} + +.ae-icon-align-center:before { + content: '\'; +} + +.ae-icon-align-justified:before { + content: '\'; +} + +.ae-icon-align-left:before { + content: '\'; +} + +.ae-icon-align-right:before { + content: '\'; +} + +.ae-icon-arrow:before { + content: '\'; +} + +.ae-icon-audio:before { + content: '\'; +} + +.ae-icon-bin:before { + content: '\'; +} + +.ae-icon-bold:before { + content: '\'; +} + +.ae-icon-bulleted-list:before { + content: '\'; +} + +.ae-icon-camera:before { + content: '\'; +} + +.ae-icon-cell:before { + content: '\'; +} + +.ae-icon-close:before { + content: '\'; +} + +.ae-icon-code:before { + content: '\'; +} + +.ae-icon-column:before { + content: '\'; +} + +.ae-icon-embed:before { + content: '\'; +} + +.ae-icon-h1:before { + content: '\'; +} + +.ae-icon-h2:before { + content: '\'; +} + +.ae-icon-image:before { + content: '\'; +} + +.ae-icon-indent-block:before { + content: '\'; +} + +.ae-icon-italic:before { + content: '\'; +} + +.ae-icon-link:before { + content: '\'; +} + +.ae-icon-numbered-list:before { + content: '\'; +} + +.ae-icon-ok:before { + content: '\'; +} + +.ae-icon-outdent-block:before { + content: '\'; +} + +.ae-icon-quote:before { + content: '\'; +} + +.ae-icon-remove:before { + content: '\'; +} + +.ae-icon-removeformat:before { + content: '\'; +} + +.ae-icon-row:before { + content: '\'; +} + +.ae-icon-separator:before { + content: '\'; +} + +.ae-icon-strike:before { + content: '\'; +} + +.ae-icon-subscript:before { + content: '\'; +} + +.ae-icon-superscript:before { + content: '\'; +} + +.ae-icon-table:before { + content: '\'; +} + +.ae-icon-twitter:before { + content: '\'; +} + +.ae-icon-underline:before { + content: '\'; +} + +.ae-icon-unlink:before { + content: '\'; +} + +.ae-icon-video:before { + content: '\'; +} + +@charset "UTF-8"; + +/** GENERAL VARIABLES */ + +/** COMPONENT VARIABLES **/ + +/** DROPDOWN-LISTBOX **/ + +/** DROPDOWN-LISTBOX HEADER **/ + +/** DROPDOWN-LISTBOX ITEM */ + +/** ARROW-BOX **/ + +/** CONTAINER **/ + +/** CONTAINER-DROPDOWN **/ + +/** CONTAINER-EDIT **/ + +/** CONTAINER-INPUT */ + +/** DROPDOWN **/ + +/** DROPDOWN-LISTBOX HEADER **/ + +/** DROPDOWN-LISTBOX ITEM **/ + +/** TOOLBAR **/ + +/** + * The order of imports is as follow: + * CSS for outer (parent) components should precede + * the CSS of nested components or components which + * stay on the same level in the hierarchy. + */ + +.ae-placeholder:not(:focus):before { + content: attr(data-placeholder); +} + +.ae-twitter-link { + padding: 0 5px; +} + +.ae-twitter-link:after { + display: inline-block; + margin: 0 0 0 4px; + vertical-align: middle; +} + +.ae-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.ae-ui IE10-PLUS::-ms-reveal, +.ae-ui [class^='ae-toolbar'] { + height: auto; + box-sizing: content-box; +} + +.ae-ui .ae-toolbar, +.ae-ui [class^='ae-toolbar-'] { + line-height: 1; + padding: 4px; + position: absolute; +} + +.ae-ui .ae-toolbar input, +.ae-ui .ae-toolbar textarea, +.ae-ui .ae-toolbar keygen, +.ae-ui .ae-toolbar select, +.ae-ui .ae-toolbar button, +.ae-ui [class^='ae-toolbar-'] input, +.ae-ui [class^='ae-toolbar-'] textarea, +.ae-ui [class^='ae-toolbar-'] keygen, +.ae-ui [class^='ae-toolbar-'] select, +.ae-ui [class^='ae-toolbar-'] button { + font-size: 15px; +} + +.ae-ui .ae-toolbar.ae-toolbar-transition, +.ae-ui [class^='ae-toolbar-'].ae-toolbar-transition { + -webkit-transition-property: height, left, opacity, top; + -moz-transition-property: height, left, opacity, top; + transition-property: height, left, opacity, top; + -webkit-transition-duration: 0.15s; + -moz-transition-duration: 0.15s; + transition-duration: 0.15s; + -webkit-transition-timing-function: ease-out; + -moz-transition-timing-function: ease-out; + transition-timing-function: ease-out; +} + +.ae-ui .ae-toolbar .ae-button, +.ae-ui .ae-toolbar .ae-toolbar-element, +.ae-ui [class^='ae-toolbar-'] .ae-button, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element { + border-width: 0; + color: inherit; + cursor: pointer; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.ae-ui .ae-toolbar .ae-button:hover:disabled, +.ae-ui .ae-toolbar .ae-button:hover.ae-button-disabled, +.ae-ui .ae-toolbar .ae-button:focus:disabled, +.ae-ui .ae-toolbar .ae-button:focus.ae-button-disabled, +.ae-ui .ae-toolbar .ae-toolbar-element:hover:disabled, +.ae-ui .ae-toolbar .ae-toolbar-element:hover.ae-button-disabled, +.ae-ui .ae-toolbar .ae-toolbar-element:focus:disabled, +.ae-ui .ae-toolbar .ae-toolbar-element:focus.ae-button-disabled, +.ae-ui [class^='ae-toolbar-'] .ae-button:hover:disabled, +.ae-ui [class^='ae-toolbar-'] .ae-button:hover.ae-button-disabled, +.ae-ui [class^='ae-toolbar-'] .ae-button:focus:disabled, +.ae-ui [class^='ae-toolbar-'] .ae-button:focus.ae-button-disabled, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element:hover:disabled, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element:hover.ae-button-disabled, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element:focus:disabled, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element:focus.ae-button-disabled { + color: inherit; +} + +.ae-ui .ae-toolbar .ae-button:disabled, +.ae-ui .ae-toolbar .ae-button.ae-button-disabled, +.ae-ui .ae-toolbar .ae-toolbar-element:disabled, +.ae-ui .ae-toolbar .ae-toolbar-element.ae-button-disabled, +.ae-ui [class^='ae-toolbar-'] .ae-button:disabled, +.ae-ui [class^='ae-toolbar-'] .ae-button.ae-button-disabled, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element:disabled, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element.ae-button-disabled { + cursor: auto; + opacity: 0.3; +} + +.ae-ui .ae-toolbar .ae-button, +.ae-ui [class^='ae-toolbar-'] .ae-button { + height: 34px; + width: 34px; +} + +.ae-ui .ae-toolbar-add { + padding: 0; +} + +.ae-ui .ae-toolbar-add .ae-button { + height: 34px; + width: 34px; +} + +.ae-ui .ae-arrow-box:after { + content: ''; + margin: auto; + position: absolute; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-bottom:after { + height: 0; + width: 0; + border-left: 9px solid transparent; + border-right: 9px solid transparent; + border-top: 8px solid currentColor; + left: 0; + right: 0; + top: 100%; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-top:after { + height: 0; + width: 0; + border-bottom: 8px solid currentColor; + border-left: 9px solid transparent; + border-right: 9px solid transparent; + left: 0; + right: 0; + top: -8px; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-top-left:after { + height: 0; + width: 0; + border-bottom: 8px solid currentColor; + border-left: 9px solid transparent; + border-right: 9px solid transparent; + left: 6px; + top: -8px; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-top-right:after { + height: 0; + width: 0; + border-bottom: 8px solid currentColor; + border-left: 9px solid transparent; + border-right: 9px solid transparent; + right: 6px; + top: -8px; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-left:after { + height: 0; + width: 0; + border-bottom: 9px solid transparent; + border-right: 8px solid currentColor; + border-top: 9px solid transparent; + left: -8px; + top: 0; + bottom: 0; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-right:after { + height: 0; + width: 0; + border-bottom: 9px solid transparent; + border-left: 8px solid currentColor; + border-top: 9px solid transparent; + right: -8px; + top: 0; + bottom: 0; +} + +.ae-ui .ae-container, +.ae-ui [class^='ae-container-'] { + -webkit-box-align: center; + -moz-box-align: center; + box-align: center; + -webkit-align-items: center; + -moz-align-items: center; + -ms-align-items: center; + -o-align-items: center; + align-items: center; + -ms-flex-align: center; + display: -webkit-inline-box; + display: -moz-inline-box; + display: inline-box; + display: -webkit-inline-flex; + display: -moz-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-orient: horizontal; + -moz-box-orient: horizontal; + box-orient: horizontal; + -webkit-box-direction: normal; + -moz-box-direction: normal; + box-direction: normal; + -webkit-flex-direction: row; + -moz-flex-direction: row; + flex-direction: row; + -ms-flex-direction: row; + -webkit-box-lines: multiple; + -moz-box-lines: multiple; + box-lines: multiple; + -webkit-flex-wrap: wrap; + -moz-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + box-sizing: border-box; +} + +.ae-ui .ae-container *, +.ae-ui .ae-container *:after, +.ae-ui .ae-container *:before, +.ae-ui [class^='ae-container-'] *, +.ae-ui [class^='ae-container-'] *:after, +.ae-ui [class^='ae-container-'] *:before { + box-sizing: inherit; + color: inherit; +} + +.ae-ui .ae-container input, +.ae-ui .ae-container textarea, +.ae-ui .ae-container keygen, +.ae-ui .ae-container select, +.ae-ui .ae-container button, +.ae-ui [class^='ae-container-'] input, +.ae-ui [class^='ae-container-'] textarea, +.ae-ui [class^='ae-container-'] keygen, +.ae-ui [class^='ae-container-'] select, +.ae-ui [class^='ae-container-'] button { + color: initial; +} + +.ae-ui .ae-container .ae-container, +.ae-ui .ae-container [class^='ae-container-'], +.ae-ui .ae-container .ae-button, +.ae-ui .ae-container .ae-toolbar-element, +.ae-ui .ae-container label, +.ae-ui [class^='ae-container-'] .ae-container, +.ae-ui [class^='ae-container-'] [class^='ae-container-'], +.ae-ui [class^='ae-container-'] .ae-button, +.ae-ui [class^='ae-container-'] .ae-toolbar-element, +.ae-ui [class^='ae-container-'] label { + font-size: 15px; + margin: 0; + position: relative; +} + +.ae-ui .ae-container .ae-container:not(:last-child), +.ae-ui .ae-container [class^='ae-container-']:not(:last-child), +.ae-ui .ae-container .ae-button:not(:last-child), +.ae-ui .ae-container .ae-toolbar-element:not(:last-child), +.ae-ui .ae-container label:not(:last-child), +.ae-ui [class^='ae-container-'] .ae-container:not(:last-child), +.ae-ui [class^='ae-container-'] [class^='ae-container-']:not(:last-child), +.ae-ui [class^='ae-container-'] .ae-button:not(:last-child), +.ae-ui [class^='ae-container-'] .ae-toolbar-element:not(:last-child), +.ae-ui [class^='ae-container-'] label:not(:last-child) { + margin-right: 0; +} + +.ae-ui .ae-container-edit-link, +.ae-ui .ae-container-edit-table { + height: 40px \9; +} + +.ae-ui .ae-container-edit-link *, +.ae-ui .ae-container-edit-table * { + float: left \9; +} + +.ae-ui .ae-container-edit-link label, +.ae-ui .ae-container-edit-table label { + padding: 0 10px; + position: relative \9; + top: 25% \9; + transform: translateY(-50%) \9; +} + +.ae-ui .ae-container-edit-link IE10-PLUS::-ms-reveal, +.ae-ui .ae-container-edit-link label, +.ae-ui .ae-container-edit-table IE10-PLUS::-ms-reveal, +.ae-ui .ae-container-edit-table label { + top: 0; + transform: translateY(0); +} + +.ae-ui .ae-container-input { + -webkit-box-lines: single; + -moz-box-lines: single; + box-lines: single; + -webkit-flex-wrap: nowrap; + -moz-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + width: 240px; +} + +.ae-ui .ae-container-input.xxl { + width: 560px; +} + +.ae-ui .ae-container-input.medium { + width: 120px; +} + +.ae-ui .ae-container-input.small { + width: 60px; +} + +.ae-ui .ae-container-input.flexible { + width: auto; + flex-grow: 1; +} + +.ae-ui .ae-container-input input, +.ae-ui .ae-container-input .ae-input { + -webkit-box-flex: 1; + -moz-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -moz-flex: 1; + -ms-flex: 1; + flex: 1; + height: 40px; + margin: 0; + max-width: 100%; + padding: 10px 6px 10px 12px; + display: inline-block \9; + width: 200px \9; +} + +.ae-ui .ae-container-input .ae-container-dropdown { + padding-left: 4px; +} + +.ae-ui .ae-container-input .ae-icon-remove { + float: right \9; +} + +.ae-ui .ae-container-dropdown, +.ae-ui [class^='ae-container-dropdown-'] { + float: left \9; + width: 120px !important; +} + +.ae-ui .ae-container-dropdown > .ae-toolbar-element, +.ae-ui [class^='ae-container-dropdown-'] > .ae-toolbar-element { + float: left \9; + height: 34px; + text-align: left; + width: 100%; +} + +.ae-ui .ae-container-dropdown > .ae-toolbar-element .ae-container, +.ae-ui [class^='ae-container-dropdown-'] > .ae-toolbar-element .ae-container { + padding: 0 10px; + height: 100% \9; + width: 100%; +} + +.ae-ui .ae-container-dropdown > .ae-toolbar-element .ae-container .ae-icon-arrow, +.ae-ui [class^='ae-container-dropdown-'] > .ae-toolbar-element .ae-container .ae-icon-arrow { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg); + float: right \9; + position: absolute \9; + right: 0 \9; + top: 25% \9; +} + +.ae-ui .ae-container-dropdown > .ae-toolbar-element .ae-container .ae-container-dropdown-selected-item, +.ae-ui [class^='ae-container-dropdown-'] > .ae-toolbar-element .ae-container .ae-container-dropdown-selected-item { + -webkit-box-flex: 1; + -moz-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -moz-flex: 1; + -ms-flex: 1; + flex: 1; + display: inline-block; + line-height: 30px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + position: relative \9; +} + +.ae-ui .ae-container-dropdown > .ae-toolbar-element .ae-container IE10-PLUS::-ms-reveal, +.ae-ui .ae-container-dropdown > .ae-toolbar-element .ae-container .ae-container-dropdown-selected-item, +.ae-ui [class^='ae-container-dropdown-'] > .ae-toolbar-element .ae-container IE10-PLUS::-ms-reveal, +.ae-ui [class^='ae-container-dropdown-'] > .ae-toolbar-element .ae-container .ae-container-dropdown-selected-item { + top: 0; + transform: translateY(0); +} + +.ae-ui .ae-container-dropdown-small { + width: 80px !important; +} + +.ae-ui .ae-container-dropdown-medium { + width: 160px !important; +} + +.ae-ui .ae-container-dropdown-xl { + width: 200px !important; +} + +.ae-ui .ae-button-bridge [class*='ae-icon-'], +.ae-ui .ae-button-bridge [class*=' ae-icon-'] { + background-repeat: no-repeat; + display: block; + height: 16px; + width: 16px; + margin-left: 12px; +} + +.ae-ui .ae-has-dropdown { + display: block; + float: left \9; + /* Set width here (not min-width) to prevent the dropdown container from moving when expanding/collapsing the it */ + width: 40px; +} + +.ae-ui .ae-dropdown, +.ae-ui [class^='ae-dropdown-'] { + left: 0; + padding: 0; + position: absolute; + z-index: 1; + top: 44px; +} + +.ae-ui .ae-dropdown .ae-list-header, +.ae-ui [class^='ae-dropdown-'] .ae-list-header { + display: inline-block; + margin: 8px 12px; +} + +.ae-ui .ae-dropdown .ae-list-header:first-child, +.ae-ui [class^='ae-dropdown-'] .ae-list-header:first-child { + margin-top: 4px; +} + +.ae-ui .ae-dropdown .ae-listbox, +.ae-ui [class^='ae-dropdown-'] .ae-listbox { + list-style: none; + margin: 0; + min-height: 44px; + max-height: 400px; + min-width: 132px; + padding: 0; + overflow-y: auto; +} + +.ae-ui .ae-dropdown .ae-listbox .ae-toolbar-element, +.ae-ui [class^='ae-dropdown-'] .ae-listbox .ae-toolbar-element { + font-size: 16px; + height: 3em; + line-height: 28px; + margin-right: 15px; + max-height: 44px; + min-width: 100%; + padding: 8px 12px; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + width: auto; +} + +.ae-ui .ae-dropdown .ae-listbox .ae-toolbar-element *, +.ae-ui [class^='ae-dropdown-'] .ae-listbox .ae-toolbar-element * { + display: inline; +} + +.ae-ui .ae-dropdown .ae-listbox .ae-toolbar-element[class^='ae-icon-']:before, +.ae-ui [class^='ae-dropdown-'] .ae-listbox .ae-toolbar-element[class^='ae-icon-']:before { + padding-right: 4px; + vertical-align: middle; +} + +.ae-ui .ae-dropdown *, +.ae-ui [class^='ae-dropdown-'] * { + display: block \9; +} + +.ae-ui .ae-camera { + align-items: center; + display: flex; + flex-flow: column; + justify-content: center; +} + +.ae-ui .ae-camera .ae-camera-canvas { + left: -10000px; + position: absolute; + top: -10000px; +} + +.ae-ui .ae-camera .ae-camera-shoot { + margin-top: 10px; +} + +/** GENERAL VARIABLES **/ + +/** + * AUTOGENERATED FONT ICON MAP + * + * This map is autogenerated in build-time based on the contents + * of the icons/svg folder. It maps the generated icon names with + * their corresponding glyphs. + * + * Use this for consistency in any skin where an icon needs to be + * referenced from within the css like this: + * + * .ae-some-link:after { + * content: map-get($font-icon-map, iconName); + * } + */ + +/** COLOR PALETTE **/ + +/** COMPONENT VARIABLES **/ + +/** ARROW-BOX **/ + +/** CONTAINER-DROPDOWN **/ + +/** CONTAINER-INPUT **/ + +/** DROPDOWN **/ + +/** DROPDOWN-LISTBOX HEADER **/ + +/** SELECTION **/ + +/** TOOLBAR **/ + +.ae-editable ::-moz-selection { + background: #869cad !important; + color: #fff; + text-shadow: none; +} + +.ae-editable ::selection { + background: #869cad !important; + color: #fff; + text-shadow: none; +} + +.ae-twitter-link { + background-color: #f8f8f8; +} + +.ae-twitter-link:after { + content: ''; + font-family: alloyeditor-ez; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-bottom:after { + color: $ez-ae-color-primary; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-top:after { + color: $ez-ae-color-primary; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-top-left:after { + color: $ez-ae-color-primary; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-top-right:after { + color: $ez-ae-color-primary; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-left:after { + color: $ez-ae-color-primary; +} + +.ae-ui .ae-arrow-box.ae-arrow-box-right:after { + color: $ez-ae-color-primary; +} + +.ae-ui .ae-container-dropdown > .ae-toolbar-element .ae-container .ae-icon-arrow, +.ae-ui [class^='ae-container-dropdown-'] > .ae-toolbar-element .ae-container .ae-icon-arrow { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); +} + +.ae-ui .ae-container-input { + background-color: #fff; + border-radius: 2px; +} + +.ae-ui .ae-container-input .ae-icon-remove { + color: #869cad; + font-size: 20px; + line-height: 20px; +} + +.ae-ui .ae-container-input .ae-container-dropdown { + background-color: #fff; + color: #000; +} + +.ae-ui .ae-container-input input, +.ae-ui .ae-container-input .ae-input { + border-radius: 2px; + border-width: 0; + color: #869cad; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 14px; + outline: none; +} + +.ae-ui .ae-container-input input::-webkit-input-placeholder, +.ae-ui .ae-container-input .ae-input::-webkit-input-placeholder { + color: #c0cbd4; +} + +.ae-ui .ae-container-input input::-moz-placeholder, +.ae-ui .ae-container-input .ae-input::-moz-placeholder { + color: #c0cbd4; +} + +.ae-ui .ae-container-input input:-moz-placeholder, +.ae-ui .ae-container-input .ae-input:-moz-placeholder { + color: #c0cbd4; +} + +.ae-ui .ae-container-input input:-ms-input-placeholder, +.ae-ui .ae-container-input .ae-input:-ms-input-placeholder { + color: #c0cbd4; +} + +.ae-ui .ae-container-input input:focus, +.ae-ui .ae-container-input .ae-input:focus { + box-shadow: none; +} + +.ae-ui .ae-container-input input::-ms-clear, +.ae-ui .ae-container-input .ae-input::-ms-clear { + display: none; +} + +.ae-ui .ae-button-bridge [class*='ae-icon-'], +.ae-ui .ae-button-bridge [class*=' ae-icon-'] { + -webkit-filter: brightness(0) invert(100%); + filter: brightness(0) invert(100%); +} + +.ae-ui .ae-button-bridge [class*='ae-icon-']:hover, +.ae-ui .ae-button-bridge [class*=' ae-icon-']:hover { + -webkit-filter: sepia(80%) hue-rotate(-20deg) invert(90%) saturate(8); + filter: sepia(80%) hue-rotate(-20deg) invert(90%) saturate(8); +} + +.ae-ui .ae-dropdown, +.ae-ui [class^='ae-dropdown-'] { + background-color: $ez-ae-color-primary; + border: 1px solid #dce0e3; + border-radius: 4px; + box-shadow: 0 2px 4px 0 #e4ebf0; + color: $ez-ae-color-primary-text; +} + +.ae-ui .ae-dropdown.ae-arrow-box:after, +.ae-ui [class^='ae-dropdown-'].ae-arrow-box:after { + color: $ez-ae-color-primary; + display: block; +} + +.ae-ui .ae-dropdown .ae-list-header, +.ae-ui [class^='ae-dropdown-'] .ae-list-header { + color: #b0b4bb; + font-size: 14px; + font-style: italic; +} + +.ae-ui .ae-dropdown .ae-listbox .ae-toolbar-element:hover, +.ae-ui [class^='ae-dropdown-'] .ae-listbox .ae-toolbar-element:hover { + background-color: #f6f8f9; +} + +.ae-ui .ae-toolbar, +.ae-ui [class^='ae-toolbar-'] { + background-color: $ez-ae-color-primary; + border-radius: 3px; + color: $ez-ae-color-primary-text; + box-shadow: 0 4px 10px 2px rgba(86, 94, 99, 0.45); + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.ae-ui .ae-toolbar-element { + box-shadow: none; +} + +.ae-ui .ae-toolbar .ae-button, +.ae-ui .ae-toolbar .ae-toolbar-element, +.ae-ui [class^='ae-toolbar-'] .ae-button, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element { + background-color: transparent; +} + +.ae-ui .ae-toolbar .ae-button:hover, +.ae-ui .ae-toolbar .ae-toolbar-element:hover, +.ae-ui [class^='ae-toolbar-'] .ae-button:hover, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element:hover { + color: $ez-ae-color-text-hover; +} + +.ae-ui .ae-toolbar .ae-button.ae-button-pressed, +.ae-ui .ae-toolbar .ae-toolbar-element.ae-button-pressed, +.ae-ui [class^='ae-toolbar-'] .ae-button.ae-button-pressed, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element.ae-button-pressed { + background-color: #575757; + border-radius: 5px; +} + +.ae-ui .ae-toolbar .ae-button.ae-button-pressed:hover, +.ae-ui .ae-toolbar .ae-toolbar-element.ae-button-pressed:hover, +.ae-ui [class^='ae-toolbar-'] .ae-button.ae-button-pressed:hover, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element.ae-button-pressed:hover { + color: $ez-ae-color-text-hover; +} + +.ae-ui .ae-toolbar .ae-button.ae-button-disabled, +.ae-ui .ae-toolbar .ae-toolbar-element.ae-button-disabled, +.ae-ui [class^='ae-toolbar-'] .ae-button.ae-button-disabled, +.ae-ui [class^='ae-toolbar-'] .ae-toolbar-element.ae-button-disabled { + opacity: 0.3; +} + +.ae-ui .ae-toolbar-add { + background-color: $ez-ae-color-primary; + border-color: $ez-ae-color-primary; + border-radius: 50%; + color: $ez-ae-color-primary-text; + box-shadow: 0 4px 12px 0 rgba(86, 94, 99, 0.45); +} + +.ae-ui .ae-toolbar-add:hover, +.ae-ui .ae-toolbar-add:focus { + opacity: 0.7; +} + +.ae-ui .ae-toolbar-add .ae-button-add:hover, +.ae-ui .ae-toolbar-add .ae-button-add:focus { + color: inherit; +} + +.ae-ui .ae-toolbar-add .ae-button-add .ae-icon-add { + font-size: 14px; + line-height: 14px; + color: $ez-ae-color-primary-text; +} + +.ez-embed-type-image img { + max-width: 100%; +} + +.ez-embed-type-image.is-linked { + position: relative; +} + +.ez-embed__icon-wrapper { + position: absolute; + top: 8px; + right: 8px; + background: $ez-ae-color-secondary; + border-radius: 50%; + padding: 4px; +} + +.ez-has-anchor { + position: relative; +} + +.ez-has-anchor > .ez-icon { + position: absolute; + top: 4px; + left: -20px; +} + +pre.ez-has-anchor > .ez-icon { + left: 0; +} + +[class*='ae-icon-'], +[class*=' ae-icon-'] { + font-size: 14px; + line-height: 1; +} + +.ae-toolbar-styles { + z-index: 1; +} + +.ae-ui .ae-toolbar-floating--fixed { + position: fixed; +} + +.ae-ui .ae-toolbar-floating.ae-toolbar-transition { + transition: none; +} + +.alloy-editor-visible { + z-index: 1; +} + +.ae-ui { + .ae-container { + .ez-ae-custom-tag { + max-height: 400px; + overflow: auto; + padding: 1rem; + + .attribute__wrapper { + margin-bottom: 0.5rem; + } + + .attribute__label { + margin: 0.5rem 0; + } + + .ez-btn-ae--custom-tag { + color: $ez-ae-color-primary; + background-color: $ez-ae-color-secondary; + } + } + } +} + +.ez-btn-ae { + &:hover { + .ez-icon { + fill: $ez-ae-color-text-hover; + } + } + + .ez-icon { + width: calculateRem(24px); + height: calculateRem(24px); + fill: $ez-ae-color-primary-text; + } + + &.ae-button-pressed { + .ez-icon { + fill: $ez-ae-color-primary; + } + } +} + +.ez-data-source__richtext { + background-color: $ez-white; + min-height: calculateRem(100px); + border: calculateRem(1px) solid rgba(0, 0, 0, 0.15); + border-radius: calculateRem(4px); + display: inline-block; + width: 100%; + padding: calculateRem(8px) calculateRem(24px) calculateRem(24px) calculateRem(24px); + + &:focus { + outline: none; + } + + .is-block-focused, + .cke_widget_wrapper.cke_widget_focused > .cke_widget_element { + outline: calculateRem(2px) dashed $ez-ae-color-ground; + outline-offset: calculateRem(1px); + } + + &.is-invalid { + background: $ez-ae-color-warning-pale; + border-color: $ez-ae-color-warning; + } + + blockquote { + margin: calculateRem(16px) calculateRem(40px); + } + + [data-ezalign='right'] { + float: right; + } + + [data-ezalign='left'] { + float: left; + } + + [data-ezalign='center'] { + text-align: center; + } + + [data-ezelement='ezconfig'] { + display: none; + } + + [data-ezelement='ezembed'], + [data-ezelement='ezembedinline'] { + background: $ez-embed-color-ground; + border: calculateRem(1px) solid $ez-embed-color-border; + border-radius: calculateRem(4px); + font-size: calculateRem(18px); + display: inline-block; + margin: calculateRem(8px) calculateRem(5px); + padding: calculateRem(2px); + box-sizing: border-box; + line-height: 1; + min-height: auto; + font-weight: normal; + + .ez-embed-content { + margin-bottom: 0; + + &__title { + vertical-align: middle; + } + + .ez-icon { + vertical-align: middle; + background: $ez-embed-color-icon-ground; + border-radius: calculateRem(4px); + padding: calculateRem(2px); + margin-right: calculateRem(5px); + } + } + } + + [data-ezelement='ezembedinline'] { + vertical-align: middle; + font-size: calculateRem(16px); + + .ez-embed-content { + .ez-icon { + width: calculateRem(22px); + height: calculateRem(22px); + } + } + } + + pre { + padding: calculateRem(10px) calculateRem(20px); + min-height: calculateRem(43px); + background-color: $ez-formatted-color-ground; + border: calculateRem(1px) solid $ez-formatted-color-border; + word-wrap: initial; + } + + p { + min-height: calculateRem(24px); + } + + h1 { + min-height: calculateRem(33px); + } + + h2 { + min-height: calculateRem(28px); + } + + h3 { + min-height: calculateRem(26px); + } + + h4 { + min-height: calculateRem(24px); + } + + h5 { + min-height: calculateRem(21px); + } + + h6 { + min-height: calculateRem(20px); + } +} diff --git a/src/bundle/Resources/public/scss/_anchor-edit.scss b/src/bundle/Resources/public/scss/_anchor-edit.scss new file mode 100644 index 00000000..62faaea0 --- /dev/null +++ b/src/bundle/Resources/public/scss/_anchor-edit.scss @@ -0,0 +1,59 @@ +.ez-ae-anchor-edit { + display: flex; + padding: calculateRem(8px) 0 calculateRem(8px) calculateRem(8px); + flex-wrap: wrap; + max-width: calculateRem(528px); + + &__input-wrapper { + margin-right: calculateRem(10px); + } + + &__input-label { + font-weight: 700; + } + + &__input { + width: calculateRem(400px); + } + + &__actions { + display: flex; + align-items: flex-end; + justify-content: space-around; + width: calculateRem(110px); + } + + & &__error { + color: $ez-ae-color-warning; + margin-top: calculateRem(8px); + } + + &__btn { + border: none; + + &--trash, + &--save { + border-radius: 50%; + width: calculateRem(40px); + height: calculateRem(40px); + padding: 0; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } + + &--trash { + background-color: $ez-ae-color-negative; + } + + &--save { + background-color: $ez-ae-color-positive; + } + + &[disabled] { + opacity: 0.3; + cursor: not-allowed; + } + } +} diff --git a/src/bundle/Resources/public/scss/_attributes.scss b/src/bundle/Resources/public/scss/_attributes.scss new file mode 100644 index 00000000..0917e13e --- /dev/null +++ b/src/bundle/Resources/public/scss/_attributes.scss @@ -0,0 +1,21 @@ +.ae-ui { + .ae-container { + .ez-ae-attributes { + max-height: 400px; + overflow: auto; + padding: 1rem; + + .ez-ae-attribute__wrapper { + margin-bottom: 0.5rem; + } + + .ez-ae-attribute__label { + margin: 0.5rem 0; + } + + .ez-btn-ae--attributes-save { + color: $ez-ae-color-primary; + } + } + } +} diff --git a/src/bundle/Resources/public/scss/_buttons.scss b/src/bundle/Resources/public/scss/_buttons.scss new file mode 100644 index 00000000..1cb0e3b9 --- /dev/null +++ b/src/bundle/Resources/public/scss/_buttons.scss @@ -0,0 +1,62 @@ +.ae-ui { + .ae-toolbar-add { + opacity: 0; + } + + .ae-toolbar { + .ae-button { + &.ez-btn-ae { + width: auto; + height: auto; + margin: 0.3rem; + display: flex; + justify-content: center; + align-items: center; + } + } + } + + .ae-container { + .ez-ae-edit-link { + .ez-btn-ae { + color: $ez-ae-color-primary; + + &--clear-link { + position: absolute; + bottom: 0; + right: 0; + color: $ez-ae-color-negative; + height: 2.25rem; + display: none; + } + + &--remove-link, + &--save-link { + border-radius: 50%; + width: calculateRem(40px); + height: calculateRem(40px); + padding: 0; + display: flex; + justify-content: center; + align-items: center; + } + + &--remove-link { + background-color: $ez-ae-color-negative; + } + + &--save-link { + background-color: $ez-ae-color-positive; + } + } + + &.is-linked { + .ez-btn-ae { + &--clear-link { + display: block; + } + } + } + } + } +} diff --git a/src/bundle/Resources/public/scss/_character-counter.scss b/src/bundle/Resources/public/scss/_character-counter.scss new file mode 100644 index 00000000..28e8cf13 --- /dev/null +++ b/src/bundle/Resources/public/scss/_character-counter.scss @@ -0,0 +1,12 @@ +.ez-character-counter { + position: relative; + border-bottom-left-radius: calculateRem(5px); + border-bottom-right-radius: calculateRem(5px); + text-align: right; + white-space: nowrap; + + &__word-count, + &__character-count { + margin-left: calculateRem(16px); + } +} diff --git a/src/bundle/Resources/public/scss/_custom-styles.scss b/src/bundle/Resources/public/scss/_custom-styles.scss new file mode 100644 index 00000000..337090ab --- /dev/null +++ b/src/bundle/Resources/public/scss/_custom-styles.scss @@ -0,0 +1,22 @@ +.ez-content-field-value .ezrichtext-field, +.ae-editable { + div[class^='ezstyle-'], + div[class*=' ezstyle-'], + div[data-ezelement='eztemplate'][data-eztype='style'] { + margin-top: 0; + margin-bottom: 1rem; + padding: 0.5rem; + border-width: 0.5rem; + border-style: solid; + border-color: $ez-ae-color-ground; + border-radius: 0.25rem; + } + + span[class^='ezstyle-'], + span[class*=' style-'], + span[data-ezelement='eztemplateinline'][data-eztype='style'] { + background-color: $ez-ae-color-ground; + padding: 0 0.25rem; + font-family: courier, monospace; + } +} diff --git a/src/bundle/Resources/public/scss/_custom-tag.scss b/src/bundle/Resources/public/scss/_custom-tag.scss new file mode 100644 index 00000000..5625e552 --- /dev/null +++ b/src/bundle/Resources/public/scss/_custom-tag.scss @@ -0,0 +1,99 @@ +.ez-custom-tag { + max-width: 50%; + border: calculateRem(1px) solid $ez-custom-tag-color-border; + margin: calculateRem(16px) 0; + border-radius: calculateRem(5px); + + & & { + max-width: 100%; + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + background-color: $ez-custom-tag-color-header-ground; + padding: calculateRem(2px) calculateRem(2px) calculateRem(2px) calculateRem(8px); + border-top-left-radius: calculateRem(5px); + border-top-right-radius: calculateRem(5px); + + &-label { + font-weight: bold; + } + + &-btns { + background-color: $ez-custom-tag-color-switcher-ground; + border: calculateRem(2px) solid $ez-custom-tag-color-switcher-active; + border-radius: calculateRem(5px); + display: flex; + padding: calculateRem(2px); + } + + &-btn { + display: flex; + border-radius: calculateRem(5px); + padding: calculateRem(6px) calculateRem(8px); + + .ez-icon { + fill: $ez-custom-tag-color-switcher-active; + } + } + } + + &__icon-wrapper { + white-space: initial; + display: flex; + align-items: center; + } + + & > [data-ezelement='ezattributes'], + & > [data-ezelement='ezcontent'] { + display: none; + padding: calculateRem(8px) calculateRem(24px); + } + + &--attributes-visible { + & > [data-ezelement='ezattributes'] { + display: block; + } + + & > .ez-custom-tag__header { + .ez-custom-tag__header-btn--attributes { + background-color: $ez-custom-tag-color-switcher-active; + + .ez-icon { + fill: $ez-custom-tag-color-switcher-inactive; + } + } + } + } + + &--content-visible { + & > [data-ezelement='ezcontent'] { + display: inherit; + } + + &[data-ezelement='eztemplateinline'] { + padding: 0 calculateRem(8px); + margin: 0 calculateRem(4px); + background-color: $ez-custom-tag-inline-color-ground; + border: none; + display: inline-flex; + max-width: none; + + & > [data-ezelement='ezcontent'] { + padding: 0; + } + } + + & > .ez-custom-tag__header { + .ez-custom-tag__header-btn--content { + background-color: $ez-custom-tag-color-switcher-active; + + .ez-icon { + fill: $ez-custom-tag-color-switcher-inactive; + } + } + } + } +} diff --git a/src/bundle/Resources/public/scss/_elements-path.scss b/src/bundle/Resources/public/scss/_elements-path.scss new file mode 100644 index 00000000..065d1426 --- /dev/null +++ b/src/bundle/Resources/public/scss/_elements-path.scss @@ -0,0 +1,12 @@ +.ez-elements-path { + list-style: none; + display: flex; + margin-bottom: 0; + padding-left: 0; + font-weight: bold; + flex-wrap: wrap; + + &__item:not(:last-child) { + margin-right: calculateRem(16px); + } +} diff --git a/src/bundle/Resources/public/scss/_link-edit.scss b/src/bundle/Resources/public/scss/_link-edit.scss new file mode 100644 index 00000000..1ace773a --- /dev/null +++ b/src/bundle/Resources/public/scss/_link-edit.scss @@ -0,0 +1,139 @@ +.ez-ae-edit-link { + display: grid; + padding: calculateRem(8px); + column-gap: calculateRem(16px); + grid-template-areas: + 'udw actions' + 'info actions'; + + &__row { + display: flex; + align-items: flex-end; + + &:first-child { + margin-bottom: calculateRem(8px); + } + + &--udw { + grid-area: udw; + } + + &--info { + grid-area: info; + } + + &--actions { + grid-area: actions; + padding: 0 0 0 calculateRem(14px); + border-left: calculateRem(2px) solid $ez-ae-color-ground; + } + + .ez-btn-ae--udw { + background-color: $ez-ae-color-secondary; + } + } + + &__block { + &--udw { + display: flex; + flex-direction: column; + } + + &--url { + position: relative; + display: flex; + flex-direction: column; + width: 25rem; + } + + &--title, + &--target { + display: flex; + flex-direction: column; + + .ez-ae-edit-link__text { + font-weight: bold; + } + } + + &--title { + width: calculateRem(464px); + margin-right: calculateRem(16px); + } + + &--target { + .ez-ae-edit-link__choice { + border: calculateRem(2px) solid $ez-ae-color-secondary; + padding: calculateRem(2px); + border-radius: calculateRem(4px); + + .ez-ae-edit-link__label { + margin-bottom: 0; + + &:not(:last-child) { + .ez-btn-ae__icon-wrapper { + margin-right: calculateRem(8px); + } + } + + .ez-icon { + fill: $ez-ae-color-secondary; + width: calculateRem(20px); + height: calculateRem(20px); + } + + .ez-btn-ae__icon-wrapper { + padding: calculateRem(4px); + border-radius: calculateRem(4px); + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + } + + input:checked + .ez-btn-ae__icon-wrapper { + background: $ez-ae-color-secondary; + + .ez-icon { + background-color: $ez-ae-color-secondary; + fill: $ez-ae-color-primary; + } + } + + [type='radio'] { + display: none; + } + } + } + } + + &--separator { + line-height: 2.25rem; + padding: 0 0.5rem; + } + + &--actions { + display: flex; + flex-direction: column; + justify-content: space-around; + height: 100%; + + .ez-btn-ae { + &__icon { + fill: $ez-ae-color-primary; + } + } + } + + .ez-ae-edit-link__label { + margin-bottom: 0.3rem; + font-weight: bold; + } + + .ez-ae-edit-link__input { + height: calculateRem(36px); + border: calculateRem(1px) solid $ez-ae-color-ground; + border-radius: calculateRem(4px); + } + } +} diff --git a/src/bundle/Resources/public/scss/_tools.scss b/src/bundle/Resources/public/scss/_tools.scss new file mode 100644 index 00000000..e88d11b3 --- /dev/null +++ b/src/bundle/Resources/public/scss/_tools.scss @@ -0,0 +1,8 @@ +.ez-richtext-tools { + display: flex; + justify-content: space-between; + font-size: calculateRem(10px); + background-color: $ez-tools-color-ground; + color: $ez-tools-color-text; + padding: 0 calculateRem(16px); +} diff --git a/src/bundle/Resources/public/scss/alloyeditor.scss b/src/bundle/Resources/public/scss/alloyeditor.scss new file mode 100644 index 00000000..7b1032f0 --- /dev/null +++ b/src/bundle/Resources/public/scss/alloyeditor.scss @@ -0,0 +1,13 @@ +@import 'variables/colors'; +@import 'functions/calculate.rem'; + +@import 'alloyeditor-ez'; +@import 'buttons'; +@import 'anchor-edit'; +@import 'attributes'; +@import 'elements-path'; +@import 'tools'; +@import 'link-edit'; +@import 'custom-tag'; +@import 'custom-styles'; +@import 'character-counter'; diff --git a/src/bundle/Resources/public/scss/functions/calculate.rem.scss b/src/bundle/Resources/public/scss/functions/calculate.rem.scss new file mode 100644 index 00000000..47b07070 --- /dev/null +++ b/src/bundle/Resources/public/scss/functions/calculate.rem.scss @@ -0,0 +1,5 @@ +@function calculateRem($size) { + $remSize: $size / 16px; + + @return #{$remSize}rem; +} diff --git a/src/bundle/Resources/public/scss/variables/colors.scss b/src/bundle/Resources/public/scss/variables/colors.scss new file mode 100644 index 00000000..188eded0 --- /dev/null +++ b/src/bundle/Resources/public/scss/variables/colors.scss @@ -0,0 +1,43 @@ +$ez-white: #fff; +$ez-black: #333; + +$ez-color-primary: #f15a10; +$ez-color-secondary: #106d95; +$ez-color-warning-pale: #fceaec; +$ez-color-warning-dark: #aa0000; +$ez-color-positive: #00825c; +$ez-color-base-dark: #555; +$ez-ground-base-pale: #fafafa; +$ez-ground-base-dark: #e5e3e3; + +// AlloyEditor +$ez-ae-color-primary: $ez-white !default; +$ez-ae-color-primary-text: $ez-black !default; +$ez-ae-color-secondary: $ez-color-secondary !default; +$ez-ae-color-warning-pale: $ez-color-warning-pale !default; +$ez-ae-color-warning: $ez-color-warning-dark !default; +$ez-ae-color-positive: $ez-color-positive !default; +$ez-ae-color-negative: $ez-color-base-dark !default; +$ez-ae-color-ground: $ez-ground-base-dark !default; +$ez-ae-color-text-hover: #65b6f0 !default; + +// Custom tag +$ez-custom-tag-color-border: $ez-color-primary !default; +$ez-custom-tag-color-header-ground: $ez-ground-base-dark !default; +$ez-custom-tag-inline-color-ground: $ez-ground-base-dark !default; +$ez-custom-tag-color-switcher-active: $ez-color-secondary !default; +$ez-custom-tag-color-switcher-inactive: $ez-white !default; +$ez-custom-tag-color-switcher-ground: $ez-white !default; + +// Tools +$ez-tools-color-ground: $ez-ground-base-dark !default; +$ez-tools-color-text: $ez-black !default; + +// Embed +$ez-embed-color-ground: $ez-ground-base-dark !default; +$ez-embed-color-icon-ground: $ez-white !default; +$ez-embed-color-border: $ez-ground-base-dark !default; + +// Formatted +$ez-formatted-color-ground: $ez-ground-base-pale !default; +$ez-formatted-color-border: $ez-ground-base-dark !default; From dabf235c52e573e7a79eea442ca833d1d752a4eb Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 19 Aug 2019 17:07:57 +0200 Subject: [PATCH 02/25] Implemented UI configuration mappers Moved from AdminUI --- .../Configuration/UI/Mapper/CustomStyle.php | 75 +++++++ src/lib/Configuration/UI/Mapper/CustomTag.php | 197 ++++++++++++++++++ .../UI/Mapper/CustomTag/AttributeMapper.php | 41 ++++ .../CustomTag/ChoiceAttributeMapper.php | 45 ++++ .../CustomTag/CommonAttributeMapper.php | 41 ++++ .../Configuration/UI/Mapper/OnlineEditor.php | 93 +++++++++ .../UI/Mapper/OnlineEditorConfigMapper.php | 35 ++++ 7 files changed, 527 insertions(+) create mode 100644 src/lib/Configuration/UI/Mapper/CustomStyle.php create mode 100644 src/lib/Configuration/UI/Mapper/CustomTag.php create mode 100644 src/lib/Configuration/UI/Mapper/CustomTag/AttributeMapper.php create mode 100644 src/lib/Configuration/UI/Mapper/CustomTag/ChoiceAttributeMapper.php create mode 100644 src/lib/Configuration/UI/Mapper/CustomTag/CommonAttributeMapper.php create mode 100644 src/lib/Configuration/UI/Mapper/OnlineEditor.php create mode 100644 src/lib/Configuration/UI/Mapper/OnlineEditorConfigMapper.php diff --git a/src/lib/Configuration/UI/Mapper/CustomStyle.php b/src/lib/Configuration/UI/Mapper/CustomStyle.php new file mode 100644 index 00000000..c5b3a767 --- /dev/null +++ b/src/lib/Configuration/UI/Mapper/CustomStyle.php @@ -0,0 +1,75 @@ +customStylesConfiguration = $customStylesConfiguration; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->packages = $packages; + } + + /** + * Map Configuration for the given list of enabled Custom Styles. + * + * @param array $enabledCustomStyles + * + * @return array Mapped configuration + */ + public function mapConfig(array $enabledCustomStyles) + { + $config = []; + foreach ($enabledCustomStyles as $styleName) { + if (!isset($this->customStylesConfiguration[$styleName])) { + throw new RuntimeException( + "RichText Custom Style configuration for {$styleName} not found." + ); + } + + $customStyleConfiguration = $this->customStylesConfiguration[$styleName]; + $config[$styleName]['inline'] = $customStyleConfiguration['inline']; + $config[$styleName]['label'] = $this->translator->trans( + /** @Ignore */ + sprintf('ezrichtext.custom_styles.%s.label', $styleName), + [], + $this->translationDomain + ); + } + + return $config; + } +} diff --git a/src/lib/Configuration/UI/Mapper/CustomTag.php b/src/lib/Configuration/UI/Mapper/CustomTag.php new file mode 100644 index 00000000..735ffd94 --- /dev/null +++ b/src/lib/Configuration/UI/Mapper/CustomTag.php @@ -0,0 +1,197 @@ +customTagsConfiguration = $customTagsConfiguration; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->packages = $packages; + $this->customTagAttributeMappers = $customTagAttributeMappers; + $this->supportedTagAttributeMappersCache = []; + } + + /** + * Map Configuration for the given list of enabled Custom Tags. + * + * @param array $enabledCustomTags + * + * @return array Mapped configuration + */ + public function mapConfig(array $enabledCustomTags) + { + $config = []; + foreach ($enabledCustomTags as $tagName) { + if (!isset($this->customTagsConfiguration[$tagName])) { + throw new RuntimeException( + "RichText Custom Tag configuration for {$tagName} not found." + ); + } + + $customTagConfiguration = $this->customTagsConfiguration[$tagName]; + + $config[$tagName] = [ + 'label' => "ezrichtext.custom_tags.{$tagName}.label", + 'description' => "ezrichtext.custom_tags.{$tagName}.description", + 'isInline' => $customTagConfiguration['is_inline'], + ]; + + if (!empty($customTagConfiguration['icon'])) { + $config[$tagName]['icon'] = $this->packages->getUrl( + $customTagConfiguration['icon'] + ); + } + + foreach ($customTagConfiguration['attributes'] as $attributeName => $properties) { + $typeMapper = $this->getAttributeTypeMapper( + $tagName, + $attributeName, + $properties['type'] + ); + $config[$tagName]['attributes'][$attributeName] = $typeMapper->mapConfig( + $tagName, + $attributeName, + $properties + ); + } + } + + return $this->translateLabels($config); + } + + /** + * Get first available Custom Tag Attribute Type mapper. + * + * @param string $tagName + * @param string $attributeName + * @param string $attributeType + * + * @return AttributeMapper + */ + private function getAttributeTypeMapper( + string $tagName, + string $attributeName, + string $attributeType + ): AttributeMapper { + if (isset($this->supportedTagAttributeMappersCache[$attributeType])) { + return $this->supportedTagAttributeMappersCache[$attributeType]; + } + + foreach ($this->customTagAttributeMappers as $attributeMapper) { + // get first supporting, order of these mappers is controlled by 'priority' DI tag attribute + if ($attributeMapper->supports($attributeType)) { + return $this->supportedTagAttributeMappersCache[$attributeType] = $attributeMapper; + } + } + + throw new RuntimeException( + "RichText Custom Tag configuration: unsupported attribute type '{$attributeType}' of '{$attributeName}' attribute of '{$tagName}' Custom Tag" + ); + } + + /** + * Process Custom Tags config and translate labels for UI. + * + * @param array $config + * + * @return array processed Custom Tags config with translated labels + */ + private function translateLabels(array $config): array + { + foreach ($config as $tagName => $tagConfig) { + $config[$tagName]['label'] = $this->translator->trans( + /** @Ignore */ + $tagConfig['label'], + [], + $this->translationDomain + ); + $config[$tagName]['description'] = $this->translator->trans( + /** @Ignore */ + $tagConfig['description'], + [], + $this->translationDomain + ); + + if (empty($tagConfig['attributes'])) { + continue; + } + + $transCatalogue = $this->translator->getCatalogue(); + foreach ($tagConfig['attributes'] as $attributeName => $attributeConfig) { + $config[$tagName]['attributes'][$attributeName]['label'] = $this->translator->trans( + /** @Ignore */ + $attributeConfig['label'], + [], + $this->translationDomain + ); + + if (isset($config[$tagName]['attributes'][$attributeName]['choicesLabel'])) { + foreach ($config[$tagName]['attributes'][$attributeName]['choicesLabel'] as $choice => $label) { + $translatedLabel = $transCatalogue->has($label, $this->translationDomain) + ? $this->translator->trans($label, [], $this->translationDomain) + : $choice; + + $config[$tagName]['attributes'][$attributeName]['choicesLabel'][$choice] = $translatedLabel; + } + } + } + } + + return $config; + } +} diff --git a/src/lib/Configuration/UI/Mapper/CustomTag/AttributeMapper.php b/src/lib/Configuration/UI/Mapper/CustomTag/AttributeMapper.php new file mode 100644 index 00000000..0877cd1d --- /dev/null +++ b/src/lib/Configuration/UI/Mapper/CustomTag/AttributeMapper.php @@ -0,0 +1,41 @@ + "ezrichtext.custom_tags.{$tagName}.attributes.{$attributeName}.label", + 'type' => $customTagAttributeProperties['type'], + 'required' => $customTagAttributeProperties['required'], + 'defaultValue' => $customTagAttributeProperties['default_value'], + ]; + } +} diff --git a/src/lib/Configuration/UI/Mapper/OnlineEditor.php b/src/lib/Configuration/UI/Mapper/OnlineEditor.php new file mode 100644 index 00000000..e8e5e8cb --- /dev/null +++ b/src/lib/Configuration/UI/Mapper/OnlineEditor.php @@ -0,0 +1,93 @@ +translator = $translator; + $this->translationDomain = $translationDomain; + } + + /** + * {@inheritdoc} + */ + public function mapCssClassesConfiguration(array $semanticConfiguration): array + { + $configuration = []; + foreach ($semanticConfiguration as $elementName => $elementConfiguration) { + $label = $this->translator->trans( + /** @Ignore */ + 'ezrichtext.classes.class.label', + [], + $this->translationDomain + ); + $configuration[$elementName] = [ + 'choices' => $elementConfiguration['choices'], + 'required' => $elementConfiguration['required'], + 'defaultValue' => $elementConfiguration['default_value'] ?? null, + 'multiple' => $elementConfiguration['multiple'], + 'label' => $label, + ]; + } + + return $configuration; + } + + /** + * {@inheritdoc} + */ + public function mapDataAttributesConfiguration(array $semanticConfiguration): array + { + $configuration = []; + foreach ($semanticConfiguration as $elementName => $elementAttributes) { + foreach ($elementAttributes as $attributeName => $attributeConfiguration) { + $type = $attributeConfiguration['type']; + $config = [ + 'type' => $type, + 'required' => $attributeConfiguration['required'], + 'defaultValue' => $attributeConfiguration['default_value'] ?? null, + ]; + if ($type === 'choice') { + $config['choices'] = $attributeConfiguration['choices']; + $config['multiple'] = $attributeConfiguration['multiple']; + } + + $config['label'] = $this->translator->trans( + /** @Ignore */ + "ezrichtext.attributes.{$elementName}.{$attributeName}.label", + [], + $this->translationDomain + ); + + $configuration[$elementName][$attributeName] = $config; + } + } + + return $configuration; + } +} diff --git a/src/lib/Configuration/UI/Mapper/OnlineEditorConfigMapper.php b/src/lib/Configuration/UI/Mapper/OnlineEditorConfigMapper.php new file mode 100644 index 00000000..f434be8f --- /dev/null +++ b/src/lib/Configuration/UI/Mapper/OnlineEditorConfigMapper.php @@ -0,0 +1,35 @@ + Date: Mon, 19 Aug 2019 17:09:01 +0200 Subject: [PATCH 03/25] Implemented API ProviderService and SPI config Provider interfaces --- src/lib/API/Configuration/ProviderService.php | 27 ++++++++++++++++ src/lib/SPI/Configuration/Provider.php | 31 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/lib/API/Configuration/ProviderService.php create mode 100644 src/lib/SPI/Configuration/Provider.php diff --git a/src/lib/API/Configuration/ProviderService.php b/src/lib/API/Configuration/ProviderService.php new file mode 100644 index 00000000..5059f8e1 --- /dev/null +++ b/src/lib/API/Configuration/ProviderService.php @@ -0,0 +1,27 @@ + Date: Mon, 19 Aug 2019 17:14:19 +0200 Subject: [PATCH 04/25] Implemented config providers for AlloyEditor, CustomStyle, and CustomTag Moved from AdminUI --- .../Configuration/Provider/AlloyEditor.php | 126 ++++++++++++++++++ .../Configuration/Provider/CustomStyle.php | 58 ++++++++ src/lib/Configuration/Provider/CustomTag.php | 58 ++++++++ 3 files changed, 242 insertions(+) create mode 100644 src/lib/Configuration/Provider/AlloyEditor.php create mode 100644 src/lib/Configuration/Provider/CustomStyle.php create mode 100644 src/lib/Configuration/Provider/CustomTag.php diff --git a/src/lib/Configuration/Provider/AlloyEditor.php b/src/lib/Configuration/Provider/AlloyEditor.php new file mode 100644 index 00000000..cca2b28b --- /dev/null +++ b/src/lib/Configuration/Provider/AlloyEditor.php @@ -0,0 +1,126 @@ +alloyEditorConfiguration = $alloyEditorConfiguration; + $this->configResolver = $configResolver; + $this->onlineEditorConfigMapper = $onlineEditorConfigMapper; + } + + public function getName(): string + { + return 'alloyEditor'; + } + + /** + * @return array AlloyEditor config + */ + public function getConfiguration(): array + { + return [ + 'extraPlugins' => $this->getExtraPlugins(), + 'extraButtons' => $this->getExtraButtons(), + 'classes' => $this->getCssClasses(), + 'attributes' => $this->getDataAttributes(), + ]; + } + + /** + * @return array Custom plugins + */ + private function getExtraPlugins(): array + { + return $this->alloyEditorConfiguration['extra_plugins'] ?? []; + } + + /** + * @deprecated 3.0.0 The alternative and more flexible solution will be introduced. + * @deprecated 3.0.0 So you will need to update Online Editor Extra Buttons as part of eZ Platform 3.x upgrade. + * + * @return array Custom buttons + */ + private function getExtraButtons(): array + { + @trigger_error( + '"ezrichtext.alloy_editor.extra_buttons" is deprecated since v2.5.1. There will be new and more flexible solution to manage buttons in Online Editor in 3.0.0', + E_USER_DEPRECATED + ); + + return $this->alloyEditorConfiguration['extra_buttons'] ?? []; + } + + /** + * Get custom CSS classes defined by the SiteAccess-aware configuration. + * + * @return array + */ + private function getCssClasses(): array + { + return $this->onlineEditorConfigMapper->mapCssClassesConfiguration( + $this->getSiteAccessConfigArray(RichText::CLASSES_SA_SETTINGS_ID) + ); + } + + /** + * Get custom data attributes defined by the SiteAccess-aware configuration. + * + * @return array + */ + private function getDataAttributes(): array + { + return $this->onlineEditorConfigMapper->mapDataAttributesConfiguration( + $this->getSiteAccessConfigArray(RichText::ATTRIBUTES_SA_SETTINGS_ID) + ); + } + + /** + * Get configuration array from the SiteAccess-aware configuration, checking first for its existence. + * + * @param string $paramName + * + * @return array + */ + private function getSiteAccessConfigArray(string $paramName): array + { + return $this->configResolver->hasParameter($paramName) + ? $this->configResolver->getParameter($paramName) + : []; + } +} diff --git a/src/lib/Configuration/Provider/CustomStyle.php b/src/lib/Configuration/Provider/CustomStyle.php new file mode 100644 index 00000000..893023a8 --- /dev/null +++ b/src/lib/Configuration/Provider/CustomStyle.php @@ -0,0 +1,58 @@ +configResolver = $configResolver; + $this->customStyleConfigurationMapper = $customStyleConfigurationMapper; + } + + public function getName(): string + { + return 'customStyles'; + } + + /** + * @return array RichText Custom Styles config + */ + public function getConfiguration(): array + { + if ($this->configResolver->hasParameter('fieldtypes.ezrichtext.custom_styles')) { + return $this->customStyleConfigurationMapper->mapConfig( + $this->configResolver->getParameter('fieldtypes.ezrichtext.custom_styles') + ); + } + + return []; + } +} diff --git a/src/lib/Configuration/Provider/CustomTag.php b/src/lib/Configuration/Provider/CustomTag.php new file mode 100644 index 00000000..f8ec1367 --- /dev/null +++ b/src/lib/Configuration/Provider/CustomTag.php @@ -0,0 +1,58 @@ +configResolver = $configResolver; + $this->customTagConfigurationMapper = $customTagConfigurationMapper; + } + + public function getName(): string + { + return 'customTags'; + } + + /** + * @return array RichText Custom Tags config + */ + public function getConfiguration(): array + { + if ($this->configResolver->hasParameter('fieldtypes.ezrichtext.custom_tags')) { + return $this->customTagConfigurationMapper->mapConfig( + $this->configResolver->getParameter('fieldtypes.ezrichtext.custom_tags') + ); + } + + return []; + } +} From 530a3f173dca31bad846f84250b5748fb3c43251 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 19 Aug 2019 17:15:37 +0200 Subject: [PATCH 05/25] Implemented RichText configuration Provider Service --- src/lib/Configuration/AggregateProvider.php | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/lib/Configuration/AggregateProvider.php diff --git a/src/lib/Configuration/AggregateProvider.php b/src/lib/Configuration/AggregateProvider.php new file mode 100644 index 00000000..1f4d476a --- /dev/null +++ b/src/lib/Configuration/AggregateProvider.php @@ -0,0 +1,40 @@ +providers = $providers; + } + + public function getConfiguration(): array + { + $configuration = []; + foreach ($this->providers as $provider) { + $configuration[$provider->getName()] = $provider->getConfiguration(); + } + + return $configuration; + } +} From 09d13e4188f14bb2d4a15e470dfdab997de93a05 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 19 Aug 2019 17:19:22 +0200 Subject: [PATCH 06/25] Configured DIC for Configuration Provider Service and dependencies --- .../EzPlatformRichTextExtension.php | 8 ++++ src/bundle/Resources/config/api.yaml | 8 ++++ .../Resources/config/configuration.yaml | 23 +++++++++++ src/bundle/Resources/config/ui/mappers.yaml | 41 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 src/bundle/Resources/config/api.yaml create mode 100644 src/bundle/Resources/config/configuration.yaml create mode 100644 src/bundle/Resources/config/ui/mappers.yaml diff --git a/src/bundle/DependencyInjection/EzPlatformRichTextExtension.php b/src/bundle/DependencyInjection/EzPlatformRichTextExtension.php index 5471f441..600e6551 100644 --- a/src/bundle/DependencyInjection/EzPlatformRichTextExtension.php +++ b/src/bundle/DependencyInjection/EzPlatformRichTextExtension.php @@ -8,6 +8,7 @@ namespace EzSystems\EzPlatformRichTextBundle\DependencyInjection; +use EzSystems\EzPlatformRichText\SPI\Configuration\Provider; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\FileResource; @@ -25,6 +26,7 @@ class EzPlatformRichTextExtension extends Extension implements PrependExtensionI const RICHTEXT_CUSTOM_STYLES_PARAMETER = 'ezplatform.ezrichtext.custom_styles'; const RICHTEXT_CUSTOM_TAGS_PARAMETER = 'ezplatform.ezrichtext.custom_tags'; const RICHTEXT_ALLOY_EDITOR_PARAMETER = 'ezplatform.ezrichtext.alloy_editor'; + public const RICHTEXT_CONFIGURATION_PROVIDER_TAG = 'ezrichtext.configuration.provider'; public function getAlias() { @@ -52,6 +54,10 @@ public function load(array $configs, ContainerBuilder $container) $ezLoader->load('storage_engines/legacy/external_storage_gateways.yaml'); $ezLoader->load('storage_engines/legacy/field_value_converters.yaml'); + $container + ->registerForAutoconfiguration(Provider::class) + ->addTag(static::RICHTEXT_CONFIGURATION_PROVIDER_TAG); + $loader = new Loader\YamlFileLoader( $container, new FileLocator(__DIR__ . '/../Resources/config') @@ -61,6 +67,8 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('templating.yaml'); $loader->load('form.yaml'); $loader->load('translation.yaml'); + $loader->load('configuration.yaml'); + $loader->load('api.yaml'); $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); diff --git a/src/bundle/Resources/config/api.yaml b/src/bundle/Resources/config/api.yaml new file mode 100644 index 00000000..c6e4484b --- /dev/null +++ b/src/bundle/Resources/config/api.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + autoconfigure: true + autowire: true + public: false + + EzSystems\EzPlatformRichText\API\Configuration\ProviderService: + alias: EzSystems\EzPlatformRichText\Configuration\AggregateProvider diff --git a/src/bundle/Resources/config/configuration.yaml b/src/bundle/Resources/config/configuration.yaml new file mode 100644 index 00000000..aae8edc5 --- /dev/null +++ b/src/bundle/Resources/config/configuration.yaml @@ -0,0 +1,23 @@ +imports: + - { resource: ui/mappers.yaml } + +parameters: + # set via compiler pass + ezplatform.ezrichtext.alloy_editor: [] + +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + EzSystems\EzPlatformRichText\Configuration\Provider\: + resource: '../../../lib/Configuration/Provider/*' + + EzSystems\EzPlatformRichText\Configuration\Provider\AlloyEditor: + arguments: + $alloyEditorConfiguration: '%ezplatform.ezrichtext.alloy_editor%' + + EzSystems\EzPlatformRichText\Configuration\AggregateProvider: + arguments: + $providers: !tagged ezrichtext.configuration.provider diff --git a/src/bundle/Resources/config/ui/mappers.yaml b/src/bundle/Resources/config/ui/mappers.yaml new file mode 100644 index 00000000..bec1e027 --- /dev/null +++ b/src/bundle/Resources/config/ui/mappers.yaml @@ -0,0 +1,41 @@ +parameters: + ezrichtext.custom_tags.translation_domain: 'custom_tags' + ezrichtext.custom_styles.translation_domain: 'custom_styles' + ezrichtext.online_editor.translation_domain: 'online_editor' + +services: + _defaults: + autowire: true + public: false + + # RichText Custom Tags UI config attribute type mappers + EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag\CommonAttributeMapper: + tags: + - { name: ezrichtext.configuration.custom_tag.mapper, priority: 0 } + + EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag\ChoiceAttributeMapper: + parent: EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag\CommonAttributeMapper + autowire: true + public: false + tags: + - { name: ezrichtext.configuration.custom_tag.mapper, priority: 10 } + + # RichText Custom Tags UI config mapper + EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag: + arguments: + $customTagsConfiguration: '%ezplatform.ezrichtext.custom_tags%' + $translationDomain: '%ezrichtext.custom_tags.translation_domain%' + $customTagAttributeMappers: !tagged ezrichtext.configuration.custom_tag.mapper + + # RichText Custom Styles UI config mapper + EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomStyle: + arguments: + $customStylesConfiguration: '%ezplatform.ezrichtext.custom_styles%' + $translationDomain: '%ezrichtext.custom_styles.translation_domain%' + + EzSystems\EzPlatformRichText\Configuration\UI\Mapper\OnlineEditorConfigMapper: + alias: EzSystems\EzPlatformRichText\Configuration\UI\Mapper\OnlineEditor + + EzSystems\EzPlatformRichText\Configuration\UI\Mapper\OnlineEditor: + arguments: + $translationDomain: '%ezrichtext.online_editor.translation_domain%' From 17c922443028053f489395fbf6747bdb5af69ddf Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 19 Aug 2019 17:26:07 +0200 Subject: [PATCH 07/25] Implemented Twig extension exposing RichText configuration --- src/bundle/Resources/config/templating.yaml | 2 + .../RichTextConfigurationExtension.php | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php diff --git a/src/bundle/Resources/config/templating.yaml b/src/bundle/Resources/config/templating.yaml index 1ccaa1a7..a0a28f16 100644 --- a/src/bundle/Resources/config/templating.yaml +++ b/src/bundle/Resources/config/templating.yaml @@ -10,3 +10,5 @@ services: $richTextEditConverter: '@ezrichtext.converter.edit.xhtml5' EzSystems\EzPlatformRichTextBundle\Templating\Twig\Extension\YoutubeIdExtractorExtension: ~ + + EzSystems\EzPlatformRichTextBundle\Templating\Twig\Extension\RichTextConfigurationExtension: ~ diff --git a/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php b/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php new file mode 100644 index 00000000..0acf4860 --- /dev/null +++ b/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php @@ -0,0 +1,41 @@ +configurationProvider = $configurationProvider; + } + + public function getName() + { + return 'ezpublish.rich_text'; + } + + public function getGlobals(): array + { + return [ + 'ez_richtext_config' => $this->configurationProvider->getConfiguration(), + ]; + } +} From 9b6a6bae71ffbd2669aabdda2310bba5e5569ba6 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Tue, 20 Aug 2019 11:49:32 +0200 Subject: [PATCH 08/25] Replaced eZ.adminUiConfig references with eZ.richText --- .../js/OnlineEditor/core/base-richtext.js | 28 +++++++++---------- .../js/OnlineEditor/core/ez-attributes.js | 2 +- .../js/OnlineEditor/core/ez-custom-tags.js | 2 +- .../plugins/base/ez-custom-tag-base.js | 4 +-- .../js/OnlineEditor/plugins/ez-custom-tag.js | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/bundle/Resources/public/js/OnlineEditor/core/base-richtext.js b/src/bundle/Resources/public/js/OnlineEditor/core/base-richtext.js index 8c9bbcd8..761a9152 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/core/base-richtext.js +++ b/src/bundle/Resources/public/js/OnlineEditor/core/base-richtext.js @@ -6,11 +6,11 @@ constructor() { this.ezNamespace = 'http://ez.no/namespaces/ezpublish5/xhtml5/edit'; this.xhtmlNamespace = 'http://www.w3.org/1999/xhtml'; - this.customTags = Object.keys(eZ.adminUiConfig.richTextCustomTags).filter( - (key) => !eZ.adminUiConfig.richTextCustomTags[key].isInline + this.customTags = Object.keys(eZ.richText.customTags).filter( + (key) => !eZ.richText.customTags[key].isInline ); - this.inlineCustomTags = Object.keys(eZ.adminUiConfig.richTextCustomTags).filter( - (key) => eZ.adminUiConfig.richTextCustomTags[key].isInline + this.inlineCustomTags = Object.keys(eZ.richText.customTags).filter( + (key) => eZ.richText.customTags[key].isInline ); this.alloyEditorExtraButtons = { ezadd: [], @@ -19,12 +19,12 @@ table: [], tr: [], td: [], - ...eZ.adminUiConfig.alloyEditor.extraButtons, + ...eZ.richText.alloyEditor.extraButtons, }; - this.attributes = global.eZ.adminUiConfig.alloyEditor.attributes; - this.classes = global.eZ.adminUiConfig.alloyEditor.classes; + this.attributes = global.eZ.richText.alloyEditor.attributes; + this.classes = global.eZ.richText.alloyEditor.classes; this.customTagsToolbars = this.customTags.map((customTag) => { - const alloyEditorConfig = eZ.adminUiConfig.richTextCustomTags[customTag]; + const alloyEditorConfig = eZ.richText.customTags[customTag]; return new eZ.ezAlloyEditor.ezCustomTagConfig({ name: customTag, @@ -33,7 +33,7 @@ }); }); this.inlineCustomTagsToolbars = this.inlineCustomTags.map((customTag) => { - const alloyEditorConfig = eZ.adminUiConfig.richTextCustomTags[customTag]; + const alloyEditorConfig = eZ.richText.customTags[customTag]; return new eZ.ezAlloyEditor.ezInlineCustomTagConfig({ name: customTag, @@ -41,14 +41,14 @@ extraButtons: this.alloyEditorExtraButtons, }); }); - this.customStylesConfigurations = Object.entries(eZ.adminUiConfig.richTextCustomStyles).map( + this.customStylesConfigurations = Object.entries(eZ.richText.customStyles).map( ([customStyleName, customStyleConfig]) => { return { - name: customStyleConfig.adminUiConfig.label, + name: customStyleConfig.label, style: { - element: customStyleConfig.adminUiConfig.inline ? 'span' : 'div', + element: customStyleConfig.inline ? 'span' : 'div', attributes: { - 'data-ezelement': customStyleConfig.adminUiConfig.inline ? 'eztemplateinline' : 'eztemplate', + 'data-ezelement': customStyleConfig.inline ? 'eztemplateinline' : 'eztemplate', 'data-eztype': 'style', 'data-ezname': customStyleName, }, @@ -56,7 +56,7 @@ }; } ); - this.alloyEditorExtraPlugins = eZ.adminUiConfig.alloyEditor.extraPlugins; + this.alloyEditorExtraPlugins = eZ.richText.alloyEditor.extraPlugins; this.xhtmlify = this.xhtmlify.bind(this); } diff --git a/src/bundle/Resources/public/js/OnlineEditor/core/ez-attributes.js b/src/bundle/Resources/public/js/OnlineEditor/core/ez-attributes.js index 162407d9..01468a4e 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/core/ez-attributes.js +++ b/src/bundle/Resources/public/js/OnlineEditor/core/ez-attributes.js @@ -1,5 +1,5 @@ (function(global, doc, eZ, AlloyEditor) { - const { attributes, classes } = eZ.adminUiConfig.alloyEditor; + const { attributes, classes } = eZ.richText.alloyEditor; const toolbarNames = new Set([...Object.keys(attributes), ...Object.keys(classes)]); toolbarNames.forEach((toolbarName) => { diff --git a/src/bundle/Resources/public/js/OnlineEditor/core/ez-custom-tags.js b/src/bundle/Resources/public/js/OnlineEditor/core/ez-custom-tags.js index b61b2b6b..c3ec0389 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/core/ez-custom-tags.js +++ b/src/bundle/Resources/public/js/OnlineEditor/core/ez-custom-tags.js @@ -1,5 +1,5 @@ (function(global, doc, eZ, AlloyEditor) { - Object.entries(eZ.adminUiConfig.richTextCustomTags).forEach(([customTag, tagConfig]) => { + Object.entries(eZ.richText.customTags).forEach(([customTag, tagConfig]) => { const isInline = tagConfig.isInline; const componentClassName = `ezBtn${customTag.charAt(0).toUpperCase() + customTag.slice(1)}`; const editComponentClassName = `${componentClassName}Edit`; diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-custom-tag-base.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-custom-tag-base.js index f85e0849..0de90869 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-custom-tag-base.js +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-custom-tag-base.js @@ -108,7 +108,7 @@ const customTagBaseDefinition = { * @method renderHeader */ renderHeader: function() { - const customTagConfig = global.eZ.adminUiConfig.richTextCustomTags[this.getName()]; + const customTagConfig = global.eZ.richText.customTags[this.getName()]; if (!customTagConfig) { return; @@ -175,7 +175,7 @@ const customTagBaseDefinition = { * @method renderAttributes */ renderAttributes: function() { - const customTagConfig = global.eZ.adminUiConfig.richTextCustomTags[this.getName()]; + const customTagConfig = global.eZ.richText.customTags[this.getName()]; if (!customTagConfig || !customTagConfig.attributes) { return; diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-custom-tag.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-custom-tag.js index a1f5f24c..fa9b7518 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-custom-tag.js +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/ez-custom-tag.js @@ -57,7 +57,7 @@ CKEDITOR.dtd.$editable.span = 1; }, renderIcon: function() { - const customTagConfig = global.eZ.adminUiConfig.richTextCustomTags[this.getName()]; + const customTagConfig = global.eZ.richText.customTags[this.getName()]; if (!customTagConfig) { return; From 17666db3e47167ae8da745f095e8a0366d34c976 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Tue, 20 Aug 2019 12:29:51 +0200 Subject: [PATCH 09/25] Implemented unit tests for Custom Tags and Styles mappers Moved from AdminUI --- .../Configuration/UI/Mapper/CustomTagTest.php | 219 ++++++++++++++++++ .../UI/Mapper/OnlineEditorTest.php | 217 +++++++++++++++++ 2 files changed, 436 insertions(+) create mode 100644 tests/lib/Configuration/UI/Mapper/CustomTagTest.php create mode 100644 tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php diff --git a/tests/lib/Configuration/UI/Mapper/CustomTagTest.php b/tests/lib/Configuration/UI/Mapper/CustomTagTest.php new file mode 100644 index 00000000..82172366 --- /dev/null +++ b/tests/lib/Configuration/UI/Mapper/CustomTagTest.php @@ -0,0 +1,219 @@ +getTranslatorMock(), + 'custom_tags', + $this->getPackagesMock(), + new ArrayObject( + [ + new CustomTag\ChoiceAttributeMapper(), + new CustomTag\CommonAttributeMapper(), + ] + ) + ); + + $actualConfig = $mapper->mapConfig($enabledCustomTags); + + self::assertEquals($expectedConfig, $actualConfig); + } + + /** + * Data provider for {@see testMapConfig}. + * + * @return array + */ + public function providerForTestMapConfig(): array + { + return [ + [ + [ + 'ezyoutube' => [ + 'template' => '@ezdesign/fields/ezrichtext/custom_tags/ezyoutube.html.twig', + 'icon' => '/bundles/ezplatformadminui/img/ez-icons.svg#video', + 'is_inline' => false, + 'attributes' => [ + 'width' => [ + 'type' => 'number', + 'required' => true, + 'default_value' => 640, + ], + 'height' => [ + 'type' => 'number', + 'required' => true, + 'default_value' => 360, + ], + 'autoplay' => [ + 'type' => 'boolean', + 'default_value' => false, + 'required' => false, + ], + ], + ], + 'eztwitter' => [ + 'template' => '@ezdesign/fields/ezrichtext/custom_tags/eztwitter.html.twig', + 'icon' => '/bundles/ezplatformadminui/img/ez-icons.svg#twitter', + 'is_inline' => false, + 'attributes' => [ + 'tweet_url' => [ + 'type' => 'string', + 'required' => true, + 'default_value' => null, + ], + 'cards' => [ + 'type' => 'choice', + 'required' => false, + 'default_value' => '', + 'choices' => [ + '', + 'hidden', + ], + ], + ], + ], + ], + ['ezyoutube', 'eztwitter'], + [ + 'ezyoutube' => [ + 'label' => 'ezrichtext.custom_tags.ezyoutube.label', + 'description' => 'ezrichtext.custom_tags.ezyoutube.description', + 'icon' => '/bundles/ezplatformadminui/img/ez-icons.svg#video', + 'isInline' => false, + 'attributes' => [ + 'width' => [ + 'label' => 'ezrichtext.custom_tags.ezyoutube.attributes.width.label', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 640, + ], + 'height' => [ + 'label' => 'ezrichtext.custom_tags.ezyoutube.attributes.height.label', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 360, + ], + 'autoplay' => [ + 'label' => 'ezrichtext.custom_tags.ezyoutube.attributes.autoplay.label', + 'type' => 'boolean', + 'required' => false, + 'defaultValue' => false, + ], + ], + ], + 'eztwitter' => [ + 'label' => 'ezrichtext.custom_tags.eztwitter.label', + 'description' => 'ezrichtext.custom_tags.eztwitter.description', + 'icon' => '/bundles/ezplatformadminui/img/ez-icons.svg#twitter', + 'isInline' => false, + 'attributes' => [ + 'tweet_url' => [ + 'label' => 'ezrichtext.custom_tags.eztwitter.attributes.tweet_url.label', + 'type' => 'string', + 'required' => true, + 'defaultValue' => null, + ], + 'cards' => [ + 'label' => 'ezrichtext.custom_tags.eztwitter.attributes.cards.label', + 'type' => 'choice', + 'required' => false, + 'defaultValue' => '', + 'choices' => [ + '', + 'hidden', + ], + 'choicesLabel' => [ + '' => '', + 'hidden' => 'hidden', + ], + ], + ], + ], + ], + ], + ]; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|\Symfony\Contracts\Translation\TranslatorInterface + */ + private function getTranslatorMock(): MockObject + { + $catalogueMock = $this->createMock(MessageCatalogueInterface::class); + $catalogueMock + ->expects($this->any()) + ->method('has') + ->withAnyParameters() + ->willReturn(false); + + $translatorMock = $this->createMock( + [TranslatorInterface::class, TranslatorBagInterface::class] + ); + $translatorMock + ->expects($this->any()) + ->method('getCatalogue') + ->willReturn( + $catalogueMock + ); + + $translatorMock + ->expects($this->any()) + ->method('trans') + ->withAnyParameters() + ->willReturnArgument(0); + + return $translatorMock; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|\Symfony\Component\Asset\Packages + */ + private function getPackagesMock(): MockObject + { + $packagesMock = $this->createMock(Packages::class); + $packagesMock + ->expects($this->any()) + ->method('getUrl') + ->withAnyParameters() + ->willReturnArgument(0); + + return $packagesMock; + } +} diff --git a/tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php b/tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php new file mode 100644 index 00000000..ede60740 --- /dev/null +++ b/tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php @@ -0,0 +1,217 @@ +createMock(TranslatorInterface::class); + $translatorMock + ->expects($this->any()) + ->method('trans') + ->willReturnArgument(0); + /** @var \Symfony\Contracts\Translation\TranslatorInterface $translatorMock */ + $this->mapper = new OnlineEditor($translatorMock, 'online_editor'); + } + + /** + * Data provider for mapCssClassesConfiguration. + * + * @see testMapCssClassesConfiguration + * + * @return array + */ + public function getSemanticConfigurationForMapCssClassesConfiguration(): array + { + return [ + [ + // semantic configuration ... + [ + 'paragraph' => [ + 'choices' => ['class1', 'class2'], + 'required' => true, + 'default_value' => 'class1', + 'multiple' => true, + ], + 'table' => [ + 'choices' => ['class1', 'class2'], + 'required' => false, + 'default_value' => 'class2', + 'multiple' => false, + ], + 'heading' => [ + 'choices' => ['class1', 'class2'], + 'required' => false, + 'multiple' => false, + ], + ], + // ... is mapped to: + [ + 'paragraph' => [ + 'choices' => ['class1', 'class2'], + 'required' => true, + 'defaultValue' => 'class1', + 'multiple' => true, + 'label' => 'ezrichtext.classes.class.label', + ], + 'table' => [ + 'choices' => ['class1', 'class2'], + 'required' => false, + 'defaultValue' => 'class2', + 'multiple' => false, + 'label' => 'ezrichtext.classes.class.label', + ], + 'heading' => [ + 'choices' => ['class1', 'class2'], + 'required' => false, + 'defaultValue' => null, + 'multiple' => false, + 'label' => 'ezrichtext.classes.class.label', + ], + ], + ], + ]; + } + + /** + * Data provider for mapDataAttributesConfiguration. + * + * @return array + * + * @see testMapDataAttributesConfiguration + */ + public function getSemanticConfigurationForMapDataAttributesConfiguration(): array + { + return [ + [ + // semantic configuration ... + [ + 'paragraph' => [ + 'select-multiple-attr' => [ + 'type' => 'choice', + 'multiple' => true, + 'required' => true, + 'choices' => ['value1', 'value2'], + 'default_value' => 'value2', + ], + 'select-single-attr' => [ + 'type' => 'choice', + 'multiple' => false, + 'required' => true, + 'choices' => ['value1', 'value2'], + 'default_value' => 'value2', + ], + ], + 'heading' => [ + 'boolean-attr' => [ + 'type' => 'boolean', + 'required' => false, + 'default_value' => true, + ], + 'text-attr' => [ + 'type' => 'string', + 'default_value' => 'foo', + 'required' => true, + ], + ], + 'tr' => [ + 'number-attr' => [ + 'type' => 'number', + 'default_value' => 1, + 'required' => true, + ], + ], + ], + // ... is mapped to: + [ + 'paragraph' => [ + 'select-multiple-attr' => [ + 'label' => 'ezrichtext.attributes.paragraph.select-multiple-attr.label', + 'type' => 'choice', + 'multiple' => true, + 'required' => true, + 'choices' => ['value1', 'value2'], + 'defaultValue' => 'value2', + ], + 'select-single-attr' => [ + 'label' => 'ezrichtext.attributes.paragraph.select-single-attr.label', + 'type' => 'choice', + 'multiple' => false, + 'required' => true, + 'choices' => ['value1', 'value2'], + 'defaultValue' => 'value2', + ], + ], + 'heading' => [ + 'boolean-attr' => [ + 'label' => 'ezrichtext.attributes.heading.boolean-attr.label', + 'type' => 'boolean', + 'required' => false, + 'defaultValue' => true, + ], + 'text-attr' => [ + 'label' => 'ezrichtext.attributes.heading.text-attr.label', + 'type' => 'string', + 'defaultValue' => 'foo', + 'required' => true, + ], + ], + 'tr' => [ + 'number-attr' => [ + 'label' => 'ezrichtext.attributes.tr.number-attr.label', + 'type' => 'number', + 'defaultValue' => 1, + 'required' => true, + ], + ], + ], + ], + ]; + } + + /** + * @dataProvider getSemanticConfigurationForMapCssClassesConfiguration + * + * @param array $semanticConfiguration + * @param array $expectedMappedConfiguration + */ + public function testMapCssClassesConfiguration( + array $semanticConfiguration, + array $expectedMappedConfiguration + ): void { + self::assertEquals( + $expectedMappedConfiguration, + $this->mapper->mapCssClassesConfiguration($semanticConfiguration) + ); + } + + /** + * @dataProvider getSemanticConfigurationForMapDataAttributesConfiguration + * + * @param array $semanticConfiguration + * @param array $expectedMappedConfiguration + */ + public function testMapDataAttributesConfiguration( + array $semanticConfiguration, + array $expectedMappedConfiguration + ): void { + self::assertEquals( + $expectedMappedConfiguration, + $this->mapper->mapDataAttributesConfiguration($semanticConfiguration) + ); + } +} From bd8c2a63753cab003592db46e440635e14b77a5d Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Tue, 20 Aug 2019 12:30:37 +0200 Subject: [PATCH 10/25] Added required Translator dependencies to composer.json --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index 378c0a90..55def2d7 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,8 @@ "symfony/options-resolver": "^4.3", "symfony/validator": "^4.3", "symfony/cache": "^4.3", + "symfony/translation": "^4.3", + "symfony/translation-contracts": "^1.1.2", "twig/twig": "^2.11", "ezsystems/doctrine-dbal-schema": "^1.0@dev", "ezsystems/ezpublish-kernel": "^8.0@dev", From 5d360b2e56e48ee2ff213c28f97378968a5498be Mon Sep 17 00:00:00 2001 From: Dariusz Szut Date: Tue, 20 Aug 2019 13:45:02 +0200 Subject: [PATCH 11/25] Moved fix for JS error with missing element path #1043 --- .../toolbars/config/ez-table-cell.js | 3 +-- .../toolbars/config/ez-table-row.js | 3 +-- .../OnlineEditor/toolbars/config/ez-table.js | 3 +-- .../public/js/OnlineEditor/toolbars/ez-add.js | 25 ++++++++++++------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-cell.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-cell.js index 18f352fc..a6a5d330 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-cell.js +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-cell.js @@ -8,9 +8,8 @@ export default class EzTableCellConfig extends EzConfigTableBase { test(payload) { const nativeEditor = payload.editor.get('nativeEditor'); const path = nativeEditor.elementPath(); - const lastElement = path.lastElement; - return lastElement.is('td'); + return path && path.lastElement.is('td'); } } diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-row.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-row.js index b7a29ee4..95c555d4 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-row.js +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table-row.js @@ -8,9 +8,8 @@ export default class EzTableRowConfig extends EzConfigTableBase { test(payload) { const nativeEditor = payload.editor.get('nativeEditor'); const path = nativeEditor.elementPath(); - const lastElement = path.lastElement; - return lastElement.is('tr'); + return path && path.lastElement.is('tr'); } } diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table.js index dfdeab3a..17a485d9 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table.js +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/config/ez-table.js @@ -8,9 +8,8 @@ export default class EzTableConfig extends EzConfigTableBase { test(payload) { const nativeEditor = payload.editor.get('nativeEditor'); const path = nativeEditor.elementPath(); - const lastElement = path.lastElement; - return lastElement.is('table'); + return path && path.lastElement.is('table'); } } diff --git a/src/bundle/Resources/public/js/OnlineEditor/toolbars/ez-add.js b/src/bundle/Resources/public/js/OnlineEditor/toolbars/ez-add.js index c11c1220..d6e50cd0 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/toolbars/ez-add.js +++ b/src/bundle/Resources/public/js/OnlineEditor/toolbars/ez-add.js @@ -11,6 +11,7 @@ export default class EzToolbarAdd extends AlloyEditor.Toolbars.add { super(props); this.setPosition = this.setPosition.bind(this); + this.setTopPosition = this.setTopPosition.bind(this); } setPosition() { @@ -21,19 +22,23 @@ export default class EzToolbarAdd extends AlloyEditor.Toolbars.add { } componentDidUpdate(prevProps, prevState) { - const { selectionData } = this.props; + const { selectionData, renderExclusive } = this.props; this._updatePosition(); - if (selectionData && !selectionData.region.top) { + if (!renderExclusive && selectionData && !selectionData.region.top) { this.setTopPosition(); } // In case of exclusive rendering, focus the first descendant (button) // so the user will be able to start interacting with the buttons immediately. - if (this.props.renderExclusive) { + if (renderExclusive) { this.focus(); + if (selectionData && !selectionData.region.top) { + this._animate(this.setTopPosition); + } + this._animate(this.setPosition); } } @@ -41,16 +46,18 @@ export default class EzToolbarAdd extends AlloyEditor.Toolbars.add { setTopPosition() { const { editor } = this.props; const domNode = ReactDOM.findDOMNode(this); - const path = editor.get('nativeEditor').elementPath(); - const table = path.elements.find((element) => element.is('table')); + const nativeEditor = editor.get('nativeEditor'); + const path = nativeEditor.elementPath(); + const table = path && path.elements.find((element) => element.is('table')); + const element = table || nativeEditor.element; + const rect = element.$.getBoundingClientRect(); + let topPosition = rect.top; if (!table) { - return; + topPosition += window.pageYOffset; } - const rect = table.$.getBoundingClientRect(); - - new CKEDITOR.dom.element(domNode).setStyles({ top: `${rect.top}px` }); + new CKEDITOR.dom.element(domNode).setStyles({ top: `${topPosition}px` }); } /** From cf8d0874f2b87a7c1947bef8158b701a4527a7dc Mon Sep 17 00:00:00 2001 From: Dariusz Szut Date: Wed, 21 Aug 2019 14:36:04 +0200 Subject: [PATCH 12/25] Moved fix for removed content in embed --- .../public/js/OnlineEditor/plugins/base/ez-embed-base.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-embed-base.js b/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-embed-base.js index 91e60abf..bf8a6e3d 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-embed-base.js +++ b/src/bundle/Resources/public/js/OnlineEditor/plugins/base/ez-embed-base.js @@ -170,6 +170,14 @@ const embedBaseDefinition = { * @param {Object} hits The result of content search */ handleContentLoaded: function(hits) { + if (hits.View.Result.searchHits.searchHit.length === 0) { + const title = Translator.trans(/*@Desc("Removed")*/ 'removed_content.label', {}, 'alloy_editor'); + + this.renderEmbedPreview(title); + + return; + } + const isEmbedImage = this.element.$.classList.contains(IMAGE_TYPE_CLASS); const content = hits.View.Result.searchHits.searchHit[0].value.Content; From 4dc9b6df6f8a3f91053bbdc88a6303a59e9b66d9 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Thu, 22 Aug 2019 21:59:00 +0200 Subject: [PATCH 13/25] fixup! Implemented UI configuration mappers --- src/lib/Configuration/UI/Mapper/CustomStyle.php | 2 +- src/lib/Configuration/UI/Mapper/CustomTag.php | 4 ++-- .../UI/Mapper/CustomTag/ChoiceAttributeMapper.php | 3 --- src/lib/Configuration/UI/Mapper/OnlineEditor.php | 4 ---- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/lib/Configuration/UI/Mapper/CustomStyle.php b/src/lib/Configuration/UI/Mapper/CustomStyle.php index c5b3a767..b66ded7d 100644 --- a/src/lib/Configuration/UI/Mapper/CustomStyle.php +++ b/src/lib/Configuration/UI/Mapper/CustomStyle.php @@ -50,7 +50,7 @@ public function __construct( * * @return array Mapped configuration */ - public function mapConfig(array $enabledCustomStyles) + public function mapConfig(array $enabledCustomStyles): array { $config = []; foreach ($enabledCustomStyles as $styleName) { diff --git a/src/lib/Configuration/UI/Mapper/CustomTag.php b/src/lib/Configuration/UI/Mapper/CustomTag.php index 735ffd94..f63d113f 100644 --- a/src/lib/Configuration/UI/Mapper/CustomTag.php +++ b/src/lib/Configuration/UI/Mapper/CustomTag.php @@ -73,7 +73,7 @@ public function __construct( * * @return array Mapped configuration */ - public function mapConfig(array $enabledCustomTags) + public function mapConfig(array $enabledCustomTags): array { $config = []; foreach ($enabledCustomTags as $tagName) { @@ -121,7 +121,7 @@ public function mapConfig(array $enabledCustomTags) * @param string $attributeName * @param string $attributeType * - * @return AttributeMapper + * @return \EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag\AttributeMapper */ private function getAttributeTypeMapper( string $tagName, diff --git a/src/lib/Configuration/UI/Mapper/CustomTag/ChoiceAttributeMapper.php b/src/lib/Configuration/UI/Mapper/CustomTag/ChoiceAttributeMapper.php index 76fa044e..0f9cfe86 100644 --- a/src/lib/Configuration/UI/Mapper/CustomTag/ChoiceAttributeMapper.php +++ b/src/lib/Configuration/UI/Mapper/CustomTag/ChoiceAttributeMapper.php @@ -15,9 +15,6 @@ */ final class ChoiceAttributeMapper extends CommonAttributeMapper implements AttributeMapper { - /** - * {@inheritdoc} - */ public function supports(string $attributeType): bool { return 'choice' === $attributeType; diff --git a/src/lib/Configuration/UI/Mapper/OnlineEditor.php b/src/lib/Configuration/UI/Mapper/OnlineEditor.php index e8e5e8cb..a326e011 100644 --- a/src/lib/Configuration/UI/Mapper/OnlineEditor.php +++ b/src/lib/Configuration/UI/Mapper/OnlineEditor.php @@ -23,10 +23,6 @@ final class OnlineEditor implements OnlineEditorConfigMapper /** @var string */ private $translationDomain; - /** - * @param \Symfony\Contracts\Translation\TranslatorInterface $translator - * @param string $translationDomain - */ public function __construct(TranslatorInterface $translator, string $translationDomain) { $this->translator = $translator; From 7458d3154f06423fa99b14f2ca4b50a3003f21c1 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Thu, 22 Aug 2019 21:59:20 +0200 Subject: [PATCH 14/25] fixup! Implemented Twig extension exposing RichText configuration --- .../Twig/Extension/RichTextConfigurationExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php b/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php index 0acf4860..0a046056 100644 --- a/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php +++ b/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php @@ -27,7 +27,7 @@ public function __construct(Configuration\ProviderService $configurationProvider $this->configurationProvider = $configurationProvider; } - public function getName() + public function getName(): string { return 'ezpublish.rich_text'; } From 31ecd7d2fc4a9e46d348142095b9678080e2366e Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Thu, 22 Aug 2019 22:01:04 +0200 Subject: [PATCH 15/25] fixup! Implemented config providers for AlloyEditor, CustomStyle, and CustomTag --- src/lib/Configuration/Provider/AlloyEditor.php | 5 ----- src/lib/Configuration/Provider/CustomStyle.php | 4 ---- src/lib/Configuration/Provider/CustomTag.php | 4 ---- 3 files changed, 13 deletions(-) diff --git a/src/lib/Configuration/Provider/AlloyEditor.php b/src/lib/Configuration/Provider/AlloyEditor.php index cca2b28b..1a123b19 100644 --- a/src/lib/Configuration/Provider/AlloyEditor.php +++ b/src/lib/Configuration/Provider/AlloyEditor.php @@ -29,11 +29,6 @@ final class AlloyEditor implements Provider /** @var \EzSystems\EzPlatformRichText\Configuration\UI\Mapper\OnlineEditorConfigMapper */ private $onlineEditorConfigMapper; - /** - * @param array $alloyEditorConfiguration - * @param \eZ\Publish\Core\MVC\ConfigResolverInterface $configResolver - * @param \EzSystems\EzPlatformRichText\Configuration\UI\Mapper\OnlineEditorConfigMapper $onlineEditorConfigMapper - */ public function __construct( array $alloyEditorConfiguration, ConfigResolverInterface $configResolver, diff --git a/src/lib/Configuration/Provider/CustomStyle.php b/src/lib/Configuration/Provider/CustomStyle.php index 893023a8..71b7353d 100644 --- a/src/lib/Configuration/Provider/CustomStyle.php +++ b/src/lib/Configuration/Provider/CustomStyle.php @@ -25,10 +25,6 @@ final class CustomStyle implements Provider /** @var \EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomStyle */ private $customStyleConfigurationMapper; - /** - * @param \eZ\Publish\Core\MVC\ConfigResolverInterface $configResolver - * @param \EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomStyle $customStyleConfigurationMapper - */ public function __construct( ConfigResolverInterface $configResolver, CustomStyleConfigurationMapper $customStyleConfigurationMapper diff --git a/src/lib/Configuration/Provider/CustomTag.php b/src/lib/Configuration/Provider/CustomTag.php index f8ec1367..4f1f7c92 100644 --- a/src/lib/Configuration/Provider/CustomTag.php +++ b/src/lib/Configuration/Provider/CustomTag.php @@ -25,10 +25,6 @@ final class CustomTag implements Provider /** @var \EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag */ private $customTagConfigurationMapper; - /** - * @param \eZ\Publish\Core\MVC\ConfigResolverInterface $configResolver - * @param \EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag $customTagConfigurationMapper - */ public function __construct( ConfigResolverInterface $configResolver, CustomTagConfigurationMapper $customTagConfigurationMapper From bddd421fd2612dc5d0fe691e3fe55feb83fb1ab0 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Thu, 22 Aug 2019 22:01:25 +0200 Subject: [PATCH 16/25] fixup! Implemented unit tests for Custom Tags and Styles mappers --- tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php b/tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php index ede60740..74bf591f 100644 --- a/tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php +++ b/tests/lib/Configuration/UI/Mapper/OnlineEditorTest.php @@ -19,12 +19,12 @@ class OnlineEditorTest extends TestCase public function setUp(): void { + /** @var \Symfony\Contracts\Translation\TranslatorInterface $translatorMock */ $translatorMock = $this->createMock(TranslatorInterface::class); $translatorMock ->expects($this->any()) ->method('trans') ->willReturnArgument(0); - /** @var \Symfony\Contracts\Translation\TranslatorInterface $translatorMock */ $this->mapper = new OnlineEditor($translatorMock, 'online_editor'); } From 6acd3d1122fd7376392e0e562c46928a3b99f200 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Thu, 22 Aug 2019 22:01:50 +0200 Subject: [PATCH 17/25] Changed reporting deprecation of ezrichtext.alloy_editor.extra_button Deprecation is now reported only if user is using this setting. --- src/lib/Configuration/Provider/AlloyEditor.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/Configuration/Provider/AlloyEditor.php b/src/lib/Configuration/Provider/AlloyEditor.php index 1a123b19..5a5b8792 100644 --- a/src/lib/Configuration/Provider/AlloyEditor.php +++ b/src/lib/Configuration/Provider/AlloyEditor.php @@ -73,12 +73,17 @@ private function getExtraPlugins(): array */ private function getExtraButtons(): array { + if (empty($this->alloyEditorConfiguration['extra_buttons'])) { + return []; + } + @trigger_error( - '"ezrichtext.alloy_editor.extra_buttons" is deprecated since v2.5.1. There will be new and more flexible solution to manage buttons in Online Editor in 3.0.0', + '"ezrichtext.alloy_editor.extra_buttons" is deprecated since v2.5.1. ' . + 'There will be new and more flexible solution to manage buttons in Online Editor in 3.0.0', E_USER_DEPRECATED ); - return $this->alloyEditorConfiguration['extra_buttons'] ?? []; + return $this->alloyEditorConfiguration['extra_buttons']; } /** From 00f92787c1f1e6871591ff10ea7951b8d6bfd513 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Thu, 22 Aug 2019 22:22:51 +0200 Subject: [PATCH 18/25] Added unit test coverage for Configuration\AggregateProvider --- .../Configuration/AggregateProviderTest.php | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/lib/Configuration/AggregateProviderTest.php diff --git a/tests/lib/Configuration/AggregateProviderTest.php b/tests/lib/Configuration/AggregateProviderTest.php new file mode 100644 index 00000000..739ea10e --- /dev/null +++ b/tests/lib/Configuration/AggregateProviderTest.php @@ -0,0 +1,89 @@ + $providerConfiguration) { + $providers[] = new class($providerName, $providerConfiguration) implements Provider { + private $name; + private $configuration; + + public function __construct(string $name, array $configuration) + { + $this->name = $name; + $this->configuration = $configuration; + } + + public function getName(): string + { + return $this->name; + } + + public function getConfiguration(): array + { + return $this->configuration; + } + }; + } + + $providerService = new AggregateProvider($providers); + + self::assertEquals($configuration, $providerService->getConfiguration()); + } + + public function getConfiguration(): array + { + return [ + [ + [], + ], + [ + [ + 'noConfigProvider' => [], + ], + ], + [ + [ + 'provider1' => [ + 'provider1_key1' => 'provider1_key1_value1', + 'provider1_key2' => 'provider1_key2_value2', + ], + 'provider2' => [ + 'provider2_key1' => 'provider2_key1_value1', + 'provider2_key2' => 'provider2_key2_value2', + ], + ], + ], + [ + [ + 'provider1' => [1, 2, 3], + 'provider2' => [1, 2, 3], + ], + ], + ]; + } +} From 47a66d6e79506f2b5a4993829bc4fc65f0b59bd6 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Fri, 23 Aug 2019 00:17:45 +0200 Subject: [PATCH 19/25] Added unit test coverage for Configuration\Provider\AlloyEditor --- .../Provider/AlloyEditorTest.php | 78 +++++++++++++++++++ .../Provider/BaseProviderTestCase.php | 39 ++++++++++ 2 files changed, 117 insertions(+) create mode 100644 tests/lib/Configuration/Provider/AlloyEditorTest.php create mode 100644 tests/lib/Configuration/Provider/BaseProviderTestCase.php diff --git a/tests/lib/Configuration/Provider/AlloyEditorTest.php b/tests/lib/Configuration/Provider/AlloyEditorTest.php new file mode 100644 index 00000000..f31c89e7 --- /dev/null +++ b/tests/lib/Configuration/Provider/AlloyEditorTest.php @@ -0,0 +1,78 @@ +mapper = $this->createMock(OnlineEditorConfigMapper::class); + } + + public function createProvider(): Provider + { + return new AlloyEditor( + [ + 'extra_plugins' => ['plugin1', 'plugin2'], + 'extra_buttons' => ['button1', 'button2'], + ], + $this->configResolver, + $this->mapper + ); + } + + public function getExpectedProviderName(): string + { + return 'alloyEditor'; + } + + /** + * @covers \EzSystems\Tests\EzPlatformRichText\Configuration\Provider\AlloyEditorTest::createProvider + */ + public function testGetConfiguration(): void + { + $provider = $this->createProvider(); + + $this->configResolver + ->expects($this->any()) + ->method('hasParameter') + ->willReturn(false); + + $this->mapper + ->expects($this->once()) + ->method('mapCssClassesConfiguration') + ->with([]) + ->willReturn(['class1', 'class2']); + + $this->mapper + ->expects($this->once()) + ->method('mapDataAttributesConfiguration') + ->with([]) + ->willReturn(['attr1', 'attr2']); + + self::assertEquals( + [ + 'extraPlugins' => ['plugin1', 'plugin2'], + 'extraButtons' => ['button1', 'button2'], + 'classes' => ['class1', 'class2'], + 'attributes' => ['attr1', 'attr2'], + ], + $provider->getConfiguration() + ); + } +} diff --git a/tests/lib/Configuration/Provider/BaseProviderTestCase.php b/tests/lib/Configuration/Provider/BaseProviderTestCase.php new file mode 100644 index 00000000..891c6dbb --- /dev/null +++ b/tests/lib/Configuration/Provider/BaseProviderTestCase.php @@ -0,0 +1,39 @@ +configResolver = $this->createMock(ConfigResolverInterface::class); + } + + abstract public function createProvider(): Provider; + + abstract public function getExpectedProviderName(): string; + + /** + * @covers \EzSystems\EzPlatformRichText\SPI\Configuration\Provider::getName + */ + final public function testGetName(): void + { + self::assertSame( + $this->getExpectedProviderName(), + $this->createProvider()->getName() + ); + } +} From d8cb3e0eb4e82aef004c8cfecc93066654aae722 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Fri, 23 Aug 2019 00:20:32 +0200 Subject: [PATCH 20/25] Refactored Custom Style and Tag config mappers to use common interface Created Configuration\UI\Mapper\CustomTemplateConfigMapper --- src/bundle/Resources/config/configuration.yaml | 8 ++++++++ src/lib/Configuration/Provider/CustomStyle.php | 4 ++-- src/lib/Configuration/Provider/CustomTag.php | 4 ++-- src/lib/Configuration/UI/Mapper/CustomStyle.php | 2 +- src/lib/Configuration/UI/Mapper/CustomTag.php | 2 +- .../UI/Mapper/CustomTemplateConfigMapper.php | 17 +++++++++++++++++ 6 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 src/lib/Configuration/UI/Mapper/CustomTemplateConfigMapper.php diff --git a/src/bundle/Resources/config/configuration.yaml b/src/bundle/Resources/config/configuration.yaml index aae8edc5..00dcf79b 100644 --- a/src/bundle/Resources/config/configuration.yaml +++ b/src/bundle/Resources/config/configuration.yaml @@ -14,6 +14,14 @@ services: EzSystems\EzPlatformRichText\Configuration\Provider\: resource: '../../../lib/Configuration/Provider/*' + EzSystems\EzPlatformRichText\Configuration\Provider\CustomStyle: + arguments: + $customStyleConfigurationMapper: '@EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomStyle' + + EzSystems\EzPlatformRichText\Configuration\Provider\CustomTag: + arguments: + $customTagConfigurationMapper: '@EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag' + EzSystems\EzPlatformRichText\Configuration\Provider\AlloyEditor: arguments: $alloyEditorConfiguration: '%ezplatform.ezrichtext.alloy_editor%' diff --git a/src/lib/Configuration/Provider/CustomStyle.php b/src/lib/Configuration/Provider/CustomStyle.php index 71b7353d..5c836303 100644 --- a/src/lib/Configuration/Provider/CustomStyle.php +++ b/src/lib/Configuration/Provider/CustomStyle.php @@ -9,7 +9,7 @@ namespace EzSystems\EzPlatformRichText\Configuration\Provider; use eZ\Publish\Core\MVC\ConfigResolverInterface; -use EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomStyle as CustomStyleConfigurationMapper; +use EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTemplateConfigMapper; use EzSystems\EzPlatformRichText\SPI\Configuration\Provider; /** @@ -27,7 +27,7 @@ final class CustomStyle implements Provider public function __construct( ConfigResolverInterface $configResolver, - CustomStyleConfigurationMapper $customStyleConfigurationMapper + CustomTemplateConfigMapper $customStyleConfigurationMapper ) { $this->configResolver = $configResolver; $this->customStyleConfigurationMapper = $customStyleConfigurationMapper; diff --git a/src/lib/Configuration/Provider/CustomTag.php b/src/lib/Configuration/Provider/CustomTag.php index 4f1f7c92..9243521a 100644 --- a/src/lib/Configuration/Provider/CustomTag.php +++ b/src/lib/Configuration/Provider/CustomTag.php @@ -9,7 +9,7 @@ namespace EzSystems\EzPlatformRichText\Configuration\Provider; use eZ\Publish\Core\MVC\ConfigResolverInterface; -use EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTag as CustomTagConfigurationMapper; +use EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomTemplateConfigMapper; use EzSystems\EzPlatformRichText\SPI\Configuration\Provider; /** @@ -27,7 +27,7 @@ final class CustomTag implements Provider public function __construct( ConfigResolverInterface $configResolver, - CustomTagConfigurationMapper $customTagConfigurationMapper + CustomTemplateConfigMapper $customTagConfigurationMapper ) { $this->configResolver = $configResolver; $this->customTagConfigurationMapper = $customTagConfigurationMapper; diff --git a/src/lib/Configuration/UI/Mapper/CustomStyle.php b/src/lib/Configuration/UI/Mapper/CustomStyle.php index b66ded7d..64521f9b 100644 --- a/src/lib/Configuration/UI/Mapper/CustomStyle.php +++ b/src/lib/Configuration/UI/Mapper/CustomStyle.php @@ -17,7 +17,7 @@ * * @internal For internal use by RichText package */ -final class CustomStyle +final class CustomStyle implements CustomTemplateConfigMapper { /** @var array */ private $customStylesConfiguration; diff --git a/src/lib/Configuration/UI/Mapper/CustomTag.php b/src/lib/Configuration/UI/Mapper/CustomTag.php index f63d113f..7f51d071 100644 --- a/src/lib/Configuration/UI/Mapper/CustomTag.php +++ b/src/lib/Configuration/UI/Mapper/CustomTag.php @@ -19,7 +19,7 @@ * * @internal For internal use by RichText package */ -final class CustomTag +final class CustomTag implements CustomTemplateConfigMapper { /** @var array */ private $customTagsConfiguration; diff --git a/src/lib/Configuration/UI/Mapper/CustomTemplateConfigMapper.php b/src/lib/Configuration/UI/Mapper/CustomTemplateConfigMapper.php new file mode 100644 index 00000000..7e38d1c7 --- /dev/null +++ b/src/lib/Configuration/UI/Mapper/CustomTemplateConfigMapper.php @@ -0,0 +1,17 @@ + Date: Fri, 23 Aug 2019 00:23:00 +0200 Subject: [PATCH 21/25] Added unit test coverage for Custom Template configuration providers --- .../BaseCustomTemplateProviderTestCase.php | 61 +++++++++++++++++++ .../Provider/CustomStyleProviderTest.php | 35 +++++++++++ .../Provider/CustomTagProviderTest.php | 35 +++++++++++ 3 files changed, 131 insertions(+) create mode 100644 tests/lib/Configuration/Provider/BaseCustomTemplateProviderTestCase.php create mode 100644 tests/lib/Configuration/Provider/CustomStyleProviderTest.php create mode 100644 tests/lib/Configuration/Provider/CustomTagProviderTest.php diff --git a/tests/lib/Configuration/Provider/BaseCustomTemplateProviderTestCase.php b/tests/lib/Configuration/Provider/BaseCustomTemplateProviderTestCase.php new file mode 100644 index 00000000..b7d8c9b8 --- /dev/null +++ b/tests/lib/Configuration/Provider/BaseCustomTemplateProviderTestCase.php @@ -0,0 +1,61 @@ +mapper = $this->createMock(CustomTemplateConfigMapper::class); + } + + /** + * @covers \EzSystems\EzPlatformRichText\SPI\Configuration\Provider::getConfiguration + */ + final public function testGetConfiguration() + { + $provider = $this->createProvider(); + + $tags = $this->getExpectedCustomTemplatesConfiguration(); + + $this->configResolver + ->expects($this->once()) + ->method('hasParameter') + ->with($this->getCustomTemplateSiteAccessConfigParamName()) + ->willReturn(true); + + $this->configResolver + ->expects($this->once()) + ->method('getParameter') + ->with($this->getCustomTemplateSiteAccessConfigParamName()) + ->willReturn($tags); + + $this->mapper + ->expects($this->once()) + ->method('mapConfig') + ->with($tags) + ->willReturnArgument(0); + + self::assertEquals( + $tags, + $provider->getConfiguration() + ); + } +} diff --git a/tests/lib/Configuration/Provider/CustomStyleProviderTest.php b/tests/lib/Configuration/Provider/CustomStyleProviderTest.php new file mode 100644 index 00000000..e92055e9 --- /dev/null +++ b/tests/lib/Configuration/Provider/CustomStyleProviderTest.php @@ -0,0 +1,35 @@ +configResolver, $this->mapper); + } + + public function getExpectedProviderName(): string + { + return 'customStyles'; + } + + protected function getExpectedCustomTemplatesConfiguration(): array + { + return ['paragraph' => ['style1']]; + } + + protected function getCustomTemplateSiteAccessConfigParamName(): string + { + return 'fieldtypes.ezrichtext.custom_styles'; + } +} diff --git a/tests/lib/Configuration/Provider/CustomTagProviderTest.php b/tests/lib/Configuration/Provider/CustomTagProviderTest.php new file mode 100644 index 00000000..0afd3194 --- /dev/null +++ b/tests/lib/Configuration/Provider/CustomTagProviderTest.php @@ -0,0 +1,35 @@ +configResolver, $this->mapper); + } + + public function getExpectedProviderName(): string + { + return 'customTags'; + } + + protected function getExpectedCustomTemplatesConfiguration(): array + { + return ['tag' => ['template' => 'tag.html.twig', 'attributes' => []]]; + } + + protected function getCustomTemplateSiteAccessConfigParamName(): string + { + return 'fieldtypes.ezrichtext.custom_tags'; + } +} From 45b1cba1eab314f997869860c8ec55a65ab815bd Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 26 Aug 2019 15:01:05 +0200 Subject: [PATCH 22/25] fixup! Configured DIC for Configuration Provider Service and dependencies --- src/bundle/DependencyInjection/EzPlatformRichTextExtension.php | 2 +- src/bundle/Resources/config/configuration.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bundle/DependencyInjection/EzPlatformRichTextExtension.php b/src/bundle/DependencyInjection/EzPlatformRichTextExtension.php index 600e6551..5eac2e5e 100644 --- a/src/bundle/DependencyInjection/EzPlatformRichTextExtension.php +++ b/src/bundle/DependencyInjection/EzPlatformRichTextExtension.php @@ -26,7 +26,7 @@ class EzPlatformRichTextExtension extends Extension implements PrependExtensionI const RICHTEXT_CUSTOM_STYLES_PARAMETER = 'ezplatform.ezrichtext.custom_styles'; const RICHTEXT_CUSTOM_TAGS_PARAMETER = 'ezplatform.ezrichtext.custom_tags'; const RICHTEXT_ALLOY_EDITOR_PARAMETER = 'ezplatform.ezrichtext.alloy_editor'; - public const RICHTEXT_CONFIGURATION_PROVIDER_TAG = 'ezrichtext.configuration.provider'; + public const RICHTEXT_CONFIGURATION_PROVIDER_TAG = 'ezplatform.ezrichtext.configuration.provider'; public function getAlias() { diff --git a/src/bundle/Resources/config/configuration.yaml b/src/bundle/Resources/config/configuration.yaml index 00dcf79b..b967b197 100644 --- a/src/bundle/Resources/config/configuration.yaml +++ b/src/bundle/Resources/config/configuration.yaml @@ -28,4 +28,4 @@ services: EzSystems\EzPlatformRichText\Configuration\AggregateProvider: arguments: - $providers: !tagged ezrichtext.configuration.provider + $providers: !tagged ezplatform.ezrichtext.configuration.provider From b333cb1fdd31730e6d084370ca570f98ad593879 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 26 Aug 2019 15:01:26 +0200 Subject: [PATCH 23/25] fixup! Refactored Custom Style and Tag config mappers to use common interface --- src/bundle/Resources/config/configuration.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/bundle/Resources/config/configuration.yaml b/src/bundle/Resources/config/configuration.yaml index b967b197..5bbd7ea8 100644 --- a/src/bundle/Resources/config/configuration.yaml +++ b/src/bundle/Resources/config/configuration.yaml @@ -11,9 +11,6 @@ services: autoconfigure: true public: false - EzSystems\EzPlatformRichText\Configuration\Provider\: - resource: '../../../lib/Configuration/Provider/*' - EzSystems\EzPlatformRichText\Configuration\Provider\CustomStyle: arguments: $customStyleConfigurationMapper: '@EzSystems\EzPlatformRichText\Configuration\UI\Mapper\CustomStyle' From 7f25704a2e9319ea3dcedf0f7695d2fa1222d728 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 26 Aug 2019 15:02:04 +0200 Subject: [PATCH 24/25] fixup! fixup! Implemented Twig extension exposing RichText configuration --- .../Twig/Extension/RichTextConfigurationExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php b/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php index 0a046056..3ec79b26 100644 --- a/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php +++ b/src/bundle/Templating/Twig/Extension/RichTextConfigurationExtension.php @@ -29,7 +29,7 @@ public function __construct(Configuration\ProviderService $configurationProvider public function getName(): string { - return 'ezpublish.rich_text'; + return 'ezrichtext.configuration'; } public function getGlobals(): array From f3a1427a70578838c82b25fa7f8a437b7ad782d7 Mon Sep 17 00:00:00 2001 From: Dariusz Szut Date: Tue, 27 Aug 2019 09:11:54 +0200 Subject: [PATCH 25/25] Moved alloyEditor config to richText namespace --- .../js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js index 79a8289c..2de66aa0 100644 --- a/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js +++ b/src/bundle/Resources/public/js/OnlineEditor/buttons/base/ez-embeddiscovercontent.js @@ -35,7 +35,7 @@ export default class EzEmbedDiscoverContentButton extends EzWidgetButton { const siteaccess = document.querySelector('meta[name="SiteAccess"]').content; const languageCode = document.querySelector('meta[name="LanguageCode"]').content; const config = JSON.parse(document.querySelector(`[data-udw-config-name="${udwConfigName}"]`).dataset.udwConfig); - const selectContent = eZ.alloyEditor.callbacks.selectContent; + const selectContent = eZ.richText.alloyEditor.callbacks.selectContent; const mergedConfig = Object.assign( { onConfirm: this.confirmHandler,