diff --git a/src/core/DependencyInjection/Configuration.php b/src/core/DependencyInjection/Configuration.php
index 9d6b419..6999049 100644
--- a/src/core/DependencyInjection/Configuration.php
+++ b/src/core/DependencyInjection/Configuration.php
@@ -109,6 +109,14 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->end()
+ ->arrayNode('editor_js')
+ ->children()
+ ->arrayNode('blocks')
+ ->scalarPrototype()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
->arrayNode('file_manager')
->children()
->arrayNode('mimes')
diff --git a/src/core/Resources/assets/css/admin.scss b/src/core/Resources/assets/css/admin.scss
index a054ff8..bf8a81f 100644
--- a/src/core/Resources/assets/css/admin.scss
+++ b/src/core/Resources/assets/css/admin.scss
@@ -14,6 +14,7 @@ $pagination-active-bg: #343a40 !default;
@import "~bootstrap/scss/bootstrap.scss";
@import "~@fortawesome/fontawesome-free/css/all.css";
@import "~flag-icon-css/sass/flag-icon.scss";
+@import "~flag-icon-css/sass/flag-icon.scss";
@for $i from 1 through 100 {
.miw-#{$i*5} {
@@ -559,3 +560,18 @@ form {
color: #333;
}
}
+
+.editorjs {
+ @extend .form-control;
+
+ height: auto;
+ padding-left: 50px;
+ padding-right: 50px;
+
+ &-block-image {
+ img {
+ max-width: 100%;
+ margin-bottom: 10px;
+ }
+ }
+}
diff --git a/src/core/Resources/assets/js/admin.js b/src/core/Resources/assets/js/admin.js
index ff1cdc0..04c7c27 100644
--- a/src/core/Resources/assets/js/admin.js
+++ b/src/core/Resources/assets/js/admin.js
@@ -12,7 +12,8 @@ require('./modules/modal.js')()
require('./modules/push-state.js')()
require('./modules/password.js')()
require('./modules/tooltip.js')()
-require('./modules/editor.js')()
+require('./modules/tinymce.js')()
+require('./modules/editorjs.js')()
require('./modules/panel.js')()
require('./modules/choices.js')()
require('./modules/checkbox-checker.js')()
diff --git a/src/core/Resources/assets/js/components/editorjs/image-tool.js b/src/core/Resources/assets/js/components/editorjs/image-tool.js
new file mode 100644
index 0000000..e4800a9
--- /dev/null
+++ b/src/core/Resources/assets/js/components/editorjs/image-tool.js
@@ -0,0 +1,193 @@
+const $ = require('jquery')
+const Vue = require('vue').default
+const FileManager = require('../file-manager/FileManager').default
+
+const createModal = function () {
+ let container = $('#fm-modal')
+ const body = $('body')
+
+ if (!container.length) {
+ container = $('
')
+
+ body.append(container)
+ }
+
+ container.html(`
+
+`)
+
+ $(container).modal('show')
+
+ return $(container)
+}
+
+const fileManagerBrowser = function (callback) {
+ const container = createModal()
+
+ const clickCallback = (e) => {
+ callback($(e.target).attr('data-value'), {})
+ $('div[id^="modal-container-"]').modal('hide')
+ container.modal('hide')
+
+ $('body').off('click', '#file-manager-insert', clickCallback)
+ }
+
+ $('body').on('click', '#file-manager-insert', clickCallback)
+
+ return new Vue({
+ el: '#fm-modal-content',
+ template: '
',
+ components: {
+ FileManager
+ }
+ })
+}
+
+class ImageTool {
+ static get isReadOnlySupported () {
+ return true
+ }
+
+ static get enableLineBreaks () {
+ return false
+ }
+
+ constructor ({ data, config, api, readOnly }) {
+ this.api = api
+ this.readOnly = readOnly
+
+ this.nodes = {
+ holder: null,
+ image: null
+ }
+
+ this.data = {
+ source: data.source || '',
+ caption: data.caption || ''
+ }
+
+ this.nodes.holder = this.drawView()
+ }
+
+ render () {
+ return this.nodes.holder
+ }
+
+ save (wrapper) {
+ const inputs = wrapper.querySelectorAll('input')
+
+ return {
+ source: inputs[0].value,
+ caption: inputs[1].value
+ }
+ }
+
+ drawView () {
+ const wrapper = document.createElement('div')
+ const image = document.createElement('img')
+
+ const opener = document.createElement('span')
+
+ const inputSource = document.createElement('input')
+ const inputCaption = document.createElement('input')
+
+ const that = this
+
+ opener.classList.add('btn', 'btn-sm', 'btn-primary', 'ml-3')
+ opener.innerHTML = ImageTool.toolbox.icon
+
+ inputSource.classList.add('cdx-input')
+ inputCaption.classList.add('cdx-input', 'mt-3')
+ inputCaption.setAttribute('placeholder', 'Caption')
+
+ inputSource.style.width = 'calc(100% - 70px)'
+
+ if (this.data.source) {
+ image.src = this.data.source
+ } else {
+ image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgD9b9DwACcwGHMIH3GgAAAABJRU5ErkJggg=='
+ }
+
+ inputSource.value = this.data.source
+ inputCaption.value = this.data.caption
+
+ wrapper.classList.add('editorjs-block-image')
+ wrapper.appendChild(image)
+ wrapper.appendChild(inputSource)
+ wrapper.appendChild(opener)
+ wrapper.appendChild(inputCaption)
+
+ opener.addEventListener('click', () => {
+ fileManagerBrowser((src) => {
+ inputSource.value = src
+ image.src = src
+ })
+ })
+
+ inputSource.addEventListener('change', () => {
+ that.data.source = inputSource.value
+
+ if (that.data.source) {
+ image.src = that.data.source
+ }
+ })
+
+ inputCaption.addEventListener('change', () => {
+ that.data.caption = inputCaption.value
+ })
+
+ this.nodes.image = image
+
+ return wrapper
+ }
+
+ onPaste (event) {
+ }
+
+ get data () {
+ return this._data
+ }
+
+ set data (data) {
+ this._data = data
+
+ if (this.nodes.image) {
+ this.nodes.image.src = data.source
+ }
+ }
+
+ static get toolbox () {
+ return {
+ icon: '
',
+ title: 'Image'
+ }
+ }
+
+ static get pasteConfig () {
+ return {
+ tags: ['img']
+ }
+ }
+
+ static get sanitize () {
+ return {
+ source: false,
+ caption: false
+ }
+ }
+
+ tabHandler (event) {
+ }
+}
+
+module.exports = ImageTool
diff --git a/src/core/Resources/assets/js/modules/editorjs.js b/src/core/Resources/assets/js/modules/editorjs.js
new file mode 100644
index 0000000..5587d6f
--- /dev/null
+++ b/src/core/Resources/assets/js/modules/editorjs.js
@@ -0,0 +1,165 @@
+const $ = require('jquery')
+const EditorJS = require('@editorjs/editorjs')
+const InlineTools = require('editorjs-inline-tool')
+
+const UnderlineInlineTool = InlineTools.UnderlineInlineTool
+const StrongInlineTool = InlineTools.StrongInlineTool
+const ItalicInlineTool = InlineTools.ItalicInlineTool
+
+const tools = {
+ header: {
+ class: require('@editorjs/header'),
+ inlineToolbar: true
+ },
+ paragraph: {
+ class: require('@editorjs/paragraph'),
+ inlineToolbar: true
+ },
+ quote: {
+ class: require('@editorjs/quote'),
+ inlineToolbar: true
+ },
+ delimiter: {
+ class: require('@editorjs/delimiter'),
+ inlineToolbar: true
+ },
+ warning: {
+ class: require('@editorjs/warning'),
+ inlineToolbar: true
+ },
+ list: {
+ class: require('@editorjs/list'),
+ inlineToolbar: true
+ },
+ nestedList: {
+ class: require('@editorjs/nested-list'),
+ inlineToolbar: true
+ },
+ checkList: {
+ class: require('@editorjs/checklist'),
+ inlineToolbar: true
+ },
+ link: {
+ class: require('@editorjs/link'),
+ inlineToolbar: true
+ },
+ table: {
+ class: require('@editorjs/table'),
+ inlineToolbar: true
+ },
+ code: {
+ class: require('@editorjs/code'),
+ inlineToolbar: true
+ },
+ raw: {
+ class: require('@editorjs/raw'),
+ inlineToolbar: true
+ },
+ marker: {
+ class: require('@editorjs/marker'),
+ inlineToolbar: true
+ },
+ inlineCode: {
+ class: require('@editorjs/inline-code'),
+ inlineToolbar: true
+ },
+ underline: {
+ class: require('@editorjs/underline'),
+ inlineToolbar: true
+ },
+ linkAutocomplete: {
+ class: require('@editorjs/link-autocomplete'),
+ inlineToolbar: true
+ },
+ image: {
+ class: require('../components/editorjs/image-tool.js')
+ }
+}
+
+const makeId = () => {
+ let result = ''
+ const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'
+ const charactersLength = characters.length
+
+ for (let i = 0; i < 20; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength))
+ }
+
+ return 'editorjs-' + result
+}
+
+const configurationBase = {
+ tools,
+ bold: StrongInlineTool,
+ italic: ItalicInlineTool,
+ underline: UnderlineInlineTool
+}
+
+const buildConfiguration = (conf) => {
+ return Object.assign({}, configurationBase, conf)
+}
+
+const doInitEditor = () => {
+ $('textarea[data-editorjs]').each((i, v) => {
+ const element = $(v)
+
+ let editorWrapper = element.next()
+ let ready = false
+ let saveTimer = null
+
+ if (!editorWrapper || !editorWrapper.is('.editorjs')) {
+ editorWrapper = $('
')
+ editorWrapper
+ .addClass('editorjs')
+ .insertAfter(element)
+ }
+
+ const editorContainer = editorWrapper.children()
+ const id = editorContainer.attr('id') ? editorContainer.attr('id') : makeId()
+
+ editorContainer.attr('id', id)
+ element.hide()
+
+ const editor = new EditorJS(buildConfiguration({
+ holder: id,
+ data: JSON.parse(element.val()),
+ onReady: () => {
+ ready = true
+ }
+ }))
+
+ const save = () => {
+ if (!ready) {
+ return
+ }
+
+ if (saveTimer) {
+ clearTimeout(saveTimer)
+ }
+
+ saveTimer = setTimeout(() => {
+ editor.save().then((data) => {
+ try {
+ const value = JSON.stringify(data)
+ element.val(value)
+ } catch (e) {
+ }
+ })
+ }, 500)
+ }
+
+ const observer = new MutationObserver(save)
+
+ observer.observe(editorWrapper.get(0), {
+ attributes: true,
+ childList: true,
+ subtree: true
+ })
+ })
+}
+
+module.exports = () => {
+ $(() => {
+ doInitEditor()
+ })
+}
diff --git a/src/core/Resources/views/editorjs/checkList.html.twig b/src/core/Resources/views/editorjs/checkList.html.twig
new file mode 100644
index 0000000..9f7aecc
--- /dev/null
+++ b/src/core/Resources/views/editorjs/checkList.html.twig
@@ -0,0 +1,13 @@
+{%- block render -%}
+
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/code.html.twig b/src/core/Resources/views/editorjs/code.html.twig
new file mode 100644
index 0000000..0cb74b1
--- /dev/null
+++ b/src/core/Resources/views/editorjs/code.html.twig
@@ -0,0 +1,3 @@
+{%- block render -%}
+
{{ code }}
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/delimiter.html.twig b/src/core/Resources/views/editorjs/delimiter.html.twig
new file mode 100644
index 0000000..9698754
--- /dev/null
+++ b/src/core/Resources/views/editorjs/delimiter.html.twig
@@ -0,0 +1,3 @@
+{%- block render -%}
+
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/header.html.twig b/src/core/Resources/views/editorjs/header.html.twig
new file mode 100644
index 0000000..a67418a
--- /dev/null
+++ b/src/core/Resources/views/editorjs/header.html.twig
@@ -0,0 +1,3 @@
+{%- block render -%}
+
{{- text -}}
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/image.html.twig b/src/core/Resources/views/editorjs/image.html.twig
new file mode 100644
index 0000000..e60d86f
--- /dev/null
+++ b/src/core/Resources/views/editorjs/image.html.twig
@@ -0,0 +1,3 @@
+{%- block render -%}
+
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/list.html.twig b/src/core/Resources/views/editorjs/list.html.twig
new file mode 100644
index 0000000..a2a4b26
--- /dev/null
+++ b/src/core/Resources/views/editorjs/list.html.twig
@@ -0,0 +1,13 @@
+{%- block render -%}
+ {% if style == 'ordered' %}
+ {% set tag = 'ol' %}
+ {% else %}
+ {% set tag = 'ul' %}
+ {% endif %}
+
+ <{{ tag }} {% block attributes %}{% endblock %}>
+ {% for item in items %}
+
{{ item|raw }}
+ {% endfor %}
+ {{ tag }}>
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/nestedList.html.twig b/src/core/Resources/views/editorjs/nestedList.html.twig
new file mode 100644
index 0000000..332f411
--- /dev/null
+++ b/src/core/Resources/views/editorjs/nestedList.html.twig
@@ -0,0 +1,23 @@
+{% import _self as macros %}
+
+{% macro list(tag, items) %}
+ <{{ tag }} {% block attributes %}{% endblock %}>
+ {% for item in items %}
+
+ {{ item.content|raw }}
+
+ {{ macros.list(tag, item.items) }}
+
+ {% endfor %}
+ {{ tag }}>
+{% endmacro %}
+
+{%- block render -%}
+ {% if style == 'ordered' %}
+ {% set tag = 'ol' %}
+ {% else %}
+ {% set tag = 'ul' %}
+ {% endif %}
+
+ {{ macros.list(tag, items) }}
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/paragraph.html.twig b/src/core/Resources/views/editorjs/paragraph.html.twig
new file mode 100644
index 0000000..b5ae037
--- /dev/null
+++ b/src/core/Resources/views/editorjs/paragraph.html.twig
@@ -0,0 +1,3 @@
+{%- block render -%}
+
{{- text -}}
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/quote.html.twig b/src/core/Resources/views/editorjs/quote.html.twig
new file mode 100644
index 0000000..dc8d4ec
--- /dev/null
+++ b/src/core/Resources/views/editorjs/quote.html.twig
@@ -0,0 +1,11 @@
+{%- block render -%}
+
+ {{- text -}}
+
+ {% if caption %}
+
+ {{- caption -}}
+
+ {% endif %}
+
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/raw.html.twig b/src/core/Resources/views/editorjs/raw.html.twig
new file mode 100644
index 0000000..b65ded9
--- /dev/null
+++ b/src/core/Resources/views/editorjs/raw.html.twig
@@ -0,0 +1,3 @@
+{%- block render -%}
+ {{- html|raw -}}
+{%- endblock -%}
diff --git a/src/core/Resources/views/editorjs/table.html.twig b/src/core/Resources/views/editorjs/table.html.twig
new file mode 100644
index 0000000..de5ede8
--- /dev/null
+++ b/src/core/Resources/views/editorjs/table.html.twig
@@ -0,0 +1,27 @@
+{% block render %}
+
+ {% if withHeadings and content[0] is defined %}
+
+
+ {% for column in content[0] %}
+ {{- column -}}
+ {% endfor %}
+
+
+ {% endif %}
+
+ {% if (not withHeadings and content[0] is defined) or (withHeadings and content[1] is defined) %}
+
+ {% for line in content %}
+ {% if (withHeadings and not loop.first) or not withHeadings %}
+
+ {% for column in line %}
+ {{- column -}}
+ {% endfor %}
+
+ {% endif %}
+ {% endfor %}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/src/core/Resources/views/editorjs/warning.html.twig b/src/core/Resources/views/editorjs/warning.html.twig
new file mode 100644
index 0000000..17fe50a
--- /dev/null
+++ b/src/core/Resources/views/editorjs/warning.html.twig
@@ -0,0 +1,9 @@
+{%- block render -%}
+
+ {%- block title -%}
+
{{- title -}}
+ {%- endblock -%}
+
+ {{- message -}}
+
+{%- endblock -%}
diff --git a/src/core/Twig/Extension/EditorJsExtension.php b/src/core/Twig/Extension/EditorJsExtension.php
new file mode 100644
index 0000000..d94a12b
--- /dev/null
+++ b/src/core/Twig/Extension/EditorJsExtension.php
@@ -0,0 +1,91 @@
+twig = $twig;
+ $blocks = $params->get('core')['editor_js']['blocks'] ?? [];
+
+ foreach ($blocks as $block => $view) {
+ $this->views[$block] = $view;
+
+ if (!in_array($block, $this->defaultAllowedBlocks)) {
+ $this->defaultAllowedBlocks[] = $block;
+ }
+ }
+
+ foreach ($this->defaultAllowedBlocks as $block) {
+ if (!isset($this->views[$block])) {
+ $this->views[$block] = sprintf('@Core/editorjs/%s.html.twig', $block);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilters()
+ {
+ return [
+ new TwigFilter('editorjs_to_html', [$this, 'buildHtml']),
+ ];
+ }
+
+ public function buildHtml(string|array|object $data, ?array $allowedBlocks = null): string
+ {
+ if (is_string($data)) {
+ $data = json_decode($data);
+ }
+
+ if (is_object($data)) {
+ $data = json_decode(json_encode($data), true);
+ }
+
+ if ($data === null) {
+ return '';
+ }
+
+ if ($allowedBlocks === null) {
+ $allowedBlocks = $this->defaultAllowedBlocks;
+ }
+
+ $blocks = $data['blocks'] ?? [];
+ $renders = '';
+
+ $blocks = array_filter($data['blocks'] ?? [], function($block) use ($allowedBlocks) {
+ return isset($block['type']) && in_array($block['type'], $allowedBlocks);
+ });
+
+ foreach ($blocks as $block) {
+ $renders.= $this->twig->render($this->views[$block['type']], $block['data'] ?? []);
+ }
+
+ return $renders;
+ }
+}