]> BookStack Code Mirror - bookstack/blob - resources/js/components/markdown-editor.js
Added content-perms API examples and docs tweaks
[bookstack] / resources / js / components / markdown-editor.js
1 import {debounce} from "../services/util";
2 import {Component} from "./component";
3 import {init as initEditor} from "../markdown/editor";
4
5 export class MarkdownEditor extends Component {
6
7     setup() {
8         this.elem = this.$el;
9
10         this.pageId = this.$opts.pageId;
11         this.textDirection = this.$opts.textDirection;
12         this.imageUploadErrorText = this.$opts.imageUploadErrorText;
13         this.serverUploadLimitText = this.$opts.serverUploadLimitText;
14
15         this.display = this.$refs.display;
16         this.input = this.$refs.input;
17         this.divider = this.$refs.divider;
18         this.displayWrap = this.$refs.displayWrap;
19
20         const settingContainer = this.$refs.settingContainer;
21         const settingInputs = settingContainer.querySelectorAll('input[type="checkbox"]');
22
23         this.editor = null;
24         initEditor({
25             pageId: this.pageId,
26             container: this.elem,
27             displayEl: this.display,
28             inputEl: this.input,
29             drawioUrl: this.getDrawioUrl(),
30             settingInputs: Array.from(settingInputs),
31             text: {
32                 serverUploadLimit: this.serverUploadLimitText,
33                 imageUploadError: this.imageUploadErrorText,
34             },
35         }).then(editor => {
36             this.editor = editor;
37             this.setupListeners();
38             this.emitEditorEvents();
39             this.scrollToTextIfNeeded();
40             this.editor.actions.updateAndRender();
41         });
42     }
43
44     emitEditorEvents() {
45         window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
46             markdownIt: this.editor.markdown.getRenderer(),
47             displayEl: this.display,
48             codeMirrorInstance: this.editor.cm,
49         });
50     }
51
52     setupListeners() {
53
54         // Button actions
55         this.elem.addEventListener('click', event => {
56             let button = event.target.closest('button[data-action]');
57             if (button === null) return;
58
59             const action = button.getAttribute('data-action');
60             if (action === 'insertImage') this.editor.actions.insertImage();
61             if (action === 'insertLink') this.editor.actions.showLinkSelector();
62             if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
63                 this.editor.actions.showImageManager();
64                 return;
65             }
66             if (action === 'insertDrawing') this.editor.actions.startDrawing();
67             if (action === 'fullscreen') this.editor.actions.fullScreen();
68         });
69
70         // Mobile section toggling
71         this.elem.addEventListener('click', event => {
72             const toolbarLabel = event.target.closest('.editor-toolbar-label');
73             if (!toolbarLabel) return;
74
75             const currentActiveSections = this.elem.querySelectorAll('.markdown-editor-wrap');
76             for (const activeElem of currentActiveSections) {
77                 activeElem.classList.remove('active');
78             }
79
80             toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
81         });
82
83         // Refresh CodeMirror on container resize
84         const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false);
85         const observer = new ResizeObserver(resizeDebounced);
86         observer.observe(this.elem);
87
88         this.handleDividerDrag();
89     }
90
91     handleDividerDrag() {
92         this.divider.addEventListener('pointerdown', event => {
93             const wrapRect = this.elem.getBoundingClientRect();
94             const moveListener = (event) => {
95                 const xRel = event.pageX - wrapRect.left;
96                 const xPct = Math.min(Math.max(20, Math.floor((xRel / wrapRect.width) * 100)), 80);
97                 this.displayWrap.style.flexBasis = `${100-xPct}%`;
98                 this.editor.settings.set('editorWidth', xPct);
99             };
100             const upListener = (event) => {
101                 window.removeEventListener('pointermove', moveListener);
102                 window.removeEventListener('pointerup', upListener);
103                 this.display.style.pointerEvents = null;
104                 document.body.style.userSelect = null;
105                 this.editor.cm.refresh();
106             };
107
108             this.display.style.pointerEvents = 'none';
109             document.body.style.userSelect = 'none';
110             window.addEventListener('pointermove', moveListener);
111             window.addEventListener('pointerup', upListener);
112         });
113         const widthSetting = this.editor.settings.get('editorWidth');
114         if (widthSetting) {
115             this.displayWrap.style.flexBasis = `${100-widthSetting}%`;
116         }
117     }
118
119     scrollToTextIfNeeded() {
120         const queryParams = (new URL(window.location)).searchParams;
121         const scrollText = queryParams.get('content-text');
122         if (scrollText) {
123             this.editor.actions.scrollToText(scrollText);
124         }
125     }
126
127     /**
128      * Get the URL for the configured drawio instance.
129      * @returns {String}
130      */
131     getDrawioUrl() {
132         const drawioAttrEl = document.querySelector('[drawio-url]');
133         if (!drawioAttrEl) {
134             return '';
135         }
136
137         return drawioAttrEl.getAttribute('drawio-url') || '';
138     }
139
140     /**
141      * Get the content of this editor.
142      * Used by the parent page editor component.
143      * @return {{html: String, markdown: String}}
144      */
145     getContent() {
146         return this.editor.actions.getContent();
147     }
148
149 }