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
}
```
-#### 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.
// Options defined for the compontent.
this.$opts
+
+// The registered name of the component, usually kebab-case.
+this.$name
+
+// Emit a custom event from this component.
+// Will be bubbled up from the dom element this is registered on,
+// as a custom event with the name `<elementName>-<eventName>`,
+// with the provided data in the event detail.
+this.$emit(eventName, data = {})
```
## Global JavaScript Helpers
// 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
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();
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;
clone.classList.remove('hidden');
this.setClonedInputNames(clone);
this.modelRow.parentNode.insertBefore(clone, this.modelRow);
- window.components.init(clone);
+ window.$components.init(clone);
}
/**
elem.name = elem.name.split('randrowid').join(rowId);
}
}
-}
-
-export default AddRemoveRows;
\ No newline at end of file
+}
\ No newline at end of file
-/**
- * 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;
this.row.style.pointerEvents = null;
});
}
-}
-
-export default AjaxDeleteRow;
\ No newline at end of file
+}
\ No newline at end of file
import {onEnterPress, onSelect} from "../services/dom";
+import {Component} from "./component";
/**
* Ajax Form
*
* 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;
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
+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;
link.removeAttribute('target');
}
}
-}
-
-export default AttachmentsList;
\ No newline at end of file
+}
\ No newline at end of file
-/**
- * Attachments
- * @extends {Component}
- */
import {showLoading} from "../services/dom";
+import {Component} from "./component";
-class Attachments {
+export class Attachments extends Component {
setup() {
this.container = this.$el;
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);
});
}
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() {
this.listContainer.classList.remove('hidden');
}
-}
-
-export default Attachments;
\ No newline at end of file
+}
\ No newline at end of file
+import {Component} from "./component";
-class AutoSubmit {
+export class AutoSubmit extends Component {
setup() {
this.form = this.$el;
this.form.submit();
}
-}
-
-export default AutoSubmit;
\ No newline at end of file
+}
\ No newline at end of file
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;
this.hideSuggestions();
}
}
-}
-
-export default AutoSuggest;
\ No newline at end of file
+}
\ No newline at end of file
+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);
}
}
requestAnimationFrame(setPos.bind(this));
}
-}
-
-export default BackToTop;
\ No newline at end of file
+}
\ No newline at end of file
import Sortable from "sortablejs";
+import {Component} from "./component";
+import {htmlToDom} from "../services/dom";
// Auto sort control
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();
* @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);
});
*/
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');
}
}
-}
-
-export default BookSort;
\ No newline at end of file
+}
\ No newline at end of file
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;
event.preventDefault();
this.isOpen ? this.close() : this.open();
}
-
}
-
-export default ChapterContents;
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;
}
this.loadHistory();
- this.popup.components.popup.show(() => {
+ this.getPopup().show(() => {
Code.updateLayout(this.editor);
this.editor.focus();
}, () => {
}
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());
window.sessionStorage.setItem(this.historyKey, historyString);
}
-}
-
-export default CodeEditor;
\ No newline at end of file
+}
\ No newline at end of file
-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
/**
* 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;
Code.inlineEditor(this.$el, mode);
}
-}
-
-export default CodeTextarea;
\ No newline at end of file
+}
\ No newline at end of file
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();
}
}
-}
-
-export default Collapsible;
\ No newline at end of file
+}
\ No newline at end of file
--- /dev/null
+export class Component {
+
+ /**
+ * The registered name of the component.
+ * @type {string}
+ */
+ $name = '';
+
+ /**
+ * The element that the component is registered upon.
+ * @type {Element}
+ */
+ $el = null;
+
+ /**
+ * Mapping of referenced elements within the component.
+ * @type {Object<string, Element>}
+ */
+ $refs = {};
+
+ /**
+ * Mapping of arrays of referenced elements within the component so multiple
+ * references, sharing the same name, can be fetched.
+ * @type {Object<string, Element[]>}
+ */
+ $manyRefs = {};
+
+ /**
+ * Options passed into this component.
+ * @type {Object<String, String>}
+ */
+ $opts = {};
+
+ /**
+ * Component-specific setup methods.
+ * Use this to assign local variables and run any initial setup or actions.
+ */
+ setup() {
+ //
+ }
+
+ /**
+ * Emit an event from this component.
+ * Will be bubbled up from the dom element this is registered on, as a custom event
+ * with the name `<elementName>-<eventName>`, with the provided data in the event detail.
+ * @param {String} eventName
+ * @param {Object} data
+ */
+ $emit(eventName, data = {}) {
+ data.from = this;
+ const componentName = this.$name;
+ const event = new CustomEvent(`${componentName}-${eventName}`, {
+ bubbles: true,
+ detail: data
+ });
+ this.$el.dispatchEvent(event);
+ }
+}
\ No newline at end of file
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;
* @returns {Popup}
*/
getPopup() {
- return this.container.components.popup;
+ return window.$components.firstOnElement(this.container, 'popup');
}
/**
}
}
-}
-
-export default ConfirmDialog;
\ No newline at end of file
+}
\ No newline at end of file
+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();
}
this.display.setAttribute('aria-checked', checked);
}
-}
-
-export default CustomCheckbox;
\ No newline at end of file
+}
\ No newline at end of file
-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
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;
this.loadingElem.style.display = show ? 'block' : 'none';
}
-}
-
-export default DropdownSearch;
\ No newline at end of file
+}
\ No newline at end of file
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;
}
hideAll() {
- for (let dropdown of window.components.dropdown) {
+ for (let dropdown of window.$components.get('dropdown')) {
dropdown.hide();
}
}
});
}
-}
-
-export default DropDown;
\ No newline at end of file
+}
\ No newline at end of file
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;
removeAll() {
this.dz.removeAllFiles(true);
}
-}
-
-export default Dropzone;
\ No newline at end of file
+}
\ No newline at end of file
-class EditorToolbox {
+import {Component} from "./component";
- constructor(elem) {
+export class EditorToolbox extends Component {
+
+ setup() {
// Elements
- this.elem = elem;
- this.buttons = elem.querySelectorAll('[toolbox-tab-button]');
- this.contentElements = elem.querySelectorAll('[toolbox-tab-content]');
- this.toggleButton = elem.querySelector('[toolbox-toggle]');
+ this.container = this.$el;
+ this.buttons = this.$manyRefs.tabButton;
+ this.contentElements = this.$manyRefs.tabContent;
+ this.toggleButton = this.$refs.toggle;
+
+ this.setupListeners();
+
+ // Set the first tab as active on load
+ this.setActiveTab(this.contentElements[0].dataset.tabContent);
+ }
+ setupListeners() {
// Toolbox toggle button click
- this.toggleButton.addEventListener('click', this.toggle.bind(this));
+ this.toggleButton.addEventListener('click', () => this.toggle());
// Tab button click
- this.elem.addEventListener('click', event => {
- let button = event.target.closest('[toolbox-tab-button]');
- if (button === null) return;
- let name = button.getAttribute('toolbox-tab-button');
- this.setActiveTab(name, true);
+ this.container.addEventListener('click', event => {
+ const button = event.target.closest('button');
+ if (this.buttons.includes(button)) {
+ const name = button.dataset.tab;
+ this.setActiveTab(name, true);
+ }
});
-
- // Set the first tab as active on load
- this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content'));
}
toggle() {
- this.elem.classList.toggle('open');
- const expanded = this.elem.classList.contains('open') ? 'true' : 'false';
+ this.container.classList.toggle('open');
+ const expanded = this.container.classList.contains('open') ? 'true' : 'false';
this.toggleButton.setAttribute('aria-expanded', expanded);
}
setActiveTab(tabName, openToolbox = false) {
+
// Set button visibility
- for (let i = 0, len = this.buttons.length; i < len; i++) {
- this.buttons[i].classList.remove('active');
- let bName = this.buttons[i].getAttribute('toolbox-tab-button');
- if (bName === tabName) this.buttons[i].classList.add('active');
+ for (const button of this.buttons) {
+ button.classList.remove('active');
+ const bName = button.dataset.tab;
+ if (bName === tabName) button.classList.add('active');
}
+
// Set content visibility
- for (let i = 0, len = this.contentElements.length; i < len; i++) {
- this.contentElements[i].style.display = 'none';
- let cName = this.contentElements[i].getAttribute('toolbox-tab-content');
- if (cName === tabName) this.contentElements[i].style.display = 'block';
+ for (const contentEl of this.contentElements) {
+ contentEl.style.display = 'none';
+ const cName = contentEl.dataset.tabContent;
+ if (cName === tabName) contentEl.style.display = 'block';
}
- if (openToolbox && !this.elem.classList.contains('open')) {
+ if (openToolbox && !this.container.classList.contains('open')) {
this.toggle();
}
}
-}
-
-export default EditorToolbox;
\ No newline at end of file
+}
\ No newline at end of file
-/**
- * @extends {Component}
- */
import {htmlToDom} from "../services/dom";
+import {Component} from "./component";
-class EntityPermissions {
+export class EntityPermissions extends Component {
setup() {
this.container = this.$el;
row.remove();
}
-}
-
-export default EntityPermissions;
\ No newline at end of file
+}
\ No newline at end of file
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;
this.loadingBlock.classList.add('hidden');
this.searchInput.value = '';
}
-}
-
-export default EntitySearch;
\ No newline at end of file
+}
\ No newline at end of file
-/**
- * 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;
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() {
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
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;
this.selectedItemData = null;
}
-}
-
-export default EntitySelector;
\ No newline at end of file
+}
\ No newline at end of file
import {onSelect} from "../services/dom";
+import {Component} from "./component";
/**
* EventEmitSelect
*
* 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;
});
}
-}
-
-export default EventEmitSelect;
\ No newline at end of file
+}
\ No newline at end of file
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) {
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);
}
});
}
-}
-
-export default ExpandToggle;
\ No newline at end of file
+}
\ No newline at end of file
+import {Component} from "./component";
-class HeaderMobileToggle {
+export class HeaderMobileToggle extends Component {
setup() {
this.elem = this.$el;
this.onToggle(event);
}
-}
-
-export default HeaderMobileToggle;
\ No newline at end of file
+}
\ No newline at end of file
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;
this.resetState();
this.setupListeners();
-
- window.ImageManager = this;
}
setupListeners() {
this.callback = callback;
this.type = type;
- this.popupEl.components.popup.show();
+ this.getPopup().show();
this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery');
if (!this.hasData) {
}
hide() {
- this.popupEl.components.popup.hide();
+ this.getPopup().hide();
+ }
+
+ /**
+ * @returns {Popup}
+ */
+ getPopup() {
+ return window.$components.firstOnElement(this.popupEl, 'popup');
}
async loadGallery() {
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);
}
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
+import {Component} from "./component";
-class ImagePicker {
+export class ImagePicker extends Component {
- constructor(elem) {
- this.elem = elem;
- this.imageElem = elem.querySelector('img');
- this.imageInput = elem.querySelector('input[type=file]');
- this.resetInput = elem.querySelector('input[data-reset-input]');
- this.removeInput = elem.querySelector('input[data-remove-input]');
+ setup() {
+ this.imageElem = this.$refs.image;
+ this.imageInput = this.$refs.imageInput;
+ this.resetInput = this.$refs.resetInput;
+ this.removeInput = this.$refs.removeInput;
+ this.resetButton = this.$refs.resetButton;
+ this.removeButton = this.$refs.removeButton || null;
- this.defaultImage = elem.getAttribute('data-default-image');
+ this.defaultImage = this.$opts.defaultImage;
- const resetButton = elem.querySelector('button[data-action="reset-image"]');
- resetButton.addEventListener('click', this.reset.bind(this));
+ this.setupListeners();
+ }
+
+ setupListeners() {
+ this.resetButton.addEventListener('click', this.reset.bind(this));
- const removeButton = elem.querySelector('button[data-action="remove-image"]');
- if (removeButton) {
- removeButton.addEventListener('click', this.removeImage.bind(this));
+ if (this.removeButton) {
+ this.removeButton.addEventListener('click', this.removeImage.bind(this));
}
this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
this.resetInput.setAttribute('disabled', 'disabled');
}
-}
-
-export default ImagePicker;
\ No newline at end of file
+}
\ No newline at end of file
-import addRemoveRows from "./add-remove-rows.js"
-import ajaxDeleteRow from "./ajax-delete-row.js"
-import ajaxForm from "./ajax-form.js"
-import attachments from "./attachments.js"
-import attachmentsList from "./attachments-list.js"
-import autoSuggest from "./auto-suggest.js"
-import autoSubmit from "./auto-submit.js";
-import backToTop from "./back-to-top.js"
-import bookSort from "./book-sort.js"
-import chapterContents from "./chapter-contents.js"
-import codeEditor from "./code-editor.js"
-import codeHighlighter from "./code-highlighter.js"
-import codeTextarea from "./code-textarea.js"
-import collapsible from "./collapsible.js"
-import confirmDialog from "./confirm-dialog"
-import customCheckbox from "./custom-checkbox.js"
-import detailsHighlighter from "./details-highlighter.js"
-import dropdown from "./dropdown.js"
-import dropdownSearch from "./dropdown-search.js"
-import dropzone from "./dropzone.js"
-import editorToolbox from "./editor-toolbox.js"
-import entityPermissions from "./entity-permissions";
-import entitySearch from "./entity-search.js"
-import entitySelector from "./entity-selector.js"
-import entitySelectorPopup from "./entity-selector-popup.js"
-import eventEmitSelect from "./event-emit-select.js"
-import expandToggle from "./expand-toggle.js"
-import headerMobileToggle from "./header-mobile-toggle.js"
-import homepageControl from "./homepage-control.js"
-import imageManager from "./image-manager.js"
-import imagePicker from "./image-picker.js"
-import listSortControl from "./list-sort-control.js"
-import markdownEditor from "./markdown-editor.js"
-import newUserPassword from "./new-user-password.js"
-import notification from "./notification.js"
-import optionalInput from "./optional-input.js"
-import pageComments from "./page-comments.js"
-import pageDisplay from "./page-display.js"
-import pageEditor from "./page-editor.js"
-import pagePicker from "./page-picker.js"
-import permissionsTable from "./permissions-table.js"
-import pointer from "./pointer.js";
-import popup from "./popup.js"
-import settingAppColorPicker from "./setting-app-color-picker.js"
-import settingColorPicker from "./setting-color-picker.js"
-import shelfSort from "./shelf-sort.js"
-import shortcuts from "./shortcuts";
-import shortcutInput from "./shortcut-input";
-import sidebar from "./sidebar.js"
-import sortableList from "./sortable-list.js"
-import submitOnChange from "./submit-on-change.js"
-import tabs from "./tabs.js"
-import tagManager from "./tag-manager.js"
-import templateManager from "./template-manager.js"
-import toggleSwitch from "./toggle-switch.js"
-import triLayout from "./tri-layout.js"
-import userSelect from "./user-select.js"
-import webhookEvents from "./webhook-events";
-import wysiwygEditor from "./wysiwyg-editor.js"
-
-const componentMapping = {
- "add-remove-rows": addRemoveRows,
- "ajax-delete-row": ajaxDeleteRow,
- "ajax-form": ajaxForm,
- "attachments": attachments,
- "attachments-list": attachmentsList,
- "auto-suggest": autoSuggest,
- "auto-submit": autoSubmit,
- "back-to-top": backToTop,
- "book-sort": bookSort,
- "chapter-contents": chapterContents,
- "code-editor": codeEditor,
- "code-highlighter": codeHighlighter,
- "code-textarea": codeTextarea,
- "collapsible": collapsible,
- "confirm-dialog": confirmDialog,
- "custom-checkbox": customCheckbox,
- "details-highlighter": detailsHighlighter,
- "dropdown": dropdown,
- "dropdown-search": dropdownSearch,
- "dropzone": dropzone,
- "editor-toolbox": editorToolbox,
- "entity-permissions": entityPermissions,
- "entity-search": entitySearch,
- "entity-selector": entitySelector,
- "entity-selector-popup": entitySelectorPopup,
- "event-emit-select": eventEmitSelect,
- "expand-toggle": expandToggle,
- "header-mobile-toggle": headerMobileToggle,
- "homepage-control": homepageControl,
- "image-manager": imageManager,
- "image-picker": imagePicker,
- "list-sort-control": listSortControl,
- "markdown-editor": markdownEditor,
- "new-user-password": newUserPassword,
- "notification": notification,
- "optional-input": optionalInput,
- "page-comments": pageComments,
- "page-display": pageDisplay,
- "page-editor": pageEditor,
- "page-picker": pagePicker,
- "permissions-table": permissionsTable,
- "pointer": pointer,
- "popup": popup,
- "setting-app-color-picker": settingAppColorPicker,
- "setting-color-picker": settingColorPicker,
- "shelf-sort": shelfSort,
- "shortcuts": shortcuts,
- "shortcut-input": shortcutInput,
- "sidebar": sidebar,
- "sortable-list": sortableList,
- "submit-on-change": submitOnChange,
- "tabs": tabs,
- "tag-manager": tagManager,
- "template-manager": templateManager,
- "toggle-switch": toggleSwitch,
- "tri-layout": triLayout,
- "user-select": userSelect,
- "webhook-events": webhookEvents,
- "wysiwyg-editor": wysiwygEditor,
-};
-
-window.components = {};
-
-/**
- * Initialize components of the given name within the given element.
- * @param {String} componentName
- * @param {HTMLElement|Document} parentElement
- */
-function searchForComponentInParent(componentName, parentElement) {
- const elems = parentElement.querySelectorAll(`[${componentName}]`);
- for (let j = 0, jLen = elems.length; j < jLen; j++) {
- initComponent(componentName, elems[j]);
- }
-}
-
-/**
- * Initialize a component instance on the given dom element.
- * @param {String} name
- * @param {Element} element
- */
-function initComponent(name, element) {
- const componentModel = componentMapping[name];
- if (componentModel === undefined) return;
-
- // Create our component instance
- let instance;
- try {
- instance = new componentModel(element);
- instance.$el = element;
- const allRefs = parseRefs(name, element);
- instance.$refs = allRefs.refs;
- instance.$manyRefs = allRefs.manyRefs;
- instance.$opts = parseOpts(name, element);
- instance.$emit = (eventName, data = {}) => {
- data.from = instance;
- const event = new CustomEvent(`${name}-${eventName}`, {
- bubbles: true,
- detail: data
- });
- instance.$el.dispatchEvent(event);
- };
- if (typeof instance.setup === 'function') {
- instance.setup();
- }
- } catch (e) {
- console.error('Failed to create component', e, name, element);
- }
-
-
- // Add to global listing
- if (typeof window.components[name] === "undefined") {
- window.components[name] = [];
- }
- window.components[name].push(instance);
-
- // Add to element listing
- if (typeof element.components === 'undefined') {
- element.components = {};
- }
- element.components[name] = instance;
-}
-
-/**
- * Parse out the element references within the given element
- * for the given component name.
- * @param {String} name
- * @param {Element} element
- */
-function parseRefs(name, element) {
- const refs = {};
- const manyRefs = {};
-
- const prefix = `${name}@`
- const selector = `[refs*="${prefix}"]`;
- const refElems = [...element.querySelectorAll(selector)];
- if (element.matches(selector)) {
- refElems.push(element);
- }
-
- for (const el of refElems) {
- const refNames = el.getAttribute('refs')
- .split(' ')
- .filter(str => str.startsWith(prefix))
- .map(str => str.replace(prefix, ''))
- .map(kebabToCamel);
- for (const ref of refNames) {
- refs[ref] = el;
- if (typeof manyRefs[ref] === 'undefined') {
- manyRefs[ref] = [];
- }
- manyRefs[ref].push(el);
- }
- }
- return {refs, manyRefs};
-}
-
-/**
- * Parse out the element component options.
- * @param {String} name
- * @param {Element} element
- * @return {Object<String, String>}
- */
-function parseOpts(name, element) {
- const opts = {};
- const prefix = `option:${name}:`;
- for (const {name, value} of element.attributes) {
- if (name.startsWith(prefix)) {
- const optName = name.replace(prefix, '');
- opts[kebabToCamel(optName)] = value || '';
- }
- }
- return opts;
-}
-
-/**
- * Convert a kebab-case string to camelCase
- * @param {String} kebab
- * @returns {string}
- */
-function kebabToCamel(kebab) {
- const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
- const words = kebab.split('-');
- return words[0] + words.slice(1).map(ucFirst).join('');
-}
-
-/**
- * Initialize all components found within the given element.
- * @param parentElement
- */
-function initAll(parentElement) {
- if (typeof parentElement === 'undefined') parentElement = document;
-
- // Old attribute system
- for (const componentName of Object.keys(componentMapping)) {
- searchForComponentInParent(componentName, parentElement);
- }
-
- // New component system
- const componentElems = parentElement.querySelectorAll(`[component],[components]`);
-
- for (const el of componentElems) {
- const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
- for (const name of componentNames) {
- initComponent(name, el);
- }
- }
-}
-
-window.components.init = initAll;
-window.components.first = (name) => (window.components[name] || [null])[0];
-
-export default initAll;
-
-/**
- * @typedef Component
- * @property {HTMLElement} $el
- * @property {Object<String, HTMLElement>} $refs
- * @property {Object<String, HTMLElement[]>} $manyRefs
- * @property {Object<String, String>} $opts
- * @property {function(string, Object)} $emit
- */
\ No newline at end of file
+export {AddRemoveRows} from "./add-remove-rows.js"
+export {AjaxDeleteRow} from "./ajax-delete-row.js"
+export {AjaxForm} from "./ajax-form.js"
+export {Attachments} from "./attachments.js"
+export {AttachmentsList} from "./attachments-list.js"
+export {AutoSuggest} from "./auto-suggest.js"
+export {AutoSubmit} from "./auto-submit.js"
+export {BackToTop} from "./back-to-top.js"
+export {BookSort} from "./book-sort.js"
+export {ChapterContents} from "./chapter-contents.js"
+export {CodeEditor} from "./code-editor.js"
+export {CodeHighlighter} from "./code-highlighter.js"
+export {CodeTextarea} from "./code-textarea.js"
+export {Collapsible} from "./collapsible.js"
+export {ConfirmDialog} from "./confirm-dialog"
+export {CustomCheckbox} from "./custom-checkbox.js"
+export {DetailsHighlighter} from "./details-highlighter.js"
+export {Dropdown} from "./dropdown.js"
+export {DropdownSearch} from "./dropdown-search.js"
+export {Dropzone} from "./dropzone.js"
+export {EditorToolbox} from "./editor-toolbox.js"
+export {EntityPermissions} from "./entity-permissions"
+export {EntitySearch} from "./entity-search.js"
+export {EntitySelector} from "./entity-selector.js"
+export {EntitySelectorPopup} from "./entity-selector-popup.js"
+export {EventEmitSelect} from "./event-emit-select.js"
+export {ExpandToggle} from "./expand-toggle.js"
+export {HeaderMobileToggle} from "./header-mobile-toggle.js"
+export {ImageManager} from "./image-manager.js"
+export {ImagePicker} from "./image-picker.js"
+export {ListSortControl} from "./list-sort-control.js"
+export {MarkdownEditor} from "./markdown-editor.js"
+export {NewUserPassword} from "./new-user-password.js"
+export {Notification} from "./notification.js"
+export {OptionalInput} from "./optional-input.js"
+export {PageComments} from "./page-comments.js"
+export {PageDisplay} from "./page-display.js"
+export {PageEditor} from "./page-editor.js"
+export {PagePicker} from "./page-picker.js"
+export {PermissionsTable} from "./permissions-table.js"
+export {Pointer} from "./pointer.js";
+export {Popup} from "./popup.js"
+export {SettingAppColorPicker} from "./setting-app-color-picker.js"
+export {SettingColorPicker} from "./setting-color-picker.js"
+export {SettingHomepageControl} from "./setting-homepage-control.js"
+export {ShelfSort} from "./shelf-sort.js"
+export {Shortcuts} from "./shortcuts"
+export {ShortcutInput} from "./shortcut-input"
+export {SortableList} from "./sortable-list.js"
+export {SubmitOnChange} from "./submit-on-change.js"
+export {Tabs} from "./tabs.js"
+export {TagManager} from "./tag-manager.js"
+export {TemplateManager} from "./template-manager.js"
+export {ToggleSwitch} from "./toggle-switch.js"
+export {TriLayout} from "./tri-layout.js"
+export {UserSelect} from "./user-select.js"
+export {WebhookEvents} from "./webhook-events";
+export {WysiwygEditor} from "./wysiwyg-editor.js"
\ No newline at end of file
/**
* 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;
this.form.submit();
}
-}
-
-export default ListSortControl;
\ No newline at end of file
+}
\ No newline at end of file
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;
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 = "[](" + image.url + ")";
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');
}
// 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();
});
}
}
-
-export default MarkdownEditor ;
+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));
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
+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);
}
}
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);
}
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
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;
});
}
-}
-
-export default OptionalInput;
\ No newline at end of file
+}
\ No newline at end of file
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;
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(() => {
};
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();
formElem.querySelector('.form-group.loading').style.display = 'none';
}
-}
-
-export default PageComments;
\ No newline at end of file
+}
\ No newline at end of file
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();
// 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);
}
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);
}
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);
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);
}
}
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
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';
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) {
}
}
-}
-
-export default PageEditor;
\ No newline at end of file
+}
\ No newline at end of file
+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();
}
showPopup() {
- window.EntitySelectorPopup.show(entity => {
+ /** @type {EntitySelectorPopup} **/
+ const selectorPopup = window.$components.first('entity-selector-popup');
+ selectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
});
}
}
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);
}
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
+import {Component} from "./component";
-class PermissionsTable {
+export class PermissionsTable extends Component {
setup() {
this.container = this.$el;
}
}
-}
-
-export default PermissionsTable;
\ No newline at end of file
+}
\ No newline at end of file
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;
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
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;
this.onHide = onHide;
}
-}
-
-export default Popup;
\ No newline at end of file
+}
\ No newline at end of file
+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();
- });
}
/**
/**
* 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);
}
}
-
-export default SettingAppColorPicker;
+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
+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();
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
import Sortable from "sortablejs";
+import {Component} from "./component";
-class ShelfSort {
+export class ShelfSort extends Component {
setup() {
this.elem = this.$el;
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',
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
+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;
this.input.removeEventListener('keydown', this.listenerRecordKey);
}
-}
-
-export default ShortcutInput;
\ No newline at end of file
+}
\ No newline at end of file
+import {Component} from "./component";
+
function reverseMap(map) {
const reversed = {};
for (const [key, value] of Object.entries(map)) {
return reversed;
}
-/**
- * @extends {Component}
- */
-class Shortcuts {
+
+export class Shortcuts extends Component {
setup() {
this.container = this.$el;
this.hintsShowing = false;
}
-}
-
-export default Shortcuts;
\ No newline at end of file
+}
\ No newline at end of file
+++ /dev/null
-
-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
import Sortable from "sortablejs";
+import {Component} from "./component";
/**
* SortableList
* 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;
dragoverBubble: false,
});
}
-}
-
-export default SortableList;
\ No newline at end of file
+}
\ No newline at end of file
+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;
});
}
-}
-
-export default SubmitOnChange;
\ No newline at end of file
+}
\ No newline at end of file
+import {onSelect} from "../services/dom";
+import {Component} from "./component";
+
/**
* Tabs
* Works by matching 'tabToggle<Key>' with 'tabContent<Key>' sections.
- * @extends {Component}
*/
-import {onSelect} from "../services/dom";
-
-class Tabs {
+export class Tabs extends Component {
setup() {
this.tabContentsByName = {};
}
}
-}
-
-export default Tabs;
\ No newline at end of file
+}
\ No newline at end of file
-/**
- * TagManager
- * @extends {Component}
- */
-class TagManager {
+import {Component} from "./component";
+
+export class TagManager extends Component {
setup() {
this.addRemoveComponentEl = this.$refs.addRemove;
this.container = this.$el;
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();
}
});
return firstEmpty !== undefined;
}
-}
-
-export default TagManager;
\ No newline at end of file
+}
\ No newline at end of file
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) {
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
+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));
}
this.input.dispatchEvent(changeEvent);
}
-}
-
-export default ToggleSwitch;
\ No newline at end of file
+}
\ No newline at end of file
+import {Component} from "./component";
-class TriLayout {
+export class TriLayout extends Component {
setup() {
this.container = this.$refs.container;
this.lastTabShown = tabName;
}
-}
-
-export default TriLayout;
\ No newline at end of file
+}
\ No newline at end of file
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
-
/**
* 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"]');
}
}
-}
-
-export default WebhookEvents;
\ No newline at end of file
+}
\ No newline at end of file
import {build as buildEditorConfig} from "../wysiwyg/config";
+import {Component} from "./component";
-class WysiwygEditor {
+export class WysiwygEditor extends Component {
setup() {
this.elem = this.$el;
return '';
}
-}
-
-export default WysiwygEditor;
+}
\ No newline at end of file
--- /dev/null
+/**
+ * A mapping of active components keyed by name, with values being arrays of component
+ * instances since there can be multiple components of the same type.
+ * @type {Object<String, Component[]>}
+ */
+const components = {};
+
+/**
+ * A mapping of component class models, keyed by name.
+ * @type {Object<String, Constructor<Component>>}
+ */
+const componentModelMap = {};
+
+/**
+ * A mapping of active component maps, keyed by the element components are assigned to.
+ * @type {WeakMap<Element, Object<String, Component>>}
+ */
+const elementComponentMap = new WeakMap();
+
+/**
+ * Initialize a component instance on the given dom element.
+ * @param {String} name
+ * @param {Element} element
+ */
+function initComponent(name, element) {
+ /** @type {Function<Component>|undefined} **/
+ const componentModel = componentModelMap[name];
+ if (componentModel === undefined) return;
+
+ // Create our component instance
+ /** @type {Component} **/
+ let instance;
+ try {
+ instance = new componentModel();
+ instance.$name = name;
+ instance.$el = element;
+ const allRefs = parseRefs(name, element);
+ instance.$refs = allRefs.refs;
+ instance.$manyRefs = allRefs.manyRefs;
+ instance.$opts = parseOpts(name, element);
+ instance.setup();
+ } catch (e) {
+ console.error('Failed to create component', e, name, element);
+ }
+
+ // Add to global listing
+ if (typeof components[name] === "undefined") {
+ components[name] = [];
+ }
+ components[name].push(instance);
+
+ // Add to element mapping
+ const elComponents = elementComponentMap.get(element) || {};
+ elComponents[name] = instance;
+ elementComponentMap.set(element, elComponents);
+}
+
+/**
+ * Parse out the element references within the given element
+ * for the given component name.
+ * @param {String} name
+ * @param {Element} element
+ */
+function parseRefs(name, element) {
+ const refs = {};
+ const manyRefs = {};
+
+ const prefix = `${name}@`
+ const selector = `[refs*="${prefix}"]`;
+ const refElems = [...element.querySelectorAll(selector)];
+ if (element.matches(selector)) {
+ refElems.push(element);
+ }
+
+ for (const el of refElems) {
+ const refNames = el.getAttribute('refs')
+ .split(' ')
+ .filter(str => str.startsWith(prefix))
+ .map(str => str.replace(prefix, ''))
+ .map(kebabToCamel);
+ for (const ref of refNames) {
+ refs[ref] = el;
+ if (typeof manyRefs[ref] === 'undefined') {
+ manyRefs[ref] = [];
+ }
+ manyRefs[ref].push(el);
+ }
+ }
+ return {refs, manyRefs};
+}
+
+/**
+ * Parse out the element component options.
+ * @param {String} name
+ * @param {Element} element
+ * @return {Object<String, String>}
+ */
+function parseOpts(name, element) {
+ const opts = {};
+ const prefix = `option:${name}:`;
+ for (const {name, value} of element.attributes) {
+ if (name.startsWith(prefix)) {
+ const optName = name.replace(prefix, '');
+ opts[kebabToCamel(optName)] = value || '';
+ }
+ }
+ return opts;
+}
+
+/**
+ * Convert a kebab-case string to camelCase
+ * @param {String} kebab
+ * @returns {string}
+ */
+function kebabToCamel(kebab) {
+ const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
+ const words = kebab.split('-');
+ return words[0] + words.slice(1).map(ucFirst).join('');
+}
+
+/**
+ * Initialize all components found within the given element.
+ * @param {Element|Document} parentElement
+ */
+export function init(parentElement = document) {
+ const componentElems = parentElement.querySelectorAll(`[component],[components]`);
+
+ for (const el of componentElems) {
+ const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
+ for (const name of componentNames) {
+ initComponent(name, el);
+ }
+ }
+}
+
+/**
+ * Register the given component mapping into the component system.
+ * @param {Object<String, ObjectConstructor<Component>>} mapping
+ */
+export function register(mapping) {
+ const keys = Object.keys(mapping);
+ for (const key of keys) {
+ componentModelMap[camelToKebab(key)] = mapping[key];
+ }
+}
+
+/**
+ * Get the first component of the given name.
+ * @param {String} name
+ * @returns {Component|null}
+ */
+export function first(name) {
+ return (components[name] || [null])[0];
+}
+
+/**
+ * Get all the components of the given name.
+ * @param {String} name
+ * @returns {Component[]}
+ */
+export function get(name) {
+ return components[name] || [];
+}
+
+/**
+ * Get the first component, of the given name, that's assigned to the given element.
+ * @param {Element} element
+ * @param {String} name
+ * @returns {Component|null}
+ */
+export function firstOnElement(element, name) {
+ const elComponents = elementComponentMap.get(element) || {};
+ return elComponents[name] || null;
+}
+
+function camelToKebab(camelStr) {
+ return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
+}
\ No newline at end of file
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
// 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,
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');
}
* @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()
});
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 () {
* @param {String} url
*/
function register(editor, url) {
-
// Custom Image picker button
editor.ui.registry.addButton('imagemanager-insert', {
title: 'Insert image',
icon: 'image',
tooltip: 'Insert image',
onAction() {
- window.ImageManager.show(function (image) {
+ /** @type {ImageManager} **/
+ const imageManager = window.$components.first('image-manager');
+ imageManager.show(function (image) {
const imageUrl = image.thumbs.display || image.url;
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${imageUrl}" alt="${image.name}">`;
// 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)));
// System wide notifications
-[notification] {
+.notification {
position: fixed;
top: 0;
right: 0;
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
}
}
-.form-group[collapsible] {
+.form-group.collapsible {
padding: 0 $-m;
border: 1px solid;
@include lightDark(border-color, #DDD, #000);
&.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 {
}
}
-[toolbox-tab-content] {
+.toolbox-tab-content {
display: none;
}
}
}
-// 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;
<div style="overflow: auto;">
- <section code-highlighter class="card content-wrap auto-height">
+ <section component="code-highlighter" class="card content-wrap auto-height">
@include('api-docs.parts.getting-started')
</section>
@endif
@if($endpoint['example_request'] ?? false)
- <details details-highlighter class="mb-m">
+ <details component="details-highlighter" class="mb-m">
<summary class="text-muted">Example Request</summary>
<pre><code class="language-json">{{ $endpoint['example_request'] }}</code></pre>
</details>
@endif
@if($endpoint['example_response'] ?? false)
- <details details-highlighter class="mb-m">
+ <details component="details-highlighter" class="mb-m">
<summary class="text-muted">Example Response</summary>
<pre><code class="language-json">{{ $endpoint['example_response'] }}</code></pre>
</details>
-<div style="display: block;" toolbox-tab-content="files"
+<div style="display: block;"
+ refs="editor-toolbox@tab-content"
+ data-tab-content="files"
component="attachments"
- option:attachments:page-id="{{ $page->id ?? 0 }}">
+ option:attachments:page-id="{{ $page->id ?? 0 }}"
+ class="toolbox-tab-content">
<h4>{{ trans('entities.attachments') }}</h4>
<div class="px-l files">
@include('form.textarea', ['name' => 'description'])
</div>
-<div class="form-group" collapsible id="logo-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label>{{ trans('common.cover_image') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
<p class="small">{{ trans('common.cover_image_description') }}</p>
@include('form.image-picker', [
</div>
</div>
-<div class="form-group" collapsible id="tags-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="tags-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label for="tag-manager">{{ trans('entities.book_tags') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
@include('entities.tag-manager', ['entity' => $book ?? null])
</div>
</div>
<span>{{ $book->name }}</span>
</h5>
<div class="sort-box-options pb-sm">
- <a href="#" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</a>
- <a href="#" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</a>
- <a href="#" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</a>
- <a href="#" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</a>
- <a href="#" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</a>
+ <button type="button" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</button>
+ <button type="button" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</button>
+ <button type="button" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</button>
+ <button type="button" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</button>
+ <button type="button" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</button>
</div>
<ul class="sortable-page-list sort-list">
<div class="grid left-focus gap-xl">
<div>
- <div book-sort class="card content-wrap">
+ <div component="book-sort" class="card content-wrap">
<h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1>
- <div book-sort-boxes>
+ <div refs="book-sort@sortContainer">
@include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
</div>
<form action="{{ $book->getUrl('/sort') }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
- <input book-sort-input type="hidden" name="sort-tree">
+ <input refs="book-sort@input" type="hidden" name="sort-tree">
<div class="list text-right">
<a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
<button class="button" type="submit">{{ trans('entities.books_sort_save') }}</button>
@include('form.textarea', ['name' => 'description'])
</div>
-<div class="form-group" collapsible id="logo-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label for="tags">{{ trans('entities.chapter_tags') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
@include('entities.tag-manager', ['entity' => $chapter ?? null])
</div>
</div>
-<div notification="success" style="display: none;" data-autohide class="pos" role="alert" @if(session()->has('success')) data-show @endif>
+<div component="notification"
+ option:notification:type="success"
+ option:notification:auto-hide="true"
+ option:notification:show="{{ session()->has('success') ? 'true' : 'false' }}"
+ style="display: none;"
+ class="notification pos"
+ role="alert">
@icon('check-circle') <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span><div class="dismiss">@icon('close')</div>
</div>
-<div notification="warning" style="display: none;" class="warning" role="alert" @if(session()->has('warning')) data-show @endif>
+<div component="notification"
+ option:notification:type="warning"
+ option:notification:auto-hide="false"
+ option:notification:show="{{ session()->has('warning') ? 'true' : 'false' }}"
+ style="display: none;"
+ class="notification warning"
+ role="alert">
@icon('info') <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span><div class="dismiss">@icon('close')</div>
</div>
-<div notification="error" style="display: none;" class="neg" role="alert" @if(session()->has('error')) data-show @endif>
+<div component="notification"
+ option:notification:type="error"
+ option:notification:auto-hide="false"
+ option:notification:show="{{ session()->has('error') ? 'true' : 'false' }}"
+ style="display: none;"
+ class="notification neg"
+ role="alert">
@icon('danger') <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span><div class="dismiss">@icon('close')</div>
-</div>
+</div>
\ No newline at end of file
$checked
$label
--}}
-<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
+<label component="custom-checkbox" class="toggle-switch @if($errors->has($name)) text-neg @endif">
<input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif @if($disabled ?? false) disabled="disabled" @endif>
<span tabindex="0" role="checkbox"
aria-checked="{{ $checked ? 'true' : 'false' }}"
-<div class="image-picker @if($errors->has($name)) has-error @endif"
- image-picker="{{$name}}"
- data-default-image="{{ $defaultImage }}">
+<div component="image-picker"
+ option:image-picker:default-image="{{ $defaultImage }}"
+ class="image-picker @if($errors->has($name)) has-error @endif">
<div class="grid half">
<div class="text-center">
- <img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
+ <img refs="image-picker@image"
+ @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif
+ class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
</div>
<div class="text-center">
-
- <input type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
+ <input refs="image-picker@image-input" type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
<label for="{{ $name }}" class="button outline">{{ trans('components.image_select_image') }}</label>
- <input type="hidden" data-reset-input name="{{ $name }}_reset" value="true" disabled="disabled">
+ <input refs="image-picker@reset-input" type="hidden" name="{{ $name }}_reset" value="true" disabled="disabled">
@if(isset($removeName))
- <input type="hidden" data-remove-input name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
+ <input refs="image-picker@remove-input" type="hidden" name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
@endif
<br>
- <button class="text-button text-muted" data-action="reset-image" type="button">{{ trans('common.reset') }}</button>
+ <button refs="image-picker@reset-button" class="text-button text-muted" type="button">{{ trans('common.reset') }}</button>
@if(isset($removeName))
<span class="sep">|</span>
- <button class="text-button text-muted" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
+ <button refs="image-picker@remove-button" class="text-button text-muted" type="button">{{ trans('common.remove') }}</button>
@endif
</div>
</div>
-<label toggle-switch="{{$name}}" custom-checkbox class="toggle-switch">
+<label components="custom-checkbox toggle-switch" class="toggle-switch">
<input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
<input type="checkbox" @if($value) checked="checked" @endif>
<span tabindex="0" role="checkbox"
$key - Unique key for checking existing stored state.
--}}
<?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
-<button type="button" expand-toggle="{{ $target }}"
- expand-toggle-update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
- expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
- class="icon-list-item {{ $classes ?? '' }}">
+<button component="expand-toggle"
+ option:expand-toggle:target-selector="{{ $target }}"
+ option:expand-toggle:update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
+ option:expand-toggle:is-open="{{ $isOpen ? 'true' : 'false' }}"
+ type="button"
+ class="icon-list-item {{ $classes ?? '' }}">
<span>@icon('expand-text')</span>
<span>{{ trans('common.toggle_details') }}</span>
</button>
@section('body')
<div class="mt-m">
<main class="content-wrap card">
- <div class="page-content" page-display="{{ $customHomepage->id }}">
+ <div component="page-display"
+ option:page-display:page-id="{{ $page->id }}"
+ class="page-content">
@include('pages.parts.page-display', ['page' => $customHomepage])
</div>
</main>
@include('common.footer')
- <div back-to-top class="primary-background print-hidden">
+ <div component="back-to-top" class="back-to-top print-hidden">
<div class="inner">
@icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
</div>
-<div editor-toolbox class="floating-toolbox">
+<div component="editor-toolbox" class="floating-toolbox">
<div class="tabs primary-background-light">
- <button type="button" toolbox-toggle aria-expanded="false">@icon('caret-left-circle')</button>
- <button type="button" toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
+ <button type="button" refs="editor-toolbox@toggle" aria-expanded="false" class="toolbox-toggle">@icon('caret-left-circle')</button>
+ <button type="button" refs="editor-toolbox@tab-button" data-tab="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
@if(userCan('attachment-create-all'))
- <button type="button" toolbox-tab-button="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
+ <button type="button" refs="editor-toolbox@tab-button" data-tab="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
@endif
- <button type="button" toolbox-tab-button="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
+ <button type="button" refs="editor-toolbox@tab-button" data-tab="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
</div>
- <div toolbox-tab-content="tags">
+ <div refs="editor-toolbox@tab-content" data-tab-content="tags" class="toolbox-tab-content">
<h4>{{ trans('entities.page_tags') }}</h4>
<div class="px-l">
@include('entities.tag-manager', ['entity' => $page])
@include('attachments.manager', ['page' => $page])
@endif
- <div toolbox-tab-content="templates">
+ <div refs="editor-toolbox@tab-content" data-tab-content="templates" class="toolbox-tab-content">
<h4>{{ trans('entities.templates') }}</h4>
<div class="px-l">
-<div template-manager>
+<div component="template-manager">
@if(userCan('templates-manage'))
<p class="text-muted small mb-none">
{{ trans('entities.templates_explain_set_as_template') }}
<hr>
@endif
- @if(count($templates) > 0)
- <div class="search-box flexible mb-m">
- <input type="text" name="template-search" placeholder="{{ trans('common.search') }}">
- <button type="button">@icon('search')</button>
- <button class="search-box-cancel text-neg hidden" type="button">@icon('close')</button>
- </div>
- @endif
+ <div class="search-box flexible mb-m" style="display: {{ count($templates) > 0 ? 'block' : 'none' }}">
+ <input refs="template-manager@searchInput" type="text" name="template-search" placeholder="{{ trans('common.search') }}">
+ <button refs="template-manager@searchButton" type="button">@icon('search')</button>
+ <button refs="template-manager@searchCancel" class="search-box-cancel text-neg" type="button" style="display: none">@icon('close')</button>
+ </div>
- <div template-manager-list>
+ <div refs="template-manager@list">
@include('pages.parts.template-manager-list', ['templates' => $templates])
</div>
</div>
\ No newline at end of file
</div>
<main class="content-wrap card">
- <div class="page-content clearfix" page-display="{{ $page->id }}">
+ <div component="page-display"
+ option:page-display:page-id="{{ $page->id }}"
+ class="page-content clearfix">
@include('pages.parts.page-display')
</div>
@include('pages.parts.pointer', ['page' => $page])
<label class="setting-list-label">{{ trans('settings.app_primary_color') }}</label>
<p class="small">{!! trans('settings.app_primary_color_desc') !!}</p>
</div>
- <div setting-app-color-picker class="text-m-right pt-xs">
- <input type="color" data-default="#206ea7" data-current="{{ setting('app-color') }}" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#206ea7">
- <input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
+ <div component="setting-app-color-picker setting-color-picker"
+ option:setting-color-picker:default="#206ea7"
+ option:setting-color-picker:current="{{ setting('app-color') }}"
+ class="text-m-right pt-xs">
+ <input refs="setting-color-picker@input setting-app-color-picker@input" type="color" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#206ea7">
+ <input refs="setting-app-color-picker@light-input" type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
<div class="pr-s">
- <button type="button" class="text-button text-muted mt-s" setting-app-color-picker-default>{{ trans('common.default') }}</button>
+ <button refs="setting-color-picker@default-button" type="button" class="text-button text-muted mt-s">{{ trans('common.default') }}</button>
<span class="sep">|</span>
- <button type="button" class="text-button text-muted mt-s" setting-app-color-picker-reset>{{ trans('common.reset') }}</button>
+ <button refs="setting-color-picker@reset-button" type="button" class="text-button text-muted mt-s">{{ trans('common.reset') }}</button>
</div>
</div>
</div>
</div>
- <div homepage-control id="homepage-control" class="grid half gap-xl items-center">
+ <div component="setting-homepage-control" id="homepage-control" class="grid half gap-xl items-center">
<div>
<label for="setting-app-homepage-type" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
<p class="small">{{ trans('settings.app_homepage_desc') }}</p>
</div>
<div>
- <select name="setting-app-homepage-type" id="setting-app-homepage-type">
+ <select refs="setting-homepage-control@type-control"
+ name="setting-app-homepage-type"
+ id="setting-app-homepage-type">
<option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
<option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>
<option @if(setting('app-homepage-type') === 'bookshelves') selected @endif value="bookshelves">{{ trans('entities.shelves') }}</option>
<option @if(setting('app-homepage-type') === 'page') selected @endif value="page">{{ trans('entities.pages_specific') }}</option>
</select>
- <div page-picker-container style="display: none;" class="mt-m">
+ <div refs="setting-homepage-control@page-picker-container" style="display: none;" class="mt-m">
@include('settings.parts.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')])
</div>
</div>
{{--Depends on entity selector popup--}}
-<div page-picker>
+<div component="page-picker">
<div class="input-base">
- <span @if($value) style="display: none" @endif page-picker-default class="text-muted italic">{{ $placeholder }}</span>
- <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" rel="noopener" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
+ <span @if($value) style="display: none" @endif refs="page-picker@default-display" class="text-muted italic">{{ $placeholder }}</span>
+ <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" rel="noopener" class="text-page" refs="page-picker@display">#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
</div>
<br>
- <input type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
- <button @if(!$value) style="display: none" @endif type="button" page-picker-reset class="text-button">{{ trans('common.reset') }}</button>
- <span @if(!$value) style="display: none" @endif class="sep">|</span>
- <button type="button" page-picker-select class="text-button">{{ trans('common.select') }}</button>
+ <input refs="page-picker@input" type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
+ <button @if(!$value) style="display: none" @endif type="button" refs="page-picker@reset-button" class="text-button">{{ trans('common.reset') }}</button>
+ <span refs="page-picker@button-seperator" @if(!$value) style="display: none" @endif class="sep">|</span>
+ <button type="button" refs="page-picker@select-button" class="text-button">{{ trans('common.select') }}</button>
</div>
\ No newline at end of file
{{--
@type - Name of entity type
--}}
-<div setting-color-picker class="grid no-break half mb-l">
+<div component="setting-color-picker"
+ option:setting-color-picker:default="{{ config('setting-defaults.'. $type .'-color') }}"
+ option:setting-color-picker:current="{{ setting($type .'-color') }}"
+ class="grid no-break half mb-l">
<div>
<label for="setting-{{ $type }}-color" class="text-dark">{{ trans('settings.'. str_replace('-', '_', $type) .'_color') }}</label>
- <button type="button" class="text-button text-muted" setting-color-picker-default>{{ trans('common.default') }}</button>
+ <button refs="setting-color-picker@default-button" type="button" class="text-button text-muted">{{ trans('common.default') }}</button>
<span class="sep">|</span>
- <button type="button" class="text-button text-muted" setting-color-picker-reset>{{ trans('common.reset') }}</button>
+ <button refs="setting-color-picker@reset-button" type="button" class="text-button text-muted">{{ trans('common.reset') }}</button>
</div>
<div>
<input type="color"
- data-default="{{ config('setting-defaults.'. $type .'-color') }}"
- data-current="{{ setting($type .'-color') }}"
+ refs="setting-color-picker@input"
value="{{ setting($type .'-color') }}"
name="setting-{{ $type }}-color"
id="setting-{{ $type }}-color"
-<div class="form-group" collapsible id="logo-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label>{{ trans('common.cover_image') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
<p class="small">{{ trans('common.cover_image_description') }}</p>
@include('form.image-picker', [
</div>
</div>
-<div class="form-group" collapsible id="tags-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="tags-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
@include('entities.tag-manager', ['entity' => $shelf ?? null])
</div>
</div>
@endif
@if($authMethod === 'standard')
- <div new-user-password>
+ <div component="new-user-password">
<label class="setting-list-label">{{ trans('settings.users_password') }}</label>
@if(!isset($model))
'value' => old('send_invite', 'true') === 'true',
'label' => trans('settings.users_send_invite_option')
])
-
@endif
- <div id="password-input-container" @if(!isset($model)) style="display: none;" @endif>
+ <div refs="new-user-password@input-container" @if(!isset($model)) style="display: none;" @endif>
<p class="small">{{ trans('settings.users_password_desc') }}</p>
@if(isset($model))
<p class="small">