'.self::escape($paragraph).'
', + $paragraphs, + )); + } + + private static function escape(string $text): string + { + return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } +} diff --git a/resources/js/app.js b/resources/js/app.js index e9554aa..93fb407 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -2,6 +2,7 @@ import './bootstrap'; import { registerLadillConfirmStore, registerLadillModalHelpers } from './ladill-modals'; import { registerLadillSearchShortcut } from './ladill-search-shortcut'; +import { registerWooRichText } from './woo-rich-text'; registerLadillModalHelpers(); registerLadillSearchShortcut(); @@ -17,6 +18,7 @@ window.qrcode = qrcode; Alpine.plugin(collapse); registerLadillClipboard(Alpine); +registerWooRichText(Alpine); Alpine.data('notificationDropdown', (config = {}) => ({ open: false, diff --git a/resources/js/woo-rich-text.js b/resources/js/woo-rich-text.js new file mode 100644 index 0000000..d6ea2a4 --- /dev/null +++ b/resources/js/woo-rich-text.js @@ -0,0 +1,21 @@ +export function registerWooRichText(Alpine) { + Alpine.data('wooRichText', (initial = '') => ({ + html: initial, + + init() { + this.$refs.editor.innerHTML = this.html || ''; + this.sync(); + }, + + sync() { + this.html = this.$refs.editor.innerHTML.trim(); + this.$refs.input.value = this.html; + }, + + format(command) { + document.execCommand(command, false, null); + this.sync(); + this.$refs.editor.focus(); + }, + })); +} diff --git a/resources/views/components/woo/rich-text.blade.php b/resources/views/components/woo/rich-text.blade.php new file mode 100644 index 0000000..5289a7d --- /dev/null +++ b/resources/views/components/woo/rich-text.blade.php @@ -0,0 +1,37 @@ +@props([ + 'name', + 'label', + 'value' => '', + 'minHeight' => '6rem', + 'help' => null, +]) + +@php + $editorValue = \App\Support\WooHtml::normalizeForEditor(old($name, $value)); +@endphp + +{{ $help }}
+ @endif + +In In Jail In My Own Society, author writes.
'; + + $this->assertSame( + 'Intro
In In Jail In My Own Society, author writes.
', + WooHtml::sanitize($html), + ); + } + + public function test_plain_to_html_wraps_legacy_paragraphs(): void + { + $plain = "First paragraph\r\nSecond paragraph\r\nThird paragraph"; + + $this->assertSame( + 'First paragraph
Second paragraph
Third paragraph
', + WooHtml::plainToHtml($plain), + ); + } + + public function test_normalize_for_editor_upgrades_plain_text(): void + { + $this->assertSame( + 'Line one
Line two
', + WooHtml::normalizeForEditor("Line one\nLine two"), + ); + } +}