1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-07-28 17:02:04 +03:00

Moved overlay component, migrated code-editor & added features

- Moved Code-editor from vue to component.
- Updated popup code so it background click only hides if the click
originated on the same background. Clicks within the popup will no
longer cause it to hide.
- Added session-level history tracking to code editor.
This commit is contained in:
Dan Brown
2020-06-27 23:56:01 +01:00
parent 9023f78cdc
commit a5fa745749
17 changed files with 289 additions and 156 deletions

View File

@ -0,0 +1,117 @@
import Code from "../services/code";
import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
/**
* Code Editor
* @extends {Component}
*/
class CodeEditor {
setup() {
this.container = this.$refs.container;
this.popup = this.$el;
this.editorInput = this.$refs.editor;
this.languageLinks = this.$manyRefs.languageLink;
this.saveButton = this.$refs.saveButton;
this.languageInput = this.$refs.languageInput;
this.historyDropDown = this.$refs.historyDropDown;
this.historyList = this.$refs.historyList;
this.callback = null;
this.editor = null;
this.history = {};
this.historyKey = 'code_history';
this.setupListeners();
}
setupListeners() {
this.container.addEventListener('keydown', event => {
if (event.ctrlKey && event.key === 'Enter') {
this.save();
}
});
onSelect(this.languageLinks, event => {
const language = event.target.dataset.lang;
this.languageInput.value = language;
this.updateEditorMode(language);
});
onEnterPress(this.languageInput, e => this.save());
onSelect(this.saveButton, e => this.save());
onChildEvent(this.historyList, 'button', 'click', (event, elem) => {
event.preventDefault();
const historyTime = elem.dataset.time;
if (this.editor) {
this.editor.setValue(this.history[historyTime]);
}
});
}
save() {
if (this.callback) {
this.callback(this.editor.getValue(), this.languageInput.value);
}
this.hide();
}
open(code, language, callback) {
this.languageInput.value = language;
this.callback = callback;
this.show();
this.updateEditorMode(language);
Code.setContent(this.editor, code);
}
show() {
if (!this.editor) {
this.editor = Code.popupEditor(this.editorInput, this.languageInput.value);
}
this.loadHistory();
this.popup.components.popup.show(() => {
Code.updateLayout(this.editor);
this.editor.focus();
}, () => {
this.addHistory()
});
}
hide() {
this.popup.components.popup.hide();
this.addHistory();
}
updateEditorMode(language) {
Code.setMode(this.editor, language, this.editor.getValue());
}
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))).toLocaleTimeString();
return `<li><button type="button" data-time="${key}">${localTime}</button></li>`;
}).join('');
}
addHistory() {
if (!this.editor) return;
const code = this.editor.getValue();
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);
}
}
export default CodeEditor;

View File

@ -1,27 +1,29 @@
/**
* Entity Selector Popup
* @extends {Component}
*/
class EntitySelectorPopup {
constructor(elem) {
this.elem = elem;
setup() {
this.elem = this.$el;
this.selectButton = this.$refs.select;
window.EntitySelectorPopup = this;
this.callback = null;
this.selection = null;
this.selectButton = elem.querySelector('.entity-link-selector-confirm');
this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));
window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));
window.$events.listen('entity-select-confirm', this.onSelectionConfirm.bind(this));
}
show(callback) {
this.callback = callback;
this.elem.components.overlay.show();
this.elem.components.popup.show();
}
hide() {
this.elem.components.overlay.hide();
this.elem.components.popup.hide();
}
onSelectButtonClick() {

View File

@ -36,7 +36,9 @@ function initComponent(name, element) {
try {
instance = new componentModel(element);
instance.$el = element;
instance.$refs = parseRefs(name, element);
const allRefs = parseRefs(name, element);
instance.$refs = allRefs.refs;
instance.$manyRefs = allRefs.manyRefs;
instance.$opts = parseOpts(name, element);
if (typeof instance.setup === 'function') {
instance.setup();
@ -67,6 +69,7 @@ function initComponent(name, element) {
*/
function parseRefs(name, element) {
const refs = {};
const manyRefs = {};
const prefix = `${name}@`
const refElems = element.querySelectorAll(`[refs*="${prefix}"]`);
for (const el of refElems) {
@ -76,9 +79,13 @@ function parseRefs(name, element) {
.map(str => str.replace(prefix, ''));
for (const ref of refNames) {
refs[ref] = el;
if (typeof manyRefs[ref] === 'undefined') {
manyRefs[ref] = [];
}
manyRefs[ref].push(el);
}
}
return refs;
return {refs, manyRefs};
}
/**
@ -134,6 +141,7 @@ function initAll(parentElement) {
}
window.components.init = initAll;
window.components.first = (name) => (window.components[name] || [null])[0];
export default initAll;
@ -141,5 +149,6 @@ export default initAll;
* @typedef Component
* @property {HTMLElement} $el
* @property {Object<String, HTMLElement>} $refs
* @property {Object<String, HTMLElement[]>} $manyRefs
* @property {Object<String, String>} $opts
*/

View File

@ -1,43 +0,0 @@
import {fadeIn, fadeOut} from "../services/animations";
class Overlay {
constructor(elem) {
this.container = elem;
elem.addEventListener('click', event => {
if (event.target === elem) return this.hide();
});
window.addEventListener('keyup', event => {
if (event.key === 'Escape') {
this.hide();
}
});
let closeButtons = elem.querySelectorAll('.popup-header-close');
for (let i=0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', this.hide.bind(this));
}
}
hide(onComplete = null) { this.toggle(false, onComplete); }
show(onComplete = null) { this.toggle(true, onComplete); }
toggle(show = true, onComplete) {
if (show) {
fadeIn(this.container, 240, onComplete);
} else {
fadeOut(this.container, 240, onComplete);
}
}
focusOnBody() {
const body = this.container.querySelector('.popup-body');
if (body) {
body.focus();
}
}
}
export default Overlay;

View File

@ -0,0 +1,61 @@
import {fadeIn, fadeOut} from "../services/animations";
import {onSelect} from "../services/dom";
/**
* Popup window that will contain other content.
* This component provides the show/hide functionality
* with the ability for popup@hide child references to close this.
* @extends {Component}
*/
class Popup {
setup() {
this.container = this.$el;
this.hideButtons = this.$manyRefs.hide || [];
this.onkeyup = null;
this.onHide = null;
this.setupListeners();
}
setupListeners() {
let lastMouseDownTarget = null;
this.container.addEventListener('mousedown', event => {
lastMouseDownTarget = event.target;
});
this.container.addEventListener('click', event => {
if (event.target === this.container && lastMouseDownTarget === this.container) {
return this.hide();
}
});
onSelect(this.hideButtons, e => this.hide());
}
hide(onComplete = null) {
fadeOut(this.container, 240, onComplete);
if (this.onkeyup) {
window.removeEventListener('keyup', this.onkeyup);
this.onkeyup = null;
}
if (this.onHide) {
this.onHide();
}
}
show(onComplete = null, onHide = null) {
fadeIn(this.container, 240, onComplete);
this.onkeyup = (event) => {
if (event.key === 'Escape') {
this.hide();
}
};
window.addEventListener('keyup', this.onkeyup);
this.onHide = onHide;
}
}
export default Popup;

View File

@ -137,7 +137,7 @@ function codePlugin() {
if (!elemIsCodeBlock(selectedNode)) {
const providedCode = editor.selection.getNode().textContent;
window.vues['code-editor'].open(providedCode, '', (code, lang) => {
window.components.first('code-editor').open(providedCode, '', (code, lang) => {
const wrap = document.createElement('div');
wrap.innerHTML = `<pre><code class="language-${lang}"></code></pre>`;
wrap.querySelector('code').innerText = code;
@ -155,7 +155,7 @@ function codePlugin() {
let lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : '';
let currentCode = selectedNode.querySelector('textarea').textContent;
window.vues['code-editor'].open(currentCode, lang, (code, lang) => {
window.components.first('code-editor').open(currentCode, lang, (code, lang) => {
const editorElem = selectedNode.querySelector('.CodeMirror');
const cmInstance = editorElem.CodeMirror;
if (cmInstance) {