add editorjs editor
This commit is contained in:
parent
d396eafde6
commit
9d72d2c0c0
|
@ -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')
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')()
|
||||
|
|
193
src/core/Resources/assets/js/components/editorjs/image-tool.js
Normal file
193
src/core/Resources/assets/js/components/editorjs/image-tool.js
Normal file
|
@ -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 = $('<div id="fm-modal" class="modal">')
|
||||
|
||||
body.append(container)
|
||||
}
|
||||
|
||||
container.html(`
|
||||
<div class="modal-dialog modal-dialog-large">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div id="fm-modal-content">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
$(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: '<FileManager context="tinymce" />',
|
||||
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: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M3.15 13.628A7.749 7.749 0 0 0 10 17.75a7.74 7.74 0 0 0 6.305-3.242l-2.387-2.127-2.765 2.244-4.389-4.496-3.614 3.5zm-.787-2.303l4.446-4.371 4.52 4.63 2.534-2.057 3.533 2.797c.23-.734.354-1.514.354-2.324a7.75 7.75 0 1 0-15.387 1.325zM10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10z"/></svg>',
|
||||
title: 'Image'
|
||||
}
|
||||
}
|
||||
|
||||
static get pasteConfig () {
|
||||
return {
|
||||
tags: ['img']
|
||||
}
|
||||
}
|
||||
|
||||
static get sanitize () {
|
||||
return {
|
||||
source: false,
|
||||
caption: false
|
||||
}
|
||||
}
|
||||
|
||||
tabHandler (event) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ImageTool
|
165
src/core/Resources/assets/js/modules/editorjs.js
Normal file
165
src/core/Resources/assets/js/modules/editorjs.js
Normal file
|
@ -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 = $('<div><div></div></div>')
|
||||
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()
|
||||
})
|
||||
}
|
13
src/core/Resources/views/editorjs/checkList.html.twig
Normal file
13
src/core/Resources/views/editorjs/checkList.html.twig
Normal file
|
@ -0,0 +1,13 @@
|
|||
{%- block render -%}
|
||||
<ul {% block attributes %}{% endblock %}>
|
||||
{%- for item in items -%}
|
||||
{%- block item -%}
|
||||
<li>
|
||||
<input type="checkbox" {% if item.checked %}checked{% endif %} disabled>
|
||||
|
||||
{{- item.text -}}
|
||||
</li>
|
||||
{%- endblock -%}
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{%- endblock -%}
|
3
src/core/Resources/views/editorjs/code.html.twig
Normal file
3
src/core/Resources/views/editorjs/code.html.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
{%- block render -%}
|
||||
<pre {% block attributes %}{% endblock %}><code>{{ code }}</code></pre>
|
||||
{%- endblock -%}
|
3
src/core/Resources/views/editorjs/delimiter.html.twig
Normal file
3
src/core/Resources/views/editorjs/delimiter.html.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
{%- block render -%}
|
||||
<hr {% block attributes %}{% endblock %}>
|
||||
{%- endblock -%}
|
3
src/core/Resources/views/editorjs/header.html.twig
Normal file
3
src/core/Resources/views/editorjs/header.html.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
{%- block render -%}
|
||||
<h{{ level }} {% block attributes %}{% endblock %}>{{- text -}}</h{{ level }}>
|
||||
{%- endblock -%}
|
3
src/core/Resources/views/editorjs/image.html.twig
Normal file
3
src/core/Resources/views/editorjs/image.html.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
{%- block render -%}
|
||||
<img src="{{ source }}" alt="{{ caption }}">
|
||||
{%- endblock -%}
|
13
src/core/Resources/views/editorjs/list.html.twig
Normal file
13
src/core/Resources/views/editorjs/list.html.twig
Normal file
|
@ -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 %}
|
||||
<li>{{ item|raw }}</li>
|
||||
{% endfor %}
|
||||
</{{ tag }}>
|
||||
{%- endblock -%}
|
23
src/core/Resources/views/editorjs/nestedList.html.twig
Normal file
23
src/core/Resources/views/editorjs/nestedList.html.twig
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% import _self as macros %}
|
||||
|
||||
{% macro list(tag, items) %}
|
||||
<{{ tag }} {% block attributes %}{% endblock %}>
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{{ item.content|raw }}
|
||||
|
||||
{{ macros.list(tag, item.items) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</{{ tag }}>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block render -%}
|
||||
{% if style == 'ordered' %}
|
||||
{% set tag = 'ol' %}
|
||||
{% else %}
|
||||
{% set tag = 'ul' %}
|
||||
{% endif %}
|
||||
|
||||
{{ macros.list(tag, items) }}
|
||||
{%- endblock -%}
|
3
src/core/Resources/views/editorjs/paragraph.html.twig
Normal file
3
src/core/Resources/views/editorjs/paragraph.html.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
{%- block render -%}
|
||||
<p {% block attributes %}{% endblock %}>{{- text -}}</p>
|
||||
{%- endblock -%}
|
11
src/core/Resources/views/editorjs/quote.html.twig
Normal file
11
src/core/Resources/views/editorjs/quote.html.twig
Normal file
|
@ -0,0 +1,11 @@
|
|||
{%- block render -%}
|
||||
<figure {% block attributes %}{% endblock %}>
|
||||
{{- text -}}
|
||||
|
||||
{% if caption %}
|
||||
<figcaption>
|
||||
{{- caption -}}
|
||||
</figcaption>
|
||||
{% endif %}
|
||||
</figure>
|
||||
{%- endblock -%}
|
3
src/core/Resources/views/editorjs/raw.html.twig
Normal file
3
src/core/Resources/views/editorjs/raw.html.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
{%- block render -%}
|
||||
{{- html|raw -}}
|
||||
{%- endblock -%}
|
27
src/core/Resources/views/editorjs/table.html.twig
Normal file
27
src/core/Resources/views/editorjs/table.html.twig
Normal file
|
@ -0,0 +1,27 @@
|
|||
{% block render %}
|
||||
<table {% block attributes %}{% endblock %}>
|
||||
{% if withHeadings and content[0] is defined %}
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column in content[0] %}
|
||||
<th>{{- column -}}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% endif %}
|
||||
|
||||
{% if (not withHeadings and content[0] is defined) or (withHeadings and content[1] is defined) %}
|
||||
<tbody>
|
||||
{% for line in content %}
|
||||
{% if (withHeadings and not loop.first) or not withHeadings %}
|
||||
<tr>
|
||||
{% for column in line %}
|
||||
<td>{{- column -}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
9
src/core/Resources/views/editorjs/warning.html.twig
Normal file
9
src/core/Resources/views/editorjs/warning.html.twig
Normal file
|
@ -0,0 +1,9 @@
|
|||
{%- block render -%}
|
||||
<div {% block attributes %}{% endblock %}>
|
||||
{%- block title -%}
|
||||
<div>{{- title -}}</div>
|
||||
{%- endblock -%}
|
||||
|
||||
{{- message -}}
|
||||
</figure>
|
||||
{%- endblock -%}
|
91
src/core/Twig/Extension/EditorJsExtension.php
Normal file
91
src/core/Twig/Extension/EditorJsExtension.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Twig\Extension;
|
||||
|
||||
use App\Core\String\StringBuilder;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\Environment;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
|
||||
class EditorJsExtension extends AbstractExtension
|
||||
{
|
||||
protected Environment $twig;
|
||||
protected array $views = [];
|
||||
protected array $defaultAllowedBlocks = [
|
||||
'paragraph',
|
||||
'header',
|
||||
'quote',
|
||||
'delimiter',
|
||||
'warning',
|
||||
'list',
|
||||
'nestedList',
|
||||
'checkList',
|
||||
'table',
|
||||
'code',
|
||||
'raw',
|
||||
'image',
|
||||
];
|
||||
|
||||
public function __construct(Environment $twig, ParameterBagInterface $params)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue