mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-01-19 08:42:48 +08:00
Converted md settings to localstorage, added preview resize
This commit is contained in:
parent
38db3a28ea
commit
31c28be57a
|
@ -29,8 +29,6 @@ return [
|
|||
'ui-shortcuts' => '{}',
|
||||
'ui-shortcuts-enabled' => false,
|
||||
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
|
||||
'md-show-preview' => true,
|
||||
'md-scroll-sync' => true,
|
||||
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||
'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'),
|
||||
'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'),
|
||||
|
|
|
@ -139,25 +139,4 @@ class UserPreferencesController extends Controller
|
|||
setting()->putForCurrentUser('code-language-favourites', implode(',', $currentFavorites));
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a boolean user preference setting.
|
||||
*/
|
||||
public function updateBooleanPreference(Request $request)
|
||||
{
|
||||
$allowedKeys = ['md-scroll-sync', 'md-show-preview'];
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['required', 'string'],
|
||||
'value' => ['required'],
|
||||
]);
|
||||
|
||||
if (!in_array($validated['name'], $allowedKeys)) {
|
||||
return response('Invalid boolean preference', 500);
|
||||
}
|
||||
|
||||
$value = $validated['value'] === 'true' ? 'true' : 'false';
|
||||
setting()->putForCurrentUser($validated['name'], $value);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,11 @@ export class MarkdownEditor extends Component {
|
|||
|
||||
this.display = this.$refs.display;
|
||||
this.input = this.$refs.input;
|
||||
this.settingContainer = this.$refs.settingContainer;
|
||||
this.divider = this.$refs.divider;
|
||||
this.displayWrap = this.$refs.displayWrap;
|
||||
|
||||
const settingContainer = this.$refs.settingContainer;
|
||||
const settingInputs = settingContainer.querySelectorAll('input[type="checkbox"]');
|
||||
|
||||
this.editor = null;
|
||||
initEditor({
|
||||
|
@ -23,11 +27,11 @@ export class MarkdownEditor extends Component {
|
|||
displayEl: this.display,
|
||||
inputEl: this.input,
|
||||
drawioUrl: this.getDrawioUrl(),
|
||||
settingInputs: Array.from(settingInputs),
|
||||
text: {
|
||||
serverUploadLimit: this.serverUploadLimitText,
|
||||
imageUploadError: this.imageUploadErrorText,
|
||||
},
|
||||
settings: this.loadSettings(),
|
||||
}).then(editor => {
|
||||
this.editor = editor;
|
||||
this.setupListeners();
|
||||
|
@ -76,30 +80,40 @@ export class MarkdownEditor extends Component {
|
|||
toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
|
||||
});
|
||||
|
||||
// Setting changes
|
||||
this.settingContainer.addEventListener('change', e => {
|
||||
const actualInput = e.target.parentNode.querySelector('input[type="hidden"]');
|
||||
const name = actualInput.getAttribute('name');
|
||||
const value = actualInput.getAttribute('value');
|
||||
window.$http.patch('/preferences/update-boolean', {name, value});
|
||||
this.editor.settings.set(name, value === 'true');
|
||||
});
|
||||
|
||||
// Refresh CodeMirror on container resize
|
||||
const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false);
|
||||
const observer = new ResizeObserver(resizeDebounced);
|
||||
observer.observe(this.elem);
|
||||
|
||||
this.handleDividerDrag();
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const settings = {};
|
||||
const inputs = this.settingContainer.querySelectorAll('input[type="hidden"]');
|
||||
handleDividerDrag() {
|
||||
this.divider.addEventListener('pointerdown', event => {
|
||||
const wrapRect = this.elem.getBoundingClientRect();
|
||||
const moveListener = (event) => {
|
||||
const xRel = event.pageX - wrapRect.left;
|
||||
const xPct = Math.min(Math.max(20, Math.floor((xRel / wrapRect.width) * 100)), 80);
|
||||
this.displayWrap.style.flexBasis = `${100-xPct}%`;
|
||||
this.editor.settings.set('editorWidth', xPct);
|
||||
};
|
||||
const upListener = (event) => {
|
||||
window.removeEventListener('pointermove', moveListener);
|
||||
window.removeEventListener('pointerup', upListener);
|
||||
this.display.style.pointerEvents = null;
|
||||
document.body.style.userSelect = null;
|
||||
this.editor.cm.refresh();
|
||||
};
|
||||
|
||||
for (const input of inputs) {
|
||||
settings[input.getAttribute('name')] = input.value === 'true';
|
||||
this.display.style.pointerEvents = 'none';
|
||||
document.body.style.userSelect = 'none';
|
||||
window.addEventListener('pointermove', moveListener);
|
||||
window.addEventListener('pointerup', upListener);
|
||||
});
|
||||
const widthSetting = this.editor.settings.get('editorWidth');
|
||||
if (widthSetting) {
|
||||
this.displayWrap.style.flexBasis = `${100-widthSetting}%`;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
scrollToTextIfNeeded() {
|
||||
|
|
|
@ -19,7 +19,7 @@ export async function init(config) {
|
|||
const editor = {
|
||||
config,
|
||||
markdown: new Markdown(),
|
||||
settings: new Settings(config.settings),
|
||||
settings: new Settings(config.settingInputs),
|
||||
};
|
||||
|
||||
editor.actions = new Actions(editor);
|
||||
|
@ -39,8 +39,8 @@ export async function init(config) {
|
|||
* @property {Element} displayEl
|
||||
* @property {HTMLTextAreaElement} inputEl
|
||||
* @property {String} drawioUrl
|
||||
* @property {HTMLInputElement[]} settingInputs
|
||||
* @property {Object<String, String>} text
|
||||
* @property {Object<String, any>} settings
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,40 +1,62 @@
|
|||
import {kebabToCamel} from "../services/text";
|
||||
|
||||
|
||||
export class Settings {
|
||||
|
||||
constructor(initialSettings) {
|
||||
this.settingMap = {};
|
||||
constructor(settingInputs) {
|
||||
this.settingMap = {
|
||||
scrollSync: true,
|
||||
showPreview: true,
|
||||
editorWidth: 50,
|
||||
};
|
||||
this.changeListeners = {};
|
||||
this.merge(initialSettings);
|
||||
this.loadFromLocalStorage();
|
||||
this.applyToInputs(settingInputs);
|
||||
this.listenToInputChanges(settingInputs);
|
||||
}
|
||||
|
||||
applyToInputs(inputs) {
|
||||
for (const input of inputs) {
|
||||
const name = input.getAttribute('name').replace('md-', '');
|
||||
input.checked = this.settingMap[name];
|
||||
}
|
||||
}
|
||||
|
||||
listenToInputChanges(inputs) {
|
||||
for (const input of inputs) {
|
||||
input.addEventListener('change', event => {
|
||||
const name = input.getAttribute('name').replace('md-', '');
|
||||
this.set(name, input.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadFromLocalStorage() {
|
||||
const lsValString = window.localStorage.getItem('md-editor-settings');
|
||||
if (!lsValString) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lsVals = JSON.parse(lsValString);
|
||||
for (const [key, value] of Object.entries(lsVals)) {
|
||||
if (value !== null && this.settingMap[key] !== undefined) {
|
||||
this.settingMap[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
key = this.normaliseKey(key);
|
||||
this.settingMap[key] = value;
|
||||
window.localStorage.setItem('md-editor-settings', JSON.stringify(this.settingMap));
|
||||
for (const listener of (this.changeListeners[key] || [])) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.settingMap[this.normaliseKey(key)] || null;
|
||||
}
|
||||
|
||||
merge(settings) {
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
this.set(key, value);
|
||||
}
|
||||
return this.settingMap[key] || null;
|
||||
}
|
||||
|
||||
onChange(key, callback) {
|
||||
key = this.normaliseKey(key);
|
||||
const listeners = this.changeListeners[this.normaliseKey(key)] || [];
|
||||
const listeners = this.changeListeners[key] || [];
|
||||
listeners.push(callback);
|
||||
this.changeListeners[this.normaliseKey(key)] = listeners;
|
||||
}
|
||||
|
||||
normaliseKey(key) {
|
||||
return kebabToCamel(key.replace('md-', ''));
|
||||
this.changeListeners[key] = listeners;
|
||||
}
|
||||
}
|
|
@ -60,10 +60,6 @@
|
|||
outline: 0;
|
||||
}
|
||||
}
|
||||
.markdown-display, .markdown-editor-wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
&.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -74,17 +70,22 @@
|
|||
}
|
||||
|
||||
.markdown-editor-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid #DDD;
|
||||
border-bottom: 1px solid #DDD;
|
||||
@include lightDark(border-color, #ddd, #000);
|
||||
width: 50%;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.markdown-editor-wrap + .markdown-editor-wrap {
|
||||
flex-basis: 50%;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.markdown-editor-wrap + .markdown-editor-wrap {
|
||||
border-inline-start: 1px solid;
|
||||
@include lightDark(border-color, #ddd, #000);
|
||||
.markdown-panel-divider {
|
||||
width: 2px;
|
||||
@include lightDark(background-color, #ddd, #000);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
@include smaller-than($m) {
|
||||
|
@ -95,6 +96,7 @@
|
|||
width: 100%;
|
||||
max-width: 100%;
|
||||
flex-grow: 1;
|
||||
flex-basis: auto !important;
|
||||
}
|
||||
.editor-toolbar-label {
|
||||
float: none !important;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
option:markdown-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
|
||||
class="flex-fill flex code-fill">
|
||||
|
||||
<div class="markdown-editor-wrap active">
|
||||
<div class="markdown-editor-wrap active flex-container-column">
|
||||
<div class="editor-toolbar flex-container-row items-stretch justify-space-between">
|
||||
<div class="editor-toolbar-label text-mono px-m py-xs flex-container-row items-center flex">
|
||||
<span>{{ trans('entities.pages_md_editor') }}</span>
|
||||
|
@ -20,11 +20,11 @@
|
|||
<button refs="dropdown@toggle" class="text-button" type="button" title="{{ trans('common.more') }}">@icon('more')</button>
|
||||
<div refs="dropdown@menu markdown-editor@setting-container" class="dropdown-menu" role="menu">
|
||||
<div class="px-m">
|
||||
@include('form.toggle-switch', ['name' => 'md-show-preview', 'label' => trans('entities.pages_md_show_preview'), 'value' => setting()->getForCurrentUser('md-show-preview')])
|
||||
@include('form.custom-checkbox', ['name' => 'md-showPreview', 'label' => trans('entities.pages_md_show_preview'), 'value' => true, 'checked' => true])
|
||||
</div>
|
||||
<hr class="m-none">
|
||||
<div class="px-m">
|
||||
@include('form.toggle-switch', ['name' => 'md-scroll-sync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => setting()->getForCurrentUser('md-scroll-sync')])
|
||||
@include('form.custom-checkbox', ['name' => 'md-scrollSync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => true, 'checked' => true])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,14 +40,17 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div class="markdown-editor-wrap" @if(!setting()->getForCurrentUser('md-show-preview')) style="display: none;" @endif>
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-toolbar-label text-mono px-m py-xs">{{ trans('entities.pages_md_preview') }}</div>
|
||||
<div refs="markdown-editor@display-wrap" class="markdown-editor-wrap flex-container-row items-stretch" style="display: none">
|
||||
<div refs="markdown-editor@divider" class="markdown-panel-divider flex-fill"></div>
|
||||
<div class="flex-container-column flex flex-fill">
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-toolbar-label text-mono px-m py-xs">{{ trans('entities.pages_md_preview') }}</div>
|
||||
</div>
|
||||
<iframe src="about:blank"
|
||||
refs="markdown-editor@display"
|
||||
class="markdown-display flex flex-fill"
|
||||
sandbox="allow-same-origin"></iframe>
|
||||
</div>
|
||||
<iframe src="about:blank"
|
||||
refs="markdown-editor@display"
|
||||
class="markdown-display"
|
||||
sandbox="allow-same-origin"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -191,22 +191,4 @@ class UserPreferencesTest extends TestCase
|
|||
$resp = $this->get($page->getUrl('/edit'));
|
||||
$resp->assertSee('option:code-editor:favourites="javascript,ruby"', false);
|
||||
}
|
||||
|
||||
public function test_update_boolean()
|
||||
{
|
||||
$editor = $this->getEditor();
|
||||
|
||||
$this->assertTrue(setting()->getUser($editor, 'md-show-preview'));
|
||||
|
||||
$resp = $this->actingAs($editor)->patch('/preferences/update-boolean', ['name' => 'md-show-preview', 'value' => 'false']);
|
||||
$resp->assertStatus(204);
|
||||
|
||||
$this->assertFalse(setting()->getUser($editor, 'md-show-preview'));
|
||||
}
|
||||
|
||||
public function test_update_boolean_rejects_unfamiliar_key()
|
||||
{
|
||||
$resp = $this->asEditor()->patch('/preferences/update-boolean', ['name' => 'md-donkey-show', 'value' => 'false']);
|
||||
$resp->assertStatus(500);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user