]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #3853 from BookStackApp/component_refactor
authorDan Brown <redacted>
Wed, 16 Nov 2022 16:05:57 +0000 (16:05 +0000)
committerGitHub <redacted>
Wed, 16 Nov 2022 16:05:57 +0000 (16:05 +0000)
Started refactor and alignment of JS component system

96 files changed:
dev/docs/javascript-code.md
resources/js/app.js
resources/js/components/add-remove-rows.js
resources/js/components/ajax-delete-row.js
resources/js/components/ajax-form.js
resources/js/components/attachments-list.js
resources/js/components/attachments.js
resources/js/components/auto-submit.js
resources/js/components/auto-suggest.js
resources/js/components/back-to-top.js
resources/js/components/book-sort.js
resources/js/components/chapter-contents.js
resources/js/components/code-editor.js
resources/js/components/code-highlighter.js
resources/js/components/code-textarea.js
resources/js/components/collapsible.js
resources/js/components/component.js [new file with mode: 0644]
resources/js/components/confirm-dialog.js
resources/js/components/custom-checkbox.js
resources/js/components/details-highlighter.js
resources/js/components/dropdown-search.js
resources/js/components/dropdown.js
resources/js/components/dropzone.js
resources/js/components/editor-toolbox.js
resources/js/components/entity-permissions.js
resources/js/components/entity-search.js
resources/js/components/entity-selector-popup.js
resources/js/components/entity-selector.js
resources/js/components/event-emit-select.js
resources/js/components/expand-toggle.js
resources/js/components/header-mobile-toggle.js
resources/js/components/image-manager.js
resources/js/components/image-picker.js
resources/js/components/index.js
resources/js/components/list-sort-control.js
resources/js/components/markdown-editor.js
resources/js/components/new-user-password.js
resources/js/components/notification.js
resources/js/components/optional-input.js
resources/js/components/page-comments.js
resources/js/components/page-display.js
resources/js/components/page-editor.js
resources/js/components/page-picker.js
resources/js/components/permissions-table.js
resources/js/components/pointer.js
resources/js/components/popup.js
resources/js/components/setting-app-color-picker.js
resources/js/components/setting-color-picker.js
resources/js/components/setting-homepage-control.js [moved from resources/js/components/homepage-control.js with 55% similarity]
resources/js/components/shelf-sort.js
resources/js/components/shortcut-input.js
resources/js/components/shortcuts.js
resources/js/components/sidebar.js [deleted file]
resources/js/components/sortable-list.js
resources/js/components/submit-on-change.js
resources/js/components/tabs.js
resources/js/components/tag-manager.js
resources/js/components/template-manager.js
resources/js/components/toggle-switch.js
resources/js/components/tri-layout.js
resources/js/components/user-select.js
resources/js/components/webhook-events.js
resources/js/components/wysiwyg-editor.js
resources/js/services/components.js [new file with mode: 0644]
resources/js/services/dom.js
resources/js/wysiwyg/config.js
resources/js/wysiwyg/plugin-codeeditor.js
resources/js/wysiwyg/plugin-drawio.js
resources/js/wysiwyg/plugins-imagemanager.js
resources/js/wysiwyg/shortcuts.js
resources/sass/_components.scss
resources/sass/_forms.scss
resources/sass/_pages.scss
resources/sass/styles.scss
resources/views/api-docs/index.blade.php
resources/views/api-docs/parts/endpoint.blade.php
resources/views/attachments/manager.blade.php
resources/views/books/parts/form.blade.php
resources/views/books/parts/sort-box.blade.php
resources/views/books/sort.blade.php
resources/views/chapters/parts/form.blade.php
resources/views/common/notifications.blade.php
resources/views/form/custom-checkbox.blade.php
resources/views/form/image-picker.blade.php
resources/views/form/toggle-switch.blade.php
resources/views/home/parts/expand-toggle.blade.php
resources/views/home/specific-page.blade.php
resources/views/layouts/base.blade.php
resources/views/pages/parts/editor-toolbox.blade.php
resources/views/pages/parts/template-manager.blade.php
resources/views/pages/show.blade.php
resources/views/settings/customization.blade.php
resources/views/settings/parts/page-picker.blade.php
resources/views/settings/parts/setting-entity-color-picker.blade.php
resources/views/shelves/parts/form.blade.php
resources/views/users/parts/form.blade.php

index 3d47a1ad8a240e6fe9c1fc48826809018807856a..a1092ce92569ffad21aec73471fbde7b8b621b93 100644 (file)
@@ -24,7 +24,7 @@ class Dropdown {
 
 All usage of $refs, $manyRefs and $opts should be done at the top of the `setup` function so any requirements can be easily seen.
 
-Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file. You'll need to import the component class then add it to `componentMapping` object, following the pattern of other components. 
+Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file by defining an additional export, following the pattern of other components. 
 
 ### Using a Component in HTML
 
@@ -80,9 +80,9 @@ Will result with `this.$opts` being:
 }
 ```
 
-#### Component Properties
+#### Component Properties & Methods
 
-A component has the below shown properties available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.
+A component has the below shown properties & methods available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.
 
 ```javascript
 // The root element that the compontent has been applied to.
@@ -98,6 +98,15 @@ this.$manyRefs
 
 // Options defined for the compontent.
 this.$opts
+
+// The registered name of the component, usually kebab-case.
+this.$name
+
+// Emit a custom event from this component.
+// Will be bubbled up from the dom element this is registered on, 
+// as a custom event with the name `<elementName>-<eventName>`,
+// with the provided data in the event detail.
+this.$emit(eventName, data = {})
 ```
 
 ## Global JavaScript Helpers
@@ -132,7 +141,16 @@ window.trans_plural(translationString, count, replacements);
 
 // Component System
 // Parse and initialise any components from the given root el down.
-window.components.init(rootEl);
-// Get the first active component of the given name
-window.components.first(name);
+window.$components.init(rootEl);
+// Register component models to be used by the component system.
+// Takes a mapping of classes/constructors keyed by component names.
+// Names will be converted to kebab-case.
+window.$components.register(mapping);
+// Get the first active component of the given name.
+window.$components.first(name);
+// Get all the active components of the given name. 
+window.$components.get(name);
+// Get the first active component of the given name that's been
+// created on the given element.
+window.$components.firstOnElement(element, name);
 ```
\ No newline at end of file
index 82748b75e223c2f21612a2662bc2fad22c23b6c0..e49bf5e955bd040d1b68114fb17e8b307a3a024e 100644 (file)
@@ -27,5 +27,8 @@ window.trans_choice = translator.getPlural.bind(translator);
 window.trans_plural = translator.parsePlural.bind(translator);
 
 // Load Components
-import components from "./components"
-components();
\ No newline at end of file
+import * as components from "./services/components"
+import * as componentMap from "./components";
+components.register(componentMap);
+window.$components = components;
+components.init();
index 9a5f019c501e66c79e8855601eefd4e087e2d1bd..19d2249fb28ece1607f00367e7e443b75ea8f8d5 100644 (file)
@@ -1,13 +1,13 @@
 import {onChildEvent} from "../services/dom";
 import {uniqueId} from "../services/util";
+import {Component} from "./component";
 
 /**
  * AddRemoveRows
  * Allows easy row add/remove controls onto a table.
  * Needs a model row to use when adding a new row.
- * @extends {Component}
  */
-class AddRemoveRows {
+export class AddRemoveRows extends Component {
     setup() {
         this.modelRow = this.$refs.model;
         this.addButton = this.$refs.add;
@@ -31,7 +31,7 @@ class AddRemoveRows {
         clone.classList.remove('hidden');
         this.setClonedInputNames(clone);
         this.modelRow.parentNode.insertBefore(clone, this.modelRow);
-        window.components.init(clone);
+        window.$components.init(clone);
     }
 
     /**
@@ -49,6 +49,4 @@ class AddRemoveRows {
             elem.name = elem.name.split('randrowid').join(rowId);
         }
     }
-}
-
-export default AddRemoveRows;
\ No newline at end of file
+}
\ No newline at end of file
index 2feb3d5ac72bcaf9e10ec252af187ad633efe4c2..f1af7f6cb10e97d2e65cfb1c4e49d1d5a4e7580e 100644 (file)
@@ -1,10 +1,7 @@
-/**
- * AjaxDelete
- * @extends {Component}
- */
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
-class AjaxDeleteRow {
+export class AjaxDeleteRow extends Component {
     setup() {
         this.row = this.$el;
         this.url = this.$opts.url;
@@ -27,6 +24,4 @@ class AjaxDeleteRow {
             this.row.style.pointerEvents = null;
         });
     }
-}
-
-export default AjaxDeleteRow;
\ No newline at end of file
+}
\ No newline at end of file
index 91029d04221247cbc4b079fd3d807a4eb25cb93f..6f4e5af08c8bfbe20483ea0805cf4a44261c3861 100644 (file)
@@ -1,4 +1,5 @@
 import {onEnterPress, onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * Ajax Form
@@ -8,10 +9,8 @@ import {onEnterPress, onSelect} from "../services/dom";
  *
  * Will handle a real form if that's what the component is added to
  * otherwise will act as a fake form element.
- *
- * @extends {Component}
  */
-class AjaxForm {
+export class AjaxForm extends Component {
     setup() {
         this.container = this.$el;
         this.responseContainer = this.container;
@@ -72,11 +71,9 @@ class AjaxForm {
             this.responseContainer.innerHTML = err.data;
         }
 
-        window.components.init(this.responseContainer);
+        window.$components.init(this.responseContainer);
         this.responseContainer.style.opacity = null;
         this.responseContainer.style.pointerEvents = null;
     }
 
-}
-
-export default AjaxForm;
\ No newline at end of file
+}
\ No newline at end of file
index 34979c2e7a8207d376aa12534b0d3745e3a34d7d..dfefd9b7f84afb24c1cdb77dd8dbe22281dc1533 100644 (file)
@@ -1,10 +1,11 @@
+import {Component} from "./component";
+
 /**
  * Attachments List
  * Adds '?open=true' query to file attachment links
  * when ctrl/cmd is pressed down.
- * @extends {Component}
  */
-class AttachmentsList {
+export class AttachmentsList extends Component {
 
     setup() {
         this.container = this.$el;
@@ -42,6 +43,4 @@ class AttachmentsList {
             link.removeAttribute('target');
         }
     }
-}
-
-export default AttachmentsList;
\ No newline at end of file
+}
\ No newline at end of file
index 6dcfe9f128cf39a8e7a2d0586b32b9320613c5f8..b4e400aeb716b53047b0d7c80492e2a9dbdba7b0 100644 (file)
@@ -1,10 +1,7 @@
-/**
- * Attachments
- * @extends {Component}
- */
 import {showLoading} from "../services/dom";
+import {Component} from "./component";
 
-class Attachments {
+export class Attachments extends Component {
 
     setup() {
         this.container = this.$el;
@@ -46,10 +43,12 @@ class Attachments {
 
     reloadList() {
         this.stopEdit();
-        this.mainTabs.components.tabs.show('items');
+        /** @var {Tabs} */
+        const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
+        tabs.show('items');
         window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
             this.list.innerHTML = resp.data;
-            window.components.init(this.list);
+            window.$components.init(this.list);
         });
     }
 
@@ -66,7 +65,7 @@ class Attachments {
         showLoading(this.editContainer);
         const resp = await window.$http.get(`/attachments/edit/${id}`);
         this.editContainer.innerHTML = resp.data;
-        window.components.init(this.editContainer);
+        window.$components.init(this.editContainer);
     }
 
     stopEdit() {
@@ -74,6 +73,4 @@ class Attachments {
         this.listContainer.classList.remove('hidden');
     }
 
-}
-
-export default Attachments;
\ No newline at end of file
+}
\ No newline at end of file
index 11494ae820f9e43d33951f0c0e98afbaec7785bd..c8726ca7e2914d5099b38f538664da7f11c44592 100644 (file)
@@ -1,5 +1,6 @@
+import {Component} from "./component";
 
-class AutoSubmit {
+export class AutoSubmit extends Component {
 
     setup() {
         this.form = this.$el;
@@ -7,6 +8,4 @@ class AutoSubmit {
         this.form.submit();
     }
 
-}
-
-export default AutoSubmit;
\ No newline at end of file
+}
\ No newline at end of file
index 80857cbe5c5cb20d121bfbcb6d40dd17ff786285..b4e6c5957879b759c7e9a358fcd5b528c7673b22 100644 (file)
@@ -1,13 +1,13 @@
 import {escapeHtml} from "../services/util";
 import {onChildEvent} from "../services/dom";
+import {Component} from "./component";
 
 const ajaxCache = {};
 
 /**
  * AutoSuggest
- * @extends {Component}
  */
-class AutoSuggest {
+export class AutoSuggest extends Component {
     setup() {
         this.parent = this.$el.parentElement;
         this.container = this.$el;
@@ -148,6 +148,4 @@ class AutoSuggest {
             this.hideSuggestions();
         }
     }
-}
-
-export default AutoSuggest;
\ No newline at end of file
+}
\ No newline at end of file
index a1d87f22eb3c3732213a2be6e06e8adfdb7180ae..4f0a46f009b19822332bd680f766bc205acc43b9 100644 (file)
@@ -1,34 +1,35 @@
+import {Component} from "./component";
 
-class BackToTop {
+export class BackToTop extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
+    setup() {
+        this.button = this.$el;
         this.targetElem = document.getElementById('header');
         this.showing = false;
         this.breakPoint = 1200;
 
         if (document.body.classList.contains('flexbox')) {
-            this.elem.style.display = 'none';
+            this.button.style.display = 'none';
             return;
         }
 
-        this.elem.addEventListener('click', this.scrollToTop.bind(this));
+        this.button.addEventListener('click', this.scrollToTop.bind(this));
         window.addEventListener('scroll', this.onPageScroll.bind(this));
     }
 
     onPageScroll() {
         let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
         if (!this.showing && scrollTopPos > this.breakPoint) {
-            this.elem.style.display = 'block';
+            this.button.style.display = 'block';
             this.showing = true;
             setTimeout(() => {
-                this.elem.style.opacity = 0.4;
+                this.button.style.opacity = 0.4;
             }, 1);
         } else if (this.showing && scrollTopPos < this.breakPoint) {
-            this.elem.style.opacity = 0;
+            this.button.style.opacity = 0;
             this.showing = false;
             setTimeout(() => {
-                this.elem.style.display = 'none';
+                this.button.style.display = 'none';
             }, 500);
         }
     }
@@ -54,6 +55,4 @@ class BackToTop {
         requestAnimationFrame(setPos.bind(this));
     }
 
-}
-
-export default BackToTop;
\ No newline at end of file
+}
\ No newline at end of file
index 2b94ca4a7a19a68ff82b31345efbc52fc28dc56e..3ffadf99150d3813b06faea5d706e789257a3e8d 100644 (file)
@@ -1,4 +1,6 @@
 import Sortable from "sortablejs";
+import {Component} from "./component";
+import {htmlToDom} from "../services/dom";
 
 // Auto sort control
 const sortOperations = {
@@ -35,14 +37,14 @@ const sortOperations = {
     },
 };
 
-class BookSort {
+export class BookSort extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.sortContainer = elem.querySelector('[book-sort-boxes]');
-        this.input = elem.querySelector('[book-sort-input]');
+    setup() {
+        this.container = this.$el;
+        this.sortContainer = this.$refs.sortContainer;
+        this.input = this.$refs.input;
 
-        const initialSortBox = elem.querySelector('.sort-box');
+        const initialSortBox = this.container.querySelector('.sort-box');
         this.setupBookSortable(initialSortBox);
         this.setupSortPresets();
 
@@ -90,14 +92,12 @@ class BookSort {
      * @param {Object} entityInfo
      */
     bookSelect(entityInfo) {
-        const alreadyAdded = this.elem.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
+        const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
         if (alreadyAdded) return;
 
         const entitySortItemUrl = entityInfo.link + '/sort-item';
         window.$http.get(entitySortItemUrl).then(resp => {
-            const wrap = document.createElement('div');
-            wrap.innerHTML = resp.data;
-            const newBookContainer = wrap.children[0];
+            const newBookContainer = htmlToDom(resp.data);
             this.sortContainer.append(newBookContainer);
             this.setupBookSortable(newBookContainer);
         });
@@ -155,7 +155,7 @@ class BookSort {
      */
     buildEntityMap() {
         const entityMap = [];
-        const lists = this.elem.querySelectorAll('.sort-list');
+        const lists = this.container.querySelectorAll('.sort-list');
 
         for (let list of lists) {
             const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
@@ -202,6 +202,4 @@ class BookSort {
         }
     }
 
-}
-
-export default BookSort;
\ No newline at end of file
+}
\ No newline at end of file
index c824d0f78b0b7fad13fcb16f84453746be6f1c7c..37df213e3c98e1ce12fa2de347e9397c3213238f 100644 (file)
@@ -1,9 +1,7 @@
 import {slideUp, slideDown} from "../services/animations";
+import {Component} from "./component";
 
-/**
- * @extends {Component}
- */
-class ChapterContents {
+export class ChapterContents extends Component {
 
     setup() {
         this.list = this.$refs.list;
@@ -31,7 +29,4 @@ class ChapterContents {
         event.preventDefault();
         this.isOpen ?  this.close() : this.open();
     }
-
 }
-
-export default ChapterContents;
index d0c6c432a37df7fae8c87bd39334d9d3cbcc54bc..205cbd8fdbc21efec49301430a7eee97fa67a991 100644 (file)
@@ -1,10 +1,8 @@
 import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
+import {Component} from "./component";
 
-/**
- * Code Editor
- * @extends {Component}
- */
-class CodeEditor {
+
+export class CodeEditor extends Component {
 
     setup() {
         this.container = this.$refs.container;
@@ -128,7 +126,7 @@ class CodeEditor {
         }
 
         this.loadHistory();
-        this.popup.components.popup.show(() => {
+        this.getPopup().show(() => {
             Code.updateLayout(this.editor);
             this.editor.focus();
         }, () => {
@@ -137,10 +135,17 @@ class CodeEditor {
     }
 
     hide() {
-        this.popup.components.popup.hide();
+        this.getPopup().hide();
         this.addHistory();
     }
 
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.popup, 'popup');
+    }
+
     async updateEditorMode(language) {
         const Code = await window.importVersioned('code');
         Code.setMode(this.editor, language, this.editor.getValue());
@@ -184,6 +189,4 @@ class CodeEditor {
         window.sessionStorage.setItem(this.historyKey, historyString);
     }
 
-}
-
-export default CodeEditor;
\ No newline at end of file
+}
\ No newline at end of file
index 5ffab377525d875f34e02f7be6d65e68dee1c4c3..14bfc97f04ed5358b2758112e9cf3f06d47ae423 100644 (file)
@@ -1,14 +1,16 @@
-class CodeHighlighter {
+import {Component} from "./component";
 
-    constructor(elem) {
-        const codeBlocks = elem.querySelectorAll('pre');
+export class CodeHighlighter extends Component{
+
+    setup() {
+        const container = this.$el;
+
+        const codeBlocks = container.querySelectorAll('pre');
         if (codeBlocks.length > 0) {
             window.importVersioned('code').then(Code => {
-               Code.highlightWithin(elem);
+               Code.highlightWithin(container);
             });
         }
     }
 
-}
-
-export default CodeHighlighter;
\ No newline at end of file
+}
\ No newline at end of file
index 988e51f199f040f57846869a029e9d6dfabc712d..0e49aec1755693c8615fa1447e37b1fe549b3545 100644 (file)
@@ -1,9 +1,10 @@
 /**
  * A simple component to render a code editor within the textarea
  * this exists upon.
- * @extends {Component}
  */
-class CodeTextarea {
+import {Component} from "./component";
+
+export class CodeTextarea extends Component {
 
     async setup() {
         const mode = this.$opts.mode;
@@ -11,6 +12,4 @@ class CodeTextarea {
         Code.inlineEditor(this.$el, mode);
     }
 
-}
-
-export default CodeTextarea;
\ No newline at end of file
+}
\ No newline at end of file
index 544f91008c7d13eaeec88a9f119ebfb47e839a85..bb8ed477ffe9f600769dace8ba6d7f2af32d5fb7 100644 (file)
@@ -1,35 +1,37 @@
 import {slideDown, slideUp} from "../services/animations";
+import {Component} from "./component";
 
 /**
  * Collapsible
  * Provides some simple logic to allow collapsible sections.
  */
-class Collapsible {
+export class Collapsible extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.trigger = elem.querySelector('[collapsible-trigger]');
-        this.content = elem.querySelector('[collapsible-content]');
+    setup() {
+        this.container = this.$el;
+        this.trigger = this.$refs.trigger;
+        this.content = this.$refs.content;
 
-        if (!this.trigger) return;
-        this.trigger.addEventListener('click', this.toggle.bind(this));
-        this.openIfContainsError();
+        if (this.trigger) {
+            this.trigger.addEventListener('click', this.toggle.bind(this));
+            this.openIfContainsError();
+        }
     }
 
     open() {
-        this.elem.classList.add('open');
+        this.container.classList.add('open');
         this.trigger.setAttribute('aria-expanded', 'true');
         slideDown(this.content, 300);
     }
 
     close() {
-        this.elem.classList.remove('open');
+        this.container.classList.remove('open');
         this.trigger.setAttribute('aria-expanded', 'false');
         slideUp(this.content, 300);
     }
 
     toggle() {
-        if (this.elem.classList.contains('open')) {
+        if (this.container.classList.contains('open')) {
             this.close();
         } else {
             this.open();
@@ -43,6 +45,4 @@ class Collapsible {
         }
     }
 
-}
-
-export default Collapsible;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/js/components/component.js b/resources/js/components/component.js
new file mode 100644 (file)
index 0000000..292bbb6
--- /dev/null
@@ -0,0 +1,58 @@
+export class Component {
+
+    /**
+     * The registered name of the component.
+     * @type {string}
+     */
+    $name = '';
+
+    /**
+     * The element that the component is registered upon.
+     * @type {Element}
+     */
+    $el = null;
+
+    /**
+     * Mapping of referenced elements within the component.
+     * @type {Object<string, Element>}
+     */
+    $refs = {};
+
+    /**
+     * Mapping of arrays of referenced elements within the component so multiple
+     * references, sharing the same name, can be fetched.
+     * @type {Object<string, Element[]>}
+     */
+    $manyRefs = {};
+
+    /**
+     * Options passed into this component.
+     * @type {Object<String, String>}
+     */
+    $opts = {};
+
+    /**
+     * Component-specific setup methods.
+     * Use this to assign local variables and run any initial setup or actions.
+     */
+    setup() {
+        //
+    }
+
+    /**
+     * Emit an event from this component.
+     * Will be bubbled up from the dom element this is registered on, as a custom event
+     * with the name `<elementName>-<eventName>`, with the provided data in the event detail.
+     * @param {String} eventName
+     * @param {Object} data
+     */
+    $emit(eventName, data = {}) {
+        data.from = this;
+        const componentName = this.$name;
+        const event = new CustomEvent(`${componentName}-${eventName}`, {
+            bubbles: true,
+            detail: data
+        });
+        this.$el.dispatchEvent(event);
+    }
+}
\ No newline at end of file
index 858be1b852be7eb23636113720294a8b878dca27..572945d5aba4ae7099a30dc29a28fe58f62d4542 100644 (file)
@@ -1,12 +1,12 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * Custom equivalent of window.confirm() using our popup component.
  * Is promise based so can be used like so:
  * `const result = await dialog.show()`
- * @extends {Component}
  */
-class ConfirmDialog {
+export class ConfirmDialog extends Component {
 
     setup() {
         this.container = this.$el;
@@ -34,7 +34,7 @@ class ConfirmDialog {
      * @returns {Popup}
      */
     getPopup() {
-        return this.container.components.popup;
+        return window.$components.firstOnElement(this.container, 'popup');
     }
 
     /**
@@ -47,6 +47,4 @@ class ConfirmDialog {
         }
     }
 
-}
-
-export default ConfirmDialog;
\ No newline at end of file
+}
\ No newline at end of file
index 65ce8c194d641db09d37ae437742611e909a1ebe..99804c4bcea010165ad4298f405860e904b9689d 100644 (file)
@@ -1,18 +1,19 @@
+import {Component} from "./component";
 
-class CustomCheckbox {
+export class CustomCheckbox extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.checkbox = elem.querySelector('input[type=checkbox]');
-        this.display = elem.querySelector('[role="checkbox"]');
+    setup() {
+        this.container = this.$el;
+        this.checkbox = this.container.querySelector('input[type=checkbox]');
+        this.display = this.container.querySelector('[role="checkbox"]');
 
         this.checkbox.addEventListener('change', this.stateChange.bind(this));
-        this.elem.addEventListener('keydown', this.onKeyDown.bind(this));
+        this.container.addEventListener('keydown', this.onKeyDown.bind(this));
     }
 
     onKeyDown(event) {
-        const isEnterOrPress = event.keyCode === 32 || event.keyCode === 13;
-        if (isEnterOrPress) {
+        const isEnterOrSpace = event.key === ' ' || event.key === 'Enter';
+        if (isEnterOrSpace) {
             event.preventDefault();
             this.toggle();
         }
@@ -29,6 +30,4 @@ class CustomCheckbox {
         this.display.setAttribute('aria-checked', checked);
     }
 
-}
-
-export default CustomCheckbox;
\ No newline at end of file
+}
\ No newline at end of file
index 1f3b66c674c2c882b7f1d71b4fcb79867c06dcfe..6466fb584882b0af36280f06dc2420f7599305e8 100644 (file)
@@ -1,21 +1,22 @@
-class DetailsHighlighter {
+import {Component} from "./component";
 
-    constructor(elem) {
-        this.elem = elem;
+export class DetailsHighlighter extends Component {
+
+    setup() {
+        this.container = this.$el;
         this.dealtWith = false;
-        elem.addEventListener('toggle', this.onToggle.bind(this));
+
+        this.container.addEventListener('toggle', this.onToggle.bind(this));
     }
 
     onToggle() {
         if (this.dealtWith) return;
 
-        if (this.elem.querySelector('pre')) {
+        if (this.container.querySelector('pre')) {
             window.importVersioned('code').then(Code => {
-                Code.highlightWithin(this.elem);
+                Code.highlightWithin(this.container);
             });
         }
         this.dealtWith = true;
     }
-}
-
-export default DetailsHighlighter;
\ No newline at end of file
+}
\ No newline at end of file
index 81fa940c24ca7cf4a38ba66fe7b55e300d746100..30a2aadc1467ddd97a1a3ccb30efea591383f07b 100644 (file)
@@ -1,7 +1,8 @@
 import {debounce} from "../services/util";
 import {transitionHeight} from "../services/animations";
+import {Component} from "./component";
 
-class DropdownSearch {
+export class DropdownSearch extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -78,6 +79,4 @@ class DropdownSearch {
         this.loadingElem.style.display = show ? 'block' : 'none';
     }
 
-}
-
-export default DropdownSearch;
\ No newline at end of file
+}
\ No newline at end of file
index 781f90860f7aa539595d56723a95d8347c1c4060..2625ff4de2f38c27d983d14d431e07e3c31fd44b 100644 (file)
@@ -1,11 +1,11 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * Dropdown
  * Provides some simple logic to create simple dropdown menus.
- * @extends {Component}
  */
-class DropDown {
+export class Dropdown extends Component {
 
     setup() {
         this.container = this.$el;
@@ -74,7 +74,7 @@ class DropDown {
     }
 
     hideAll() {
-        for (let dropdown of window.components.dropdown) {
+        for (let dropdown of window.$components.get('dropdown')) {
             dropdown.hide();
         }
     }
@@ -171,6 +171,4 @@ class DropDown {
         });
     }
 
-}
-
-export default DropDown;
\ No newline at end of file
+}
\ No newline at end of file
index 44fdf2d0d3fba800e31bf68364088751186428a0..911a033c776b81ca757db2ad1e85875d599c0877 100644 (file)
@@ -1,11 +1,8 @@
 import DropZoneLib from "dropzone";
 import {fadeOut} from "../services/animations";
+import {Component} from "./component";
 
-/**
- * Dropzone
- * @extends {Component}
- */
-class Dropzone {
+export class Dropzone extends Component {
     setup() {
         this.container = this.$el;
         this.url = this.$opts.url;
@@ -73,6 +70,4 @@ class Dropzone {
     removeAll() {
         this.dz.removeAllFiles(true);
     }
-}
-
-export default Dropzone;
\ No newline at end of file
+}
\ No newline at end of file
index 3a1442d75150f043b821795d21e709f4578fcccc..a581ae7b4609727200ad3251b6f0e5e59dd1213e 100644 (file)
@@ -1,51 +1,58 @@
-class EditorToolbox {
+import {Component} from "./component";
 
-    constructor(elem) {
+export class EditorToolbox extends Component {
+
+    setup() {
         // Elements
-        this.elem = elem;
-        this.buttons = elem.querySelectorAll('[toolbox-tab-button]');
-        this.contentElements = elem.querySelectorAll('[toolbox-tab-content]');
-        this.toggleButton = elem.querySelector('[toolbox-toggle]');
+        this.container = this.$el;
+        this.buttons = this.$manyRefs.tabButton;
+        this.contentElements = this.$manyRefs.tabContent;
+        this.toggleButton = this.$refs.toggle;
+
+        this.setupListeners();
+
+        // Set the first tab as active on load
+        this.setActiveTab(this.contentElements[0].dataset.tabContent);
+    }
 
+    setupListeners() {
         // Toolbox toggle button click
-        this.toggleButton.addEventListener('click', this.toggle.bind(this));
+        this.toggleButton.addEventListener('click', () => this.toggle());
         // Tab button click
-        this.elem.addEventListener('click', event => {
-            let button = event.target.closest('[toolbox-tab-button]');
-            if (button === null) return;
-            let name = button.getAttribute('toolbox-tab-button');
-            this.setActiveTab(name, true);
+        this.container.addEventListener('click', event => {
+            const button = event.target.closest('button');
+            if (this.buttons.includes(button)) {
+                const name = button.dataset.tab;
+                this.setActiveTab(name, true);
+            }
         });
-
-        // Set the first tab as active on load
-        this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content'));
     }
 
     toggle() {
-        this.elem.classList.toggle('open');
-        const expanded = this.elem.classList.contains('open') ? 'true' : 'false';
+        this.container.classList.toggle('open');
+        const expanded = this.container.classList.contains('open') ? 'true' : 'false';
         this.toggleButton.setAttribute('aria-expanded', expanded);
     }
 
     setActiveTab(tabName, openToolbox = false) {
+
         // Set button visibility
-        for (let i = 0, len = this.buttons.length; i < len; i++) {
-            this.buttons[i].classList.remove('active');
-            let bName =  this.buttons[i].getAttribute('toolbox-tab-button');
-            if (bName === tabName) this.buttons[i].classList.add('active');
+        for (const button of this.buttons) {
+            button.classList.remove('active');
+            const bName =  button.dataset.tab;
+            if (bName === tabName) button.classList.add('active');
         }
+
         // Set content visibility
-        for (let i = 0, len = this.contentElements.length; i < len; i++) {
-            this.contentElements[i].style.display = 'none';
-            let cName = this.contentElements[i].getAttribute('toolbox-tab-content');
-            if (cName === tabName) this.contentElements[i].style.display = 'block';
+        for (const contentEl of this.contentElements) {
+            contentEl.style.display = 'none';
+            const cName = contentEl.dataset.tabContent;
+            if (cName === tabName) contentEl.style.display = 'block';
         }
 
-        if (openToolbox && !this.elem.classList.contains('open')) {
+        if (openToolbox && !this.container.classList.contains('open')) {
             this.toggle();
         }
     }
 
-}
-
-export default EditorToolbox;
\ No newline at end of file
+}
\ No newline at end of file
index 0dec5ca0937dda6620122628e5aef22b5ccd8013..d4a616ff1d5765e2f278028016e8fa95bf1d8cbc 100644 (file)
@@ -1,9 +1,7 @@
-/**
- * @extends {Component}
- */
 import {htmlToDom} from "../services/dom";
+import {Component} from "./component";
 
-class EntityPermissions {
+export class EntityPermissions extends Component {
 
     setup() {
         this.container = this.$el;
@@ -74,6 +72,4 @@ class EntityPermissions {
         row.remove();
     }
 
-}
-
-export default EntityPermissions;
\ No newline at end of file
+}
\ No newline at end of file
index 8b1861ecf6afec1eb66e9d46843e34561471efc5..b0e42401d51b1520807cfb97d295afaf27ecd826 100644 (file)
@@ -1,10 +1,7 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
-/**
- * Class EntitySearch
- * @extends {Component}
- */
-class EntitySearch {
+export class EntitySearch extends Component {
     setup() {
         this.entityId = this.$opts.entityId;
         this.entityType = this.$opts.entityType;
@@ -54,6 +51,4 @@ class EntitySearch {
         this.loadingBlock.classList.add('hidden');
         this.searchInput.value = '';
     }
-}
-
-export default EntitySearch;
\ No newline at end of file
+}
\ No newline at end of file
index e7cb60b1f6c4c05c751033ebec4424b8de7fea1d..d455f7ee7d5286f3bbfb9979ec0651ea6dfac98c 100644 (file)
@@ -1,14 +1,10 @@
-/**
- * Entity Selector Popup
- * @extends {Component}
- */
-class EntitySelectorPopup {
+import {Component} from "./component";
+
+export class EntitySelectorPopup extends Component {
 
     setup() {
-        this.elem = this.$el;
+        this.container = this.$el;
         this.selectButton = this.$refs.select;
-
-        window.EntitySelectorPopup = this;
         this.selectorEl = this.$refs.selector;
 
         this.callback = null;
@@ -21,16 +17,26 @@ class EntitySelectorPopup {
 
     show(callback) {
         this.callback = callback;
-        this.elem.components.popup.show();
+        this.getPopup().show();
         this.getSelector().focusSearch();
     }
 
     hide() {
-        this.elem.components.popup.hide();
+        this.getPopup().hide();
     }
 
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.container, 'popup');
+    }
+
+    /**
+     * @returns {EntitySelector}
+     */
     getSelector() {
-        return this.selectorEl.components['entity-selector'];
+        return window.$components.firstOnElement(this.selectorEl, 'entity-selector');
     }
 
     onSelectButtonClick() {
@@ -51,6 +57,4 @@ class EntitySelectorPopup {
         this.getSelector().reset();
         if (this.callback && entity) this.callback(entity);
     }
-}
-
-export default EntitySelectorPopup;
\ No newline at end of file
+}
\ No newline at end of file
index e2596998aedea01a105b40496f1909c0d6b60f20..1496ea89e6da2e118b90ce0ade757df43778be12 100644 (file)
@@ -1,10 +1,10 @@
 import {onChildEvent} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * Entity Selector
- * @extends {Component}
  */
-class EntitySelector {
+export class EntitySelector extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -185,6 +185,4 @@ class EntitySelector {
         this.selectedItemData = null;
     }
 
-}
-
-export default EntitySelector;
\ No newline at end of file
+}
\ No newline at end of file
index cf0215850d07c6f4acb2c98c2899cf373e87dba0..2e6fd5fdbac008566f7fc88701000327853e7f0d 100644 (file)
@@ -1,4 +1,5 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * EventEmitSelect
@@ -10,10 +11,8 @@ import {onSelect} from "../services/dom";
  *
  * All options will be set as the "detail" of the event with
  * their values included.
- *
- * @extends {Component}
  */
-class EventEmitSelect {
+export class EventEmitSelect extends Component{
     setup() {
         this.container = this.$el;
         this.name = this.$opts.name;
@@ -24,6 +23,4 @@ class EventEmitSelect {
         });
     }
 
-}
-
-export default EventEmitSelect;
\ No newline at end of file
+}
\ No newline at end of file
index cce1b215c9378f30659befc189424f76145f4c6c..ab4d38ab1df2224270a27aab2f1fca6ae3786e10 100644 (file)
@@ -1,17 +1,15 @@
 import {slideUp, slideDown} from "../services/animations";
+import {Component} from "./component";
 
-class ExpandToggle {
+export class ExpandToggle extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-
-        // Component state
-        this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
-        this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
-        this.selector = elem.getAttribute('expand-toggle');
+    setup(elem) {
+        this.targetSelector = this.$opts.targetSelector;
+        this.isOpen = this.$opts.isOpen === 'true';
+        this.updateEndpoint = this.$opts.updateEndpoint;
 
         // Listener setup
-        elem.addEventListener('click', this.click.bind(this));
+        this.$el.addEventListener('click', this.click.bind(this));
     }
 
     open(elemToToggle) {
@@ -25,7 +23,7 @@ class ExpandToggle {
     click(event) {
         event.preventDefault();
 
-        const matchingElems = document.querySelectorAll(this.selector);
+        const matchingElems = document.querySelectorAll(this.targetSelector);
         for (let match of matchingElems) {
             this.isOpen ?  this.close(match) : this.open(match);
         }
@@ -40,6 +38,4 @@ class ExpandToggle {
         });
     }
 
-}
-
-export default ExpandToggle;
\ No newline at end of file
+}
\ No newline at end of file
index 99737bfb8b0fb741564a5c854d566fd3f774067a..11b23cca6cc06f833d8d3146ceb6c0bb0ddf6bdf 100644 (file)
@@ -1,5 +1,6 @@
+import {Component} from "./component";
 
-class HeaderMobileToggle {
+export class HeaderMobileToggle extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -36,6 +37,4 @@ class HeaderMobileToggle {
         this.onToggle(event);
     }
 
-}
-
-export default HeaderMobileToggle;
\ No newline at end of file
+}
\ No newline at end of file
index 23a6c4cbb9bc1ffb29120997af3291dd36cb2d31..a44fffc1b437776af3d723e445eef617db7757b9 100644 (file)
@@ -1,13 +1,9 @@
 import {onChildEvent, onSelect, removeLoading, showLoading} from "../services/dom";
+import {Component} from "./component";
 
-/**
- * ImageManager
- * @extends {Component}
- */
-class ImageManager {
+export class ImageManager extends Component {
 
     setup() {
-
         // Options
         this.uploadedTo = this.$opts.uploadedTo;
 
@@ -36,8 +32,6 @@ class ImageManager {
         this.resetState();
 
         this.setupListeners();
-
-        window.ImageManager = this;
     }
 
     setupListeners() {
@@ -100,7 +94,7 @@ class ImageManager {
 
         this.callback = callback;
         this.type = type;
-        this.popupEl.components.popup.show();
+        this.getPopup().show();
         this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery');
 
         if (!this.hasData) {
@@ -110,7 +104,14 @@ class ImageManager {
     }
 
     hide() {
-        this.popupEl.components.popup.hide();
+        this.getPopup().hide();
+    }
+
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.popupEl, 'popup');
     }
 
     async loadGallery() {
@@ -132,7 +133,7 @@ class ImageManager {
     addReturnedHtmlElementsToList(html) {
         const el = document.createElement('div');
         el.innerHTML = html;
-        window.components.init(el);
+        window.$components.init(el);
         for (const child of [...el.children]) {
             this.listContainer.appendChild(child);
         }
@@ -207,9 +208,7 @@ class ImageManager {
         const params = requestDelete ? {delete: true} : {};
         const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);
         this.formContainer.innerHTML = formHtml;
-        window.components.init(this.formContainer);
+        window.$components.init(this.formContainer);
     }
 
-}
-
-export default ImageManager;
\ No newline at end of file
+}
\ No newline at end of file
index 7455fa622379a798ae102dd65dc7c72aed28417e..03d9567d22e331c82a04c75bc1840e207c5d1eec 100644 (file)
@@ -1,21 +1,25 @@
+import {Component} from "./component";
 
-class ImagePicker {
+export class ImagePicker extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.imageElem = elem.querySelector('img');
-        this.imageInput = elem.querySelector('input[type=file]');
-        this.resetInput = elem.querySelector('input[data-reset-input]');
-        this.removeInput = elem.querySelector('input[data-remove-input]');
+    setup() {
+        this.imageElem = this.$refs.image;
+        this.imageInput = this.$refs.imageInput;
+        this.resetInput = this.$refs.resetInput;
+        this.removeInput = this.$refs.removeInput;
+        this.resetButton = this.$refs.resetButton;
+        this.removeButton = this.$refs.removeButton || null;
 
-        this.defaultImage = elem.getAttribute('data-default-image');
+        this.defaultImage = this.$opts.defaultImage;
 
-        const resetButton = elem.querySelector('button[data-action="reset-image"]');
-        resetButton.addEventListener('click', this.reset.bind(this));
+        this.setupListeners();
+    }
+
+    setupListeners() {
+        this.resetButton.addEventListener('click', this.reset.bind(this));
 
-        const removeButton = elem.querySelector('button[data-action="remove-image"]');
-        if (removeButton) {
-            removeButton.addEventListener('click', this.removeImage.bind(this));
+        if (this.removeButton) {
+            this.removeButton.addEventListener('click', this.removeImage.bind(this));
         }
 
         this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
@@ -50,6 +54,4 @@ class ImagePicker {
         this.resetInput.setAttribute('disabled', 'disabled');
     }
 
-}
-
-export default ImagePicker;
\ No newline at end of file
+}
\ No newline at end of file
index 9f801668e8b4958f1e8440587c38a7b2a365b4d5..2aac33f7fec4aa9a4aa855de95f6d9b8a09063da 100644 (file)
-import addRemoveRows from "./add-remove-rows.js"
-import ajaxDeleteRow from "./ajax-delete-row.js"
-import ajaxForm from "./ajax-form.js"
-import attachments from "./attachments.js"
-import attachmentsList from "./attachments-list.js"
-import autoSuggest from "./auto-suggest.js"
-import autoSubmit from "./auto-submit.js";
-import backToTop from "./back-to-top.js"
-import bookSort from "./book-sort.js"
-import chapterContents from "./chapter-contents.js"
-import codeEditor from "./code-editor.js"
-import codeHighlighter from "./code-highlighter.js"
-import codeTextarea from "./code-textarea.js"
-import collapsible from "./collapsible.js"
-import confirmDialog from "./confirm-dialog"
-import customCheckbox from "./custom-checkbox.js"
-import detailsHighlighter from "./details-highlighter.js"
-import dropdown from "./dropdown.js"
-import dropdownSearch from "./dropdown-search.js"
-import dropzone from "./dropzone.js"
-import editorToolbox from "./editor-toolbox.js"
-import entityPermissions from "./entity-permissions";
-import entitySearch from "./entity-search.js"
-import entitySelector from "./entity-selector.js"
-import entitySelectorPopup from "./entity-selector-popup.js"
-import eventEmitSelect from "./event-emit-select.js"
-import expandToggle from "./expand-toggle.js"
-import headerMobileToggle from "./header-mobile-toggle.js"
-import homepageControl from "./homepage-control.js"
-import imageManager from "./image-manager.js"
-import imagePicker from "./image-picker.js"
-import listSortControl from "./list-sort-control.js"
-import markdownEditor from "./markdown-editor.js"
-import newUserPassword from "./new-user-password.js"
-import notification from "./notification.js"
-import optionalInput from "./optional-input.js"
-import pageComments from "./page-comments.js"
-import pageDisplay from "./page-display.js"
-import pageEditor from "./page-editor.js"
-import pagePicker from "./page-picker.js"
-import permissionsTable from "./permissions-table.js"
-import pointer from "./pointer.js";
-import popup from "./popup.js"
-import settingAppColorPicker from "./setting-app-color-picker.js"
-import settingColorPicker from "./setting-color-picker.js"
-import shelfSort from "./shelf-sort.js"
-import shortcuts from "./shortcuts";
-import shortcutInput from "./shortcut-input";
-import sidebar from "./sidebar.js"
-import sortableList from "./sortable-list.js"
-import submitOnChange from "./submit-on-change.js"
-import tabs from "./tabs.js"
-import tagManager from "./tag-manager.js"
-import templateManager from "./template-manager.js"
-import toggleSwitch from "./toggle-switch.js"
-import triLayout from "./tri-layout.js"
-import userSelect from "./user-select.js"
-import webhookEvents from "./webhook-events";
-import wysiwygEditor from "./wysiwyg-editor.js"
-
-const componentMapping = {
-    "add-remove-rows": addRemoveRows,
-    "ajax-delete-row": ajaxDeleteRow,
-    "ajax-form": ajaxForm,
-    "attachments": attachments,
-    "attachments-list": attachmentsList,
-    "auto-suggest": autoSuggest,
-    "auto-submit": autoSubmit,
-    "back-to-top": backToTop,
-    "book-sort": bookSort,
-    "chapter-contents": chapterContents,
-    "code-editor": codeEditor,
-    "code-highlighter": codeHighlighter,
-    "code-textarea": codeTextarea,
-    "collapsible": collapsible,
-    "confirm-dialog": confirmDialog,
-    "custom-checkbox": customCheckbox,
-    "details-highlighter": detailsHighlighter,
-    "dropdown": dropdown,
-    "dropdown-search": dropdownSearch,
-    "dropzone": dropzone,
-    "editor-toolbox": editorToolbox,
-    "entity-permissions": entityPermissions,
-    "entity-search": entitySearch,
-    "entity-selector": entitySelector,
-    "entity-selector-popup": entitySelectorPopup,
-    "event-emit-select": eventEmitSelect,
-    "expand-toggle": expandToggle,
-    "header-mobile-toggle": headerMobileToggle,
-    "homepage-control": homepageControl,
-    "image-manager": imageManager,
-    "image-picker": imagePicker,
-    "list-sort-control": listSortControl,
-    "markdown-editor": markdownEditor,
-    "new-user-password": newUserPassword,
-    "notification": notification,
-    "optional-input": optionalInput,
-    "page-comments": pageComments,
-    "page-display": pageDisplay,
-    "page-editor": pageEditor,
-    "page-picker": pagePicker,
-    "permissions-table": permissionsTable,
-    "pointer": pointer,
-    "popup": popup,
-    "setting-app-color-picker": settingAppColorPicker,
-    "setting-color-picker": settingColorPicker,
-    "shelf-sort": shelfSort,
-    "shortcuts": shortcuts,
-    "shortcut-input": shortcutInput,
-    "sidebar": sidebar,
-    "sortable-list": sortableList,
-    "submit-on-change": submitOnChange,
-    "tabs": tabs,
-    "tag-manager": tagManager,
-    "template-manager": templateManager,
-    "toggle-switch": toggleSwitch,
-    "tri-layout": triLayout,
-    "user-select": userSelect,
-    "webhook-events": webhookEvents,
-    "wysiwyg-editor": wysiwygEditor,
-};
-
-window.components = {};
-
-/**
- * Initialize components of the given name within the given element.
- * @param {String} componentName
- * @param {HTMLElement|Document} parentElement
- */
-function searchForComponentInParent(componentName, parentElement) {
-    const elems = parentElement.querySelectorAll(`[${componentName}]`);
-    for (let j = 0, jLen = elems.length; j < jLen; j++) {
-        initComponent(componentName, elems[j]);
-    }
-}
-
-/**
- * Initialize a component instance on the given dom element.
- * @param {String} name
- * @param {Element} element
- */
-function initComponent(name, element) {
-    const componentModel = componentMapping[name];
-    if (componentModel === undefined) return;
-
-    // Create our component instance
-    let instance;
-    try {
-        instance = new componentModel(element);
-        instance.$el = element;
-        const allRefs = parseRefs(name, element);
-        instance.$refs = allRefs.refs;
-        instance.$manyRefs = allRefs.manyRefs;
-        instance.$opts = parseOpts(name, element);
-        instance.$emit = (eventName, data = {}) => {
-            data.from = instance;
-            const event = new CustomEvent(`${name}-${eventName}`, {
-                bubbles: true,
-                detail: data
-            });
-            instance.$el.dispatchEvent(event);
-        };
-        if (typeof instance.setup === 'function') {
-            instance.setup();
-        }
-    } catch (e) {
-        console.error('Failed to create component', e, name, element);
-    }
-
-
-    // Add to global listing
-    if (typeof window.components[name] === "undefined") {
-        window.components[name] = [];
-    }
-    window.components[name].push(instance);
-
-    // Add to element listing
-    if (typeof element.components === 'undefined') {
-        element.components = {};
-    }
-    element.components[name] = instance;
-}
-
-/**
- * Parse out the element references within the given element
- * for the given component name.
- * @param {String} name
- * @param {Element} element
- */
-function parseRefs(name, element) {
-    const refs = {};
-    const manyRefs = {};
-
-    const prefix = `${name}@`
-    const selector = `[refs*="${prefix}"]`;
-    const refElems = [...element.querySelectorAll(selector)];
-    if (element.matches(selector)) {
-        refElems.push(element);
-    }
-
-    for (const el of refElems) {
-        const refNames = el.getAttribute('refs')
-            .split(' ')
-            .filter(str => str.startsWith(prefix))
-            .map(str => str.replace(prefix, ''))
-            .map(kebabToCamel);
-        for (const ref of refNames) {
-            refs[ref] = el;
-            if (typeof manyRefs[ref] === 'undefined') {
-                manyRefs[ref] = [];
-            }
-            manyRefs[ref].push(el);
-        }
-    }
-    return {refs, manyRefs};
-}
-
-/**
- * Parse out the element component options.
- * @param {String} name
- * @param {Element} element
- * @return {Object<String, String>}
- */
-function parseOpts(name, element) {
-    const opts = {};
-    const prefix = `option:${name}:`;
-    for (const {name, value} of element.attributes) {
-        if (name.startsWith(prefix)) {
-            const optName = name.replace(prefix, '');
-            opts[kebabToCamel(optName)] = value || '';
-        }
-    }
-    return opts;
-}
-
-/**
- * Convert a kebab-case string to camelCase
- * @param {String} kebab
- * @returns {string}
- */
-function kebabToCamel(kebab) {
-    const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
-    const words = kebab.split('-');
-    return words[0] + words.slice(1).map(ucFirst).join('');
-}
-
-/**
- * Initialize all components found within the given element.
- * @param parentElement
- */
-function initAll(parentElement) {
-    if (typeof parentElement === 'undefined') parentElement = document;
-
-    // Old attribute system
-    for (const componentName of Object.keys(componentMapping)) {
-        searchForComponentInParent(componentName, parentElement);
-    }
-
-    // New component system
-    const componentElems = parentElement.querySelectorAll(`[component],[components]`);
-
-    for (const el of componentElems) {
-        const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
-        for (const name of componentNames) {
-            initComponent(name, el);
-        }
-    }
-}
-
-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
- * @property {function(string, Object)} $emit
- */
\ No newline at end of file
+export {AddRemoveRows} from "./add-remove-rows.js"
+export {AjaxDeleteRow} from "./ajax-delete-row.js"
+export {AjaxForm} from "./ajax-form.js"
+export {Attachments} from "./attachments.js"
+export {AttachmentsList} from "./attachments-list.js"
+export {AutoSuggest} from "./auto-suggest.js"
+export {AutoSubmit} from "./auto-submit.js"
+export {BackToTop} from "./back-to-top.js"
+export {BookSort} from "./book-sort.js"
+export {ChapterContents} from "./chapter-contents.js"
+export {CodeEditor} from "./code-editor.js"
+export {CodeHighlighter} from "./code-highlighter.js"
+export {CodeTextarea} from "./code-textarea.js"
+export {Collapsible} from "./collapsible.js"
+export {ConfirmDialog} from "./confirm-dialog"
+export {CustomCheckbox} from "./custom-checkbox.js"
+export {DetailsHighlighter} from "./details-highlighter.js"
+export {Dropdown} from "./dropdown.js"
+export {DropdownSearch} from "./dropdown-search.js"
+export {Dropzone} from "./dropzone.js"
+export {EditorToolbox} from "./editor-toolbox.js"
+export {EntityPermissions} from "./entity-permissions"
+export {EntitySearch} from "./entity-search.js"
+export {EntitySelector} from "./entity-selector.js"
+export {EntitySelectorPopup} from "./entity-selector-popup.js"
+export {EventEmitSelect} from "./event-emit-select.js"
+export {ExpandToggle} from "./expand-toggle.js"
+export {HeaderMobileToggle} from "./header-mobile-toggle.js"
+export {ImageManager} from "./image-manager.js"
+export {ImagePicker} from "./image-picker.js"
+export {ListSortControl} from "./list-sort-control.js"
+export {MarkdownEditor} from "./markdown-editor.js"
+export {NewUserPassword} from "./new-user-password.js"
+export {Notification} from "./notification.js"
+export {OptionalInput} from "./optional-input.js"
+export {PageComments} from "./page-comments.js"
+export {PageDisplay} from "./page-display.js"
+export {PageEditor} from "./page-editor.js"
+export {PagePicker} from "./page-picker.js"
+export {PermissionsTable} from "./permissions-table.js"
+export {Pointer} from "./pointer.js";
+export {Popup} from "./popup.js"
+export {SettingAppColorPicker} from "./setting-app-color-picker.js"
+export {SettingColorPicker} from "./setting-color-picker.js"
+export {SettingHomepageControl} from "./setting-homepage-control.js"
+export {ShelfSort} from "./shelf-sort.js"
+export {Shortcuts} from "./shortcuts"
+export {ShortcutInput} from "./shortcut-input"
+export {SortableList} from "./sortable-list.js"
+export {SubmitOnChange} from "./submit-on-change.js"
+export {Tabs} from "./tabs.js"
+export {TagManager} from "./tag-manager.js"
+export {TemplateManager} from "./template-manager.js"
+export {ToggleSwitch} from "./toggle-switch.js"
+export {TriLayout} from "./tri-layout.js"
+export {UserSelect} from "./user-select.js"
+export {WebhookEvents} from "./webhook-events";
+export {WysiwygEditor} from "./wysiwyg-editor.js"
\ No newline at end of file
index 3b642dbde09604485d78f81b87f280c42f2daceb..b8d4de73a0e7b8cd3326ca9e30679366043af7be 100644 (file)
@@ -1,9 +1,10 @@
 /**
  * ListSortControl
  * Manages the logic for the control which provides list sorting options.
- * @extends {Component}
  */
-class ListSortControl {
+import {Component} from "./component";
+
+export class ListSortControl extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -44,6 +45,4 @@ class ListSortControl {
         this.form.submit();
     }
 
-}
-
-export default ListSortControl;
\ No newline at end of file
+}
\ No newline at end of file
index 3290fc3001ac59786af0f4f140f301171926da5a..b9567d8643779184e74553b52c2aa9adb23420f3 100644 (file)
@@ -4,8 +4,9 @@ import Clipboard from "../services/clipboard";
 import {debounce} from "../services/util";
 import {patchDomFromHtmlString} from "../services/vdom";
 import DrawIO from "../services/drawio";
+import {Component} from "./component";
 
-class MarkdownEditor {
+export class MarkdownEditor extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -430,7 +431,9 @@ class MarkdownEditor {
 
     actionInsertImage() {
         const cursorPos = this.cm.getCursor('from');
-        window.ImageManager.show(image => {
+        /** @type {ImageManager} **/
+        const imageManager = window.$components.first('image-manager');
+        imageManager.show(image => {
             const imageUrl = image.thumbs.display || image.url;
             let selectedText = this.cm.getSelection();
             let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
@@ -442,7 +445,9 @@ class MarkdownEditor {
 
     actionShowImageManager() {
         const cursorPos = this.cm.getCursor('from');
-        window.ImageManager.show(image => {
+        /** @type {ImageManager} **/
+        const imageManager = window.$components.first('image-manager');
+        imageManager.show(image => {
             this.insertDrawing(image, cursorPos);
         }, 'drawio');
     }
@@ -450,7 +455,9 @@ class MarkdownEditor {
     // Show the popup link selector and insert a link when finished
     actionShowLinkSelector() {
         const cursorPos = this.cm.getCursor('from');
-        window.EntitySelectorPopup.show(entity => {
+        /** @type {EntitySelectorPopup} **/
+        const selector = window.$components.first('entity-selector-popup');
+        selector.show(entity => {
             let selectedText = this.cm.getSelection() || entity.name;
             let newText = `[${selectedText}](${entity.link})`;
             this.cm.focus();
@@ -619,5 +626,3 @@ class MarkdownEditor {
         });
     }
 }
-
-export default MarkdownEditor ;
index 9c4c21c14cc0bae368f8bf9be2fd5abd546b8482..a4ed4d15b300c5e255e86ef43eee9f8c7b31d014 100644 (file)
@@ -1,9 +1,11 @@
+import {Component} from "./component";
 
-class NewUserPassword {
+export class NewUserPassword extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.inviteOption = elem.querySelector('input[name=send_invite]');
+    setup() {
+        this.container = this.$el;
+        this.inputContainer = this.$refs.inputContainer;
+        this.inviteOption = this.container.querySelector('input[name=send_invite]');
 
         if (this.inviteOption) {
             this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
@@ -13,16 +15,12 @@ class NewUserPassword {
 
     inviteOptionChange() {
         const inviting = (this.inviteOption.value === 'true');
-        const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
+        const passwordBoxes = this.container.querySelectorAll('input[type=password]');
         for (const input of passwordBoxes) {
             input.disabled = inviting;
         }
-        const container = this.elem.querySelector('#password-input-container');
-        if (container) {
-            container.style.display = inviting ? 'none' : 'block';
-        }
-    }
 
-}
+        this.inputContainer.style.display = inviting ? 'none' : 'block';
+    }
 
-export default NewUserPassword;
\ No newline at end of file
+}
\ No newline at end of file
index 35bab4ea656b1c052e875fac2bcdf0e4ef27d268..8a0876241fe15232b82264e93556357691816614 100644 (file)
@@ -1,19 +1,21 @@
+import {Component} from "./component";
 
-class Notification {
+export class Notification  extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.type = elem.getAttribute('notification');
-        this.textElem = elem.querySelector('span');
-        this.autohide = this.elem.hasAttribute('data-autohide');
-        this.elem.style.display = 'grid';
+    setup() {
+        this.container = this.$el;
+        this.type = this.$opts.type;
+        this.textElem = this.container.querySelector('span');
+        this.autoHide = this.$opts.autoHide === 'true';
+        this.initialShow = this.$opts.show === 'true'
+        this.container.style.display = 'grid';
 
         window.$events.listen(this.type, text => {
             this.show(text);
         });
-        elem.addEventListener('click', this.hide.bind(this));
+        this.container.addEventListener('click', this.hide.bind(this));
 
-        if (elem.hasAttribute('data-show')) {
+        if (this.initialShow) {
             setTimeout(() => this.show(this.textElem.textContent), 100);
         }
 
@@ -21,14 +23,14 @@ class Notification {
     }
 
     show(textToShow = '') {
-        this.elem.removeEventListener('transitionend', this.hideCleanup);
+        this.container.removeEventListener('transitionend', this.hideCleanup);
         this.textElem.textContent = textToShow;
-        this.elem.style.display = 'grid';
+        this.container.style.display = 'grid';
         setTimeout(() => {
-            this.elem.classList.add('showing');
+            this.container.classList.add('showing');
         }, 1);
 
-        if (this.autohide) {
+        if (this.autoHide) {
             const words = textToShow.split(' ').length;
             const timeToShow = Math.max(2000, 1000 + (250 * words));
             setTimeout(this.hide.bind(this), timeToShow);
@@ -36,15 +38,13 @@ class Notification {
     }
 
     hide() {
-        this.elem.classList.remove('showing');
-        this.elem.addEventListener('transitionend', this.hideCleanup);
+        this.container.classList.remove('showing');
+        this.container.addEventListener('transitionend', this.hideCleanup);
     }
 
     hideCleanup() {
-        this.elem.style.display = 'none';
-        this.elem.removeEventListener('transitionend', this.hideCleanup);
+        this.container.style.display = 'none';
+        this.container.removeEventListener('transitionend', this.hideCleanup);
     }
 
-}
-
-export default Notification;
\ No newline at end of file
+}
\ No newline at end of file
index eab58e42a584a53c18a1727a3fe497789c23c381..cc429c991522bcd9b51a5fc811127e7775a4a066 100644 (file)
@@ -1,6 +1,7 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
-class OptionalInput {
+export class OptionalInput extends Component {
     setup() {
         this.removeButton = this.$refs.remove;
         this.showButton = this.$refs.show;
@@ -23,6 +24,4 @@ class OptionalInput {
         });
     }
 
-}
-
-export default OptionalInput;
\ No newline at end of file
+}
\ No newline at end of file
index c86eead1b865bd8bdaa8184f44bd4ab55a961d7b..726531e951f86f4a8447b41844803eb1d01dd083 100644 (file)
@@ -1,9 +1,8 @@
 import {scrollAndHighlightElement} from "../services/util";
+import {Component} from "./component";
+import {htmlToDom} from "../services/dom";
 
-/**
- * @extends {Component}
- */
-class PageComments {
+export class PageComments extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -90,7 +89,7 @@ class PageComments {
             newComment.innerHTML = resp.data;
             this.editingComment.innerHTML = newComment.children[0].innerHTML;
             window.$events.success(this.updatedText);
-            window.components.init(this.editingComment);
+            window.$components.init(this.editingComment);
             this.closeUpdateForm();
             this.editingComment = null;
         }).catch(window.$events.showValidationErrors).then(() => {
@@ -119,11 +118,9 @@ class PageComments {
         };
         this.showLoading(this.form);
         window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
-            let newComment = document.createElement('div');
-            newComment.innerHTML = resp.data;
-            let newElem = newComment.children[0];
+            const newElem = htmlToDom(resp.data);
             this.container.appendChild(newElem);
-            window.components.init(newElem);
+            window.$components.init(newElem);
             window.$events.success(this.createdText);
             this.resetForm();
             this.updateCount();
@@ -199,6 +196,4 @@ class PageComments {
         formElem.querySelector('.form-group.loading').style.display = 'none';
     }
 
-}
-
-export default PageComments;
\ No newline at end of file
+}
\ No newline at end of file
index b4f1cca4fbc5682fc124d0342ee3189c198a0c3a..c06c3310dee2c44c221b331c8a78b8d3eee9d3f3 100644 (file)
@@ -1,11 +1,12 @@
 import * as DOM from "../services/dom";
 import {scrollAndHighlightElement} from "../services/util";
+import {Component} from "./component";
 
-class PageDisplay {
+export class PageDisplay extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.pageId = elem.getAttribute('page-display');
+    setup() {
+        this.container = this.$el;
+        this.pageId = this.$opts.pageId;
 
         window.importVersioned('code').then(Code => Code.highlight());
         this.setupNavHighlighting();
@@ -13,7 +14,7 @@ class PageDisplay {
 
         // Check the hash on load
         if (window.location.hash) {
-            let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
+            const text = window.location.hash.replace(/%20/g, ' ').substring(1);
             this.goToText(text);
         }
 
@@ -22,7 +23,7 @@ class PageDisplay {
         if (sidebarPageNav) {
             DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
                 event.preventDefault();
-                window.components['tri-layout'][0].showContent();
+                window.$components.first('tri-layout').showContent();
                 const contentId = child.getAttribute('href').substr(1);
                 this.goToText(contentId);
                 window.history.pushState(null, null, '#' + contentId);
@@ -49,17 +50,10 @@ class PageDisplay {
     }
 
     setupNavHighlighting() {
-        // Check if support is present for IntersectionObserver
-        if (!('IntersectionObserver' in window) ||
-            !('IntersectionObserverEntry' in window) ||
-            !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
-            return;
-        }
-
-        let pageNav = document.querySelector('.sidebar-page-nav');
+        const pageNav = document.querySelector('.sidebar-page-nav');
 
         // fetch all the headings.
-        let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
+        const headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
         // if headings are present, add observers.
         if (headings.length > 0 && pageNav !== null) {
             addNavObserver(headings);
@@ -67,21 +61,21 @@ class PageDisplay {
 
         function addNavObserver(headings) {
             // Setup the intersection observer.
-            let intersectOpts = {
+            const intersectOpts = {
                 rootMargin: '0px 0px 0px 0px',
                 threshold: 1.0
             };
-            let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
+            const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
 
             // observe each heading
-            for (let heading of headings) {
+            for (const heading of headings) {
                 pageNavObserver.observe(heading);
             }
         }
 
         function headingVisibilityChange(entries, observer) {
-            for (let entry of entries) {
-                let isVisible = (entry.intersectionRatio === 1);
+            for (const entry of entries) {
+                const isVisible = (entry.intersectionRatio === 1);
                 toggleAnchorHighlighting(entry.target.id, isVisible);
             }
         }
@@ -99,9 +93,7 @@ class PageDisplay {
             codeMirrors.forEach(cm => cm.CodeMirror && cm.CodeMirror.refresh());
         };
 
-        const details = [...this.elem.querySelectorAll('details')];
+        const details = [...this.container.querySelectorAll('details')];
         details.forEach(detail => detail.addEventListener('toggle', onToggle));
     }
-}
-
-export default PageDisplay;
+}
\ No newline at end of file
index ce123e987b055db94769f7326a8fdf63f841342b..d6faabd054129d76e7d94bf6c0a69d430c6688d1 100644 (file)
@@ -1,11 +1,8 @@
 import * as Dates from "../services/dates";
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
-/**
- * Page Editor
- * @extends {Component}
- */
-class PageEditor {
+export class PageEditor extends Component {
     setup() {
         // Options
         this.draftsEnabled = this.$opts.draftsEnabled === 'true';
@@ -199,7 +196,8 @@ class PageEditor {
         event.preventDefault();
 
         const link = event.target.closest('a').href;
-        const dialog = this.switchDialogContainer.components['confirm-dialog'];
+        /** @var {ConfirmDialog} **/
+        const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
         const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
 
         if (saved && confirmed) {
@@ -207,6 +205,4 @@ class PageEditor {
         }
     }
 
-}
-
-export default PageEditor;
\ No newline at end of file
+}
\ No newline at end of file
index 577e9f6db7c8a19491c37860a20a6bd6711d3445..fba0a0a43f779a8802c0a7130789fb948f2e0226 100644 (file)
@@ -1,14 +1,14 @@
+import {Component} from "./component";
 
-class PagePicker {
+export class PagePicker extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.input = elem.querySelector('input');
-        this.resetButton = elem.querySelector('[page-picker-reset]');
-        this.selectButton = elem.querySelector('[page-picker-select]');
-        this.display = elem.querySelector('[page-picker-display]');
-        this.defaultDisplay = elem.querySelector('[page-picker-default]');
-        this.buttonSep = elem.querySelector('span.sep');
+    setup() {
+        this.input = this.$refs.input;
+        this.resetButton = this.$refs.resetButton;
+        this.selectButton = this.$refs.selectButton;
+        this.display = this.$refs.display;
+        this.defaultDisplay = this.$refs.defaultDisplay;
+        this.buttonSep = this.$refs.buttonSeperator;
 
         this.value = this.input.value;
         this.setupListeners();
@@ -24,7 +24,9 @@ class PagePicker {
     }
 
     showPopup() {
-        window.EntitySelectorPopup.show(entity => {
+        /** @type {EntitySelectorPopup} **/
+        const selectorPopup = window.$components.first('entity-selector-popup');
+        selectorPopup.show(entity => {
             this.setValue(entity.id, entity.name);
         });
     }
@@ -36,7 +38,7 @@ class PagePicker {
     }
 
     controlView(name) {
-        let hasValue = this.value && this.value !== 0;
+        const hasValue = this.value && this.value !== 0;
         toggleElem(this.resetButton, hasValue);
         toggleElem(this.buttonSep, hasValue);
         toggleElem(this.defaultDisplay, !hasValue);
@@ -55,8 +57,5 @@ class PagePicker {
 }
 
 function toggleElem(elem, show) {
-    let display = (elem.tagName === 'BUTTON' || elem.tagName === 'SPAN') ? 'inline-block' : 'block';
-    elem.style.display = show ? display : 'none';
-}
-
-export default PagePicker;
\ No newline at end of file
+    elem.style.display = show ? null : 'none';
+}
\ No newline at end of file
index d33c9928f9412998be6523f2244fcb0bc32b30c3..58ead1d57620b58a798ccdc0dfc3708121c8a1f9 100644 (file)
@@ -1,5 +1,6 @@
+import {Component} from "./component";
 
-class PermissionsTable {
+export class PermissionsTable extends Component {
 
     setup() {
         this.container = this.$el;
@@ -62,6 +63,4 @@ class PermissionsTable {
         }
     }
 
-}
-
-export default PermissionsTable;
\ No newline at end of file
+}
\ No newline at end of file
index d1967acd01c7821e81291ac22e4b1474568ac529..d884dc7214ce3ac71673ac0bddf71f39c2755fee 100644 (file)
@@ -1,10 +1,9 @@
 import * as DOM from "../services/dom";
 import Clipboard from "clipboard/dist/clipboard.min";
+import {Component} from "./component";
 
-/**
- * @extends Component
- */
-class Pointer {
+
+export class Pointer extends Component {
 
     setup() {
         this.container = this.$el;
@@ -126,6 +125,4 @@ class Pointer {
             editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
         }
     }
-}
-
-export default Pointer;
\ No newline at end of file
+}
\ No newline at end of file
index ec111963f51e65358c51d5d3333bd5d47fb8e55d..4c20876f854dbfd8441b7cd4d3d8ffb38bc6c28d 100644 (file)
@@ -1,13 +1,13 @@
 import {fadeIn, fadeOut} from "../services/animations";
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * 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 {
+export class Popup extends Component {
 
     setup() {
         this.container = this.$el;
@@ -56,6 +56,4 @@ class Popup {
         this.onHide = onHide;
     }
 
-}
-
-export default Popup;
\ No newline at end of file
+}
\ No newline at end of file
index ee894c9325c5c5fc0bef05f684a2e6e8032f3661..68e5abce5a2d8fddb670a9b8a5ee2c482490a3d8 100644 (file)
@@ -1,23 +1,13 @@
+import {Component} from "./component";
 
-class SettingAppColorPicker {
+export class SettingAppColorPicker extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.colorInput = elem.querySelector('input[type=color]');
-        this.lightColorInput = elem.querySelector('input[name="setting-app-color-light"]');
-        this.resetButton = elem.querySelector('[setting-app-color-picker-reset]');
-        this.defaultButton = elem.querySelector('[setting-app-color-picker-default]');
+    setup() {
+        this.colorInput = this.$refs.input;
+        this.lightColorInput = this.$refs.lightInput;
 
         this.colorInput.addEventListener('change', this.updateColor.bind(this));
         this.colorInput.addEventListener('input', this.updateColor.bind(this));
-        this.resetButton.addEventListener('click', event => {
-            this.colorInput.value = this.colorInput.dataset.current;
-            this.updateColor();
-        });
-        this.defaultButton.addEventListener('click', event => {
-            this.colorInput.value = this.colorInput.dataset.default;
-            this.updateColor();
-        });
     }
 
     /**
@@ -44,8 +34,8 @@ class SettingAppColorPicker {
     /**
      * Covert a hex color code to rgb components.
      * @attribution https://stackoverflow.com/a/5624139
-     * @param hex
-     * @returns {*}
+     * @param {String} hex
+     * @returns {{r: Number, g: Number, b: Number}}
      */
     hexToRgb(hex) {
         const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@@ -57,5 +47,3 @@ class SettingAppColorPicker {
     }
 
 }
-
-export default SettingAppColorPicker;
index 4d8ce0f933ced670923e375902712cc82b7dd0dd..876e14f20c276a2fc7c4401316f0b80e1fa6a0a8 100644 (file)
@@ -1,18 +1,20 @@
+import {Component} from "./component";
 
-class SettingColorPicker {
+export class SettingColorPicker extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.colorInput = elem.querySelector('input[type=color]');
-        this.resetButton = elem.querySelector('[setting-color-picker-reset]');
-        this.defaultButton = elem.querySelector('[setting-color-picker-default]');
-        this.resetButton.addEventListener('click', event => {
-            this.colorInput.value = this.colorInput.dataset.current;
-        });
-        this.defaultButton.addEventListener('click', event => {
-          this.colorInput.value = this.colorInput.dataset.default;
-        });
+    setup() {
+        this.colorInput = this.$refs.input;
+        this.resetButton = this.$refs.resetButton;
+        this.defaultButton = this.$refs.defaultButton;
+        this.currentColor = this.$opts.current;
+        this.defaultColor = this.$opts.default;
+
+        this.resetButton.addEventListener('click', () => this.setValue(this.currentColor));
+        this.defaultButton.addEventListener('click', () => this.setValue(this.defaultColor));
     }
-}
 
-export default SettingColorPicker;
+    setValue(value) {
+        this.colorInput.value = value;
+        this.colorInput.dispatchEvent(new Event('change'));
+    }
+}
\ No newline at end of file
similarity index 55%
rename from resources/js/components/homepage-control.js
rename to resources/js/components/setting-homepage-control.js
index 9db9e17b88ee58419256b6576a8ff76a7b5a59a1..992be9f826dad07c36b74abcdbc647ff53dc0ce2 100644 (file)
@@ -1,10 +1,10 @@
+import {Component} from "./component";
 
-class HomepageControl {
+export class SettingHomepageControl extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.typeControl = elem.querySelector('[name="setting-app-homepage-type"]');
-        this.pagePickerContainer = elem.querySelector('[page-picker-container]');
+    setup() {
+        this.typeControl = this.$refs.typeControl;
+        this.pagePickerContainer = this.$refs.pagePickerContainer;
 
         this.typeControl.addEventListener('change', this.controlPagePickerVisibility.bind(this));
         this.controlPagePickerVisibility();
@@ -14,9 +14,4 @@ class HomepageControl {
         const showPagePicker = this.typeControl.value === 'page';
         this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none');
     }
-
-
-
-}
-
-export default HomepageControl;
\ No newline at end of file
+}
\ No newline at end of file
index 30eda5a21f7cf920fc99f254ac051b3e36b79408..d10470bd79a4ea7e19774bcb5eedc3c6e533ffad 100644 (file)
@@ -1,6 +1,7 @@
 import Sortable from "sortablejs";
+import {Component} from "./component";
 
-class ShelfSort {
+export class ShelfSort extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -15,7 +16,7 @@ class ShelfSort {
 
     initSortable() {
         const scrollBoxes = this.elem.querySelectorAll('.scroll-box');
-        for (let scrollBox of scrollBoxes) {
+        for (const scrollBox of scrollBoxes) {
             new Sortable(scrollBox, {
                 group: 'shelf-books',
                 ghostClass: 'primary-background-light',
@@ -78,6 +79,4 @@ class ShelfSort {
         this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
     }
 
-}
-
-export default ShelfSort;
\ No newline at end of file
+}
\ No newline at end of file
index fa137898856da8e12dabfb96d998bb8e959e32e6..2a2aaa225a518c34413df94de2851745129dfa9e 100644 (file)
@@ -1,13 +1,12 @@
+import {Component} from "./component";
+
 /**
  * Keys to ignore when recording shortcuts.
  * @type {string[]}
  */
 const ignoreKeys = ['Control', 'Alt', 'Shift', 'Meta', 'Super', ' ', '+', 'Tab', 'Escape'];
 
-/**
- * @extends {Component}
- */
-class ShortcutInput {
+export class ShortcutInput extends Component {
 
     setup() {
         this.input = this.$el;
@@ -52,6 +51,4 @@ class ShortcutInput {
         this.input.removeEventListener('keydown', this.listenerRecordKey);
     }
 
-}
-
-export default ShortcutInput;
\ No newline at end of file
+}
\ No newline at end of file
index 4efa3d42b8734458d9f6bc5fa2a782dcaa58702d..a87213b2e8968070b5a0934cc3db0e6a1ef75fa0 100644 (file)
@@ -1,3 +1,5 @@
+import {Component} from "./component";
+
 function reverseMap(map) {
     const reversed = {};
     for (const [key, value] of Object.entries(map)) {
@@ -6,10 +8,8 @@ function reverseMap(map) {
     return reversed;
 }
 
-/**
- * @extends {Component}
- */
-class Shortcuts {
+
+export class Shortcuts extends Component {
 
     setup() {
         this.container = this.$el;
@@ -159,6 +159,4 @@ class Shortcuts {
 
         this.hintsShowing = false;
     }
-}
-
-export default Shortcuts;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/js/components/sidebar.js b/resources/js/components/sidebar.js
deleted file mode 100644 (file)
index 0fecc5e..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-
-class Sidebar {
-
-    constructor(elem) {
-        this.elem = elem;
-        this.toggleElem = elem.querySelector('.sidebar-toggle');
-        this.toggleElem.addEventListener('click', this.toggle.bind(this));
-    }
-
-    toggle(show = true) {
-        this.elem.classList.toggle('open');
-    }
-
-}
-
-export default Sidebar;
\ No newline at end of file
index 0af0e11c901a5b58d98f4ef7687004c117334e3d..bbbd92088ab9f3191e516cecae9833c2058b05a7 100644 (file)
@@ -1,4 +1,5 @@
 import Sortable from "sortablejs";
+import {Component} from "./component";
 
 /**
  * SortableList
@@ -6,10 +7,8 @@ import Sortable from "sortablejs";
  * Can have data set on the dragged items by setting a 'data-drag-content' attribute.
  * This attribute must contain JSON where the keys are content types and the values are
  * the data to set on the data-transfer.
- *
- * @extends {Component}
  */
-class SortableList {
+export class SortableList extends Component {
     setup() {
         this.container = this.$el;
         this.handleSelector = this.$opts.handleSelector;
@@ -34,6 +33,4 @@ class SortableList {
             dragoverBubble: false,
         });
     }
-}
-
-export default SortableList;
\ No newline at end of file
+}
\ No newline at end of file
index aeacae23213bd96b94290ed9ab6a4e9e131a4b78..da4ac699608f21a11a5392dfd272a3492ae178a9 100644 (file)
@@ -1,9 +1,10 @@
+import {Component} from "./component";
+
 /**
  * Submit on change
  * Simply submits a parent form when this input is changed.
- * @extends {Component}
  */
-class SubmitOnChange {
+export class SubmitOnChange extends Component {
 
     setup() {
         this.filter = this.$opts.filter;
@@ -21,6 +22,4 @@ class SubmitOnChange {
         });
     }
 
-}
-
-export default SubmitOnChange;
\ No newline at end of file
+}
\ No newline at end of file
index 7121d70448c728530231eed0cbd74dc5647326f3..46063d240e3e3704910fe5a0782b6df4bfd73d27 100644 (file)
@@ -1,11 +1,11 @@
+import {onSelect} from "../services/dom";
+import {Component} from "./component";
+
 /**
  * Tabs
  * Works by matching 'tabToggle<Key>' with 'tabContent<Key>' sections.
- * @extends {Component}
  */
-import {onSelect} from "../services/dom";
-
-class Tabs {
+export class Tabs extends Component {
 
     setup() {
         this.tabContentsByName = {};
@@ -46,6 +46,4 @@ class Tabs {
         }
     }
 
-}
-
-export default Tabs;
\ No newline at end of file
+}
\ No newline at end of file
index 99302b6c05176e057ffd161edc9fbd3b9b8ca46f..cfbc514a07250070563798cb04c1b1f330e92c1f 100644 (file)
@@ -1,8 +1,6 @@
-/**
- * TagManager
- * @extends {Component}
- */
-class TagManager {
+import {Component} from "./component";
+
+export class TagManager extends Component {
     setup() {
         this.addRemoveComponentEl = this.$refs.addRemove;
         this.container = this.$el;
@@ -13,7 +11,8 @@ class TagManager {
 
     setupListeners() {
         this.container.addEventListener('change', event => {
-            const addRemoveComponent = this.addRemoveComponentEl.components['add-remove-rows'];
+            /** @var {AddRemoveRows} **/
+            const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows');
             if (!this.hasEmptyRows()) {
                 addRemoveComponent.add();
             }
@@ -27,6 +26,4 @@ class TagManager {
         });
         return firstEmpty !== undefined;
     }
-}
-
-export default TagManager;
\ No newline at end of file
+}
\ No newline at end of file
index f8b19a40c86c5baf51d4239e887c55beba97c48b..774336471f00c954ec74b6625566d886d2a1ac7b 100644 (file)
@@ -1,25 +1,48 @@
 import * as DOM from "../services/dom";
+import {Component} from "./component";
 
-class TemplateManager {
+export class TemplateManager extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.list = elem.querySelector('[template-manager-list]');
-        this.searching = false;
+    setup() {
+        this.container = this.$el;
+        this.list = this.$refs.list;
 
+        this.searchInput = this.$refs.searchInput;
+        this.searchButton = this.$refs.searchButton;
+        this.searchCancel = this.$refs.searchCancel;
+
+        this.setupListeners();
+    }
+
+    setupListeners() {
         // Template insert action buttons
-        DOM.onChildEvent(this.elem, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));
+        DOM.onChildEvent(this.container, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));
 
         // Template list pagination click
-        DOM.onChildEvent(this.elem, '.pagination a', 'click', this.handlePaginationClick.bind(this));
+        DOM.onChildEvent(this.container, '.pagination a', 'click', this.handlePaginationClick.bind(this));
 
         // Template list item content click
-        DOM.onChildEvent(this.elem, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
+        DOM.onChildEvent(this.container, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
 
         // Template list item drag start
-        DOM.onChildEvent(this.elem, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
+        DOM.onChildEvent(this.container, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
 
-        this.setupSearchBox();
+        // Search box enter press
+        this.searchInput.addEventListener('keypress', event => {
+            if (event.key === 'Enter') {
+                event.preventDefault();
+                this.performSearch();
+            }
+        });
+
+        // Search submit button press
+        this.searchButton.addEventListener('click', event => this.performSearch());
+
+        // Search cancel button press
+        this.searchCancel.addEventListener('click', event => {
+            this.searchInput.value = '';
+            this.performSearch();
+        });
     }
 
     handleTemplateItemClick(event, templateItem) {
@@ -54,45 +77,12 @@ class TemplateManager {
         this.list.innerHTML = resp.data;
     }
 
-    setupSearchBox() {
-        const searchBox = this.elem.querySelector('.search-box');
-
-        // Search box may not exist if there are no existing templates in the system.
-        if (!searchBox) return;
-
-        const input = searchBox.querySelector('input');
-        const submitButton = searchBox.querySelector('button');
-        const cancelButton = searchBox.querySelector('button.search-box-cancel');
-
-        async function performSearch() {
-            const searchTerm = input.value;
-            const resp = await window.$http.get(`/templates`, {
-                search: searchTerm
-            });
-            cancelButton.style.display = searchTerm ? 'block' : 'none';
-            this.list.innerHTML = resp.data;
-        }
-        performSearch = performSearch.bind(this);
-
-        // Search box enter press
-        searchBox.addEventListener('keypress', event => {
-            if (event.key === 'Enter') {
-                event.preventDefault();
-                performSearch();
-            }
-        });
-
-        // Submit button press
-        submitButton.addEventListener('click', event => {
-            performSearch();
-        });
-
-        // Cancel button press
-        cancelButton.addEventListener('click', event => {
-            input.value = '';
-            performSearch();
+    async performSearch() {
+        const searchTerm = this.searchInput.value;
+        const resp = await window.$http.get(`/templates`, {
+            search: searchTerm
         });
+        this.searchCancel.style.display = searchTerm ? 'block' : 'none';
+        this.list.innerHTML = resp.data;
     }
-}
-
-export default TemplateManager;
\ No newline at end of file
+}
\ No newline at end of file
index b9b96afc5d07728d992e9cd49eab9e29a6df1f2a..b749eb54132492efa06b53a05a151fe35c282c3c 100644 (file)
@@ -1,10 +1,10 @@
+import {Component} from "./component";
 
-class ToggleSwitch {
+export class ToggleSwitch extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.input = elem.querySelector('input[type=hidden]');
-        this.checkbox = elem.querySelector('input[type=checkbox]');
+    setup() {
+        this.input = this.$el.querySelector('input[type=hidden]');
+        this.checkbox = this.$el.querySelector('input[type=checkbox]');
 
         this.checkbox.addEventListener('change', this.stateChange.bind(this));
     }
@@ -18,6 +18,4 @@ class ToggleSwitch {
         this.input.dispatchEvent(changeEvent);
     }
 
-}
-
-export default ToggleSwitch;
\ No newline at end of file
+}
\ No newline at end of file
index f801e52a193715fdea427fa4eac03c2372ca9abf..ead2ac3d0dae85d4f865cd191d6ff768307eaa76 100644 (file)
@@ -1,5 +1,6 @@
+import {Component} from "./component";
 
-class TriLayout {
+export class TriLayout extends Component {
 
     setup() {
         this.container = this.$refs.container;
@@ -108,6 +109,4 @@ class TriLayout {
         this.lastTabShown = tabName;
     }
 
-}
-
-export default TriLayout;
\ No newline at end of file
+}
\ No newline at end of file
index aba43e0a920a5c66b0cdb8097061cec9c039d211..d4d88a633c115ab06a9bd718e32ca41090efc109 100644 (file)
@@ -1,25 +1,28 @@
 import {onChildEvent} from "../services/dom";
+import {Component} from "./component";
 
-class UserSelect {
+export class UserSelect extends Component {
 
     setup() {
+        this.container = this.$el;
         this.input = this.$refs.input;
         this.userInfoContainer = this.$refs.userInfo;
 
-        this.hide = this.$el.components.dropdown.hide;
-
-        onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
+        onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
     }
 
     selectUser(event, userEl) {
         event.preventDefault();
-        const id = userEl.getAttribute('data-id');
-        this.input.value = id;
+        this.input.value = userEl.getAttribute('data-id');
         this.userInfoContainer.innerHTML = userEl.innerHTML;
         this.input.dispatchEvent(new Event('change', {bubbles: true}));
         this.hide();
     }
 
-}
+    hide() {
+        /** @var {Dropdown} **/
+        const dropdown = window.$components.firstOnElement(this.container, 'dropdown');
+        dropdown.hide();
+    }
 
-export default UserSelect;
\ No newline at end of file
+}
\ No newline at end of file
index aa50aa9d883f1541cbf08eed3d9865b65c252789..ad8c59ac2abd7c73a1857602f1f465b754f016e9 100644 (file)
@@ -1,10 +1,10 @@
-
 /**
  * Webhook Events
  * Manages dynamic selection control in the webhook form interface.
- * @extends {Component}
  */
-class WebhookEvents {
+import {Component} from "./component";
+
+export class WebhookEvents extends Component {
 
     setup() {
         this.checkboxes = this.$el.querySelectorAll('input[type="checkbox"]');
@@ -27,6 +27,4 @@ class WebhookEvents {
         }
     }
 
-}
-
-export default WebhookEvents;
\ No newline at end of file
+}
\ No newline at end of file
index 446f2ca4938696e6538774fa8357419f727b72c0..976dba68f6eefc654f83495dc2d245d8eeb164bb 100644 (file)
@@ -1,6 +1,7 @@
 import {build as buildEditorConfig} from "../wysiwyg/config";
+import {Component} from "./component";
 
-class WysiwygEditor {
+export class WysiwygEditor extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -35,6 +36,4 @@ class WysiwygEditor {
         return '';
     }
 
-}
-
-export default WysiwygEditor;
+}
\ No newline at end of file
diff --git a/resources/js/services/components.js b/resources/js/services/components.js
new file mode 100644 (file)
index 0000000..7434f64
--- /dev/null
@@ -0,0 +1,178 @@
+/**
+ * A mapping of active components keyed by name, with values being arrays of component
+ * instances since there can be multiple components of the same type.
+ * @type {Object<String, Component[]>}
+ */
+const components = {};
+
+/**
+ * A mapping of component class models, keyed by name.
+ * @type {Object<String, Constructor<Component>>}
+ */
+const componentModelMap = {};
+
+/**
+ * A mapping of active component maps, keyed by the element components are assigned to.
+ * @type {WeakMap<Element, Object<String, Component>>}
+ */
+const elementComponentMap = new WeakMap();
+
+/**
+ * Initialize a component instance on the given dom element.
+ * @param {String} name
+ * @param {Element} element
+ */
+function initComponent(name, element) {
+    /** @type {Function<Component>|undefined} **/
+    const componentModel = componentModelMap[name];
+    if (componentModel === undefined) return;
+
+    // Create our component instance
+    /** @type {Component} **/
+    let instance;
+    try {
+        instance = new componentModel();
+        instance.$name = name;
+        instance.$el = element;
+        const allRefs = parseRefs(name, element);
+        instance.$refs = allRefs.refs;
+        instance.$manyRefs = allRefs.manyRefs;
+        instance.$opts = parseOpts(name, element);
+        instance.setup();
+    } catch (e) {
+        console.error('Failed to create component', e, name, element);
+    }
+
+    // Add to global listing
+    if (typeof components[name] === "undefined") {
+        components[name] = [];
+    }
+    components[name].push(instance);
+
+    // Add to element mapping
+    const elComponents = elementComponentMap.get(element) || {};
+    elComponents[name] = instance;
+    elementComponentMap.set(element, elComponents);
+}
+
+/**
+ * Parse out the element references within the given element
+ * for the given component name.
+ * @param {String} name
+ * @param {Element} element
+ */
+function parseRefs(name, element) {
+    const refs = {};
+    const manyRefs = {};
+
+    const prefix = `${name}@`
+    const selector = `[refs*="${prefix}"]`;
+    const refElems = [...element.querySelectorAll(selector)];
+    if (element.matches(selector)) {
+        refElems.push(element);
+    }
+
+    for (const el of refElems) {
+        const refNames = el.getAttribute('refs')
+            .split(' ')
+            .filter(str => str.startsWith(prefix))
+            .map(str => str.replace(prefix, ''))
+            .map(kebabToCamel);
+        for (const ref of refNames) {
+            refs[ref] = el;
+            if (typeof manyRefs[ref] === 'undefined') {
+                manyRefs[ref] = [];
+            }
+            manyRefs[ref].push(el);
+        }
+    }
+    return {refs, manyRefs};
+}
+
+/**
+ * Parse out the element component options.
+ * @param {String} name
+ * @param {Element} element
+ * @return {Object<String, String>}
+ */
+function parseOpts(name, element) {
+    const opts = {};
+    const prefix = `option:${name}:`;
+    for (const {name, value} of element.attributes) {
+        if (name.startsWith(prefix)) {
+            const optName = name.replace(prefix, '');
+            opts[kebabToCamel(optName)] = value || '';
+        }
+    }
+    return opts;
+}
+
+/**
+ * Convert a kebab-case string to camelCase
+ * @param {String} kebab
+ * @returns {string}
+ */
+function kebabToCamel(kebab) {
+    const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
+    const words = kebab.split('-');
+    return words[0] + words.slice(1).map(ucFirst).join('');
+}
+
+/**
+ * Initialize all components found within the given element.
+ * @param {Element|Document} parentElement
+ */
+export function init(parentElement = document) {
+    const componentElems = parentElement.querySelectorAll(`[component],[components]`);
+
+    for (const el of componentElems) {
+        const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
+        for (const name of componentNames) {
+            initComponent(name, el);
+        }
+    }
+}
+
+/**
+ * Register the given component mapping into the component system.
+ * @param {Object<String, ObjectConstructor<Component>>} mapping
+ */
+export function register(mapping) {
+    const keys = Object.keys(mapping);
+    for (const key of keys) {
+        componentModelMap[camelToKebab(key)] = mapping[key];
+    }
+}
+
+/**
+ * Get the first component of the given name.
+ * @param {String} name
+ * @returns {Component|null}
+ */
+export function first(name) {
+    return (components[name] || [null])[0];
+}
+
+/**
+ * Get all the components of the given name.
+ * @param {String} name
+ * @returns {Component[]}
+ */
+export function get(name) {
+    return components[name] || [];
+}
+
+/**
+ * Get the first component, of the given name, that's assigned to the given element.
+ * @param {Element} element
+ * @param {String} name
+ * @returns {Component|null}
+ */
+export function firstOnElement(element, name) {
+    const elComponents = elementComponentMap.get(element) || {};
+    return elComponents[name] || null;
+}
+
+function camelToKebab(camelStr) {
+    return camelStr.replace(/[A-Z]/g, (str, offset) =>  (offset > 0 ? '-' : '') + str.toLowerCase());
+}
\ No newline at end of file
index eb5f6a8530d512c6a4a965baa57d2b24a14c7d51..882d5228d7593abf80e4cb98c2e7c5133611d47b 100644 (file)
@@ -128,6 +128,6 @@ export function removeLoading(element) {
 export function htmlToDom(html) {
     const wrap = document.createElement('div');
     wrap.innerHTML = html;
-    window.components.init(wrap);
+    window.$components.init(wrap);
     return wrap.children[0];
 }
\ No newline at end of file
index acf5e1d52530c9b8478e4eeb10455a7b664d08fa..d5ec20e2631356ba2d09ed7e4f0b7cfd2521e7ca 100644 (file)
@@ -73,7 +73,9 @@ function file_picker_callback(callback, value, meta) {
 
     // field_name, url, type, win
     if (meta.filetype === 'file') {
-        window.EntitySelectorPopup.show(entity => {
+        /** @type {EntitySelectorPopup} **/
+        const selector = window.$components.first('entity-selector-popup');
+        selector.show(entity => {
             callback(entity.link, {
                 text: entity.name,
                 title: entity.name,
@@ -83,7 +85,9 @@ function file_picker_callback(callback, value, meta) {
 
     if (meta.filetype === 'image') {
         // Show image manager
-        window.ImageManager.show(function (image) {
+        /** @type {ImageManager} **/
+        const imageManager = window.$components.first('image-manager');
+        imageManager.show(function (image) {
             callback(image.url, {alt: image.name});
         }, 'gallery');
     }
index 66441c87e9ee8387bf32653fa6ca3bd41b468218..cd0078b1d914da39ccdf3df993527f44f7ce4626 100644 (file)
@@ -9,7 +9,7 @@ function elemIsCodeBlock(elem) {
  * @param {function(string, string)} callback (Receives (code: string,language: string)
  */
 function showPopup(editor, code, language, callback) {
-    window.components.first('code-editor').open(code, language, (newCode, newLang) => {
+    window.$components.first('code-editor').open(code, language, (newCode, newLang) => {
         callback(newCode, newLang)
         editor.focus()
     });
index 64ef1fffbcfc26cd63f2c06c90b4f4ed762dbdc8..ad7e09f95fabbcf6f79a0b0f19a08e2d353df77a 100644 (file)
@@ -15,8 +15,10 @@ function isDrawing(node) {
 function showDrawingManager(mceEditor, selectedNode = null) {
     pageEditor = mceEditor;
     currentNode = selectedNode;
-    // Show image manager
-    window.ImageManager.show(function (image) {
+
+    /** @type {ImageManager} **/
+    const imageManager = window.$components.first('image-manager');
+    imageManager.show(function (image) {
         if (selectedNode) {
             const imgElem = selectedNode.querySelector('img');
             pageEditor.undoManager.transact(function () {
index 225f271fdb9c2c7aef16d56e0aaf578685e222f8..6969a50e22264bb77e96c626dc93d04329edeb3e 100644 (file)
@@ -3,14 +3,15 @@
  * @param {String} url
  */
 function register(editor, url) {
-
     // Custom Image picker button
     editor.ui.registry.addButton('imagemanager-insert', {
         title: 'Insert image',
         icon: 'image',
         tooltip: 'Insert image',
         onAction() {
-            window.ImageManager.show(function (image) {
+            /** @type {ImageManager} **/
+            const imageManager = window.$components.first('image-manager');
+            imageManager.show(function (image) {
                 const imageUrl = image.thumbs.display || image.url;
                 let html = `<a href="${image.url}" target="_blank">`;
                 html += `<img src="${imageUrl}" alt="${image.name}">`;
index b42851a46588b7274b9feec28fee13db704fea4d..ef364ddadab16b1e95105e7218c4b742cb91a6e4 100644 (file)
@@ -44,7 +44,9 @@ export function register(editor) {
 
     // Link selector shortcut
     editor.shortcuts.add('meta+shift+K', '', function() {
-        window.EntitySelectorPopup.show(function(entity) {
+        /** @var {EntitySelectorPopup} **/
+        const selectorPopup = window.$components.first('entity-selector-popup');
+        selectorPopup.show(function(entity) {
 
             if (editor.selection.isCollapsed()) {
                 editor.insertContent(editor.dom.createHTML('a', {href: entity.link}, editor.dom.encode(entity.name)));
index 66d76aaa244133d16dcd4034bc6abeb55b0cc56e..ff60cd50ad613cd2dd112bacce191dae6a22b14b 100644 (file)
@@ -1,6 +1,6 @@
 
 // System wide notifications
-[notification] {
+.notification {
   position: fixed;
   top: 0;
   right: 0;
@@ -1010,4 +1010,41 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   border: 1px solid #b4b4b4;
   box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
   color: #333;
+}
+
+// Back to top link
+$btt-size: 40px;
+.back-to-top {
+  background-color: var(--color-primary);
+  position: fixed;
+  bottom: $-m;
+  right: $-l;
+  padding: 5px 7px;
+  cursor: pointer;
+  color: #FFF;
+  fill: #FFF;
+  svg {
+    width: math.div($btt-size, 1.5);
+    height: math.div($btt-size, 1.5);
+    margin-inline-end: 4px;
+  }
+  width: $btt-size;
+  height: $btt-size;
+  border-radius: $btt-size;
+  transition: all ease-in-out 180ms;
+  opacity: 0;
+  z-index: 999;
+  overflow: hidden;
+  &:hover {
+    width: $btt-size*3.4;
+    opacity: 1 !important;
+  }
+  .inner {
+    width: $btt-size*3.4;
+  }
+  span {
+    position: relative;
+    vertical-align: top;
+    line-height: 2;
+  }
 }
\ No newline at end of file
index 7de8a9d7dc53782f47f07c00087769c8e08ecd63..f341ce48683f8cc90cc01288ef61a36d2a548aa5 100644 (file)
@@ -328,7 +328,7 @@ input[type=color] {
   }
 }
 
-.form-group[collapsible] {
+.form-group.collapsible {
   padding: 0 $-m;
   border: 1px solid;
   @include lightDark(border-color, #DDD, #000);
index eeb51ebb511def1001cdd55f3cfa76ae539e02a9..720203a422aef9b5c7480d2a694a3a3aed685a7c 100755 (executable)
@@ -278,16 +278,16 @@ body.tox-fullscreen, body.markdown-fullscreen {
   &.open {
     width: 480px;
   }
-  [toolbox-toggle] svg {
+  .toolbox-toggle svg {
     transition: transform ease-in-out 180ms;
   }
-  [toolbox-toggle] {
+  .toolbox-toggle {
     transition: background-color ease-in-out 180ms;
   }
-  &.open [toolbox-toggle] {
+  &.open .toolbox-toggle {
     background-color: rgba(255, 0, 0, 0.29);
   }
-  &.open [toolbox-toggle] svg {
+  &.open .toolbox-toggle svg {
     transform: rotate(180deg);
   }
   > div {
@@ -357,7 +357,7 @@ body.tox-fullscreen, body.markdown-fullscreen {
   }
 }
 
-[toolbox-tab-content] {
+.toolbox-tab-content {
   display: none;
 }
 
index 5e31dbdfb577016667bdb15c901f3d11c33a8325..23959d1f85699c878d68361dfc518a44144e0570 100644 (file)
@@ -100,43 +100,6 @@ $loadingSize: 10px;
   }
 }
 
-// Back to top link
-$btt-size: 40px;
-[back-to-top] {
-  background-color: var(--color-primary);
-  position: fixed;
-  bottom: $-m;
-  right: $-l;
-  padding: 5px 7px;
-  cursor: pointer;
-  color: #FFF;
-  fill: #FFF;
-  svg {
-    width: math.div($btt-size, 1.5);
-    height: math.div($btt-size, 1.5);
-    margin-inline-end: 4px;
-  }
-  width: $btt-size;
-  height: $btt-size;
-  border-radius: $btt-size;
-  transition: all ease-in-out 180ms;
-  opacity: 0;
-  z-index: 999;
-  overflow: hidden;
-  &:hover {
-    width: $btt-size*3.4;
-    opacity: 1 !important;
-  }
-  .inner {
-    width: $btt-size*3.4;
-  }
-  span {
-    position: relative;
-    vertical-align: top;
-    line-height: 2;
-  }
-}
-
 .skip-to-content-link {
   position: fixed;
   top: -52px;
index 8ce24baaec5e8e447d541f7010ffcc2420c5fe5b..9345a7bcead45420fdb3825b1cf51227edb21861 100644 (file)
@@ -38,7 +38,7 @@
 
             <div style="overflow: auto;">
 
-                <section code-highlighter class="card content-wrap auto-height">
+                <section component="code-highlighter" class="card content-wrap auto-height">
                     @include('api-docs.parts.getting-started')
                 </section>
 
index 6e3d93659d2e76af07a2a2cc868e8663f4958b74..60c478fe563deaacd6c60c368c8f8cf16fafee6d 100644 (file)
 @endif
 
 @if($endpoint['example_request'] ?? false)
-    <details details-highlighter class="mb-m">
+    <details component="details-highlighter" class="mb-m">
         <summary class="text-muted">Example Request</summary>
         <pre><code class="language-json">{{ $endpoint['example_request'] }}</code></pre>
     </details>
 @endif
 
 @if($endpoint['example_response'] ?? false)
-    <details details-highlighter class="mb-m">
+    <details component="details-highlighter" class="mb-m">
         <summary class="text-muted">Example Response</summary>
         <pre><code class="language-json">{{ $endpoint['example_response'] }}</code></pre>
     </details>
index 024cb583c522e8147fca04626f1a67ba09e4e31a..724ca9c8eb93c38de37c75610aeaf31583732617 100644 (file)
@@ -1,6 +1,9 @@
-<div style="display: block;" toolbox-tab-content="files"
+<div style="display: block;"
+     refs="editor-toolbox@tab-content"
+     data-tab-content="files"
      component="attachments"
-     option:attachments:page-id="{{ $page->id ?? 0 }}">
+     option:attachments:page-id="{{ $page->id ?? 0 }}"
+     class="toolbox-tab-content">
 
     <h4>{{ trans('entities.attachments') }}</h4>
     <div class="px-l files">
index bb87089b272ac3a30e62f5846c7611c67ca8bb4a..e893bceadc55e10acf2a5b986cf9fcdceb6fe98f 100644 (file)
     @include('form.textarea', ['name' => 'description'])
 </div>
 
-<div class="form-group" collapsible id="logo-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label>{{ trans('common.cover_image') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         <p class="small">{{ trans('common.cover_image_description') }}</p>
 
         @include('form.image-picker', [
     </div>
 </div>
 
-<div class="form-group" collapsible id="tags-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="tags-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label for="tag-manager">{{ trans('entities.book_tags') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         @include('entities.tag-manager', ['entity' => $book ?? null])
     </div>
 </div>
index f043735bbf4c9c0df001d5f40fc565756b467550..ef9929e46486b9e9fd944ca8cc1652a5551f366d 100644 (file)
@@ -4,11 +4,11 @@
         <span>{{ $book->name }}</span>
     </h5>
     <div class="sort-box-options pb-sm">
-        <a href="#" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</a>
-        <a href="#" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</a>
-        <a href="#" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</a>
-        <a href="#" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</a>
-        <a href="#" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</a>
+        <button type="button" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</button>
+        <button type="button" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</button>
+        <button type="button" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</button>
+        <button type="button" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</button>
+        <button type="button" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</button>
     </div>
     <ul class="sortable-page-list sort-list">
 
index a24bd8959203c9b5f24b263aec0374d8fced88c2..077da101d13bddb518f141ad2cde2fe60e2173e1 100644 (file)
 
         <div class="grid left-focus gap-xl">
             <div>
-                <div book-sort class="card content-wrap">
+                <div component="book-sort" class="card content-wrap">
                     <h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1>
-                    <div book-sort-boxes>
+                    <div refs="book-sort@sortContainer">
                         @include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
                     </div>
 
                     <form action="{{ $book->getUrl('/sort') }}" method="POST">
                         {!! csrf_field() !!}
                         <input type="hidden" name="_method" value="PUT">
-                        <input book-sort-input type="hidden" name="sort-tree">
+                        <input refs="book-sort@input" type="hidden" name="sort-tree">
                         <div class="list text-right">
                             <a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
                             <button class="button" type="submit">{{ trans('entities.books_sort_save') }}</button>
index 3908d0693fb7a3ec635be5d609dfb61f579c3d7c..068c033ab90fb67864b56955e8b6f445b12bd109 100644 (file)
     @include('form.textarea', ['name' => 'description'])
 </div>
 
-<div class="form-group" collapsible id="logo-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label for="tags">{{ trans('entities.chapter_tags') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         @include('entities.tag-manager', ['entity' => $chapter ?? null])
     </div>
 </div>
index 752920917570a9a11d974f7088bf7e572a5e6a0f..e06bd5fd13861b0e85a03ebdbea69817fc8644f6 100644 (file)
@@ -1,11 +1,29 @@
-<div notification="success" style="display: none;" data-autohide class="pos" role="alert" @if(session()->has('success')) data-show @endif>
+<div component="notification"
+     option:notification:type="success"
+     option:notification:auto-hide="true"
+     option:notification:show="{{ session()->has('success') ? 'true' : 'false' }}"
+     style="display: none;"
+     class="notification pos"
+     role="alert">
     @icon('check-circle') <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span><div class="dismiss">@icon('close')</div>
 </div>
 
-<div notification="warning" style="display: none;" class="warning" role="alert" @if(session()->has('warning')) data-show @endif>
+<div component="notification"
+     option:notification:type="warning"
+     option:notification:auto-hide="false"
+     option:notification:show="{{ session()->has('warning') ? 'true' : 'false' }}"
+     style="display: none;"
+     class="notification warning"
+     role="alert">
     @icon('info') <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span><div class="dismiss">@icon('close')</div>
 </div>
 
-<div notification="error" style="display: none;" class="neg" role="alert" @if(session()->has('error')) data-show @endif>
+<div component="notification"
+     option:notification:type="error"
+     option:notification:auto-hide="false"
+     option:notification:show="{{ session()->has('error') ? 'true' : 'false' }}"
+     style="display: none;"
+     class="notification neg"
+     role="alert">
     @icon('danger') <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span><div class="dismiss">@icon('close')</div>
-</div>
+</div>
\ No newline at end of file
index de3ffe922c13145c04a2e08c87d5ca80d33857b0..7750c6f00078b5a89b59fa0d269e29d6e19fffb5 100644 (file)
@@ -4,7 +4,7 @@ $value
 $checked
 $label
 --}}
-<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
+<label component="custom-checkbox" class="toggle-switch @if($errors->has($name)) text-neg @endif">
     <input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif @if($disabled ?? false) disabled="disabled" @endif>
     <span tabindex="0" role="checkbox"
           aria-checked="{{ $checked ? 'true' : 'false' }}"
index 9c2661cccbd3d19c412a39af93837ad0be8ab205..f7e2c6bb97bd321a6f2b892f8044f05aab81006b 100644 (file)
@@ -1,26 +1,27 @@
-<div class="image-picker @if($errors->has($name)) has-error @endif"
-     image-picker="{{$name}}"
-     data-default-image="{{ $defaultImage }}">
+<div component="image-picker"
+     option:image-picker:default-image="{{ $defaultImage }}"
+     class="image-picker @if($errors->has($name)) has-error @endif">
 
     <div class="grid half">
         <div class="text-center">
-            <img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif  class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
+            <img refs="image-picker@image"
+                @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif
+                class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
         </div>
         <div class="text-center">
-
-            <input type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
+            <input refs="image-picker@image-input" type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
             <label for="{{ $name }}" class="button outline">{{ trans('components.image_select_image') }}</label>
-            <input type="hidden" data-reset-input name="{{ $name }}_reset" value="true" disabled="disabled">
+            <input refs="image-picker@reset-input" type="hidden" name="{{ $name }}_reset" value="true" disabled="disabled">
             @if(isset($removeName))
-                <input type="hidden" data-remove-input name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
+                <input refs="image-picker@remove-input" type="hidden" name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
             @endif
 
             <br>
-            <button class="text-button text-muted" data-action="reset-image" type="button">{{ trans('common.reset') }}</button>
+            <button refs="image-picker@reset-button" class="text-button text-muted" type="button">{{ trans('common.reset') }}</button>
 
             @if(isset($removeName))
                 <span class="sep">|</span>
-                <button class="text-button text-muted" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
+                <button refs="image-picker@remove-button" class="text-button text-muted" type="button">{{ trans('common.remove') }}</button>
             @endif
         </div>
     </div>
index a5eec30051b32a9bcd9e983c0fd98a40c4356ca9..375eda3d7b5cfaea2ece5fd44ab0f3f1590cfe60 100644 (file)
@@ -1,4 +1,4 @@
-<label toggle-switch="{{$name}}" custom-checkbox class="toggle-switch">
+<label components="custom-checkbox toggle-switch" class="toggle-switch">
     <input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
     <input type="checkbox" @if($value) checked="checked" @endif>
     <span tabindex="0" role="checkbox"
index 291e5db344114d1cfe060e141099ad040c1b6d5c..73d21f5f7b9e1e4d4be38d44471973d2577e8b8a 100644 (file)
@@ -3,10 +3,12 @@ $target - CSS selector of items to expand
 $key - Unique key for checking existing stored state.
 --}}
 <?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
-<button type="button" expand-toggle="{{ $target }}"
-   expand-toggle-update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
-   expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
-   class="icon-list-item {{ $classes ?? '' }}">
+<button component="expand-toggle"
+        option:expand-toggle:target-selector="{{ $target }}"
+        option:expand-toggle:update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
+        option:expand-toggle:is-open="{{ $isOpen ? 'true' : 'false' }}"
+        type="button"
+        class="icon-list-item {{ $classes ?? '' }}">
     <span>@icon('expand-text')</span>
     <span>{{ trans('common.toggle_details') }}</span>
 </button>
index 936433b276adb445df7a3713396b645d7728d04f..cc8ff22fa0a22f73a87c9220b9c73ddabd606aa8 100644 (file)
@@ -3,7 +3,9 @@
 @section('body')
     <div class="mt-m">
         <main class="content-wrap card">
-            <div class="page-content" page-display="{{ $customHomepage->id }}">
+            <div component="page-display"
+                 option:page-display:page-id="{{ $page->id }}"
+                 class="page-content">
                 @include('pages.parts.page-display', ['page' => $customHomepage])
             </div>
         </main>
index 2f649423da2c155614f73dc53ede214d77b7db18..76d220952924b6f382453411e0e375845f032412 100644 (file)
@@ -49,7 +49,7 @@
 
     @include('common.footer')
 
-    <div back-to-top class="primary-background print-hidden">
+    <div component="back-to-top" class="back-to-top print-hidden">
         <div class="inner">
             @icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
         </div>
index f3b54ddcd6eb0efb0978dea032555a54feeeab1d..8f332702486105f35c523e5e5660167eded8dfc2 100644 (file)
@@ -1,15 +1,15 @@
-<div editor-toolbox class="floating-toolbox">
+<div component="editor-toolbox" class="floating-toolbox">
 
     <div class="tabs primary-background-light">
-        <button type="button" toolbox-toggle aria-expanded="false">@icon('caret-left-circle')</button>
-        <button type="button" toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
+        <button type="button" refs="editor-toolbox@toggle" aria-expanded="false" class="toolbox-toggle">@icon('caret-left-circle')</button>
+        <button type="button" refs="editor-toolbox@tab-button" data-tab="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
         @if(userCan('attachment-create-all'))
-            <button type="button" toolbox-tab-button="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
+            <button type="button" refs="editor-toolbox@tab-button" data-tab="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
         @endif
-        <button type="button" toolbox-tab-button="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
+        <button type="button" refs="editor-toolbox@tab-button" data-tab="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
     </div>
 
-    <div toolbox-tab-content="tags">
+    <div refs="editor-toolbox@tab-content" data-tab-content="tags" class="toolbox-tab-content">
         <h4>{{ trans('entities.page_tags') }}</h4>
         <div class="px-l">
             @include('entities.tag-manager', ['entity' => $page])
@@ -20,7 +20,7 @@
         @include('attachments.manager', ['page' => $page])
     @endif
 
-    <div toolbox-tab-content="templates">
+    <div refs="editor-toolbox@tab-content" data-tab-content="templates" class="toolbox-tab-content">
         <h4>{{ trans('entities.templates') }}</h4>
 
         <div class="px-l">
index 66d53ae7e9fc29d845ef3158fd7933d8a3de0403..43c161ad288fca485d7a4953ef41c68a310f6f93 100644 (file)
@@ -1,4 +1,4 @@
-<div template-manager>
+<div component="template-manager">
     @if(userCan('templates-manage'))
         <p class="text-muted small mb-none">
             {{ trans('entities.templates_explain_set_as_template') }}
         <hr>
     @endif
 
-    @if(count($templates) > 0)
-        <div class="search-box flexible mb-m">
-            <input type="text" name="template-search" placeholder="{{ trans('common.search') }}">
-            <button type="button">@icon('search')</button>
-            <button class="search-box-cancel text-neg hidden" type="button">@icon('close')</button>
-        </div>
-    @endif
+    <div class="search-box flexible mb-m" style="display: {{ count($templates) > 0 ? 'block' : 'none' }}">
+        <input refs="template-manager@searchInput" type="text" name="template-search" placeholder="{{ trans('common.search') }}">
+        <button refs="template-manager@searchButton" type="button">@icon('search')</button>
+        <button refs="template-manager@searchCancel" class="search-box-cancel text-neg" type="button" style="display: none">@icon('close')</button>
+    </div>
 
-    <div template-manager-list>
+    <div refs="template-manager@list">
         @include('pages.parts.template-manager-list', ['templates' => $templates])
     </div>
 </div>
\ No newline at end of file
index 32718b7f1e47873b98483fa9b0d5bbebebb55662..e05d9d7393fce09b93662b498a77290562dc7642 100644 (file)
@@ -17,7 +17,9 @@
     </div>
 
     <main class="content-wrap card">
-        <div class="page-content clearfix" page-display="{{ $page->id }}">
+        <div component="page-display"
+             option:page-display:page-id="{{ $page->id }}"
+             class="page-content clearfix">
             @include('pages.parts.page-display')
         </div>
         @include('pages.parts.pointer', ['page' => $page])
index a7392196b68be6ad0329a57dffbb9c0191ea4052..3748267df8869c74c658afebe704a3219ed91557 100644 (file)
                     <label class="setting-list-label">{{ trans('settings.app_primary_color') }}</label>
                     <p class="small">{!! trans('settings.app_primary_color_desc') !!}</p>
                 </div>
-                <div setting-app-color-picker class="text-m-right pt-xs">
-                    <input type="color" data-default="#206ea7" data-current="{{ setting('app-color') }}" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#206ea7">
-                    <input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
+                <div component="setting-app-color-picker setting-color-picker"
+                     option:setting-color-picker:default="#206ea7"
+                     option:setting-color-picker:current="{{ setting('app-color') }}"
+                     class="text-m-right pt-xs">
+                    <input refs="setting-color-picker@input setting-app-color-picker@input" type="color" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#206ea7">
+                    <input refs="setting-app-color-picker@light-input" type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
                     <div class="pr-s">
-                        <button type="button" class="text-button text-muted mt-s" setting-app-color-picker-default>{{ trans('common.default') }}</button>
+                        <button refs="setting-color-picker@default-button" type="button" class="text-button text-muted mt-s">{{ trans('common.default') }}</button>
                         <span class="sep">|</span>
-                        <button type="button" class="text-button text-muted mt-s" setting-app-color-picker-reset>{{ trans('common.reset') }}</button>
+                        <button refs="setting-color-picker@reset-button" type="button" class="text-button text-muted mt-s">{{ trans('common.reset') }}</button>
                     </div>
 
                 </div>
                 </div>
             </div>
 
-            <div homepage-control id="homepage-control" class="grid half gap-xl items-center">
+            <div component="setting-homepage-control" id="homepage-control" class="grid half gap-xl items-center">
                 <div>
                     <label for="setting-app-homepage-type" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
                     <p class="small">{{ trans('settings.app_homepage_desc') }}</p>
                 </div>
                 <div>
-                    <select name="setting-app-homepage-type" id="setting-app-homepage-type">
+                    <select refs="setting-homepage-control@type-control"
+                            name="setting-app-homepage-type"
+                            id="setting-app-homepage-type">
                         <option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
                         <option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>
                         <option @if(setting('app-homepage-type') === 'bookshelves') selected @endif value="bookshelves">{{ trans('entities.shelves') }}</option>
                         <option @if(setting('app-homepage-type') === 'page') selected @endif value="page">{{ trans('entities.pages_specific') }}</option>
                     </select>
 
-                    <div page-picker-container style="display: none;" class="mt-m">
+                    <div refs="setting-homepage-control@page-picker-container" style="display: none;" class="mt-m">
                         @include('settings.parts.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')])
                     </div>
                 </div>
index 0df42e3cef9993f12c7552881a44a76ebbfbcb03..d599a19ab6a1cf87d76fecd1ad0c3221f7f3691c 100644 (file)
@@ -1,13 +1,13 @@
 
 {{--Depends on entity selector popup--}}
-<div page-picker>
+<div component="page-picker">
     <div class="input-base">
-        <span @if($value) style="display: none" @endif page-picker-default class="text-muted italic">{{ $placeholder }}</span>
-        <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" rel="noopener" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
+        <span @if($value) style="display: none" @endif refs="page-picker@default-display" class="text-muted italic">{{ $placeholder }}</span>
+        <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" rel="noopener" class="text-page" refs="page-picker@display">#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
     </div>
     <br>
-    <input type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
-    <button @if(!$value) style="display: none" @endif type="button" page-picker-reset class="text-button">{{ trans('common.reset') }}</button>
-    <span @if(!$value) style="display: none" @endif class="sep">|</span>
-    <button type="button" page-picker-select class="text-button">{{ trans('common.select') }}</button>
+    <input refs="page-picker@input" type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
+    <button @if(!$value) style="display: none" @endif type="button" refs="page-picker@reset-button" class="text-button">{{ trans('common.reset') }}</button>
+    <span refs="page-picker@button-seperator" @if(!$value) style="display: none" @endif class="sep">|</span>
+    <button type="button" refs="page-picker@select-button" class="text-button">{{ trans('common.select') }}</button>
 </div>
\ No newline at end of file
index 3b99d0b7cd681a130f83a9dea5c65da7515136f0..e7bfc3fe9f360432d5db717542ece7fc9ae7ee08 100644 (file)
@@ -1,17 +1,19 @@
 {{--
     @type - Name of entity type
 --}}
-<div setting-color-picker class="grid no-break half mb-l">
+<div component="setting-color-picker"
+     option:setting-color-picker:default="{{ config('setting-defaults.'. $type .'-color') }}"
+     option:setting-color-picker:current="{{ setting($type .'-color') }}"
+     class="grid no-break half mb-l">
     <div>
         <label for="setting-{{ $type }}-color" class="text-dark">{{ trans('settings.'. str_replace('-', '_', $type) .'_color') }}</label>
-        <button type="button" class="text-button text-muted" setting-color-picker-default>{{ trans('common.default') }}</button>
+        <button refs="setting-color-picker@default-button" type="button" class="text-button text-muted">{{ trans('common.default') }}</button>
         <span class="sep">|</span>
-        <button type="button" class="text-button text-muted" setting-color-picker-reset>{{ trans('common.reset') }}</button>
+        <button refs="setting-color-picker@reset-button" type="button" class="text-button text-muted">{{ trans('common.reset') }}</button>
     </div>
     <div>
         <input type="color"
-               data-default="{{ config('setting-defaults.'. $type .'-color') }}"
-               data-current="{{ setting($type .'-color') }}"
+               refs="setting-color-picker@input"
                value="{{ setting($type .'-color') }}"
                name="setting-{{ $type }}-color"
                id="setting-{{ $type }}-color"
index 1dcc4192f04acaddfd1e51198defee4e5be4f294..a5445270314ea0b3df2351a4f3e36f54fc3aa198 100644 (file)
 
 
 
-<div class="form-group" collapsible id="logo-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label>{{ trans('common.cover_image') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         <p class="small">{{ trans('common.cover_image_description') }}</p>
 
         @include('form.image-picker', [
     </div>
 </div>
 
-<div class="form-group" collapsible id="tags-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="tags-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         @include('entities.tag-manager', ['entity' => $shelf ?? null])
     </div>
 </div>
index 2a5002c3b766c34cc97494336130268c448b499d..7ff48a83dd881b9a96f6c1c9f0438dddc844ba40 100644 (file)
@@ -48,7 +48,7 @@
 @endif
 
 @if($authMethod === 'standard')
-    <div new-user-password>
+    <div component="new-user-password">
         <label class="setting-list-label">{{ trans('settings.users_password') }}</label>
 
         @if(!isset($model))
                 'value' => old('send_invite', 'true') === 'true',
                 'label' => trans('settings.users_send_invite_option')
             ])
-
         @endif
 
-        <div id="password-input-container" @if(!isset($model)) style="display: none;" @endif>
+        <div refs="new-user-password@input-container" @if(!isset($model)) style="display: none;" @endif>
             <p class="small">{{ trans('settings.users_password_desc') }}</p>
             @if(isset($model))
                 <p class="small">