Skip to content

Commit d32670c

Browse files
committed
Fix wysiwyg component binding for better consistency
1 parent d6c5ca4 commit d32670c

8 files changed

Lines changed: 75 additions & 27 deletions

File tree

app/Livewire/Posts/PostFormComponent.php

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
namespace App\Livewire\Posts;
66

77
use App\Dtos\PostDto;
8-
use App\Enums\LivewireEventEnum;
98
use App\Enums\PostStateEnum;
109
use App\Http\Requests\Post\StorePostRequest;
1110
use App\Services\PostService;
1211
use App\Traits\LoggingTrait;
1312
use App\Traits\SubsiteTrait;
1413
use Illuminate\Contracts\View\View;
15-
use Livewire\Attributes\On;
14+
use Livewire\Attributes\Locked;
1615
use Livewire\Component;
1716

1817
final class PostFormComponent extends Component
@@ -24,7 +23,11 @@ final class PostFormComponent extends Component
2423
public string $body = '';
2524
public string $more_inside = '';
2625
public string $tags = '';
26+
27+
#[Locked]
2728
public int $subsiteId = 0;
29+
30+
#[Locked]
2831
public int $userId = 0;
2932

3033
private PostService $postService;
@@ -49,17 +52,6 @@ public function render(): View
4952
return view('livewire.posts.post-form-component');
5053
}
5154

52-
#[On(LivewireEventEnum::EditorUpdated->value)]
53-
public function saveEditorContent($editorId, $content): void
54-
{
55-
if ($editorId === 'post-body') {
56-
$this->body = $content;
57-
}
58-
if ($editorId === 'more-inside') {
59-
$this->more_inside = $content;
60-
}
61-
}
62-
6355
public function store(): void
6456
{
6557
$this->validate();

app/Livewire/Wysiwyg/WysiwygComponent.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@
66

77
use App\Enums\LivewireEventEnum;
88
use Illuminate\Contracts\View\View;
9+
use Livewire\Attributes\Locked;
10+
use Livewire\Attributes\Modelable;
911
use Livewire\Component;
1012

1113
final class WysiwygComponent extends Component
1214
{
15+
#[Modelable]
1316
public string $content = '';
17+
18+
#[Locked]
1419
public string $editorId;
20+
21+
#[Locked]
1522
public string $label;
23+
24+
#[Locked]
1625
public string $name;
1726

1827
public function mount(string $editorId, string $content = '', string $label = '', string $name = ''): void

app/Services/PostService.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ public function store(PostDto $dto): ?Post
3434
'more_inside' => $this->purifierService->clean($dto->more_inside),
3535
'user_id' => $dto->user_id,
3636
'subsite_id' => $dto->subsite_id,
37-
'state' => $dto->state,
3837
'published_at' => $dto->published_at,
3938
'is_published' => $dto->is_published,
4039
];

resources/js/wysiwyg.js

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,76 @@ const cleanupKey = Symbol('cleanup');
44
// Set up a single textarea with CKEditor
55
async function initializeEditor(textarea, component) {
66
try {
7-
const editorConfig = JSON.parse(document.querySelector('meta[name="ckeditor-config"]')?.content || 'null') || {}
7+
let globalEditorConfig = null;
8+
try {
9+
globalEditorConfig = JSON.parse(document.querySelector('meta[name="ckeditor-config"]')?.content || 'null');
10+
} catch (e) {
11+
// Ignore
12+
console.debug('Unable to parse CKEditor config from meta tag', e);
13+
}
14+
15+
let localEditorConfig = null;
16+
try {
17+
localEditorConfig = JSON.parse(textarea.dataset.editorConfig || 'null');
18+
} catch (e) {
19+
// Ignore
20+
console.debug('Unable to parse CKEditor config from textarea data-editor-config', e);
21+
}
22+
23+
const editorConfig = {
24+
...(globalEditorConfig || {}),
25+
...(localEditorConfig || {}),
26+
toolbar: {
27+
items: [
28+
'bold', 'italic', 'link',
29+
'bulletedList', 'numberedList', 'blockQuote', '|',
30+
'heading', 'insertTable', 'mediaEmbed', '|',
31+
'undo', 'redo'
32+
],
33+
// Collapse into three dots menu when the toolbar is full.
34+
shouldNotGroupWhenFull: false
35+
}
36+
}
37+
838
const editor = await ClassicEditor.create(textarea, editorConfig);
939
const editorId = textarea.dataset.editorId;
1040

11-
// Listen for changes to the editor content and update the component's content property
12-
editor.model.document.on('change:data', () => {
13-
component.$wire.$set('content', editor.getData());
41+
// Listen for changes to the editor content and update the component's content property.
42+
const sync = () => {
43+
const content = editor.getData();
44+
textarea.value = content;
45+
component.$wire.$set('content', content, false);
46+
};
47+
48+
editor.model.document.on('change:data', sync);
49+
50+
// Also sync when the editor loses focus.
51+
editor.ui.focusTracker.on('change:isFocused', ( _event, _name, isFocused ) => {
52+
if (!isFocused) sync();
1453
});
1554

1655
// Reset the editor content when we receive a notification from the backend.
17-
const onEditorClear = (event) => {
56+
const clearEditor = (event) => {
1857
if (event.detail.editorId === editorId) {
1958
editor.setData('');
59+
textarea.value = '';
2060
}
2161
};
2262

23-
document.addEventListener('editor:clear', onEditorClear);
63+
document.addEventListener('editor:clear', clearEditor);
2464

2565
// Update the component's content property when the force sync event is received
26-
const onForceSync = () => {
27-
component.$wire.$set('content', editor.getData());
66+
const syncAndFlush = () => {
67+
const content = editor.getData();
68+
textarea.value = content;
69+
component.$wire.$set('content', content);
2870
};
2971

30-
document.addEventListener('livewire:force-sync', onForceSync);
72+
document.addEventListener('livewire:force-sync', syncAndFlush);
3173

3274
textarea[cleanupKey] = () => {
33-
document.removeEventListener('editor:clear', onEditorClear);
34-
document.removeEventListener('livewire:force-sync', onForceSync);
75+
document.removeEventListener('editor:clear', clearEditor);
76+
document.removeEventListener('livewire:force-sync', syncAndFlush);
3577
editor.destroy().catch(e => console.error('Error destroying editor', e));
3678
};
3779
} catch (error) {

resources/sass/modules/_forms.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ fieldset {
157157
border: none;
158158
position: relative;
159159
margin: 0;
160+
// Reset min-inline-size to unset - Chrome sets it to min-content, which
161+
// prevents CKEditor from responding to the screen width correctly.
162+
min-inline-size: unset;
160163
padding: 0;
161164
}
162165

resources/views/livewire/comments/comment-form-component.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<livewire:wysiwyg.wysiwyg-component
2020
editor-id="{{ $this->editorId }}"
2121
name="text"
22-
content="{!! $comment->body !!}" />
22+
wire:model="body" />
2323

2424
<div class="level">
2525
@if($isEditing === true || $isReplying === true)

resources/views/livewire/posts/post-form-component.blade.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
<div wire:ignore>
1212
<livewire:wysiwyg.wysiwyg-component
1313
editor-id="post-body"
14+
wire:model="body"
1415
/>
1516
</div>
1617

1718
<div wire:ignore>
1819
<livewire:wysiwyg.wysiwyg-component
1920
editor-id="more-inside"
2021
label="{{ trans('More Inside') }}"
22+
wire:model="more_inside"
2123
/>
2224
</div>
2325
<x-forms.input

resources/views/livewire/wysiwyg/wysiwyg-component.blade.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
class="wysiwyg"
99
id="{{ $editorId }}"
1010
data-editor-id="{{ $editorId }}"
11-
wire:model.lazy="content">{{ $content }}</textarea>
11+
data-editor-config="{{ json_encode($editorConfig) }}"
12+
wire:model.lazy="content">{!! $content !!}</textarea>
1213
</div>

0 commit comments

Comments
 (0)