$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'],
$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'],
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
+ ) {
}
/**
{
$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', ''));
$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')) {
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
+ ) {
}
/**
*/
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());
}
*/
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());
}
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'];
/**
class Bookshelf extends Entity implements HasCoverImage
{
use HasFactory;
+ use HasHtmlDescription;
protected $table = 'bookshelves';
class Chapter extends BookChild
{
use HasFactory;
+ use HasHtmlDescription;
public $searchFactor = 1.2;
--- /dev/null
+<?php
+
+namespace BookStack\Entities\Models;
+
+use BookStack\Util\HtmlContentFilter;
+
+/**
+ * @property string $description
+ * @property string $description_html
+ */
+trait HasHtmlDescription
+{
+ /**
+ * Get the HTML description for this book.
+ */
+ public function descriptionHtml(): string
+ {
+ $html = $this->description_html ?: '<p>' . e($this->description) . '</p>';
+ return HtmlContentFilter::removeScriptsFromHtmlString($html);
+ }
+}
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;
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
+ ) {
}
/**
public function create(Entity $entity, array $input)
{
$entity->fill($input);
+ $this->updateDescription($entity, $input);
$entity->forceFill([
'created_by' => user()->id,
'updated_by' => user()->id,
$oldUrl = $entity->getUrl();
$entity->fill($input);
+ $this->updateDescription($entity, $input);
$entity->updated_by = user()->id;
if ($entity->isDirty('name') || empty($entity->slug)) {
$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();
+ }
+ }
}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ $addColumn = fn(Blueprint $table) => $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);
+ }
+};
// Return config object
return {
width: '100%',
- height: '300px',
+ height: '185px',
target: options.containerElement,
cache_suffix: `?version=${version}`,
content_css: [
],
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,
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();
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;
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;
</div>
<div class="form-group description-input">
- <label for="description">{{ trans('common.description') }}</label>
- @include('form.textarea', ['name' => 'description'])
-
- <textarea component="wysiwyg-input"
- option:wysiwyg-input:language="{{ $locale->htmlLang() }}"
- option:wysiwyg-input:text-direction="{{ $locale->htmlDirection() }}"
- id="description_html" name="description_html" rows="5"
- @if($errors->has('description_html')) class="text-neg" @endif>@if(isset($model) || old('description_html')){{ old('description_html') ? old($name) : $model->description_html}}@endif</textarea>
- @if($errors->has('description_html'))
- <div class="text-neg text-small">{{ $errors->first('description_html') }}</div>
- @endif
+ <label for="description_html">{{ trans('common.description') }}</label>
+ @include('form.description-html-input')
</div>
<div class="form-group collapsible" component="collapsible" id="logo-control">
<main class="content-wrap card">
<h1 class="break-text">{{$book->name}}</h1>
<div refs="entity-search@contentView" class="book-content">
- <p class="text-muted">{!! nl2br(e($book->description)) !!}</p>
+ <p class="text-muted">{!! $book->descriptionHtml() !!}</p>
@if(count($bookChildren) > 0)
<div class="entity-list book-contents">
@foreach($bookChildren as $childElement)
+@push('head')
+ <script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}"></script>
+@endpush
-{!! csrf_field() !!}
-
+{{ csrf_field() }}
<div class="form-group title-input">
<label for="name">{{ trans('common.name') }}</label>
@include('form.text', ['name' => 'name', 'autofocus' => true])
</div>
<div class="form-group description-input">
- <label for="description">{{ trans('common.description') }}</label>
- @include('form.textarea', ['name' => 'description'])
+ <label for="description_html">{{ trans('common.description') }}</label>
+ @include('form.description-html-input')
</div>
<div class="form-group collapsible" component="collapsible" id="logo-control">
<a href="{{ isset($chapter) ? $chapter->getUrl() : $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button">{{ trans('entities.chapters_save') }}</button>
</div>
+
+@include('entities.selector-popup', ['entityTypes' => 'page', 'selectorEndpoint' => '/search/entity-selector-templates'])
+@include('form.editor-translations')
\ No newline at end of file
--- /dev/null
+<textarea component="wysiwyg-input"
+ option:wysiwyg-input:language="{{ $locale->htmlLang() }}"
+ option:wysiwyg-input:text-direction="{{ $locale->htmlDirection() }}"
+ id="description_html" name="description_html" rows="5"
+ @if($errors->has('description_html')) class="text-neg" @endif>@if(isset($model) || old('description_html')){{ old('description_html') ?? $model->descriptionHtml()}}@endif</textarea>
+@if($errors->has('description_html'))
+ <div class="text-neg text-small">{{ $errors->first('description_html') }}</div>
+@endif
\ No newline at end of file
-{{ csrf_field() }}
+@push('head')
+ <script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}"></script>
+@endpush
+{{ csrf_field() }}
<div class="form-group title-input">
<label for="name">{{ trans('common.name') }}</label>
@include('form.text', ['name' => 'name', 'autofocus' => true])
</div>
<div class="form-group description-input">
- <label for="description">{{ trans('common.description') }}</label>
- @include('form.textarea', ['name' => 'description'])
+ <label for="description_html">{{ trans('common.description') }}</label>
+ @include('form.description-html-input')
</div>
<div component="shelf-sort" class="grid half gap-xl">
<div class="form-group text-right">
<a href="{{ isset($shelf) ? $shelf->getUrl() : url('/shelves') }}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button">{{ trans('entities.shelves_save') }}</button>
-</div>
\ No newline at end of file
+</div>
+
+@include('entities.selector-popup', ['entityTypes' => 'page', 'selectorEndpoint' => '/search/entity-selector-templates'])
+@include('form.editor-translations')
\ No newline at end of file