diff --git a/app/Entities/Controllers/BookController.php b/app/Entities/Controllers/BookController.php index faa578893..481c621e6 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -93,7 +93,7 @@ class BookController extends Controller $this->checkPermission('book-create-all'); $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], + 'description_html' => ['string', 'max:2000'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), 'tags' => ['array'], 'default_template_id' => ['nullable', 'integer'], @@ -168,7 +168,7 @@ class BookController extends Controller $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], + 'description_html' => ['string', 'max:2000'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), 'tags' => ['array'], 'default_template_id' => ['nullable', 'integer'], diff --git a/app/Entities/Controllers/BookshelfController.php b/app/Entities/Controllers/BookshelfController.php index fcfd37538..acc972348 100644 --- a/app/Entities/Controllers/BookshelfController.php +++ b/app/Entities/Controllers/BookshelfController.php @@ -18,15 +18,11 @@ use Illuminate\Validation\ValidationException; class BookshelfController extends Controller { - protected BookshelfRepo $shelfRepo; - protected ShelfContext $shelfContext; - protected ReferenceFetcher $referenceFetcher; - - public function __construct(BookshelfRepo $shelfRepo, ShelfContext $shelfContext, ReferenceFetcher $referenceFetcher) - { - $this->shelfRepo = $shelfRepo; - $this->shelfContext = $shelfContext; - $this->referenceFetcher = $referenceFetcher; + public function __construct( + protected BookshelfRepo $shelfRepo, + protected ShelfContext $shelfContext, + protected ReferenceFetcher $referenceFetcher + ) { } /** @@ -81,10 +77,10 @@ class BookshelfController extends Controller { $this->checkPermission('bookshelf-create-all'); $validated = $this->validate($request, [ - 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], - 'image' => array_merge(['nullable'], $this->getImageValidationRules()), - 'tags' => ['array'], + 'name' => ['required', 'string', 'max:255'], + 'description_html' => ['string', 'max:2000'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + 'tags' => ['array'], ]); $bookIds = explode(',', $request->get('books', '')); @@ -164,10 +160,10 @@ class BookshelfController extends Controller $shelf = $this->shelfRepo->getBySlug($slug); $this->checkOwnablePermission('bookshelf-update', $shelf); $validated = $this->validate($request, [ - 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], - 'image' => array_merge(['nullable'], $this->getImageValidationRules()), - 'tags' => ['array'], + 'name' => ['required', 'string', 'max:255'], + 'description_html' => ['string', 'max:2000'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + 'tags' => ['array'], ]); if ($request->has('image_reset')) { diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php index 40a537303..73f314ab6 100644 --- a/app/Entities/Controllers/ChapterController.php +++ b/app/Entities/Controllers/ChapterController.php @@ -22,13 +22,10 @@ use Throwable; class ChapterController extends Controller { - protected ChapterRepo $chapterRepo; - protected ReferenceFetcher $referenceFetcher; - - public function __construct(ChapterRepo $chapterRepo, ReferenceFetcher $referenceFetcher) - { - $this->chapterRepo = $chapterRepo; - $this->referenceFetcher = $referenceFetcher; + public function __construct( + protected ChapterRepo $chapterRepo, + protected ReferenceFetcher $referenceFetcher + ) { } /** @@ -51,14 +48,16 @@ class ChapterController extends Controller */ public function store(Request $request, string $bookSlug) { - $this->validate($request, [ - 'name' => ['required', 'string', 'max:255'], + $validated = $this->validate($request, [ + 'name' => ['required', 'string', 'max:255'], + 'description_html' => ['string', 'max:2000'], + 'tags' => ['array'], ]); $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail(); $this->checkOwnablePermission('chapter-create', $book); - $chapter = $this->chapterRepo->create($request->all(), $book); + $chapter = $this->chapterRepo->create($validated, $book); return redirect($chapter->getUrl()); } @@ -111,10 +110,16 @@ class ChapterController extends Controller */ public function update(Request $request, string $bookSlug, string $chapterSlug) { + $validated = $this->validate($request, [ + 'name' => ['required', 'string', 'max:255'], + 'description_html' => ['string', 'max:2000'], + 'tags' => ['array'], + ]); + $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-update', $chapter); - $this->chapterRepo->update($chapter, $request->all()); + $this->chapterRepo->update($chapter, $validated); return redirect($chapter->getUrl()); } diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index ee9a7f447..7bbe2d8a4 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -26,10 +26,11 @@ use Illuminate\Support\Collection; class Book extends Entity implements HasCoverImage { use HasFactory; + use HasHtmlDescription; public $searchFactor = 1.2; - protected $fillable = ['name', 'description']; + protected $fillable = ['name']; protected $hidden = ['pivot', 'image_id', 'deleted_at']; /** diff --git a/app/Entities/Models/Bookshelf.php b/app/Entities/Models/Bookshelf.php index 4b44025a4..cf22195f7 100644 --- a/app/Entities/Models/Bookshelf.php +++ b/app/Entities/Models/Bookshelf.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Bookshelf extends Entity implements HasCoverImage { use HasFactory; + use HasHtmlDescription; protected $table = 'bookshelves'; diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php index 98889ce3f..17fccfd6c 100644 --- a/app/Entities/Models/Chapter.php +++ b/app/Entities/Models/Chapter.php @@ -15,6 +15,7 @@ use Illuminate\Support\Collection; class Chapter extends BookChild { use HasFactory; + use HasHtmlDescription; public $searchFactor = 1.2; diff --git a/app/Entities/Models/HasHtmlDescription.php b/app/Entities/Models/HasHtmlDescription.php new file mode 100644 index 000000000..cc431f7fc --- /dev/null +++ b/app/Entities/Models/HasHtmlDescription.php @@ -0,0 +1,21 @@ +description_html ?: '

' . e($this->description) . '

'; + return HtmlContentFilter::removeScriptsFromHtmlString($html); + } +} diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index 2894a04e3..f6b9ff578 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -5,6 +5,7 @@ namespace BookStack\Entities\Repos; use BookStack\Activity\TagRepo; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\HasCoverImage; +use BookStack\Entities\Models\HasHtmlDescription; use BookStack\Exceptions\ImageUploadException; use BookStack\References\ReferenceUpdater; use BookStack\Uploads\ImageRepo; @@ -12,15 +13,11 @@ use Illuminate\Http\UploadedFile; class BaseRepo { - protected TagRepo $tagRepo; - protected ImageRepo $imageRepo; - protected ReferenceUpdater $referenceUpdater; - - public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo, ReferenceUpdater $referenceUpdater) - { - $this->tagRepo = $tagRepo; - $this->imageRepo = $imageRepo; - $this->referenceUpdater = $referenceUpdater; + public function __construct( + protected TagRepo $tagRepo, + protected ImageRepo $imageRepo, + protected ReferenceUpdater $referenceUpdater + ) { } /** @@ -29,6 +26,7 @@ class BaseRepo public function create(Entity $entity, array $input) { $entity->fill($input); + $this->updateDescription($entity, $input); $entity->forceFill([ 'created_by' => user()->id, 'updated_by' => user()->id, @@ -54,6 +52,7 @@ class BaseRepo $oldUrl = $entity->getUrl(); $entity->fill($input); + $this->updateDescription($entity, $input); $entity->updated_by = user()->id; if ($entity->isDirty('name') || empty($entity->slug)) { @@ -99,4 +98,20 @@ class BaseRepo $entity->save(); } } + + protected function updateDescription(Entity $entity, array $input): void + { + if (!in_array(HasHtmlDescription::class, class_uses($entity))) { + return; + } + + /** @var HasHtmlDescription $entity */ + if (isset($input['description_html'])) { + $entity->description_html = $input['description_html']; + $entity->description = html_entity_decode(strip_tags($input['description_html'])); + } else if (isset($input['description'])) { + $entity->description = $input['description']; + $entity->description_html = $entity->descriptionHtml(); + } + } } diff --git a/database/migrations/2023_12_17_140913_add_description_html_to_entities.php b/database/migrations/2023_12_17_140913_add_description_html_to_entities.php new file mode 100644 index 000000000..68c52e81b --- /dev/null +++ b/database/migrations/2023_12_17_140913_add_description_html_to_entities.php @@ -0,0 +1,36 @@ + $table->text('description_html'); + + Schema::table('books', $addColumn); + Schema::table('chapters', $addColumn); + Schema::table('bookshelves', $addColumn); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $removeColumn = fn(Blueprint $table) => $table->removeColumn('description_html'); + + Schema::table('books', $removeColumn); + Schema::table('chapters', $removeColumn); + Schema::table('bookshelves', $removeColumn); + } +}; diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index d7c6bba72..f0a2dbe1c 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -304,7 +304,7 @@ export function buildForInput(options) { // Return config object return { width: '100%', - height: '300px', + height: '185px', target: options.containerElement, cache_suffix: `?version=${version}`, content_css: [ @@ -312,7 +312,7 @@ export function buildForInput(options) { ], branding: false, skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5', - body_class: 'page-content', + body_class: 'wysiwyg-input', browser_spellcheck: true, relative_urls: false, language: options.language, @@ -323,11 +323,13 @@ export function buildForInput(options) { remove_trailing_brs: false, statusbar: false, menubar: false, - plugins: 'link autolink', + plugins: 'link autolink lists', contextmenu: false, - toolbar: 'bold italic underline link', + toolbar: 'bold italic underline link bullist numlist', content_style: getContentStyle(options), color_map: colorMap, + file_picker_types: 'file', + file_picker_callback: filePickerCallback, init_instance_callback(editor) { const head = editor.getDoc().querySelector('head'); head.innerHTML += fetchCustomHeadContent(); diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index cd5d929f4..b63f9cdd5 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -406,6 +406,14 @@ input[type=color] { height: auto; } +.description-input > .tox-tinymce { + border: 1px solid #DDD !important; + border-radius: 3px; + .tox-toolbar__primary { + justify-content: end; + } +} + .search-box { max-width: 100%; position: relative; diff --git a/resources/sass/_tinymce.scss b/resources/sass/_tinymce.scss index 8e036fc46..c4336da7c 100644 --- a/resources/sass/_tinymce.scss +++ b/resources/sass/_tinymce.scss @@ -23,6 +23,13 @@ display: block; } +.wysiwyg-input.mce-content-body { + padding-block-start: 1rem; + padding-block-end: 1rem; + outline: 0; + display: block; +} + // Default styles for our custom root nodes .page-content.mce-content-body doc-root { display: block; diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php index 3a2e30da6..d380c5871 100644 --- a/resources/views/books/parts/form.blade.php +++ b/resources/views/books/parts/form.blade.php @@ -9,17 +9,8 @@
- - @include('form.textarea', ['name' => 'description']) - - - @if($errors->has('description_html')) -
{{ $errors->first('description_html') }}
- @endif + + @include('form.description-html-input')
diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index 8f7c3f6cf..5884e41fd 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -26,7 +26,7 @@

{{$book->name}}

-

{!! nl2br(e($book->description)) !!}

+

{!! $book->descriptionHtml() !!}

@if(count($bookChildren) > 0)
@foreach($bookChildren as $childElement) diff --git a/resources/views/chapters/parts/form.blade.php b/resources/views/chapters/parts/form.blade.php index 8abcebe13..7c565f43c 100644 --- a/resources/views/chapters/parts/form.blade.php +++ b/resources/views/chapters/parts/form.blade.php @@ -1,14 +1,16 @@ +@push('head') + +@endpush -{!! csrf_field() !!} - +{{ csrf_field() }}
@include('form.text', ['name' => 'name', 'autofocus' => true])
- - @include('form.textarea', ['name' => 'description']) + + @include('form.description-html-input')
@@ -24,3 +26,6 @@ {{ trans('common.cancel') }}
+ +@include('entities.selector-popup', ['entityTypes' => 'page', 'selectorEndpoint' => '/search/entity-selector-templates']) +@include('form.editor-translations') \ No newline at end of file diff --git a/resources/views/form/description-html-input.blade.php b/resources/views/form/description-html-input.blade.php new file mode 100644 index 000000000..3cf726ba4 --- /dev/null +++ b/resources/views/form/description-html-input.blade.php @@ -0,0 +1,8 @@ + +@if($errors->has('description_html')) +
{{ $errors->first('description_html') }}
+@endif \ No newline at end of file diff --git a/resources/views/shelves/parts/form.blade.php b/resources/views/shelves/parts/form.blade.php index ad67cb85c..a724c99ce 100644 --- a/resources/views/shelves/parts/form.blade.php +++ b/resources/views/shelves/parts/form.blade.php @@ -1,13 +1,16 @@ -{{ csrf_field() }} +@push('head') + +@endpush +{{ csrf_field() }}
@include('form.text', ['name' => 'name', 'autofocus' => true])
- - @include('form.textarea', ['name' => 'description']) + + @include('form.description-html-input')
@@ -84,4 +87,7 @@
{{ trans('common.cancel') }} -
\ No newline at end of file +
+ +@include('entities.selector-popup', ['entityTypes' => 'page', 'selectorEndpoint' => '/search/entity-selector-templates']) +@include('form.editor-translations') \ No newline at end of file