mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-12-02 13:58:57 +08:00
f9e087330b
Editor popup will now reflect the direction of the opened code block. This also updates in-editor codemirror instances to correcly reflect/use the direction if set on the inner code elem. This also defaults new code blocks, when in RTL languages, to be started in LTR, which can then be changed via in-editor direction controls if needed. This is on the assumption that most code will be LTR (could not find much examples of RTL code use). Fixes #4943
224 lines
6.7 KiB
JavaScript
224 lines
6.7 KiB
JavaScript
import {onChildEvent, onEnterPress, onSelect} from '../services/dom';
|
|
import {Component} from './component';
|
|
|
|
export class CodeEditor extends Component {
|
|
|
|
/**
|
|
* @type {null|SimpleEditorInterface}
|
|
*/
|
|
editor = null;
|
|
|
|
/**
|
|
* @type {?Function}
|
|
*/
|
|
saveCallback = null;
|
|
|
|
/**
|
|
* @type {?Function}
|
|
*/
|
|
cancelCallback = null;
|
|
|
|
history = {};
|
|
|
|
historyKey = 'code_history';
|
|
|
|
setup() {
|
|
this.container = this.$refs.container;
|
|
this.popup = this.$el;
|
|
this.editorInput = this.$refs.editor;
|
|
this.languageButtons = this.$manyRefs.languageButton;
|
|
this.languageOptionsContainer = this.$refs.languageOptionsContainer;
|
|
this.saveButton = this.$refs.saveButton;
|
|
this.languageInput = this.$refs.languageInput;
|
|
this.historyDropDown = this.$refs.historyDropDown;
|
|
this.historyList = this.$refs.historyList;
|
|
this.favourites = new Set(this.$opts.favourites.split(','));
|
|
|
|
this.setupListeners();
|
|
this.setupFavourites();
|
|
}
|
|
|
|
setupListeners() {
|
|
this.container.addEventListener('keydown', event => {
|
|
if (event.ctrlKey && event.key === 'Enter') {
|
|
this.save();
|
|
}
|
|
});
|
|
|
|
onSelect(this.languageButtons, event => {
|
|
const language = event.target.dataset.lang;
|
|
this.languageInput.value = language;
|
|
this.languageInputChange(language);
|
|
});
|
|
|
|
onEnterPress(this.languageInput, () => this.save());
|
|
this.languageInput.addEventListener('input', () => this.languageInputChange(this.languageInput.value));
|
|
onSelect(this.saveButton, () => this.save());
|
|
|
|
onChildEvent(this.historyList, 'button', 'click', (event, elem) => {
|
|
event.preventDefault();
|
|
const historyTime = elem.dataset.time;
|
|
if (this.editor) {
|
|
this.editor.setContent(this.history[historyTime]);
|
|
}
|
|
});
|
|
}
|
|
|
|
setupFavourites() {
|
|
for (const button of this.languageButtons) {
|
|
this.setupFavouritesForButton(button);
|
|
}
|
|
|
|
this.sortLanguageList();
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLButtonElement} button
|
|
*/
|
|
setupFavouritesForButton(button) {
|
|
const language = button.dataset.lang;
|
|
let isFavorite = this.favourites.has(language);
|
|
button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
|
|
|
|
onChildEvent(button.parentElement, '.lang-option-favorite-toggle', 'click', () => {
|
|
isFavorite = !isFavorite;
|
|
|
|
if (isFavorite) {
|
|
this.favourites.add(language);
|
|
} else {
|
|
this.favourites.delete(language);
|
|
}
|
|
|
|
button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
|
|
|
|
window.$http.patch('/preferences/update-code-language-favourite', {
|
|
language,
|
|
active: isFavorite,
|
|
});
|
|
|
|
this.sortLanguageList();
|
|
if (isFavorite) {
|
|
button.scrollIntoView({block: 'center', behavior: 'smooth'});
|
|
}
|
|
});
|
|
}
|
|
|
|
sortLanguageList() {
|
|
const sortedParents = this.languageButtons.sort((a, b) => {
|
|
const aFav = a.dataset.favourite === 'true';
|
|
const bFav = b.dataset.favourite === 'true';
|
|
|
|
if (aFav && !bFav) {
|
|
return -1;
|
|
} if (bFav && !aFav) {
|
|
return 1;
|
|
}
|
|
|
|
return a.dataset.lang > b.dataset.lang ? 1 : -1;
|
|
}).map(button => button.parentElement);
|
|
|
|
for (const parent of sortedParents) {
|
|
this.languageOptionsContainer.append(parent);
|
|
}
|
|
}
|
|
|
|
save() {
|
|
if (this.saveCallback) {
|
|
this.saveCallback(this.editor.getContent(), this.languageInput.value);
|
|
}
|
|
this.hide();
|
|
}
|
|
|
|
async open(code, language, direction, saveCallback, cancelCallback) {
|
|
this.languageInput.value = language;
|
|
this.saveCallback = saveCallback;
|
|
this.cancelCallback = cancelCallback;
|
|
|
|
await this.show();
|
|
this.languageInputChange(language);
|
|
this.editor.setContent(code);
|
|
this.setDirection(direction);
|
|
}
|
|
|
|
async show() {
|
|
const Code = await window.importVersioned('code');
|
|
if (!this.editor) {
|
|
this.editor = Code.popupEditor(this.editorInput, this.languageInput.value);
|
|
}
|
|
|
|
this.loadHistory();
|
|
this.getPopup().show(() => {
|
|
this.editor.focus();
|
|
}, () => {
|
|
this.addHistory();
|
|
if (this.cancelCallback) {
|
|
this.cancelCallback();
|
|
}
|
|
});
|
|
}
|
|
|
|
setDirection(direction) {
|
|
const target = this.editorInput.parentElement;
|
|
if (direction) {
|
|
target.setAttribute('dir', direction);
|
|
} else {
|
|
target.removeAttribute('dir');
|
|
}
|
|
}
|
|
|
|
hide() {
|
|
this.getPopup().hide();
|
|
this.addHistory();
|
|
}
|
|
|
|
/**
|
|
* @returns {Popup}
|
|
*/
|
|
getPopup() {
|
|
return window.$components.firstOnElement(this.popup, 'popup');
|
|
}
|
|
|
|
async updateEditorMode(language) {
|
|
this.editor.setMode(language, this.editor.getContent());
|
|
}
|
|
|
|
languageInputChange(language) {
|
|
this.updateEditorMode(language);
|
|
const inputLang = language.toLowerCase();
|
|
|
|
for (const link of this.languageButtons) {
|
|
const lang = link.dataset.lang.toLowerCase().trim();
|
|
const isMatch = inputLang === lang;
|
|
link.classList.toggle('active', isMatch);
|
|
if (isMatch) {
|
|
link.scrollIntoView({block: 'center', behavior: 'smooth'});
|
|
}
|
|
}
|
|
}
|
|
|
|
loadHistory() {
|
|
this.history = JSON.parse(window.sessionStorage.getItem(this.historyKey) || '{}');
|
|
const historyKeys = Object.keys(this.history).reverse();
|
|
this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0);
|
|
this.historyList.innerHTML = historyKeys.map(key => {
|
|
const localTime = (new Date(parseInt(key, 10))).toLocaleTimeString();
|
|
return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
|
|
}).join('');
|
|
}
|
|
|
|
addHistory() {
|
|
if (!this.editor) return;
|
|
const code = this.editor.getContent();
|
|
if (!code) return;
|
|
|
|
// Stop if we'd be storing the same as the last item
|
|
const lastHistoryKey = Object.keys(this.history).pop();
|
|
if (this.history[lastHistoryKey] === code) return;
|
|
|
|
this.history[String(Date.now())] = code;
|
|
const historyString = JSON.stringify(this.history);
|
|
window.sessionStorage.setItem(this.historyKey, historyString);
|
|
}
|
|
|
|
}
|