]> BookStack Code Mirror - bookstack/blob - resources/js/components/code-editor.js
0188eb250e2cec33cc8d60c929e0c795245cd725
[bookstack] / resources / js / components / code-editor.js
1 import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
2 import {Component} from "./component";
3
4
5 export class CodeEditor extends Component {
6
7     /**
8      * @type {null|SimpleEditorInterface}
9      */
10     editor = null;
11
12     callback = null;
13     history = {};
14     historyKey = 'code_history';
15
16     setup() {
17         this.container = this.$refs.container;
18         this.popup = this.$el;
19         this.editorInput = this.$refs.editor;
20         this.languageButtons = this.$manyRefs.languageButton;
21         this.languageOptionsContainer = this.$refs.languageOptionsContainer;
22         this.saveButton = this.$refs.saveButton;
23         this.languageInput = this.$refs.languageInput;
24         this.historyDropDown = this.$refs.historyDropDown;
25         this.historyList = this.$refs.historyList;
26         this.favourites = new Set(this.$opts.favourites.split(','));
27
28         this.setupListeners();
29         this.setupFavourites();
30     }
31
32     setupListeners() {
33         this.container.addEventListener('keydown', event => {
34             if (event.ctrlKey && event.key === 'Enter') {
35                 this.save();
36             }
37         });
38
39         onSelect(this.languageButtons, event => {
40             const language = event.target.dataset.lang;
41             this.languageInput.value = language;
42             this.languageInputChange(language);
43         });
44
45         onEnterPress(this.languageInput, e => this.save());
46         this.languageInput.addEventListener('input', e => this.languageInputChange(this.languageInput.value));
47         onSelect(this.saveButton, e => this.save());
48
49         onChildEvent(this.historyList, 'button', 'click', (event, elem) => {
50             event.preventDefault();
51             const historyTime = elem.dataset.time;
52             if (this.editor) {
53                 this.editor.setContent(this.history[historyTime]);
54             }
55         });
56     }
57
58     setupFavourites() {
59         for (const button of this.languageButtons) {
60             this.setupFavouritesForButton(button);
61         }
62
63         this.sortLanguageList();
64     }
65
66     /**
67      * @param {HTMLButtonElement} button
68      */
69     setupFavouritesForButton(button) {
70         const language = button.dataset.lang;
71         let isFavorite = this.favourites.has(language);
72         button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
73
74         onChildEvent(button.parentElement, '.lang-option-favorite-toggle', 'click', () => {
75             isFavorite = !isFavorite;
76             isFavorite ? this.favourites.add(language) : this.favourites.delete(language);
77             button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
78
79             window.$http.patch('/preferences/update-code-language-favourite', {
80                 language: language,
81                 active: isFavorite
82             });
83
84             this.sortLanguageList();
85             if (isFavorite) {
86                 button.scrollIntoView({block: "center", behavior: "smooth"});
87             }
88         });
89     }
90
91     sortLanguageList() {
92         const sortedParents = this.languageButtons.sort((a, b) => {
93             const aFav = a.dataset.favourite === 'true';
94             const bFav = b.dataset.favourite === 'true';
95
96             if (aFav && !bFav) {
97                 return -1;
98             } else if (bFav && !aFav) {
99                 return 1;
100             }
101
102             return a.dataset.lang > b.dataset.lang ? 1 : -1;
103         }).map(button => button.parentElement);
104
105         for (const parent of sortedParents) {
106             this.languageOptionsContainer.append(parent);
107         }
108     }
109
110     save() {
111         if (this.callback) {
112             this.callback(this.editor.getContent(), this.languageInput.value);
113         }
114         this.hide();
115     }
116
117     async open(code, language, callback) {
118         this.languageInput.value = language;
119         this.callback = callback;
120
121         await this.show();
122         this.languageInputChange(language);
123         this.editor.setContent(code);
124     }
125
126     async show() {
127         const Code = await window.importVersioned('code');
128         if (!this.editor) {
129             this.editor = Code.popupEditor(this.editorInput, this.languageInput.value);
130         }
131
132         this.loadHistory();
133         this.getPopup().show(() => {
134             this.editor.focus();
135         }, () => {
136             this.addHistory()
137         });
138     }
139
140     hide() {
141         this.getPopup().hide();
142         this.addHistory();
143     }
144
145     /**
146      * @returns {Popup}
147      */
148     getPopup() {
149         return window.$components.firstOnElement(this.popup, 'popup');
150     }
151
152     async updateEditorMode(language) {
153         this.editor.setMode(language, this.editor.getContent());
154     }
155
156     languageInputChange(language) {
157         this.updateEditorMode(language);
158         const inputLang = language.toLowerCase();
159
160         for (const link of this.languageButtons) {
161             const lang = link.dataset.lang.toLowerCase().trim();
162             const isMatch = inputLang === lang;
163             link.classList.toggle('active', isMatch);
164             if (isMatch) {
165                 link.scrollIntoView({block: "center", behavior: "smooth"});
166             }
167         }
168     }
169
170     loadHistory() {
171         this.history = JSON.parse(window.sessionStorage.getItem(this.historyKey) || '{}');
172         const historyKeys = Object.keys(this.history).reverse();
173         this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0);
174         this.historyList.innerHTML = historyKeys.map(key => {
175              const localTime = (new Date(parseInt(key))).toLocaleTimeString();
176              return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
177         }).join('');
178     }
179
180     addHistory() {
181         if (!this.editor) return;
182         const code = this.editor.getContent();
183         if (!code) return;
184
185         // Stop if we'd be storing the same as the last item
186         const lastHistoryKey = Object.keys(this.history).pop();
187         if (this.history[lastHistoryKey] === code) return;
188
189         this.history[String(Date.now())] = code;
190         const historyString = JSON.stringify(this.history);
191         window.sessionStorage.setItem(this.historyKey, historyString);
192     }
193
194 }