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