diff --git a/resources/js/components/code-editor.js b/resources/js/components/code-editor.js new file mode 100644 index 000000000..2e3506ec7 --- /dev/null +++ b/resources/js/components/code-editor.js @@ -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 `
`; + }).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; \ No newline at end of file diff --git a/resources/js/components/entity-selector-popup.js b/resources/js/components/entity-selector-popup.js index 147f7b583..0104eace7 100644 --- a/resources/js/components/entity-selector-popup.js +++ b/resources/js/components/entity-selector-popup.js @@ -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() { diff --git a/resources/js/components/index.js b/resources/js/components/index.js index da194e438..1cea8949e 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -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
`;
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) {
diff --git a/resources/js/services/dom.js b/resources/js/services/dom.js
index 966a4540e..2a9fad8b3 100644
--- a/resources/js/services/dom.js
+++ b/resources/js/services/dom.js
@@ -25,17 +25,34 @@ export function onEvents(listenerElement, events, callback) {
/**
* Helper to run an action when an element is selected.
* A "select" is made to be accessible, So can be a click, space-press or enter-press.
- * @param listenerElement
- * @param callback
+ * @param {HTMLElement|Array} elements
+ * @param {function} callback
*/
-export function onSelect(listenerElement, callback) {
- listenerElement.addEventListener('click', callback);
- listenerElement.addEventListener('keydown', (event) => {
- if (event.key === 'Enter' || event.key === ' ') {
- event.preventDefault();
- callback(event);
- }
- });
+export function onSelect(elements, callback) {
+ if (!Array.isArray(elements)) {
+ elements = [elements];
+ }
+
+ for (const listenerElement of elements) {
+ listenerElement.addEventListener('click', callback);
+ listenerElement.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ callback(event);
+ }
+ });
+ }
+}
+
+/**
+ * Listen to enter press on the given element(s).
+ * @param {HTMLElement|Array} elements
+ * @param {function} callback
+ */
+export function onEnterPress(elements, callback) {
+ if (!Array.isArray(elements)) {
+ elements = [elements];
+ }
}
/**
diff --git a/resources/js/vues/code-editor.js b/resources/js/vues/code-editor.js
deleted file mode 100644
index f888e6227..000000000
--- a/resources/js/vues/code-editor.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import codeLib from "../services/code";
-
-const methods = {
- show() {
- if (!this.editor) this.editor = codeLib.popupEditor(this.$refs.editor, this.language);
- this.$refs.overlay.components.overlay.show(() => {
- codeLib.updateLayout(this.editor);
- this.editor.focus();
- });
- },
- hide() {
- this.$refs.overlay.components.overlay.hide();
- },
- updateEditorMode(language) {
- codeLib.setMode(this.editor, language, this.editor.getValue());
- },
- updateLanguage(lang) {
- this.language = lang;
- this.updateEditorMode(lang);
- },
- open(code, language, callback) {
- this.show();
- this.updateEditorMode(language);
- this.language = language;
- codeLib.setContent(this.editor, code);
- this.code = code;
- this.callback = callback;
- },
- save() {
- if (!this.callback) return;
- this.callback(this.editor.getValue(), this.language);
- this.hide();
- }
-};
-
-const data = {
- editor: null,
- language: '',
- code: '',
- callback: null
-};
-
-export default {
- methods,
- data
-};
\ No newline at end of file
diff --git a/resources/js/vues/image-manager.js b/resources/js/vues/image-manager.js
index 6df12d16d..b87734556 100644
--- a/resources/js/vues/image-manager.js
+++ b/resources/js/vues/image-manager.js
@@ -35,7 +35,7 @@ const methods = {
show(providedCallback, imageType = null) {
callback = providedCallback;
this.showing = true;
- this.$el.children[0].components.overlay.show();
+ this.$el.children[0].components.popup.show();
// Get initial images if they have not yet been loaded in.
if (dataLoaded && imageType === this.imageType) return;
@@ -53,7 +53,7 @@ const methods = {
}
this.showing = false;
this.selectedImage = false;
- this.$el.children[0].components.overlay.hide();
+ this.$el.children[0].components.popup.hide();
},
async fetchData() {
diff --git a/resources/js/vues/vues.js b/resources/js/vues/vues.js
index 125d541ce..a53adf53f 100644
--- a/resources/js/vues/vues.js
+++ b/resources/js/vues/vues.js
@@ -5,7 +5,6 @@ function exists(id) {
}
import entityDashboard from "./entity-dashboard";
-import codeEditor from "./code-editor";
import imageManager from "./image-manager";
import tagManager from "./tag-manager";
import attachmentManager from "./attachment-manager";
@@ -13,7 +12,6 @@ import pageEditor from "./page-editor";
let vueMapping = {
'entity-dashboard': entityDashboard,
- 'code-editor': codeEditor,
'image-manager': imageManager,
'tag-manager': tagManager,
'attachment-manager': attachmentManager,
diff --git a/resources/lang/en/components.php b/resources/lang/en/components.php
index d8e8981fb..32667eb4e 100644
--- a/resources/lang/en/components.php
+++ b/resources/lang/en/components.php
@@ -29,5 +29,6 @@ return [
'code_editor' => 'Edit Code',
'code_language' => 'Code Language',
'code_content' => 'Code Content',
+ 'code_session_history' => 'Session History',
'code_save' => 'Save Code',
];
diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss
index 13e16e42f..c73c503b4 100644
--- a/resources/sass/_components.scss
+++ b/resources/sass/_components.scss
@@ -81,7 +81,7 @@
}
}
-[overlay] {
+[overlay], .popup-background {
@include lightDark(background-color, rgba(0, 0, 0, 0.333), rgba(0, 0, 0, 0.6));
position: fixed;
z-index: 95536;
@@ -611,11 +611,11 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
display: none;
}
-#code-editor .CodeMirror {
+.code-editor .CodeMirror {
height: 400px;
}
-#code-editor .lang-options {
+.code-editor .lang-options {
max-width: 480px;
margin-bottom: $-s;
a {
@@ -625,10 +625,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
}
@include smaller-than($m) {
- #code-editor .lang-options {
+ .code-editor .lang-options {
max-width: 100%;
}
- #code-editor .CodeMirror {
+ .code-editor .CodeMirror {
height: 200px;
}
}
diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss
index 4d044245a..226f5ccdb 100644
--- a/resources/sass/_layout.scss
+++ b/resources/sass/_layout.scss
@@ -49,6 +49,9 @@
&.v-center {
align-items: center;
}
+ &.v-end {
+ align-items: end;
+ }
&.no-gap {
grid-row-gap: 0;
grid-column-gap: 0;
diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss
index 77727060e..856cfed89 100644
--- a/resources/sass/_lists.scss
+++ b/resources/sass/_lists.scss
@@ -569,6 +569,8 @@ ul.pagination {
@include lightDark(color, #555, #eee);
fill: currentColor;
text-align: start !important;
+ max-height: 500px;
+ overflow-y: auto;
&.wide {
min-width: 220px;
}
diff --git a/resources/views/components/code-editor.blade.php b/resources/views/components/code-editor.blade.php
index 15f6ae252..6822bb28d 100644
--- a/resources/views/components/code-editor.blade.php
+++ b/resources/views/components/code-editor.blade.php
@@ -1,10 +1,10 @@
-