'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'),
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);
- }
}
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({
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();
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);
- }
- loadSettings() {
- const settings = {};
- const inputs = this.settingContainer.querySelectorAll('input[type="hidden"]');
+ this.handleDividerDrag();
+ }
- for (const input of inputs) {
- settings[input.getAttribute('name')] = input.value === 'true';
+ 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();
+ };
+
+ 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() {
const editor = {
config,
markdown: new Markdown(),
- settings: new Settings(config.settings),
+ settings: new Settings(config.settingInputs),
};
editor.actions = new Actions(editor);
* @property {Element} displayEl
* @property {HTMLTextAreaElement} inputEl
* @property {String} drawioUrl
+ * @property {HTMLInputElement[]} settingInputs
* @property {Object<String, String>} text
- * @property {Object<String, any>} settings
*/
/**
-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;
}
}
\ No newline at end of file
outline: 0;
}
}
- .markdown-display, .markdown-editor-wrap {
- flex: 1;
- position: relative;
- }
&.fullscreen {
position: fixed;
top: 0;
}
.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 {
- border-inline-start: 1px solid;
- @include lightDark(border-color, #ddd, #000);
+ flex-basis: 50%;
+ flex-shrink: 0;
+ flex-grow: 0;
+}
+
+.markdown-panel-divider {
+ width: 2px;
+ @include lightDark(background-color, #ddd, #000);
+ cursor: col-resize;
}
@include smaller-than($m) {
width: 100%;
max-width: 100%;
flex-grow: 1;
+ flex-basis: auto !important;
}
.editor-toolbar-label {
float: none !important;
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>
<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>
</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>
$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);
- }
}