* @property bool $template
* @property bool $draft
* @property int $revision_count
+ * @property string $editor
* @property Chapter $chapter
* @property Collection $attachments
* @property Collection $revisions
*
* @property mixed $id
* @property int $page_id
+ * @property string $name
* @property string $slug
* @property string $book_slug
* @property int $created_by
* @property string $summary
* @property string $markdown
* @property string $html
+ * @property string $text
* @property int $revision_number
* @property Page $page
* @property-read ?User $createdBy
*/
class PageRevision extends Model
{
- protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
+ protected $fillable = ['name', 'text', 'summary'];
protected $hidden = ['html', 'markdown', 'restricted', 'text'];
/**
use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\PageContent;
+use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
}
$pageContent = new PageContent($page);
- if (!empty($input['markdown'] ?? '')) {
+ $currentEditor = $page->editor ?: PageEditorData::getSystemDefaultEditor();
+ $newEditor = $currentEditor;
+
+ $haveInput = isset($input['markdown']) || isset($input['html']);
+ $inputEmpty = empty($input['markdown']) && empty($input['html']);
+
+ if ($haveInput && $inputEmpty) {
+ $pageContent->setNewHTML('');
+ } elseif (!empty($input['markdown']) && is_string($input['markdown'])) {
+ $newEditor = 'markdown';
$pageContent->setNewMarkdown($input['markdown']);
} elseif (isset($input['html'])) {
+ $newEditor = 'wysiwyg';
$pageContent->setNewHTML($input['html']);
}
+
+ if ($newEditor !== $currentEditor && userCan('editor-change')) {
+ $page->editor = $newEditor;
+ }
}
/**
*/
protected function savePageRevision(Page $page, string $summary = null): PageRevision
{
- $revision = new PageRevision($page->getAttributes());
+ $revision = new PageRevision();
+ $revision->name = $page->name;
+ $revision->html = $page->html;
+ $revision->markdown = $page->markdown;
+ $revision->text = $page->text;
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
return $page;
}
- // Otherwise save the data to a revision
+ // Otherwise, save the data to a revision
$draft = $this->getPageRevisionToUpdate($page);
$draft->fill($input);
- if (setting('app-editor') !== 'markdown') {
+
+ if (!empty($input['markdown'])) {
+ $draft->markdown = $input['markdown'];
+ $draft->html = '';
+ } else {
+ $draft->html = $input['html'];
$draft->markdown = '';
}
class HtmlToMarkdown
{
- protected $html;
+ protected string $html;
public function __construct(string $html)
{
--- /dev/null
+<?php
+
+namespace BookStack\Entities\Tools\Markdown;
+
+use BookStack\Facades\Theme;
+use BookStack\Theming\ThemeEvents;
+use League\CommonMark\Block\Element\ListItem;
+use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\Environment;
+use League\CommonMark\Extension\Table\TableExtension;
+use League\CommonMark\Extension\TaskList\TaskListExtension;
+
+class MarkdownToHtml
+{
+
+ protected string $markdown;
+
+ public function __construct(string $markdown)
+ {
+ $this->markdown = $markdown;
+ }
+
+ public function convert(): string
+ {
+ $environment = Environment::createCommonMarkEnvironment();
+ $environment->addExtension(new TableExtension());
+ $environment->addExtension(new TaskListExtension());
+ $environment->addExtension(new CustomStrikeThroughExtension());
+ $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
+ $converter = new CommonMarkConverter([], $environment);
+
+ $environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
+
+ return $converter->convertToHtml($this->markdown);
+ }
+
+}
\ No newline at end of file
namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page;
-use BookStack\Entities\Tools\Markdown\CustomListItemRenderer;
-use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
+use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
use BookStack\Exceptions\ImageUploadException;
-use BookStack\Facades\Theme;
-use BookStack\Theming\ThemeEvents;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService;
use BookStack\Util\HtmlContentFilter;
use DOMNodeList;
use DOMXPath;
use Illuminate\Support\Str;
-use League\CommonMark\Block\Element\ListItem;
-use League\CommonMark\CommonMarkConverter;
-use League\CommonMark\Environment;
-use League\CommonMark\Extension\Table\TableExtension;
-use League\CommonMark\Extension\TaskList\TaskListExtension;
class PageContent
{
- protected $page;
+ protected Page $page;
/**
* PageContent constructor.
{
$markdown = $this->extractBase64ImagesFromMarkdown($markdown);
$this->page->markdown = $markdown;
- $html = $this->markdownToHtml($markdown);
+ $html = (new MarkdownToHtml($markdown))->convert();
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
}
- /**
- * Convert the given Markdown content to a HTML string.
- */
- protected function markdownToHtml(string $markdown): string
- {
- $environment = Environment::createCommonMarkEnvironment();
- $environment->addExtension(new TableExtension());
- $environment->addExtension(new TaskListExtension());
- $environment->addExtension(new CustomStrikeThroughExtension());
- $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
- $converter = new CommonMarkConverter([], $environment);
-
- $environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
-
- return $converter->convertToHtml($markdown);
- }
-
/**
* Convert all base64 image data to saved images.
*/
class PageEditActivity
{
- protected $page;
+ protected Page $page;
/**
* PageEditActivity constructor.
--- /dev/null
+<?php
+
+namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
+use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
+
+class PageEditorData
+{
+ protected Page $page;
+ protected PageRepo $pageRepo;
+ protected string $requestedEditor;
+
+ protected array $viewData;
+ protected array $warnings;
+
+ public function __construct(Page $page, PageRepo $pageRepo, string $requestedEditor)
+ {
+ $this->page = $page;
+ $this->pageRepo = $pageRepo;
+ $this->requestedEditor = $requestedEditor;
+
+ $this->viewData = $this->build();
+ }
+
+ public function getViewData(): array
+ {
+ return $this->viewData;
+ }
+
+ public function getWarnings(): array
+ {
+ return $this->warnings;
+ }
+
+ protected function build(): array
+ {
+ $page = clone $this->page;
+ $isDraft = boolval($this->page->draft);
+ $templates = $this->pageRepo->getTemplates(10);
+ $draftsEnabled = auth()->check();
+
+ $isDraftRevision = false;
+ $this->warnings = [];
+ $editActivity = new PageEditActivity($page);
+
+ if ($editActivity->hasActiveEditing()) {
+ $this->warnings[] = $editActivity->activeEditingMessage();
+ }
+
+ // Check for a current draft version for this user
+ $userDraft = $this->pageRepo->getUserDraft($page);
+ if ($userDraft !== null) {
+ $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
+ $isDraftRevision = true;
+ $this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
+ }
+
+ $editorType = $this->getEditorType($page);
+ $this->updateContentForEditor($page, $editorType);
+
+ return [
+ 'page' => $page,
+ 'book' => $page->book,
+ 'isDraft' => $isDraft,
+ 'isDraftRevision' => $isDraftRevision,
+ 'draftsEnabled' => $draftsEnabled,
+ 'templates' => $templates,
+ 'editor' => $editorType,
+ ];
+ }
+
+ protected function updateContentForEditor(Page $page, string $editorType): void
+ {
+ $isHtml = !empty($page->html) && empty($page->markdown);
+
+ // HTML to markdown-clean conversion
+ if ($editorType === 'markdown' && $isHtml && $this->requestedEditor === 'markdown-clean') {
+ $page->markdown = (new HtmlToMarkdown($page->html))->convert();
+ }
+
+ // Markdown to HTML conversion if we don't have HTML
+ if ($editorType === 'wysiwyg' && !$isHtml) {
+ $page->html = (new MarkdownToHtml($page->markdown))->convert();
+ }
+ }
+
+ /**
+ * Get the type of editor to show for editing the given page.
+ * Defaults based upon the current content of the page otherwise will fall back
+ * to system default but will take a requested type (if provided) if permissions allow.
+ */
+ protected function getEditorType(Page $page): string
+ {
+ $editorType = $page->editor ?: self::getSystemDefaultEditor();
+
+ // Use requested editor if valid and if we have permission
+ $requestedType = explode('-', $this->requestedEditor)[0];
+ if (($requestedType === 'markdown' || $requestedType === 'wysiwyg') && userCan('editor-change')) {
+ $editorType = $requestedType;
+ }
+
+ return $editorType;
+ }
+
+ /**
+ * Get the configured system default editor.
+ */
+ public static function getSystemDefaultEditor(): string
+ {
+ return setting('app-editor') === 'markdown' ? 'markdown' : 'wysiwyg';
+ }
+
+}
\ No newline at end of file
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
+use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
class PageController extends Controller
{
- protected $pageRepo;
+ protected PageRepo $pageRepo;
/**
* PageController constructor.
*
* @throws NotFoundException
*/
- public function editDraft(string $bookSlug, int $pageId)
+ public function editDraft(Request $request, string $bookSlug, int $pageId)
{
$draft = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-create', $draft->getParent());
- $this->setPageTitle(trans('entities.pages_edit_draft'));
- $draftsEnabled = $this->isSignedIn();
- $templates = $this->pageRepo->getTemplates(10);
+ $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
+ $this->setPageTitle(trans('entities.pages_edit_draft'));
- return view('pages.edit', [
- 'page' => $draft,
- 'book' => $draft->book,
- 'isDraft' => true,
- 'draftsEnabled' => $draftsEnabled,
- 'templates' => $templates,
- ]);
+ return view('pages.edit', $editorData->getViewData());
}
/**
*
* @throws NotFoundException
*/
- public function edit(string $bookSlug, string $pageSlug)
+ public function edit(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
- $page->isDraft = false;
- $editActivity = new PageEditActivity($page);
-
- // Check for active editing
- $warnings = [];
- if ($editActivity->hasActiveEditing()) {
- $warnings[] = $editActivity->activeEditingMessage();
+ $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
+ if ($editorData->getWarnings()) {
+ $this->showWarningNotification(implode("\n", $editorData->getWarnings()));
}
- // Check for a current draft version for this user
- $userDraft = $this->pageRepo->getUserDraft($page);
- if ($userDraft !== null) {
- $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
- $page->isDraft = true;
- $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
- }
-
- if (count($warnings) > 0) {
- $this->showWarningNotification(implode("\n", $warnings));
- }
-
- $templates = $this->pageRepo->getTemplates(10);
- $draftsEnabled = $this->isSignedIn();
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
- return view('pages.edit', [
- 'page' => $page,
- 'book' => $page->book,
- 'current' => $page,
- 'draftsEnabled' => $draftsEnabled,
- 'templates' => $templates,
- ]);
+ return view('pages.edit', $editorData->getViewData());
}
/**
--- /dev/null
+<?php
+
+use Carbon\Carbon;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+class AddEditorChangeFieldAndPermission extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ // Add the new 'editor' column to the pages table
+ Schema::table('pages', function(Blueprint $table) {
+ $table->string('editor', 50)->default('');
+ });
+
+ // Populate the new 'editor' column
+ // We set it to 'markdown' for pages currently with markdown content
+ DB::table('pages')->where('markdown', '!=', '')->update(['editor' => 'markdown']);
+ // We set it to 'wysiwyg' where we have HTML but no markdown
+ DB::table('pages')->where('markdown', '=', '')
+ ->where('html', '!=', '')
+ ->update(['editor' => 'wysiwyg']);
+
+ // Give the admin user permission to change the editor
+ $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
+
+ $permissionId = DB::table('role_permissions')->insertGetId([
+ 'name' => 'editor-change',
+ 'display_name' => 'Change page editor',
+ 'created_at' => Carbon::now()->toDateTimeString(),
+ 'updated_at' => Carbon::now()->toDateTimeString(),
+ ]);
+
+ DB::table('permission_role')->insert([
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
+ ]);
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ // Drop the new column from the pages table
+ Schema::table('pages', function(Blueprint $table) {
+ $table->dropColumn('editor');
+ });
+
+ // Remove traces of the role permission
+ DB::table('role_permissions')->where('name', '=', 'editor-change')->delete();
+ }
+}
--- /dev/null
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.99 16H14v-2H6.99v-3L3 15l3.99 4ZM21 9l-3.99-4v3H10v2h7.01v3z"/></svg>
\ No newline at end of file
return this.hideSuggestions();
}
- this.list.innerHTML = suggestions.map(value => `<li><button type="button">${escapeHtml(value)}</button></li>`).join('');
+ this.list.innerHTML = suggestions.map(value => `<li><button type="button" class="text-item">${escapeHtml(value)}</button></li>`).join('');
this.list.style.display = 'block';
for (const button of this.list.querySelectorAll('button')) {
button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0);
this.historyList.innerHTML = historyKeys.map(key => {
const localTime = (new Date(parseInt(key))).toLocaleTimeString();
- return `<li><button type="button" data-time="${key}">${localTime}</button></li>`;
+ return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
}).join('');
}
--- /dev/null
+import {onSelect} from "../services/dom";
+
+/**
+ * Custom equivalent of window.confirm() using our popup component.
+ * Is promise based so can be used like so:
+ * `const result = await dialog.show()`
+ * @extends {Component}
+ */
+class ConfirmDialog {
+
+ setup() {
+ this.container = this.$el;
+ this.confirmButton = this.$refs.confirm;
+
+ this.res = null;
+
+ onSelect(this.confirmButton, () => {
+ this.sendResult(true);
+ this.getPopup().hide();
+ });
+ }
+
+ show() {
+ this.getPopup().show(null, () => {
+ this.sendResult(false);
+ });
+
+ return new Promise((res, rej) => {
+ this.res = res;
+ });
+ }
+
+ /**
+ * @returns {Popup}
+ */
+ getPopup() {
+ return this.container.components.popup;
+ }
+
+ /**
+ * @param {Boolean} result
+ */
+ sendResult(result) {
+ if (this.res) {
+ this.res(result)
+ this.res = null;
+ }
+ }
+
+}
+
+export default ConfirmDialog;
\ No newline at end of file
import codeEditor from "./code-editor.js"
import codeHighlighter from "./code-highlighter.js"
import collapsible from "./collapsible.js"
+import confirmDialog from "./confirm-dialog"
import customCheckbox from "./custom-checkbox.js"
import detailsHighlighter from "./details-highlighter.js"
import dropdown from "./dropdown.js"
import homepageControl from "./homepage-control.js"
import imageManager from "./image-manager.js"
import imagePicker from "./image-picker.js"
-import index from "./index.js"
import listSortControl from "./list-sort-control.js"
import markdownEditor from "./markdown-editor.js"
import newUserPassword from "./new-user-password.js"
"code-editor": codeEditor,
"code-highlighter": codeHighlighter,
"collapsible": collapsible,
+ "confirm-dialog": confirmDialog,
"custom-checkbox": customCheckbox,
"details-highlighter": detailsHighlighter,
"dropdown": dropdown,
"homepage-control": homepageControl,
"image-manager": imageManager,
"image-picker": imagePicker,
- "index": index,
"list-sort-control": listSortControl,
"markdown-editor": markdownEditor,
"new-user-password": newUserPassword,
this.draftDisplayIcon = this.$refs.draftDisplayIcon;
this.changelogInput = this.$refs.changelogInput;
this.changelogDisplay = this.$refs.changelogDisplay;
+ this.changeEditorButtons = this.$manyRefs.changeEditor;
+ this.switchDialogContainer = this.$refs.switchDialog;
// Translations
this.draftText = this.$opts.draftText;
// Draft Controls
onSelect(this.saveDraftButton, this.saveDraft.bind(this));
onSelect(this.discardDraftButton, this.discardDraft.bind(this));
+
+ // Change editor controls
+ onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
}
setInitialFocus() {
data.markdown = this.editorMarkdown;
}
+ let didSave = false;
try {
const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
if (!this.isNewDraft) {
this.toggleDiscardDraftVisibility(true);
}
+
this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
this.autoSave.last = Date.now();
if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
window.$events.emit('warning', resp.data.warning);
this.shownWarningsCache.add(resp.data.warning);
}
+
+ didSave = true;
} catch (err) {
// Save the editor content in LocalStorage as a last resort, just in case.
try {
window.$events.emit('error', this.autosaveFailText);
}
+ return didSave;
}
draftNotifyChange(text) {
this.discardDraftWrap.classList.toggle('hidden', !show);
}
+ async changeEditor(event) {
+ event.preventDefault();
+
+ const link = event.target.closest('a').href;
+ const dialog = this.switchDialogContainer.components['confirm-dialog'];
+ const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
+
+ if (saved && confirmed) {
+ window.location = link;
+ }
+ }
+
}
export default PageEditor;
\ No newline at end of file
}
hide(onComplete = null) {
- fadeOut(this.container, 240, onComplete);
+ fadeOut(this.container, 120, onComplete);
if (this.onkeyup) {
window.removeEventListener('keyup', this.onkeyup);
this.onkeyup = null;
}
show(onComplete = null, onHide = null) {
- fadeIn(this.container, 240, onComplete);
+ fadeIn(this.container, 120, onComplete);
this.onkeyup = (event) => {
if (event.key === 'Escape') {
'pages_edit_draft_save_at' => 'Draft saved at ',
'pages_edit_delete_draft' => 'Delete Draft',
'pages_edit_discard_draft' => 'Discard Draft',
+ 'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
+ 'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
+ 'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
+ 'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_set_changelog' => 'Set Changelog',
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
'pages_edit_enter_changelog' => 'Enter Changelog',
+ 'pages_editor_switch_title' => 'Switch Editor',
+ 'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
+ 'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
+ 'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
+ 'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
+ 'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
'pages_save' => 'Save Page',
'pages_title' => 'Page Title',
'pages_name' => 'Page Name',
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'Revision #:id',
'pages_revisions_numbered_changes' => 'Revision #:id Changes',
+ 'pages_revisions_editor' => 'Editor Type',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version',
'app_secure_images' => 'Higher Security Image Uploads',
'app_secure_images_toggle' => 'Enable higher security image uploads',
'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',
- 'app_editor' => 'Page Editor',
- 'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
+ 'app_default_editor' => 'Default Page Editor',
+ 'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
'app_custom_html' => 'Custom HTML Head Content',
'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',
'role_access_api' => 'Access system API',
'role_manage_settings' => 'Manage app settings',
'role_export_content' => 'Export content',
+ 'role_editor_change' => 'Change page editor',
'role_asset' => 'Asset Permissions',
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
width: 800px;
max-width: 90%;
}
+ &.very-small {
+ margin: 2% auto;
+ width: 600px;
+ max-width: 90%;
+ }
&:before {
display: flex;
align-self: flex-start;
li.active a {
font-weight: 600;
}
- a, button {
- display: block;
- padding: $-xs $-m;
+ button {
+ width: 100%;
+ text-align: start;
+ }
+ li.border-bottom {
+ border-bottom: 1px solid #DDD;
+ }
+ li hr {
+ margin: $-xs 0;
+ }
+ .icon-item, .text-item, .label-item {
+ padding: 8px $-m;
@include lightDark(color, #555, #eee);
fill: currentColor;
white-space: nowrap;
- line-height: 1.6;
+ line-height: 1.4;
cursor: pointer;
&:hover, &:focus {
text-decoration: none;
width: 16px;
}
}
- button {
- width: 100%;
- text-align: start;
+ .text-item {
+ display: block;
}
- li.border-bottom {
- border-bottom: 1px solid #DDD;
+ .label-item {
+ display: grid;
+ align-items: center;
+ grid-template-columns: auto min-content;
+ gap: $-m;
}
- li hr {
- margin: $-xs 0;
+ .label-item > *:nth-child(2) {
+ opacity: 0.7;
+ &:hover {
+ opacity: 1;
+ }
+ }
+ .icon-item {
+ display: grid;
+ align-items: start;
+ grid-template-columns: 16px auto;
+ gap: $-m;
+ svg {
+ margin-inline-end: 0;
+ margin-block-start: 1px;
+ }
}
}
small, p.small, span.small, .text-small {
font-size: 0.75rem;
- @include lightDark(color, #5e5e5e, #999);
}
sup, .superscript {
class="drag-card-action text-center text-neg">@icon('close')</button>
<div refs="dropdown@menu" class="dropdown-menu">
<p class="text-neg small px-m mb-xs">{{ trans('entities.attachments_delete') }}</p>
- <button refs="ajax-delete-row@delete" type="button" class="text-primary small delete">{{ trans('common.confirm') }}</button>
+ <button refs="ajax-delete-row@delete" type="button" class="text-primary small delete text-item">{{ trans('common.confirm') }}</button>
</div>
</div>
</div>
<button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li>
- <li><button action="delete" type="button" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</button></li>
+ <li>
+ <button action="delete" type="button" class="text-button text-neg icon-item">
+ @icon('delete')
+ <div>{{ trans('common.delete') }}</div>
+ </button>
+ </li>
</ul>
</div>
@endif
--- /dev/null
+<div components="popup confirm-dialog"
+ refs="confirm-dialog@popup {{ $ref }}"
+ class="popup-background">
+ <div class="popup-body very-small" tabindex="-1">
+
+ <div class="popup-header primary-background">
+ <div class="popup-title">{{ $title }}</div>
+ <button refs="popup@hide" type="button" class="popup-header-close">x</button>
+ </div>
+
+ <div class="px-m py-m">
+ {{ $slot }}
+
+ <div class="text-right">
+ <button type="button" class="button outline" refs="popup@hide">{{ trans('common.cancel') }}</button>
+ <button type="button" class="button" refs="confirm-dialog@confirm">{{ trans('common.continue') }}</button>
+ </div>
+ </div>
+
+ </div>
+</div>
\ No newline at end of file
</span>
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li>
- <a href="{{ url('/favourites') }}">@icon('star'){{ trans('entities.my_favourites') }}</a>
+ <a href="{{ url('/favourites') }}" class="icon-item">
+ @icon('star')
+ <div>{{ trans('entities.my_favourites') }}</div>
+ </a>
</li>
<li>
- <a href="{{ $currentUser->getProfileUrl() }}">@icon('user'){{ trans('common.view_profile') }}</a>
+ <a href="{{ $currentUser->getProfileUrl() }}" class="icon-item">
+ @icon('user')
+ <div>{{ trans('common.view_profile') }}</div>
+ </a>
</li>
<li>
- <a href="{{ $currentUser->getEditUrl() }}">@icon('edit'){{ trans('common.edit_profile') }}</a>
+ <a href="{{ $currentUser->getEditUrl() }}" class="icon-item">
+ @icon('edit')
+ <div>{{ trans('common.edit_profile') }}</div>
+ </a>
</li>
<li>
<form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
method="post">
{{ csrf_field() }}
- <button class="text-muted icon-list-item text-primary">
- @icon('logout'){{ trans('auth.logout') }}
+ <button class="icon-item">
+ @icon('logout')
+ <div>{{ trans('auth.logout') }}</div>
</button>
</form>
</li>
<li><hr></li>
<li>
- @include('common.dark-mode-toggle')
+ @include('common.dark-mode-toggle', ['classes' => 'icon-item'])
</li>
</ul>
</div>
<span>{{ trans('entities.export') }}</span>
</div>
<ul refs="dropdown@menu" class="wide dropdown-menu" role="menu">
- <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank" rel="noopener">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
- <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" rel="noopener">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
- <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" rel="noopener">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
- <li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" rel="noopener">{{ trans('entities.export_md') }} <span class="text-muted float right">.md</span></a></li>
+ <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_html') }}</span><span>.html</span></a></li>
+ <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }}</span><span>.pdf</span></a></li>
+ <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_text') }}</span><span>.txt</span></a></li>
+ <li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_md') }}</span><span>.md</span></a></li>
</ul>
</div>
<div refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" tabindex="0">{{ $options[$selectedSort] }}</div>
<ul refs="dropdown@menu" class="dropdown-menu">
@foreach($options as $key => $label)
- <li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}">{{ $label }}</a></li>
+ <li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}" class="text-item">{{ $label }}</a></li>
@endforeach
</ul>
</div>
</div>
</div>
+ <script nonce="{{ $cspNonce }}">
+ setTimeout(async () => {
+ const result = await window.components["confirm-dialog"][0].show();
+ console.log({result});
+ }, 1000);
+ </script>
+
<div class="container" id="home-default">
<div class="grid third gap-xxl no-row-gap" >
<div>
<form action="{{ url('/mfa/' . $method . '/remove') }}" method="post">
{{ csrf_field() }}
{{ method_field('delete') }}
- <button class="text-primary small delete">{{ trans('common.confirm') }}</button>
+ <button class="text-primary small text-item">{{ trans('common.confirm') }}</button>
</form>
</div>
</div>
@extends('layouts.base')
-@section('head')
- <script src="{{ url('/libs/tinymce/tinymce.min.js?ver=5.10.2') }}" nonce="{{ $cspNonce }}"></script>
-@stop
-
@section('body-class', 'flexbox')
@section('content')
<form action="{{ $page->getUrl() }}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
{{ csrf_field() }}
- @if(!isset($isDraft))
- <input type="hidden" name="_method" value="PUT">
- @endif
+ @if(!$isDraft) {{ method_field('PUT') }} @endif
@include('pages.parts.form', ['model' => $page])
@include('pages.parts.editor-toolbox')
</form>
--- /dev/null
+<div class="primary-background-light toolbar page-edit-toolbar">
+ <div class="grid third no-break v-center">
+
+ <div class="action-buttons text-left px-m py-xs">
+ <a href="{{ $isDraft ? $page->getParent()->getUrl() : $page->getUrl() }}"
+ class="text-button text-primary">@icon('back')<span class="hide-under-l">{{ trans('common.back') }}</span></a>
+ </div>
+
+ <div class="text-center px-m">
+ <div component="dropdown"
+ option:dropdown:move-menu="true"
+ class="dropdown-container draft-display text {{ $draftsEnabled ? '' : 'hidden' }}">
+ <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" title="{{ trans('entities.pages_edit_draft_options') }}" class="text-primary text-button py-s px-m"><span refs="page-editor@draftDisplay" class="faded-text"></span> @icon('more')</button>
+ @icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', 'refs' => 'page-editor@draftDisplayIcon'])
+ <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
+ <li>
+ <button refs="page-editor@saveDraft" type="button" class="text-pos icon-item">
+ @icon('save')
+ <div>{{ trans('entities.pages_edit_save_draft') }}</div>
+ </button>
+ </li>
+ @if($isDraft)
+ <li>
+ <a href="{{ $model->getUrl('/delete') }}" class="text-neg icon-item">
+ @icon('delete')
+ {{ trans('entities.pages_edit_delete_draft') }}
+ </a>
+ </li>
+ @endif
+ <li refs="page-editor@discardDraftWrap" class="{{ $isDraft ? '' : 'hidden' }}">
+ <button refs="page-editor@discardDraft" type="button" class="text-neg icon-item">
+ @icon('cancel')
+ <div>{{ trans('entities.pages_edit_discard_draft') }}</div>
+ </button>
+ </li>
+ @if(userCan('editor-change'))
+ <li>
+ @if($editor === 'wysiwyg')
+ <a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-clean" refs="page-editor@changeEditor" class="icon-item">
+ @icon('swap-horizontal')
+ <div>
+ {{ trans('entities.pages_edit_switch_to_markdown') }}
+ <br>
+ <small>{{ trans('entities.pages_edit_switch_to_markdown_clean') }}</small>
+ </div>
+ </a>
+ <a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-stable" refs="page-editor@changeEditor" class="icon-item">
+ @icon('swap-horizontal')
+ <div>
+ {{ trans('entities.pages_edit_switch_to_markdown') }}
+ <br>
+ <small>{{ trans('entities.pages_edit_switch_to_markdown_stable') }}</small>
+ </div>
+ </a>
+ @else
+ <a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=wysiwyg" refs="page-editor@changeEditor" class="icon-item">
+ @icon('swap-horizontal')
+ <div>{{ trans('entities.pages_edit_switch_to_wysiwyg') }}</div>
+ </a>
+ @endif
+ </li>
+ @endif
+ </ul>
+ </div>
+ </div>
+
+ <div class="action-buttons px-m py-xs">
+ <div component="dropdown" dropdown-move-menu class="dropdown-container">
+ <button refs="dropdown@toggle" type="button" aria-haspopup="true" aria-expanded="false" class="text-primary text-button">@icon('edit') <span refs="page-editor@changelogDisplay">{{ trans('entities.pages_edit_set_changelog') }}</span></button>
+ <ul refs="dropdown@menu" class="wide dropdown-menu">
+ <li class="px-l py-m">
+ <p class="text-muted pb-s">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
+ <input refs="page-editor@changelogInput"
+ name="summary"
+ id="summary-input"
+ type="text"
+ placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" />
+ </li>
+ </ul>
+ <span>{{-- Prevents button jumping on menu show --}}</span>
+ </div>
+
+ <button type="submit" id="save-button" class="float-left text-primary text-button text-pos-hover hide-under-m">@icon('save')<span>{{ trans('entities.pages_save') }}</span></button>
+ </div>
+ </div>
+</div>
\ No newline at end of file
@if($model->name === trans('entities.pages_initial_name'))
option:page-editor:has-default-title="true"
@endif
- option:page-editor:editor-type="{{ setting('app-editor') }}"
+ option:page-editor:editor-type="{{ $editor }}"
option:page-editor:page-id="{{ $model->id ?? '0' }}"
- option:page-editor:page-new-draft="{{ ($model->draft ?? false) ? 'true' : 'false' }}"
- option:page-editor:draft-text="{{ ($model->draft || $model->isDraft) ? trans('entities.pages_editing_draft') : trans('entities.pages_editing_page') }}"
+ option:page-editor:page-new-draft="{{ $isDraft ? 'true' : 'false' }}"
+ option:page-editor:draft-text="{{ ($isDraft || $isDraftRevision) ? trans('entities.pages_editing_draft') : trans('entities.pages_editing_page') }}"
option:page-editor:autosave-fail-text="{{ trans('errors.page_draft_autosave_fail') }}"
option:page-editor:editing-page-text="{{ trans('entities.pages_editing_page') }}"
option:page-editor:draft-discarded-text="{{ trans('entities.pages_draft_discarded') }}"
option:page-editor:set-changelog-text="{{ trans('entities.pages_edit_set_changelog') }}">
- {{--Header Bar--}}
- <div class="primary-background-light toolbar page-edit-toolbar">
- <div class="grid third no-break v-center">
-
- <div class="action-buttons text-left px-m py-xs">
- <a href="{{ $page->draft ? $page->getParent()->getUrl() : $page->getUrl() }}"
- class="text-button text-primary">@icon('back')<span class="hide-under-l">{{ trans('common.back') }}</span></a>
- </div>
-
- <div class="text-center px-m py-xs">
- <div component="dropdown"
- option:dropdown:move-menu="true"
- class="dropdown-container draft-display text {{ $draftsEnabled ? '' : 'hidden' }}">
- <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" title="{{ trans('entities.pages_edit_draft_options') }}" class="text-primary text-button"><span refs="page-editor@draftDisplay" class="faded-text"></span> @icon('more')</button>
- @icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', 'refs' => 'page-editor@draftDisplayIcon'])
- <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
- <li>
- <button refs="page-editor@saveDraft" type="button" class="text-pos">@icon('save'){{ trans('entities.pages_edit_save_draft') }}</button>
- </li>
- @if ($model->draft)
- <li>
- <a href="{{ $model->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('entities.pages_edit_delete_draft') }}</a>
- </li>
- @endif
- <li refs="page-editor@discardDraftWrap" class="{{ ($model->isDraft ?? false) ? '' : 'hidden' }}">
- <button refs="page-editor@discardDraft" type="button" class="text-neg">@icon('cancel'){{ trans('entities.pages_edit_discard_draft') }}</button>
- </li>
- </ul>
- </div>
- </div>
-
- <div class="action-buttons px-m py-xs">
- <div component="dropdown" dropdown-move-menu class="dropdown-container">
- <button refs="dropdown@toggle" type="button" aria-haspopup="true" aria-expanded="false" class="text-primary text-button">@icon('edit') <span refs="page-editor@changelogDisplay">{{ trans('entities.pages_edit_set_changelog') }}</span></button>
- <ul refs="dropdown@menu" class="wide dropdown-menu">
- <li class="px-l py-m">
- <p class="text-muted pb-s">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
- <input refs="page-editor@changelogInput"
- name="summary"
- id="summary-input"
- type="text"
- placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" />
- </li>
- </ul>
- <span>{{-- Prevents button jumping on menu show --}}</span>
- </div>
-
- <button type="submit" id="save-button" class="float-left text-primary text-button text-pos-hover hide-under-m">@icon('save')<span>{{ trans('entities.pages_save') }}</span></button>
- </div>
- </div>
- </div>
+ {{--Header Toolbar--}}
+ @include('pages.parts.editor-toolbar', ['model' => $model, 'editor' => $editor, 'isDraft' => $isDraft, 'draftsEnabled' => $draftsEnabled])
{{--Title input--}}
<div class="title-input page-title clearfix">
<div class="edit-area flex-fill flex">
{{--WYSIWYG Editor--}}
- @if(setting('app-editor') === 'wysiwyg')
+ @if($editor === 'wysiwyg')
@include('pages.parts.wysiwyg-editor', ['model' => $model])
@endif
{{--Markdown Editor--}}
- @if(setting('app-editor') === 'markdown')
+ @if($editor === 'markdown')
@include('pages.parts.markdown-editor', ['model' => $model])
@endif
</div>
+ {{--Mobile Save Button--}}
<button type="submit"
id="save-button-mobile"
title="{{ trans('entities.pages_save') }}"
class="text-primary text-button hide-over-m page-save-mobile-button">@icon('save')</button>
+
+ {{--Editor Change Dialog--}}
+ @component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switchDialog'])
+ <p>
+ {{ trans('entities.pages_editor_switch_are_you_sure') }}
+ <br>
+ {{ trans('entities.pages_editor_switch_consider_following') }}
+ </p>
+
+ <ul>
+ <li>{{ trans('entities.pages_editor_switch_consideration_a') }}</li>
+ <li>{{ trans('entities.pages_editor_switch_consideration_b') }}</li>
+ <li>{{ trans('entities.pages_editor_switch_consideration_c') }}</li>
+ </ul>
+ @endcomponent
</div>
\ No newline at end of file
+@push('head')
+ <script src="{{ url('/libs/tinymce/tinymce.min.js?ver=5.10.2') }}" nonce="{{ $cspNonce }}"></script>
+@endpush
+
<div component="wysiwyg-editor"
option:wysiwyg-editor:language="{{ config('app.lang') }}"
option:wysiwyg-editor:page-id="{{ $model->id ?? 0 }}"
<table class="table">
<tr>
- <th width="3%">{{ trans('entities.pages_revisions_number') }}</th>
- <th width="23%">{{ trans('entities.pages_name') }}</th>
- <th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th>
- <th width="15%">{{ trans('entities.pages_revisions_date') }}</th>
- <th width="25%">{{ trans('entities.pages_revisions_changelog') }}</th>
- <th width="20%">{{ trans('common.actions') }}</th>
+ <th width="40">{{ trans('entities.pages_revisions_number') }}</th>
+ <th>
+ {{ trans('entities.pages_name') }} / {{ trans('entities.pages_revisions_editor') }}
+ </th>
+ <th colspan="2">{{ trans('entities.pages_revisions_created_by') }} / {{ trans('entities.pages_revisions_date') }}</th>
+ <th>{{ trans('entities.pages_revisions_changelog') }}</th>
+ <th class="text-right">{{ trans('common.actions') }}</th>
</tr>
@foreach($page->revisions as $index => $revision)
<tr>
<td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
- <td>{{ $revision->name }}</td>
- <td style="line-height: 0;">
+ <td>
+ {{ $revision->name }}
+ <br>
+ <small class="text-muted">({{ $revision->markdown ? 'Markdown' : 'WYSIWYG' }})</small>
+ </td>
+ <td style="line-height: 0;" width="30">
@if($revision->createdBy)
<img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
@endif
</td>
- <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif</td>
- <td><small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
- <td>{{ $revision->summary }}</td>
- <td class="actions">
+ <td width="260">
+ @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
+ <br>
+ <div class="text-muted">
+ <small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }}</small>
+ <small>({{ $revision->created_at->diffForHumans() }})</small>
+ </div>
+ </td>
+ <td>
+ {{ $revision->summary }}
+ </td>
+ <td class="actions text-small text-right">
<a href="{{ $revision->getUrl('changes') }}" target="_blank" rel="noopener">{{ trans('entities.pages_revisions_changes') }}</a>
<span class="text-muted"> | </span>
<form action="{{ $revision->getUrl('/restore') }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
- <button type="submit" class="text-button text-primary">@icon('history'){{ trans('entities.pages_revisions_restore') }}</button>
+ <button type="submit" class="text-primary icon-item">
+ @icon('history')
+ <div>{{ trans('entities.pages_revisions_restore') }}</div>
+ </button>
</form>
</li>
</ul>
<form action="{{ $revision->getUrl('/delete/') }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
- <button type="submit" class="text-button text-neg">@icon('delete'){{ trans('common.delete') }}</button>
+ <button type="submit" class="text-neg icon-item">
+ @icon('delete')
+ <div>{{ trans('common.delete') }}</div>
+ </button>
</form>
</li>
</ul>
<label for="">{{ trans('settings.audit_event_filter') }}</label>
<button refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" class="input-base text-left">{{ $listDetails['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
<ul refs="dropdown@menu" class="dropdown-menu">
- <li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
+ <li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}" class="text-item">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
@foreach($activityTypes as $type)
- <li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}">{{ $type }}</a></li>
+ <li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}" class="text-item">{{ $type }}</a></li>
@endforeach
</ul>
</div>
</div>
</div>
- <div class="grid half gap-xl">
+ <div class="grid half gap-xl items-center">
<div>
- <label class="setting-list-label">{{ trans('settings.app_editor') }}</label>
- <p class="small">{{ trans('settings.app_editor_desc') }}</p>
+ <label class="setting-list-label" for="setting-app-editor">{{ trans('settings.app_default_editor') }}</label>
+ <p class="small">{{ trans('settings.app_default_editor_desc') }}</p>
</div>
- <div class="pt-xs">
+ <div>
<select name="setting-app-editor" id="setting-app-editor">
<option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
<option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
</div>
</div>
- <div homepage-control id="homepage-control" class="grid half gap-xl">
+ <div homepage-control id="homepage-control" class="grid half gap-xl items-center">
<div>
- <label for="setting-app-homepage" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
+ <label for="setting-app-homepage-type" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
<p class="small">{{ trans('settings.app_homepage_desc') }}</p>
</div>
- <div class="pt-xs">
+ <div>
<select name="setting-app-homepage-type" id="setting-app-homepage-type">
<option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
<option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>
<form action="{{ url('/settings/recycle-bin/empty') }}" method="POST">
{!! csrf_field() !!}
- <button type="submit" class="text-primary small delete">{{ trans('common.confirm') }}</button>
+ <button type="submit" class="text-primary small delete text-item">{{ trans('common.confirm') }}</button>
</form>
</div>
</div>
<div component="dropdown" class="dropdown-container">
<button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
<ul refs="dropdown@menu" class="dropdown-menu">
- <li><a class="block" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
- <li><a class="block" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
+ <li><a class="text-item" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
+ <li><a class="text-item" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
</ul>
</div>
</td>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'editor-change', 'label' => trans('settings.role_editor_change')])</div>
</div>
<div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
'tags' => [['name' => 'Category', 'value' => 'Testing']]
];
- $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+ $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+ $resp->assertOk();
+
$page->refresh();
$this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
}
$this->page = Page::query()->first();
}
- public function test_default_editor_is_wysiwyg()
+ public function test_default_editor_is_wysiwyg_for_new_pages()
{
$this->assertEquals('wysiwyg', setting('app-editor'));
- $this->asAdmin()->get($this->page->getUrl() . '/edit')
- ->assertElementExists('#html-editor');
+ $resp = $this->asAdmin()->get($this->page->book->getUrl('/create-page'));
+ $this->followRedirects($resp)->assertElementExists('#html-editor');
}
- public function test_markdown_setting_shows_markdown_editor()
+ public function test_markdown_setting_shows_markdown_editor_for_new_pages()
{
$this->setSettings(['app-editor' => 'markdown']);
- $this->asAdmin()->get($this->page->getUrl() . '/edit')
+
+ $resp = $this->asAdmin()->get($this->page->book->getUrl('/create-page'));
+ $this->followRedirects($resp)
->assertElementNotExists('#html-editor')
->assertElementExists('#markdown-editor');
}
public function test_markdown_content_given_to_editor()
{
- $this->setSettings(['app-editor' => 'markdown']);
-
$mdContent = '# hello. This is a test';
$this->page->markdown = $mdContent;
+ $this->page->editor = 'markdown';
$this->page->save();
- $this->asAdmin()->get($this->page->getUrl() . '/edit')
+ $this->asAdmin()->get($this->page->getUrl('/edit'))
->assertElementContains('[name="markdown"]', $mdContent);
}
public function test_html_content_given_to_editor_if_no_markdown()
{
- $this->setSettings(['app-editor' => 'markdown']);
+ $this->page->editor = 'markdown';
+ $this->page->save();
+
$this->asAdmin()->get($this->page->getUrl() . '/edit')
->assertElementContains('[name="markdown"]', $this->page->html);
}
$resp = $this->get($draft->getUrl('/edit'));
$resp->assertElementContains('a[href="' . $draft->getUrl() . '"]', 'Back');
}
+
+ public function test_switching_from_html_to_clean_markdown_works()
+ {
+ /** @var Page $page */
+ $page = Page::query()->first();
+ $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
+ $page->save();
+
+ $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=markdown-clean'));
+ $resp->assertStatus(200);
+ $resp->assertSee("## A Header\n\nSome **bold** content.");
+ $resp->assertElementExists('#markdown-editor');
+ }
+
+ public function test_switching_from_html_to_stable_markdown_works()
+ {
+ /** @var Page $page */
+ $page = Page::query()->first();
+ $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
+ $page->save();
+
+ $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=markdown-stable'));
+ $resp->assertStatus(200);
+ $resp->assertSee("<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>", true);
+ $resp->assertElementExists('[component="markdown-editor"]');
+ }
+
+ public function test_switching_from_markdown_to_wysiwyg_works()
+ {
+ /** @var Page $page */
+ $page = Page::query()->first();
+ $page->html = '';
+ $page->markdown = "## A Header\n\nSome content with **bold** text!";
+ $page->save();
+
+ $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=wysiwyg'));
+ $resp->assertStatus(200);
+ $resp->assertElementExists('[component="wysiwyg-editor"]');
+ $resp->assertSee("<h2>A Header</h2>\n<p>Some content with <strong>bold</strong> text!</p>", true);
+ }
+
+ public function test_page_editor_changes_with_editor_property()
+ {
+ $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
+ $resp->assertElementExists('[component="wysiwyg-editor"]');
+
+ $this->page->markdown = "## A Header\n\nSome content with **bold** text!";
+ $this->page->editor = 'markdown';
+ $this->page->save();
+
+ $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
+ $resp->assertElementExists('[component="markdown-editor"]');
+ }
+
+ public function test_editor_type_switch_options_show()
+ {
+ $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
+ $editLink = $this->page->getUrl('/edit') . '?editor=';
+ $resp->assertElementContains("a[href=\"${editLink}markdown-clean\"]", '(Clean Content)');
+ $resp->assertElementContains("a[href=\"${editLink}markdown-stable\"]", '(Stable Content)');
+
+ $resp = $this->asAdmin()->get($this->page->getUrl('/edit?editor=markdown-stable'));
+ $editLink = $this->page->getUrl('/edit') . '?editor=';
+ $resp->assertElementContains("a[href=\"${editLink}wysiwyg\"]", 'Switch to WYSIWYG Editor');
+ }
+
+ public function test_editor_type_switch_options_dont_show_if_without_change_editor_permissions()
+ {
+ $resp = $this->asEditor()->get($this->page->getUrl('/edit'));
+ $editLink = $this->page->getUrl('/edit') . '?editor=';
+ $resp->assertElementNotExists("a[href*=\"${editLink}\"]");
+ }
+
+ public function test_page_editor_type_switch_does_not_work_without_change_editor_permissions()
+ {
+ /** @var Page $page */
+ $page = Page::query()->first();
+ $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
+ $page->save();
+
+ $resp = $this->asEditor()->get($page->getUrl('/edit?editor=markdown-stable'));
+ $resp->assertStatus(200);
+ $resp->assertElementExists('[component="wysiwyg-editor"]');
+ $resp->assertElementNotExists('[component="markdown-editor"]');
+ }
+
+ public function test_page_save_does_not_change_active_editor_without_change_editor_permissions()
+ {
+ /** @var Page $page */
+ $page = Page::query()->first();
+ $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
+ $page->editor = 'wysiwyg';
+ $page->save();
+
+ $this->asEditor()->put($page->getUrl(), ['name' => $page->name, 'markdown' => '## Updated content abc']);
+ $this->assertEquals('wysiwyg', $page->refresh()->editor);
+ }
+
}
$revisionCount = $page->revisions()->count();
$this->assertEquals(12, $revisionCount);
}
+
+ public function test_revision_list_shows_editor_type()
+ {
+ /** @var Page $page */
+ $page = Page::first();
+ $this->asAdmin()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html']);
+
+ $resp = $this->get($page->refresh()->getUrl('/revisions'));
+ $resp->assertElementContains('td', '(WYSIWYG)');
+ $resp->assertElementNotContains('td', '(Markdown)');
+
+ $this->asAdmin()->put($page->getUrl(), ['name' => 'Updated page', 'markdown' => '# Some markdown content']);
+ $resp = $this->get($page->refresh()->getUrl('/revisions'));
+ $resp->assertElementContains('td', '(Markdown)');
+ }
}