mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-03-24 23:35:15 +08:00
Input WYSIWYG: Added description_html field, added store logic
Rolled out HTML editor field and store logic across all target entity types. Cleaned up WYSIWYG input logic and design. Cleaned up some injected classes while there.
This commit is contained in:
parent
569542f0bb
commit
c622b785a9
@ -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'],
|
||||
|
@ -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')) {
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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'];
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
class Bookshelf extends Entity implements HasCoverImage
|
||||
{
|
||||
use HasFactory;
|
||||
use HasHtmlDescription;
|
||||
|
||||
protected $table = 'bookshelves';
|
||||
|
||||
|
@ -15,6 +15,7 @@ use Illuminate\Support\Collection;
|
||||
class Chapter extends BookChild
|
||||
{
|
||||
use HasFactory;
|
||||
use HasHtmlDescription;
|
||||
|
||||
public $searchFactor = 1.2;
|
||||
|
||||
|
21
app/Entities/Models/HasHtmlDescription.php
Normal file
21
app/Entities/Models/HasHtmlDescription.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
<?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);
|
||||
}
|
||||
};
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -9,17 +9,8 @@
|
||||
</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">
|
||||
|
@ -26,7 +26,7 @@
|
||||
<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)
|
||||
|
@ -1,14 +1,16 @@
|
||||
@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">
|
||||
@ -24,3 +26,6 @@
|
||||
<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')
|
8
resources/views/form/description-html-input.blade.php
Normal file
8
resources/views/form/description-html-input.blade.php
Normal file
@ -0,0 +1,8 @@
|
||||
<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
|
@ -1,13 +1,16 @@
|
||||
{{ 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">
|
||||
@ -84,4 +87,7 @@
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@include('entities.selector-popup', ['entityTypes' => 'page', 'selectorEndpoint' => '/search/entity-selector-templates'])
|
||||
@include('form.editor-translations')
|
Loading…
x
Reference in New Issue
Block a user