Merge branch 'feature/editorjs' into develop

This commit is contained in:
Simon Vieille 2022-03-22 11:06:19 +01:00
commit 2ad1d86b76
19 changed files with 589 additions and 1 deletions

View file

@ -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')

View file

@ -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;
}
}
}

View file

@ -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')()

View 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

View 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()
})
}

View 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 -%}

View file

@ -0,0 +1,3 @@
{%- block render -%}
<pre {% block attributes %}{% endblock %}><code>{{ code }}</code></pre>
{%- endblock -%}

View file

@ -0,0 +1,3 @@
{%- block render -%}
<hr {% block attributes %}{% endblock %}>
{%- endblock -%}

View file

@ -0,0 +1,3 @@
{%- block render -%}
<h{{ level }} {% block attributes %}{% endblock %}>{{- text -}}</h{{ level }}>
{%- endblock -%}

View file

@ -0,0 +1,3 @@
{%- block render -%}
<img src="{{ source }}" alt="{{ caption }}">
{%- endblock -%}

View 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 -%}

View 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 -%}

View file

@ -0,0 +1,3 @@
{%- block render -%}
<p {% block attributes %}{% endblock %}>{{- text -}}</p>
{%- endblock -%}

View file

@ -0,0 +1,11 @@
{%- block render -%}
<figure {% block attributes %}{% endblock %}>
{{- text -}}
{% if caption %}
<figcaption>
{{- caption -}}
</figcaption>
{% endif %}
</figure>
{%- endblock -%}

View file

@ -0,0 +1,3 @@
{%- block render -%}
{{- html|raw -}}
{%- endblock -%}

View 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 %}

View file

@ -0,0 +1,9 @@
{%- block render -%}
<div {% block attributes %}{% endblock %}>
{%- block title -%}
<div>{{- title -}}</div>
{%- endblock -%}
{{- message -}}
</figure>
{%- endblock -%}

View 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;
}
}