- 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.
--- /dev/null
+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;
\ No newline at end of file
-
+/**
+ * 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() {
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();
*/
function parseRefs(name, element) {
const refs = {};
+ const manyRefs = {};
const prefix = `${name}@`
const refElems = element.querySelectorAll(`[refs*="${prefix}"]`);
for (const el of refElems) {
.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};
}
/**
}
window.components.init = initAll;
+window.components.first = (name) => (window.components[name] || [null])[0];
export default initAll;
* @typedef Component
* @property {HTMLElement} $el
* @property {Object<String, HTMLElement>} $refs
+ * @property {Object<String, HTMLElement[]>} $manyRefs
* @property {Object<String, String>} $opts
*/
\ No newline at end of file
+++ /dev/null
-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;
\ No newline at end of file
--- /dev/null
+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;
\ No newline at end of file
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;
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) {
/**
* 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];
+ }
}
/**
+++ /dev/null
-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
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;
}
this.showing = false;
this.selectedImage = false;
- this.$el.children[0].components.overlay.hide();
+ this.$el.children[0].components.popup.hide();
},
async fetchData() {
}
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";
let vueMapping = {
'entity-dashboard': entityDashboard,
- 'code-editor': codeEditor,
'image-manager': imageManager,
'tag-manager': tagManager,
'attachment-manager': attachmentManager,
'code_editor' => 'Edit Code',
'code_language' => 'Code Language',
'code_content' => 'Code Content',
+ 'code_session_history' => 'Session History',
'code_save' => 'Save Code',
];
}
}
-[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;
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 {
}
@include smaller-than($m) {
- #code-editor .lang-options {
+ .code-editor .lang-options {
max-width: 100%;
}
- #code-editor .CodeMirror {
+ .code-editor .CodeMirror {
height: 200px;
}
}
&.v-center {
align-items: center;
}
+ &.v-end {
+ align-items: end;
+ }
&.no-gap {
grid-row-gap: 0;
grid-column-gap: 0;
@include lightDark(color, #555, #eee);
fill: currentColor;
text-align: start !important;
+ max-height: 500px;
+ overflow-y: auto;
&.wide {
min-width: 220px;
}
-<div id="code-editor">
- <div overlay ref="overlay" v-cloak @click="hide()">
- <div class="popup-body" tabindex="-1" @click.stop @keydown.enter.ctrl="save">
+<div>
+ <div components="popup code-editor" class="popup-background code-editor">
+ <div refs="code-editor@container" class="popup-body" tabindex="-1">
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('components.code_editor') }}</div>
- <button class="popup-header-close" @click="hide()">x</button>
+ <button class="popup-header-close" refs="popup@hide">x</button>
</div>
<div class="p-l popup-content">
<label for="code-editor-language">{{ trans('components.code_language') }}</label>
<div class="lang-options">
<small>
- <a @click="updateLanguage('CSS')">CSS</a>
- <a @click="updateLanguage('C')">C</a>
- <a @click="updateLanguage('C++')">C++</a>
- <a @click="updateLanguage('C#')">C#</a>
- <a @click="updateLanguage('Fortran')">Fortran</a>
- <a @click="updateLanguage('Go')">Go</a>
- <a @click="updateLanguage('HTML')">HTML</a>
- <a @click="updateLanguage('INI')">INI</a>
- <a @click="updateLanguage('Java')">Java</a>
- <a @click="updateLanguage('JavaScript')">JavaScript</a>
- <a @click="updateLanguage('JSON')">JSON</a>
- <a @click="updateLanguage('Lua')">Lua</a>
- <a @click="updateLanguage('MarkDown')">MarkDown</a>
- <a @click="updateLanguage('Nginx')">Nginx</a>
- <a @click="updateLanguage('PASCAL')">Pascal</a>
- <a @click="updateLanguage('Perl')">Perl</a>
- <a @click="updateLanguage('PHP')">PHP</a>
- <a @click="updateLanguage('Powershell')">Powershell</a>
- <a @click="updateLanguage('Python')">Python</a>
- <a @click="updateLanguage('Ruby')">Ruby</a>
- <a @click="updateLanguage('shell')">Shell/Bash</a>
- <a @click="updateLanguage('SQL')">SQL</a>
- <a @click="updateLanguage('XML')">XML</a>
- <a @click="updateLanguage('YAML')">YAML</a>
+ <a refs="code-editor@languageLink" data-lang="CSS">CSS</a>
+ <a refs="code-editor@languageLink" data-lang="C">C</a>
+ <a refs="code-editor@languageLink" data-lang="C++">C++</a>
+ <a refs="code-editor@languageLink" data-lang="C#">C#</a>
+ <a refs="code-editor@languageLink" data-lang="Fortran">Fortran</a>
+ <a refs="code-editor@languageLink" data-lang="Go">Go</a>
+ <a refs="code-editor@languageLink" data-lang="HTML">HTML</a>
+ <a refs="code-editor@languageLink" data-lang="INI">INI</a>
+ <a refs="code-editor@languageLink" data-lang="Java">Java</a>
+ <a refs="code-editor@languageLink" data-lang="JavaScript">JavaScript</a>
+ <a refs="code-editor@languageLink" data-lang="JSON">JSON</a>
+ <a refs="code-editor@languageLink" data-lang="Lua">Lua</a>
+ <a refs="code-editor@languageLink" data-lang="MarkDown">MarkDown</a>
+ <a refs="code-editor@languageLink" data-lang="Nginx">Nginx</a>
+ <a refs="code-editor@languageLink" data-lang="PASCAL">Pascal</a>
+ <a refs="code-editor@languageLink" data-lang="Perl">Perl</a>
+ <a refs="code-editor@languageLink" data-lang="PHP">PHP</a>
+ <a refs="code-editor@languageLink" data-lang="Powershell">Powershell</a>
+ <a refs="code-editor@languageLink" data-lang="Python">Python</a>
+ <a refs="code-editor@languageLink" data-lang="Ruby">Ruby</a>
+ <a refs="code-editor@languageLink" data-lang="shell">Shell/Bash</a>
+ <a refs="code-editor@languageLink" data-lang="SQL">SQL</a>
+ <a refs="code-editor@languageLink" data-lang="XML">XML</a>
+ <a refs="code-editor@languageLink" data-lang="YAML">YAML</a>
</small>
</div>
- <input @keypress.enter="save()" id="code-editor-language" type="text" @input="updateEditorMode(language)" v-model="language">
+ <input refs="code-editor@languageInput" id="code-editor-language" type="text">
</div>
<div class="form-group">
- <label for="code-editor-content">{{ trans('components.code_content') }}</label>
- <textarea ref="editor" v-model="code"></textarea>
+ <div class="grid half no-break v-end mb-xs">
+ <div>
+ <label for="code-editor-content">{{ trans('components.code_content') }}</label>
+ </div>
+ <div class="text-right">
+ <div component="dropdown" refs="code-editor@historyDropDown" class="inline block">
+ <button refs="dropdown@toggle" class="text-button text-small">@icon('history') {{ trans('components.code_session_history') }}</button>
+ <ul refs="dropdown@menu code-editor@historyList" class="dropdown-menu"></ul>
+ </div>
+ </div>
+ </div>
+
+ <div class="clearfix"></div>
+ <textarea refs="code-editor@editor"></textarea>
</div>
<div class="form-group">
- <button type="button" class="button" @click="save()">{{ trans('components.code_save') }}</button>
+ <button refs="code-editor@saveButton" type="button" class="button">{{ trans('components.code_save') }}</button>
</div>
</div>
</div>
</div>
-</div>
+</div>
\ No newline at end of file
<div id="entity-selector-wrap">
- <div overlay entity-selector-popup>
+ <div components="popup entity-selector-popup" class="popup-background">
<div class="popup-body small" tabindex="-1">
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('entities.entity_select') }}</div>
- <button type="button" class="popup-header-close">x</button>
+ <button refs="popup@hide" type="button" class="popup-header-close">x</button>
</div>
@include('components.entity-selector', ['name' => 'entity-selector'])
<div class="popup-footer">
- <button type="button" disabled="true" class="button entity-link-selector-confirm corner-button">{{ trans('common.select') }}</button>
+ <button refs="entity-selector-popup@select" type="button" disabled="true" class="button corner-button">{{ trans('common.select') }}</button>
</div>
</div>
</div>
'components.file_upload_timeout',
])
- <div overlay v-cloak @click="hide">
+ <div component="popup" class="popup-background" v-cloak @click="hide">
<div class="popup-body" tabindex="-1" @click.stop>
<div class="popup-header primary-background">