diff --git a/dev/docs/javascript-code.md b/dev/docs/javascript-code.md index 3d47a1ad8..a1092ce92 100644 --- a/dev/docs/javascript-code.md +++ b/dev/docs/javascript-code.md @@ -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 `-`, +// 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 diff --git a/resources/js/app.js b/resources/js/app.js index 82748b75e..e49bf5e95 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -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(); diff --git a/resources/js/components/add-remove-rows.js b/resources/js/components/add-remove-rows.js index 9a5f019c5..19d2249fb 100644 --- a/resources/js/components/add-remove-rows.js +++ b/resources/js/components/add-remove-rows.js @@ -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 diff --git a/resources/js/components/ajax-delete-row.js b/resources/js/components/ajax-delete-row.js index 2feb3d5ac..f1af7f6cb 100644 --- a/resources/js/components/ajax-delete-row.js +++ b/resources/js/components/ajax-delete-row.js @@ -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 diff --git a/resources/js/components/ajax-form.js b/resources/js/components/ajax-form.js index 91029d042..6f4e5af08 100644 --- a/resources/js/components/ajax-form.js +++ b/resources/js/components/ajax-form.js @@ -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 diff --git a/resources/js/components/attachments-list.js b/resources/js/components/attachments-list.js index 34979c2e7..dfefd9b7f 100644 --- a/resources/js/components/attachments-list.js +++ b/resources/js/components/attachments-list.js @@ -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 diff --git a/resources/js/components/attachments.js b/resources/js/components/attachments.js index 6dcfe9f12..b4e400aeb 100644 --- a/resources/js/components/attachments.js +++ b/resources/js/components/attachments.js @@ -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 diff --git a/resources/js/components/auto-submit.js b/resources/js/components/auto-submit.js index 11494ae82..c8726ca7e 100644 --- a/resources/js/components/auto-submit.js +++ b/resources/js/components/auto-submit.js @@ -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 diff --git a/resources/js/components/auto-suggest.js b/resources/js/components/auto-suggest.js index 80857cbe5..b4e6c5957 100644 --- a/resources/js/components/auto-suggest.js +++ b/resources/js/components/auto-suggest.js @@ -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 diff --git a/resources/js/components/back-to-top.js b/resources/js/components/back-to-top.js index a1d87f22e..4f0a46f00 100644 --- a/resources/js/components/back-to-top.js +++ b/resources/js/components/back-to-top.js @@ -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 diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 2b94ca4a7..3ffadf991 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -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 diff --git a/resources/js/components/chapter-contents.js b/resources/js/components/chapter-contents.js index c824d0f78..37df213e3 100644 --- a/resources/js/components/chapter-contents.js +++ b/resources/js/components/chapter-contents.js @@ -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; diff --git a/resources/js/components/code-editor.js b/resources/js/components/code-editor.js index d0c6c432a..205cbd8fd 100644 --- a/resources/js/components/code-editor.js +++ b/resources/js/components/code-editor.js @@ -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 diff --git a/resources/js/components/code-highlighter.js b/resources/js/components/code-highlighter.js index 5ffab3775..14bfc97f0 100644 --- a/resources/js/components/code-highlighter.js +++ b/resources/js/components/code-highlighter.js @@ -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 diff --git a/resources/js/components/code-textarea.js b/resources/js/components/code-textarea.js index 988e51f19..0e49aec17 100644 --- a/resources/js/components/code-textarea.js +++ b/resources/js/components/code-textarea.js @@ -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 diff --git a/resources/js/components/collapsible.js b/resources/js/components/collapsible.js index 544f91008..bb8ed477f 100644 --- a/resources/js/components/collapsible.js +++ b/resources/js/components/collapsible.js @@ -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 index 000000000..292bbb624 --- /dev/null +++ b/resources/js/components/component.js @@ -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} + */ + $refs = {}; + + /** + * Mapping of arrays of referenced elements within the component so multiple + * references, sharing the same name, can be fetched. + * @type {Object} + */ + $manyRefs = {}; + + /** + * Options passed into this component. + * @type {Object} + */ + $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 `-`, 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 diff --git a/resources/js/components/confirm-dialog.js b/resources/js/components/confirm-dialog.js index 858be1b85..572945d5a 100644 --- a/resources/js/components/confirm-dialog.js +++ b/resources/js/components/confirm-dialog.js @@ -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 diff --git a/resources/js/components/custom-checkbox.js b/resources/js/components/custom-checkbox.js index 65ce8c194..99804c4bc 100644 --- a/resources/js/components/custom-checkbox.js +++ b/resources/js/components/custom-checkbox.js @@ -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 diff --git a/resources/js/components/details-highlighter.js b/resources/js/components/details-highlighter.js index 1f3b66c67..6466fb584 100644 --- a/resources/js/components/details-highlighter.js +++ b/resources/js/components/details-highlighter.js @@ -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 diff --git a/resources/js/components/dropdown-search.js b/resources/js/components/dropdown-search.js index 81fa940c2..30a2aadc1 100644 --- a/resources/js/components/dropdown-search.js +++ b/resources/js/components/dropdown-search.js @@ -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 diff --git a/resources/js/components/dropdown.js b/resources/js/components/dropdown.js index 781f90860..2625ff4de 100644 --- a/resources/js/components/dropdown.js +++ b/resources/js/components/dropdown.js @@ -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 diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index 44fdf2d0d..911a033c7 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -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 diff --git a/resources/js/components/editor-toolbox.js b/resources/js/components/editor-toolbox.js index 3a1442d75..a581ae7b4 100644 --- a/resources/js/components/editor-toolbox.js +++ b/resources/js/components/editor-toolbox.js @@ -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; - // Toolbox toggle button click - this.toggleButton.addEventListener('click', this.toggle.bind(this)); - // 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.setupListeners(); // Set the first tab as active on load - this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content')); + this.setActiveTab(this.contentElements[0].dataset.tabContent); + } + + setupListeners() { + // Toolbox toggle button click + this.toggleButton.addEventListener('click', () => this.toggle()); + // Tab button click + 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); + } + }); } 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'); - } - // 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 button of this.buttons) { + button.classList.remove('active'); + const bName = button.dataset.tab; + if (bName === tabName) button.classList.add('active'); } - if (openToolbox && !this.elem.classList.contains('open')) { + // Set content visibility + 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.container.classList.contains('open')) { this.toggle(); } } -} - -export default EditorToolbox; \ No newline at end of file +} \ No newline at end of file diff --git a/resources/js/components/entity-permissions.js b/resources/js/components/entity-permissions.js index 0dec5ca09..d4a616ff1 100644 --- a/resources/js/components/entity-permissions.js +++ b/resources/js/components/entity-permissions.js @@ -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 diff --git a/resources/js/components/entity-search.js b/resources/js/components/entity-search.js index 8b1861ecf..b0e42401d 100644 --- a/resources/js/components/entity-search.js +++ b/resources/js/components/entity-search.js @@ -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 diff --git a/resources/js/components/entity-selector-popup.js b/resources/js/components/entity-selector-popup.js index e7cb60b1f..d455f7ee7 100644 --- a/resources/js/components/entity-selector-popup.js +++ b/resources/js/components/entity-selector-popup.js @@ -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 diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js index e2596998a..1496ea89e 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.js @@ -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 diff --git a/resources/js/components/event-emit-select.js b/resources/js/components/event-emit-select.js index cf0215850..2e6fd5fdb 100644 --- a/resources/js/components/event-emit-select.js +++ b/resources/js/components/event-emit-select.js @@ -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 diff --git a/resources/js/components/expand-toggle.js b/resources/js/components/expand-toggle.js index cce1b215c..ab4d38ab1 100644 --- a/resources/js/components/expand-toggle.js +++ b/resources/js/components/expand-toggle.js @@ -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 diff --git a/resources/js/components/header-mobile-toggle.js b/resources/js/components/header-mobile-toggle.js index 99737bfb8..11b23cca6 100644 --- a/resources/js/components/header-mobile-toggle.js +++ b/resources/js/components/header-mobile-toggle.js @@ -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 diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index 23a6c4cbb..a44fffc1b 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -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 diff --git a/resources/js/components/image-picker.js b/resources/js/components/image-picker.js index 7455fa622..03d9567d2 100644 --- a/resources/js/components/image-picker.js +++ b/resources/js/components/image-picker.js @@ -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(); + } - const removeButton = elem.querySelector('button[data-action="remove-image"]'); - if (removeButton) { - removeButton.addEventListener('click', this.removeImage.bind(this)); + setupListeners() { + this.resetButton.addEventListener('click', this.reset.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 diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 9f801668e..2aac33f7f 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -1,282 +1,58 @@ -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} - */ -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} $refs - * @property {Object} $manyRefs - * @property {Object} $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 diff --git a/resources/js/components/list-sort-control.js b/resources/js/components/list-sort-control.js index 3b642dbde..b8d4de73a 100644 --- a/resources/js/components/list-sort-control.js +++ b/resources/js/components/list-sort-control.js @@ -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 diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js index 3290fc300..b9567d864 100644 --- a/resources/js/components/markdown-editor.js +++ b/resources/js/components/markdown-editor.js @@ -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 ; diff --git a/resources/js/components/new-user-password.js b/resources/js/components/new-user-password.js index 9c4c21c14..a4ed4d15b 100644 --- a/resources/js/components/new-user-password.js +++ b/resources/js/components/new-user-password.js @@ -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 diff --git a/resources/js/components/notification.js b/resources/js/components/notification.js index 35bab4ea6..8a0876241 100644 --- a/resources/js/components/notification.js +++ b/resources/js/components/notification.js @@ -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 diff --git a/resources/js/components/optional-input.js b/resources/js/components/optional-input.js index eab58e42a..cc429c991 100644 --- a/resources/js/components/optional-input.js +++ b/resources/js/components/optional-input.js @@ -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 diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index c86eead1b..726531e95 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -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 diff --git a/resources/js/components/page-display.js b/resources/js/components/page-display.js index b4f1cca4f..c06c3310d 100644 --- a/resources/js/components/page-display.js +++ b/resources/js/components/page-display.js @@ -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 diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js index ce123e987..d6faabd05 100644 --- a/resources/js/components/page-editor.js +++ b/resources/js/components/page-editor.js @@ -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 diff --git a/resources/js/components/page-picker.js b/resources/js/components/page-picker.js index 577e9f6db..fba0a0a43 100644 --- a/resources/js/components/page-picker.js +++ b/resources/js/components/page-picker.js @@ -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 diff --git a/resources/js/components/permissions-table.js b/resources/js/components/permissions-table.js index d33c9928f..58ead1d57 100644 --- a/resources/js/components/permissions-table.js +++ b/resources/js/components/permissions-table.js @@ -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 diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.js index d1967acd0..d884dc721 100644 --- a/resources/js/components/pointer.js +++ b/resources/js/components/pointer.js @@ -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 diff --git a/resources/js/components/popup.js b/resources/js/components/popup.js index ec111963f..4c20876f8 100644 --- a/resources/js/components/popup.js +++ b/resources/js/components/popup.js @@ -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 diff --git a/resources/js/components/setting-app-color-picker.js b/resources/js/components/setting-app-color-picker.js index ee894c932..68e5abce5 100644 --- a/resources/js/components/setting-app-color-picker.js +++ b/resources/js/components/setting-app-color-picker.js @@ -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; diff --git a/resources/js/components/setting-color-picker.js b/resources/js/components/setting-color-picker.js index 4d8ce0f93..876e14f20 100644 --- a/resources/js/components/setting-color-picker.js +++ b/resources/js/components/setting-color-picker.js @@ -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 diff --git a/resources/js/components/homepage-control.js b/resources/js/components/setting-homepage-control.js similarity index 55% rename from resources/js/components/homepage-control.js rename to resources/js/components/setting-homepage-control.js index 9db9e17b8..992be9f82 100644 --- a/resources/js/components/homepage-control.js +++ b/resources/js/components/setting-homepage-control.js @@ -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 diff --git a/resources/js/components/shelf-sort.js b/resources/js/components/shelf-sort.js index 30eda5a21..d10470bd7 100644 --- a/resources/js/components/shelf-sort.js +++ b/resources/js/components/shelf-sort.js @@ -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 diff --git a/resources/js/components/shortcut-input.js b/resources/js/components/shortcut-input.js index fa1378988..2a2aaa225 100644 --- a/resources/js/components/shortcut-input.js +++ b/resources/js/components/shortcut-input.js @@ -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 diff --git a/resources/js/components/shortcuts.js b/resources/js/components/shortcuts.js index 4efa3d42b..a87213b2e 100644 --- a/resources/js/components/shortcuts.js +++ b/resources/js/components/shortcuts.js @@ -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 index 0fecc5eae..000000000 --- a/resources/js/components/sidebar.js +++ /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 diff --git a/resources/js/components/sortable-list.js b/resources/js/components/sortable-list.js index 0af0e11c9..bbbd92088 100644 --- a/resources/js/components/sortable-list.js +++ b/resources/js/components/sortable-list.js @@ -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 diff --git a/resources/js/components/submit-on-change.js b/resources/js/components/submit-on-change.js index aeacae232..da4ac6996 100644 --- a/resources/js/components/submit-on-change.js +++ b/resources/js/components/submit-on-change.js @@ -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 diff --git a/resources/js/components/tabs.js b/resources/js/components/tabs.js index 7121d7044..46063d240 100644 --- a/resources/js/components/tabs.js +++ b/resources/js/components/tabs.js @@ -1,11 +1,11 @@ +import {onSelect} from "../services/dom"; +import {Component} from "./component"; + /** * Tabs * Works by matching 'tabToggle' with 'tabContent' 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 diff --git a/resources/js/components/tag-manager.js b/resources/js/components/tag-manager.js index 99302b6c0..cfbc514a0 100644 --- a/resources/js/components/tag-manager.js +++ b/resources/js/components/tag-manager.js @@ -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 diff --git a/resources/js/components/template-manager.js b/resources/js/components/template-manager.js index f8b19a40c..774336471 100644 --- a/resources/js/components/template-manager.js +++ b/resources/js/components/template-manager.js @@ -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 diff --git a/resources/js/components/toggle-switch.js b/resources/js/components/toggle-switch.js index b9b96afc5..b749eb541 100644 --- a/resources/js/components/toggle-switch.js +++ b/resources/js/components/toggle-switch.js @@ -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 diff --git a/resources/js/components/tri-layout.js b/resources/js/components/tri-layout.js index f801e52a1..ead2ac3d0 100644 --- a/resources/js/components/tri-layout.js +++ b/resources/js/components/tri-layout.js @@ -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 diff --git a/resources/js/components/user-select.js b/resources/js/components/user-select.js index aba43e0a9..d4d88a633 100644 --- a/resources/js/components/user-select.js +++ b/resources/js/components/user-select.js @@ -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 diff --git a/resources/js/components/webhook-events.js b/resources/js/components/webhook-events.js index aa50aa9d8..ad8c59ac2 100644 --- a/resources/js/components/webhook-events.js +++ b/resources/js/components/webhook-events.js @@ -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 diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index 446f2ca49..976dba68f 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -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 index 000000000..7434f6430 --- /dev/null +++ b/resources/js/services/components.js @@ -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} + */ +const components = {}; + +/** + * A mapping of component class models, keyed by name. + * @type {Object>} + */ +const componentModelMap = {}; + +/** + * A mapping of active component maps, keyed by the element components are assigned to. + * @type {WeakMap>} + */ +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|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} + */ +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>} 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 diff --git a/resources/js/services/dom.js b/resources/js/services/dom.js index eb5f6a853..882d5228d 100644 --- a/resources/js/services/dom.js +++ b/resources/js/services/dom.js @@ -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 diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index acf5e1d52..d5ec20e26 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -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'); } diff --git a/resources/js/wysiwyg/plugin-codeeditor.js b/resources/js/wysiwyg/plugin-codeeditor.js index 66441c87e..cd0078b1d 100644 --- a/resources/js/wysiwyg/plugin-codeeditor.js +++ b/resources/js/wysiwyg/plugin-codeeditor.js @@ -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() }); diff --git a/resources/js/wysiwyg/plugin-drawio.js b/resources/js/wysiwyg/plugin-drawio.js index 64ef1fffb..ad7e09f95 100644 --- a/resources/js/wysiwyg/plugin-drawio.js +++ b/resources/js/wysiwyg/plugin-drawio.js @@ -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 () { diff --git a/resources/js/wysiwyg/plugins-imagemanager.js b/resources/js/wysiwyg/plugins-imagemanager.js index 225f271fd..6969a50e2 100644 --- a/resources/js/wysiwyg/plugins-imagemanager.js +++ b/resources/js/wysiwyg/plugins-imagemanager.js @@ -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 = ``; html += `${image.name}`; diff --git a/resources/js/wysiwyg/shortcuts.js b/resources/js/wysiwyg/shortcuts.js index b42851a46..ef364ddad 100644 --- a/resources/js/wysiwyg/shortcuts.js +++ b/resources/js/wysiwyg/shortcuts.js @@ -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))); diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 66d76aaa2..ff60cd50a 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -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 diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 7de8a9d7d..f341ce486 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -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); diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index eeb51ebb5..720203a42 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -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; } diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 5e31dbdfb..23959d1f8 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -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; diff --git a/resources/views/api-docs/index.blade.php b/resources/views/api-docs/index.blade.php index 8ce24baae..9345a7bce 100644 --- a/resources/views/api-docs/index.blade.php +++ b/resources/views/api-docs/index.blade.php @@ -38,7 +38,7 @@
-
+
@include('api-docs.parts.getting-started')
diff --git a/resources/views/api-docs/parts/endpoint.blade.php b/resources/views/api-docs/parts/endpoint.blade.php index 6e3d93659..60c478fe5 100644 --- a/resources/views/api-docs/parts/endpoint.blade.php +++ b/resources/views/api-docs/parts/endpoint.blade.php @@ -34,14 +34,14 @@ @endif @if($endpoint['example_request'] ?? false) -
+
Example Request
{{ $endpoint['example_request'] }}
@endif @if($endpoint['example_response'] ?? false) -
+
Example Response
{{ $endpoint['example_response'] }}
diff --git a/resources/views/attachments/manager.blade.php b/resources/views/attachments/manager.blade.php index 024cb583c..724ca9c8e 100644 --- a/resources/views/attachments/manager.blade.php +++ b/resources/views/attachments/manager.blade.php @@ -1,6 +1,9 @@ -
+ option:attachments:page-id="{{ $page->id ?? 0 }}" + class="toolbox-tab-content">

{{ trans('entities.attachments') }}

diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php index bb87089b2..e893bcead 100644 --- a/resources/views/books/parts/form.blade.php +++ b/resources/views/books/parts/form.blade.php @@ -10,11 +10,11 @@ @include('form.textarea', ['name' => 'description'])
-
- -
+

{{ trans('common.cover_image_description') }}

@include('form.image-picker', [ @@ -26,11 +26,11 @@
-
- -
+
@include('entities.tag-manager', ['entity' => $book ?? null])
diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php index f043735bb..ef9929e46 100644 --- a/resources/views/books/parts/sort-box.blade.php +++ b/resources/views/books/parts/sort-box.blade.php @@ -4,11 +4,11 @@ {{ $book->name }}
- {{ trans('entities.books_sort_name') }} - {{ trans('entities.books_sort_created') }} - {{ trans('entities.books_sort_updated') }} - {{ trans('entities.books_sort_chapters_first') }} - {{ trans('entities.books_sort_chapters_last') }} + + + + +
    diff --git a/resources/views/books/sort.blade.php b/resources/views/books/sort.blade.php index a24bd8959..077da101d 100644 --- a/resources/views/books/sort.blade.php +++ b/resources/views/books/sort.blade.php @@ -16,16 +16,16 @@
    -
    +

    {{ trans('entities.books_sort') }}

    -
    +
    @include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
    {!! csrf_field() !!} - +
    {{ trans('common.cancel') }} diff --git a/resources/views/chapters/parts/form.blade.php b/resources/views/chapters/parts/form.blade.php index 3908d0693..068c033ab 100644 --- a/resources/views/chapters/parts/form.blade.php +++ b/resources/views/chapters/parts/form.blade.php @@ -11,11 +11,11 @@ @include('form.textarea', ['name' => 'description'])
    -
    - -
    +
    @include('entities.tag-manager', ['entity' => $chapter ?? null])
    diff --git a/resources/views/common/notifications.blade.php b/resources/views/common/notifications.blade.php index 752920917..e06bd5fd1 100644 --- a/resources/views/common/notifications.blade.php +++ b/resources/views/common/notifications.blade.php @@ -1,11 +1,29 @@ -