diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js index 162dd6771..373fedf48 100644 --- a/resources/js/components/markdown-editor.js +++ b/resources/js/components/markdown-editor.js @@ -26,7 +26,8 @@ export class MarkdownEditor extends Component { text: { serverUploadLimit: this.serverUploadLimitText, imageUploadError: this.imageUploadErrorText, - } + }, + settings: this.loadSettings(), }).then(editor => { this.editor = editor; this.setupListeners(); @@ -81,7 +82,7 @@ export class MarkdownEditor extends Component { const name = actualInput.getAttribute('name'); const value = actualInput.getAttribute('value'); window.$http.patch('/preferences/update-boolean', {name, value}); - // TODO - Update state locally + this.editor.settings.set(name, value === 'true'); }); // Refresh CodeMirror on container resize @@ -90,6 +91,17 @@ export class MarkdownEditor extends Component { observer.observe(this.elem); } + loadSettings() { + const settings = {}; + const inputs = this.settingContainer.querySelectorAll('input[type="hidden"]'); + + for (const input of inputs) { + settings[input.getAttribute('name')] = input.value === 'true'; + } + + return settings; + } + scrollToTextIfNeeded() { const queryParams = (new URL(window.location)).searchParams; const scrollText = queryParams.get('content-text'); diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index 06860b929..8724a23c8 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -24,7 +24,13 @@ export async function init(editor) { // Handle scroll to sync display view const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false); - cm.on('scroll', instance => onScrollDebounced(instance)); + let syncActive = editor.settings.get('scrollSync'); + editor.settings.onChange('scrollSync', val => syncActive = val); + cm.on('scroll', instance => { + if (syncActive) { + onScrollDebounced(instance); + } + }); // Handle image paste cm.on('paste', (cm, event) => { diff --git a/resources/js/markdown/display.js b/resources/js/markdown/display.js index 7e1925431..2c78da189 100644 --- a/resources/js/markdown/display.js +++ b/resources/js/markdown/display.js @@ -17,6 +17,14 @@ export class Display { } else { this.container.addEventListener('load', this.onLoad.bind(this)); } + + this.updateVisibility(editor.settings.get('showPreview')); + editor.settings.onChange('showPreview', show => this.updateVisibility(show)); + } + + updateVisibility(show) { + const wrap = this.container.closest('.markdown-editor-wrap'); + wrap.style.display = show ? null : 'none'; } onLoad() { diff --git a/resources/js/markdown/editor.js b/resources/js/markdown/editor.js index decebe5f4..f2e34b9f0 100644 --- a/resources/js/markdown/editor.js +++ b/resources/js/markdown/editor.js @@ -1,6 +1,7 @@ import {Markdown} from "./markdown"; import {Display} from "./display"; import {Actions} from "./actions"; +import {Settings} from "./settings"; import {listen} from "./common-events"; import {init as initCodemirror} from "./codemirror"; @@ -18,6 +19,7 @@ export async function init(config) { const editor = { config, markdown: new Markdown(), + settings: new Settings(config.settings), }; editor.actions = new Actions(editor); @@ -38,6 +40,7 @@ export async function init(config) { * @property {HTMLTextAreaElement} inputEl * @property {String} drawioUrl * @property {Object<String, String>} text + * @property {Object<String, any>} settings */ /** @@ -47,4 +50,5 @@ export async function init(config) { * @property {Markdown} markdown * @property {Actions} actions * @property {CodeMirror} cm + * @property {Settings} settings */ \ No newline at end of file diff --git a/resources/js/markdown/settings.js b/resources/js/markdown/settings.js new file mode 100644 index 000000000..6dd142210 --- /dev/null +++ b/resources/js/markdown/settings.js @@ -0,0 +1,40 @@ +import {kebabToCamel} from "../services/text"; + + +export class Settings { + + constructor(initialSettings) { + this.settingMap = {}; + this.changeListeners = {}; + this.merge(initialSettings); + } + + set(key, value) { + key = this.normaliseKey(key); + this.settingMap[key] = value; + 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); + } + } + + onChange(key, callback) { + key = this.normaliseKey(key); + const listeners = this.changeListeners[this.normaliseKey(key)] || []; + listeners.push(callback); + this.changeListeners[this.normaliseKey(key)] = listeners; + } + + normaliseKey(key) { + return kebabToCamel(key.replace('md-', '')); + } +} \ No newline at end of file diff --git a/resources/js/services/components.js b/resources/js/services/components.js index 7434f6430..d1503db4d 100644 --- a/resources/js/services/components.js +++ b/resources/js/services/components.js @@ -1,3 +1,5 @@ +import {kebabToCamel, camelToKebab} from "./text"; + /** * A mapping of active components keyed by name, with values being arrays of component * instances since there can be multiple components of the same type. @@ -107,17 +109,6 @@ function parseOpts(name, element) { return opts; } -/** - * Convert a kebab-case string to camelCase - * @param {String} kebab - * @returns {string} - */ -function kebabToCamel(kebab) { - const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1); - const words = kebab.split('-'); - return words[0] + words.slice(1).map(ucFirst).join(''); -} - /** * Initialize all components found within the given element. * @param {Element|Document} parentElement @@ -171,8 +162,4 @@ export function get(name) { export function firstOnElement(element, name) { const elComponents = elementComponentMap.get(element) || {}; return elComponents[name] || null; -} - -function camelToKebab(camelStr) { - return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase()); } \ No newline at end of file diff --git a/resources/js/services/text.js b/resources/js/services/text.js new file mode 100644 index 000000000..ea82f993e --- /dev/null +++ b/resources/js/services/text.js @@ -0,0 +1,19 @@ +/** + * Convert a kebab-case string to camelCase + * @param {String} kebab + * @returns {string} + */ +export function kebabToCamel(kebab) { + const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1); + const words = kebab.split('-'); + return words[0] + words.slice(1).map(ucFirst).join(''); +} + +/** + * Convert a camelCase string to a kebab-case string. + * @param {String} camelStr + * @returns {String} + */ +export function camelToKebab(camelStr) { + return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase()); +} \ No newline at end of file diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 978e67464..fa2586f8d 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -224,6 +224,8 @@ return [ 'pages_md_insert_image' => 'Insert Image', 'pages_md_insert_link' => 'Insert Entity Link', 'pages_md_insert_drawing' => 'Insert Drawing', + 'pages_md_show_preview' => 'Show preview', + 'pages_md_sync_scroll' => 'Sync preview scroll', 'pages_not_in_chapter' => 'Page is not in a chapter', 'pages_move' => 'Move Page', 'pages_move_success' => 'Page moved to ":parentName"', diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 7d92d0aa0..e4331a03f 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -80,7 +80,6 @@ border-bottom: 1px solid #DDD; @include lightDark(border-color, #ddd, #000); width: 50%; - max-width: 50%; } .markdown-editor-wrap + .markdown-editor-wrap { @@ -97,12 +96,6 @@ max-width: 100%; flex-grow: 1; } - #markdown-editor .editor-toolbar { - padding: 0; - } - #markdown-editor .editor-toolbar > * { - padding: $-xs $-s; - } .editor-toolbar-label { float: none !important; @include lightDark(border-color, #DDD, #555); diff --git a/resources/views/pages/parts/markdown-editor.blade.php b/resources/views/pages/parts/markdown-editor.blade.php index 41ee3933f..f90a2f54c 100644 --- a/resources/views/pages/parts/markdown-editor.blade.php +++ b/resources/views/pages/parts/markdown-editor.blade.php @@ -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' => 'Show preview', 'value' => setting()->getForCurrentUser('md-show-preview')]) + @include('form.toggle-switch', ['name' => 'md-show-preview', 'label' => trans('entities.pages_md_show_preview'), 'value' => setting()->getForCurrentUser('md-show-preview')]) </div> <hr class="m-none"> <div class="px-m"> - @include('form.toggle-switch', ['name' => 'md-scroll-sync', 'label' => 'Sync preview scroll', 'value' => setting()->getForCurrentUser('md-scroll-sync')]) + @include('form.toggle-switch', ['name' => 'md-scroll-sync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => setting()->getForCurrentUser('md-scroll-sync')]) </div> </div> </div> @@ -40,7 +40,7 @@ </div> - <div class="markdown-editor-wrap"> + <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>