9 changed files with 351 additions and 6 deletions
@ -0,0 +1,39 @@ |
|||||||
|
import {Controller} from '@hotwired/stimulus'; |
||||||
|
import Quill from 'quill'; |
||||||
|
|
||||||
|
import('quill/dist/quill.core.css'); |
||||||
|
import('quill/dist/quill.snow.css'); |
||||||
|
|
||||||
|
|
||||||
|
export default class extends Controller { |
||||||
|
|
||||||
|
connect() { |
||||||
|
const toolbarOptions = [ |
||||||
|
['bold', 'italic', 'underline', 'strike'], |
||||||
|
['link', 'blockquote', 'code-block', 'image'], |
||||||
|
[{ 'header': 1 }, { 'header': 2 }], |
||||||
|
[{ list: 'ordered' }, { list: 'bullet' }], |
||||||
|
]; |
||||||
|
|
||||||
|
const options = { |
||||||
|
theme: 'snow', |
||||||
|
modules: { |
||||||
|
toolbar: toolbarOptions, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let quill = new Quill('#editor', options); |
||||||
|
let target = document.querySelector('#editor_content'); |
||||||
|
|
||||||
|
|
||||||
|
quill.on('text-change', function(delta, oldDelta, source) { |
||||||
|
console.log('Text change!'); |
||||||
|
console.log(delta); |
||||||
|
console.log(oldDelta); |
||||||
|
console.log(source); |
||||||
|
// save as html
|
||||||
|
target.value = quill.root.innerHTML; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
{ |
||||||
|
"name": "newsroom", |
||||||
|
"version": "1.0.0", |
||||||
|
"description": "", |
||||||
|
"main": "index.js", |
||||||
|
"scripts": { |
||||||
|
"test": "echo \"Error: no test specified\" && exit 1" |
||||||
|
}, |
||||||
|
"repository": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/decent-newsroom/newsroom.git" |
||||||
|
}, |
||||||
|
"private": true, |
||||||
|
"dependencies": { |
||||||
|
"quill": "^2.0.3" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace App\Form\DataTransformer; |
||||||
|
|
||||||
|
use Symfony\Component\Form\DataTransformerInterface; |
||||||
|
|
||||||
|
class CommaSeparatedToJsonTransformer implements DataTransformerInterface |
||||||
|
{ |
||||||
|
|
||||||
|
/** |
||||||
|
* Transforms an array to a comma-separated string. |
||||||
|
* @inheritDoc |
||||||
|
*/ |
||||||
|
public function transform(mixed $value): mixed |
||||||
|
{ |
||||||
|
if ($value === null) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
$array = json_decode($value, true); |
||||||
|
|
||||||
|
return implode(',', $array); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Transforms a comma-separated string to an array. |
||||||
|
* @inheritDoc |
||||||
|
*/ |
||||||
|
public function reverseTransform(mixed $value): mixed |
||||||
|
{ |
||||||
|
if (!$value) { |
||||||
|
return json_encode([]); |
||||||
|
} |
||||||
|
|
||||||
|
$array = array_map('trim', explode(',', $value)); |
||||||
|
|
||||||
|
return json_encode($array); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace App\Form\DataTransformer; |
||||||
|
|
||||||
|
use League\HTMLToMarkdown\HtmlConverter; |
||||||
|
use Symfony\Component\Form\DataTransformerInterface; |
||||||
|
use Symfony\Component\Form\Exception\TransformationFailedException; |
||||||
|
|
||||||
|
class HtmlToMdTransformer implements DataTransformerInterface |
||||||
|
{ |
||||||
|
|
||||||
|
private $converter; |
||||||
|
|
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
$this->converter = new HtmlConverter(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Transforms Markdown into HTML (for displaying in the form). |
||||||
|
* @inheritDoc |
||||||
|
*/ |
||||||
|
public function transform(mixed $value): mixed |
||||||
|
{ |
||||||
|
dump($value); |
||||||
|
if ($value === null) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
// Optional: You can add a markdown-to-html conversion if needed |
||||||
|
return $value; // You could return rendered markdown here. |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Transforms a HTML string to Markdown. |
||||||
|
* @inheritDoc |
||||||
|
*/ |
||||||
|
public function reverseTransform(mixed $value): mixed |
||||||
|
{ |
||||||
|
dump($value); |
||||||
|
if (!$value) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
// Convert HTML to Markdown |
||||||
|
return $this->converter->convert($value); |
||||||
|
} catch (\Exception $e) { |
||||||
|
throw new TransformationFailedException('Failed to convert HTML to Markdown'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,76 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Form; |
||||||
|
|
||||||
|
use App\Entity\Article; |
||||||
|
use App\Form\DataTransformer\CommaSeparatedToJsonTransformer; |
||||||
|
use App\Form\DataTransformer\HtmlToMdTransformer; |
||||||
|
use App\Form\Type\QuillType; |
||||||
|
use Symfony\Component\Form\AbstractType; |
||||||
|
use Symfony\Component\Form\Extension\Core\Type\FormType; |
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType; |
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType; |
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType; |
||||||
|
use Symfony\Component\Form\Extension\Core\Type\UrlType; |
||||||
|
use Symfony\Component\Form\FormBuilderInterface; |
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver; |
||||||
|
|
||||||
|
class EditorType extends AbstractType |
||||||
|
{ |
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void |
||||||
|
{ |
||||||
|
// create a form with a title field, a QuillType content field and a submit button |
||||||
|
$builder |
||||||
|
->add('title', TextType::class, [ |
||||||
|
'required' => false, |
||||||
|
'sanitize_html' => true, |
||||||
|
'attr' => ['placeholder' => 'Enter title', 'class' => 'form-control']]) |
||||||
|
->add('summary', TextareaType::class, [ |
||||||
|
'required' => false, |
||||||
|
'sanitize_html' => true, |
||||||
|
'attr' => ['placeholder' => 'Enter summary', 'class' => 'form-control']]) |
||||||
|
->add('content', QuillType::class, [ |
||||||
|
'required' => false, |
||||||
|
'attr' => ['placeholder' => 'Enter content', 'class' => 'form-control']]) |
||||||
|
->add('imgUrl', UrlType::class, [ |
||||||
|
'required' => false, |
||||||
|
'label' => 'Image URL', |
||||||
|
'attr' => ['placeholder' => 'Enter image URL', 'class' => 'form-control']]) |
||||||
|
->add('topics', TextType::class, [ |
||||||
|
'required' => false, |
||||||
|
'sanitize_html' => true, |
||||||
|
'help' => 'Separate tags with commas', |
||||||
|
'attr' => ['placeholder' => 'Enter tags', 'class' => 'form-control']]) |
||||||
|
->add( |
||||||
|
$builder->create('actions', FormType::class, |
||||||
|
['row_attr' => ['class' => 'actions'], 'label' => false, 'mapped' => false]) |
||||||
|
->add('submit', SubmitType::class, [ |
||||||
|
'label' => 'Submit', |
||||||
|
'attr' => ['class' => 'btn btn-primary']]) |
||||||
|
->add('draft', SubmitType::class, [ |
||||||
|
'label' => 'Save as draft', |
||||||
|
'attr' => ['class' => 'btn btn-secondary']]) |
||||||
|
->add('preview', SubmitType::class, [ |
||||||
|
'label' => 'Preview', |
||||||
|
'attr' => ['class' => 'btn btn-secondary']]) |
||||||
|
); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Apply the custom transformer |
||||||
|
$builder->get('topics') |
||||||
|
->addModelTransformer(new CommaSeparatedToJsonTransformer()); |
||||||
|
$builder->get('content') |
||||||
|
->addModelTransformer(new HtmlToMdTransformer()); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void |
||||||
|
{ |
||||||
|
$resolver->setDefaults([ |
||||||
|
'data_class' => Article::class, |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace App\Form\Type; |
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType; |
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType; |
||||||
|
use Symfony\Component\Form\FormInterface; |
||||||
|
use Symfony\Component\Form\FormView; |
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Nicolas Assing <nicolas.assing@gmail.com> |
||||||
|
*/ |
||||||
|
class QuillType extends AbstractType |
||||||
|
{ |
||||||
|
|
||||||
|
public function buildView(FormView $view, FormInterface $form, array $options): void |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void |
||||||
|
{ |
||||||
|
$resolver->setDefaults([]); |
||||||
|
} |
||||||
|
|
||||||
|
public function getParent(): string |
||||||
|
{ |
||||||
|
return TextareaType::class; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue