]> BookStack Code Mirror - bookstack/blob - resources/js/markdown/display.ts
MD Editor: Updated actions to use input interface
[bookstack] / resources / js / markdown / display.ts
1 import { patchDomFromHtmlString } from '../services/vdom';
2 import {MarkdownEditor} from "./index.mjs";
3
4 export class Display {
5     protected editor: MarkdownEditor;
6     protected container: HTMLIFrameElement;
7     protected doc: Document | null = null;
8     protected lastDisplayClick: number = 0;
9
10     constructor(editor: MarkdownEditor) {
11         this.editor = editor;
12         this.container = editor.config.displayEl;
13
14         if (this.container.contentDocument?.readyState === 'complete') {
15             this.onLoad();
16         } else {
17             this.container.addEventListener('load', this.onLoad.bind(this));
18         }
19
20         this.updateVisibility(Boolean(editor.settings.get('showPreview')));
21         editor.settings.onChange('showPreview', (show) => this.updateVisibility(Boolean(show)));
22     }
23
24     protected updateVisibility(show: boolean): void {
25         const wrap = this.container.closest('.markdown-editor-wrap') as HTMLElement;
26         wrap.style.display = show ? '' : 'none';
27     }
28
29     protected onLoad(): void {
30         this.doc = this.container.contentDocument;
31
32         if (!this.doc) return;
33
34         this.loadStylesIntoDisplay();
35         this.doc.body.className = 'page-content';
36
37         // Prevent markdown display link click redirect
38         this.doc.addEventListener('click', this.onDisplayClick.bind(this));
39     }
40
41     protected onDisplayClick(event: MouseEvent): void {
42         const isDblClick = Date.now() - this.lastDisplayClick < 300;
43
44         const link = (event.target as Element).closest('a');
45         if (link !== null) {
46             event.preventDefault();
47             const href = link.getAttribute('href');
48             if (href) {
49                 window.open(href);
50             }
51             return;
52         }
53
54         const drawing = (event.target as Element).closest('[drawio-diagram]') as HTMLElement;
55         if (drawing !== null && isDblClick) {
56             this.editor.actions.editDrawing(drawing);
57             return;
58         }
59
60         this.lastDisplayClick = Date.now();
61     }
62
63     protected loadStylesIntoDisplay(): void {
64         if (!this.doc) return;
65
66         this.doc.documentElement.classList.add('markdown-editor-display');
67
68         // Set display to be dark mode if the parent is
69         if (document.documentElement.classList.contains('dark-mode')) {
70             this.doc.documentElement.style.backgroundColor = '#222';
71             this.doc.documentElement.classList.add('dark-mode');
72         }
73
74         this.doc.head.innerHTML = '';
75         const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
76         for (const style of styles) {
77             const copy = style.cloneNode(true) as HTMLElement;
78             this.doc.head.appendChild(copy);
79         }
80     }
81
82     /**
83      * Patch the display DOM with the given HTML content.
84      */
85     public patchWithHtml(html: string): void {
86         if (!this.doc) return;
87
88         const { body } = this.doc;
89
90         if (body.children.length === 0) {
91             const wrap = document.createElement('div');
92             this.doc.body.append(wrap);
93         }
94
95         const target = body.children[0] as HTMLElement;
96
97         patchDomFromHtmlString(target, html);
98     }
99
100     /**
101      * Scroll to the given block index within the display content.
102      * Will scroll to the end if the index is -1.
103      */
104     public scrollToIndex(index: number): void {
105         const elems = this.doc?.body?.children[0]?.children;
106         if (!elems || elems.length <= index) return;
107
108         const topElem = (index === -1) ? elems[elems.length - 1] : elems[index];
109         (topElem as Element).scrollIntoView({
110             block: 'start',
111             inline: 'nearest',
112             behavior: 'smooth'
113         });
114     }
115 }