mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-12 16:08:08 +00:00
Started refactor and alignment of component system
- Updates old components to newer format, removes legacy component support. - Makes component registration easier and less duplicated. - Adds base component class to extend for better editor support. - Aligns global window exposure usage and aligns with other service names.
This commit is contained in:
parent
a1b1f8138a
commit
09c6a3c240
21 changed files with 355 additions and 378 deletions
resources
|
@ -27,5 +27,8 @@ window.trans_choice = translator.getPlural.bind(translator);
|
||||||
window.trans_plural = translator.parsePlural.bind(translator);
|
window.trans_plural = translator.parsePlural.bind(translator);
|
||||||
|
|
||||||
// Load Components
|
// Load Components
|
||||||
import components from "./components"
|
import * as components from "./services/components"
|
||||||
components();
|
import * as componentMap from "./components";
|
||||||
|
components.register(componentMap);
|
||||||
|
window.$components = components;
|
||||||
|
components.init();
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import {onChildEvent} from "../services/dom";
|
import {onChildEvent} from "../services/dom";
|
||||||
import {uniqueId} from "../services/util";
|
import {uniqueId} from "../services/util";
|
||||||
|
import {Component} from "./component";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AddRemoveRows
|
* AddRemoveRows
|
||||||
* Allows easy row add/remove controls onto a table.
|
* Allows easy row add/remove controls onto a table.
|
||||||
* Needs a model row to use when adding a new row.
|
* Needs a model row to use when adding a new row.
|
||||||
* @extends {Component}
|
|
||||||
*/
|
*/
|
||||||
class AddRemoveRows {
|
export class AddRemoveRows extends Component {
|
||||||
setup() {
|
setup() {
|
||||||
this.modelRow = this.$refs.model;
|
this.modelRow = this.$refs.model;
|
||||||
this.addButton = this.$refs.add;
|
this.addButton = this.$refs.add;
|
||||||
|
@ -31,7 +31,7 @@ class AddRemoveRows {
|
||||||
clone.classList.remove('hidden');
|
clone.classList.remove('hidden');
|
||||||
this.setClonedInputNames(clone);
|
this.setClonedInputNames(clone);
|
||||||
this.modelRow.parentNode.insertBefore(clone, this.modelRow);
|
this.modelRow.parentNode.insertBefore(clone, this.modelRow);
|
||||||
window.components.init(clone);
|
window.$components.init(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,6 +49,4 @@ class AddRemoveRows {
|
||||||
elem.name = elem.name.split('randrowid').join(rowId);
|
elem.name = elem.name.split('randrowid').join(rowId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddRemoveRows;
|
|
|
@ -1,10 +1,7 @@
|
||||||
/**
|
|
||||||
* AjaxDelete
|
|
||||||
* @extends {Component}
|
|
||||||
*/
|
|
||||||
import {onSelect} from "../services/dom";
|
import {onSelect} from "../services/dom";
|
||||||
|
import {Component} from "./component";
|
||||||
|
|
||||||
class AjaxDeleteRow {
|
export class AjaxDeleteRow extends Component {
|
||||||
setup() {
|
setup() {
|
||||||
this.row = this.$el;
|
this.row = this.$el;
|
||||||
this.url = this.$opts.url;
|
this.url = this.$opts.url;
|
||||||
|
@ -27,6 +24,4 @@ class AjaxDeleteRow {
|
||||||
this.row.style.pointerEvents = null;
|
this.row.style.pointerEvents = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AjaxDeleteRow;
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {onEnterPress, onSelect} from "../services/dom";
|
import {onEnterPress, onSelect} from "../services/dom";
|
||||||
|
import {Component} from "./component";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ajax Form
|
* Ajax Form
|
||||||
|
@ -8,10 +9,8 @@ import {onEnterPress, onSelect} from "../services/dom";
|
||||||
*
|
*
|
||||||
* Will handle a real form if that's what the component is added to
|
* Will handle a real form if that's what the component is added to
|
||||||
* otherwise will act as a fake form element.
|
* otherwise will act as a fake form element.
|
||||||
*
|
|
||||||
* @extends {Component}
|
|
||||||
*/
|
*/
|
||||||
class AjaxForm {
|
export class AjaxForm extends Component {
|
||||||
setup() {
|
setup() {
|
||||||
this.container = this.$el;
|
this.container = this.$el;
|
||||||
this.responseContainer = this.container;
|
this.responseContainer = this.container;
|
||||||
|
@ -72,11 +71,9 @@ class AjaxForm {
|
||||||
this.responseContainer.innerHTML = err.data;
|
this.responseContainer.innerHTML = err.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.components.init(this.responseContainer);
|
window.$components.init(this.responseContainer);
|
||||||
this.responseContainer.style.opacity = null;
|
this.responseContainer.style.opacity = null;
|
||||||
this.responseContainer.style.pointerEvents = null;
|
this.responseContainer.style.pointerEvents = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AjaxForm;
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
import {Component} from "./component";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attachments List
|
* Attachments List
|
||||||
* Adds '?open=true' query to file attachment links
|
* Adds '?open=true' query to file attachment links
|
||||||
* when ctrl/cmd is pressed down.
|
* when ctrl/cmd is pressed down.
|
||||||
* @extends {Component}
|
|
||||||
*/
|
*/
|
||||||
class AttachmentsList {
|
export class AttachmentsList extends Component {
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.container = this.$el;
|
this.container = this.$el;
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
/**
|
|
||||||
* Attachments
|
|
||||||
* @extends {Component}
|
|
||||||
*/
|
|
||||||
import {showLoading} from "../services/dom";
|
import {showLoading} from "../services/dom";
|
||||||
|
import {Component} from "./component";
|
||||||
|
|
||||||
class Attachments {
|
export class Attachments extends Component {
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.container = this.$el;
|
this.container = this.$el;
|
||||||
|
@ -49,7 +46,7 @@ class Attachments {
|
||||||
this.mainTabs.components.tabs.show('items');
|
this.mainTabs.components.tabs.show('items');
|
||||||
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
|
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
|
||||||
this.list.innerHTML = resp.data;
|
this.list.innerHTML = resp.data;
|
||||||
window.components.init(this.list);
|
window.$components.init(this.list);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +63,7 @@ class Attachments {
|
||||||
showLoading(this.editContainer);
|
showLoading(this.editContainer);
|
||||||
const resp = await window.$http.get(`/attachments/edit/${id}`);
|
const resp = await window.$http.get(`/attachments/edit/${id}`);
|
||||||
this.editContainer.innerHTML = resp.data;
|
this.editContainer.innerHTML = resp.data;
|
||||||
window.components.init(this.editContainer);
|
window.$components.init(this.editContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopEdit() {
|
stopEdit() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import {Component} from "./component";
|
||||||
|
|
||||||
class AutoSubmit {
|
export class AutoSubmit extends Component {
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.form = this.$el;
|
this.form = this.$el;
|
||||||
|
@ -7,6 +8,4 @@ class AutoSubmit {
|
||||||
this.form.submit();
|
this.form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AutoSubmit;
|
|
|
@ -1,13 +1,13 @@
|
||||||
import {escapeHtml} from "../services/util";
|
import {escapeHtml} from "../services/util";
|
||||||
import {onChildEvent} from "../services/dom";
|
import {onChildEvent} from "../services/dom";
|
||||||
|
import {Component} from "./component";
|
||||||
|
|
||||||
const ajaxCache = {};
|
const ajaxCache = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AutoSuggest
|
* AutoSuggest
|
||||||
* @extends {Component}
|
|
||||||
*/
|
*/
|
||||||
class AutoSuggest {
|
export class AutoSuggest extends Component {
|
||||||
setup() {
|
setup() {
|
||||||
this.parent = this.$el.parentElement;
|
this.parent = this.$el.parentElement;
|
||||||
this.container = this.$el;
|
this.container = this.$el;
|
||||||
|
@ -148,6 +148,4 @@ class AutoSuggest {
|
||||||
this.hideSuggestions();
|
this.hideSuggestions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AutoSuggest;
|
|
|
@ -1,34 +1,35 @@
|
||||||
|
import {Component} from "./component";
|
||||||
|
|
||||||
class BackToTop {
|
export class BackToTop extends Component {
|
||||||
|
|
||||||
constructor(elem) {
|
setup() {
|
||||||
this.elem = elem;
|
this.button = this.$el;
|
||||||
this.targetElem = document.getElementById('header');
|
this.targetElem = document.getElementById('header');
|
||||||
this.showing = false;
|
this.showing = false;
|
||||||
this.breakPoint = 1200;
|
this.breakPoint = 1200;
|
||||||
|
|
||||||
if (document.body.classList.contains('flexbox')) {
|
if (document.body.classList.contains('flexbox')) {
|
||||||
this.elem.style.display = 'none';
|
this.button.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.elem.addEventListener('click', this.scrollToTop.bind(this));
|
this.button.addEventListener('click', this.scrollToTop.bind(this));
|
||||||
window.addEventListener('scroll', this.onPageScroll.bind(this));
|
window.addEventListener('scroll', this.onPageScroll.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageScroll() {
|
onPageScroll() {
|
||||||
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
|
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
if (!this.showing && scrollTopPos > this.breakPoint) {
|
if (!this.showing && scrollTopPos > this.breakPoint) {
|
||||||
this.elem.style.display = 'block';
|
this.button.style.display = 'block';
|
||||||
this.showing = true;
|
this.showing = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.elem.style.opacity = 0.4;
|
this.button.style.opacity = 0.4;
|
||||||
}, 1);
|
}, 1);
|
||||||
} else if (this.showing && scrollTopPos < this.breakPoint) {
|
} else if (this.showing && scrollTopPos < this.breakPoint) {
|
||||||
this.elem.style.opacity = 0;
|
this.button.style.opacity = 0;
|
||||||
this.showing = false;
|
this.showing = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.elem.style.display = 'none';
|
this.button.style.display = 'none';
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
58
resources/js/components/component.js
Normal file
58
resources/js/components/component.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
export class Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The registered name of the component.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
$name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element that the component is registered upon.
|
||||||
|
* @type {Element}
|
||||||
|
*/
|
||||||
|
$el = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of referenced elements within the component.
|
||||||
|
* @type {Object<string, Element>}
|
||||||
|
*/
|
||||||
|
$refs = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of arrays of referenced elements within the component so multiple
|
||||||
|
* references, sharing the same name, can be fetched.
|
||||||
|
* @type {Object<string, Element[]>}
|
||||||
|
*/
|
||||||
|
$manyRefs = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options passed into this component.
|
||||||
|
* @type {Object<String, String>}
|
||||||
|
*/
|
||||||
|
$opts = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component-specific setup methods.
|
||||||
|
* Use this to assign local variables and run any initial setup or actions.
|
||||||
|
*/
|
||||||
|
setup() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit an event from this component.
|
||||||
|
* Will be bubbled up from the dom element this is registered on, as a custom event
|
||||||
|
* with the name `<elementName>-<eventName>`, with the provided data in the event detail.
|
||||||
|
* @param {String} eventName
|
||||||
|
* @param {Object} data
|
||||||
|
*/
|
||||||
|
$emit(eventName, data = {}) {
|
||||||
|
data.from = this;
|
||||||
|
const componentName = this.$name;
|
||||||
|
const event = new CustomEvent(`${componentName}-${eventName}`, {
|
||||||
|
bubbles: true,
|
||||||
|
detail: data
|
||||||
|
});
|
||||||
|
this.$el.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,7 +74,7 @@ class DropDown {
|
||||||
}
|
}
|
||||||
|
|
||||||
hideAll() {
|
hideAll() {
|
||||||
for (let dropdown of window.components.dropdown) {
|
for (let dropdown of window.$components.get('dropdown')) {
|
||||||
dropdown.hide();
|
dropdown.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,7 +132,7 @@ class ImageManager {
|
||||||
addReturnedHtmlElementsToList(html) {
|
addReturnedHtmlElementsToList(html) {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
window.components.init(el);
|
window.$components.init(el);
|
||||||
for (const child of [...el.children]) {
|
for (const child of [...el.children]) {
|
||||||
this.listContainer.appendChild(child);
|
this.listContainer.appendChild(child);
|
||||||
}
|
}
|
||||||
|
@ -207,7 +207,7 @@ class ImageManager {
|
||||||
const params = requestDelete ? {delete: true} : {};
|
const params = requestDelete ? {delete: true} : {};
|
||||||
const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);
|
const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);
|
||||||
this.formContainer.innerHTML = formHtml;
|
this.formContainer.innerHTML = formHtml;
|
||||||
window.components.init(this.formContainer);
|
window.$components.init(this.formContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,282 +1,59 @@
|
||||||
import addRemoveRows from "./add-remove-rows.js"
|
export {AddRemoveRows} from "./add-remove-rows.js"
|
||||||
import ajaxDeleteRow from "./ajax-delete-row.js"
|
export {AjaxDeleteRow} from "./ajax-delete-row.js"
|
||||||
import ajaxForm from "./ajax-form.js"
|
export {AjaxForm} from "./ajax-form.js"
|
||||||
import attachments from "./attachments.js"
|
export {Attachments} from "./attachments.js"
|
||||||
import attachmentsList from "./attachments-list.js"
|
export {AttachmentsList} from "./attachments-list.js"
|
||||||
import autoSuggest from "./auto-suggest.js"
|
export {AutoSuggest} from "./auto-suggest.js"
|
||||||
import autoSubmit from "./auto-submit.js";
|
export {AutoSubmit} from "./auto-submit.js";
|
||||||
import backToTop from "./back-to-top.js"
|
export {BackToTop} from "./back-to-top.js"
|
||||||
import bookSort from "./book-sort.js"
|
// export {BookSort} from "./book-sort.js"
|
||||||
import chapterContents from "./chapter-contents.js"
|
// export {ChapterContents} from "./chapter-contents.js"
|
||||||
import codeEditor from "./code-editor.js"
|
// export {CodeEditor} from "./code-editor.js"
|
||||||
import codeHighlighter from "./code-highlighter.js"
|
// export {CodeHighlighter} from "./code-highlighter.js"
|
||||||
import codeTextarea from "./code-textarea.js"
|
// export {CodeTextarea} from "./code-textarea.js"
|
||||||
import collapsible from "./collapsible.js"
|
// export {Collapsible} from "./collapsible.js"
|
||||||
import confirmDialog from "./confirm-dialog"
|
// export {ConfirmDialog} from "./confirm-dialog"
|
||||||
import customCheckbox from "./custom-checkbox.js"
|
// export {CustomCheckbox} from "./custom-checkbox.js"
|
||||||
import detailsHighlighter from "./details-highlighter.js"
|
// export {DetailsHighlighter} from "./details-highlighter.js"
|
||||||
import dropdown from "./dropdown.js"
|
// export {Dropdown} from "./dropdown.js"
|
||||||
import dropdownSearch from "./dropdown-search.js"
|
// export {DropdownSearch} from "./dropdown-search.js"
|
||||||
import dropzone from "./dropzone.js"
|
// export {Dropzone} from "./dropzone.js"
|
||||||
import editorToolbox from "./editor-toolbox.js"
|
// export {EditorToolbox} from "./editor-toolbox.js"
|
||||||
import entityPermissions from "./entity-permissions";
|
// export {EntityPermissions} from "./entity-permissions";
|
||||||
import entitySearch from "./entity-search.js"
|
// export {EntitySearch} from "./entity-search.js"
|
||||||
import entitySelector from "./entity-selector.js"
|
// export {EntitySelector} from "./entity-selector.js"
|
||||||
import entitySelectorPopup from "./entity-selector-popup.js"
|
// export {EntitySelectorPopup} from "./entity-selector-popup.js"
|
||||||
import eventEmitSelect from "./event-emit-select.js"
|
// export {EventEmitSelect} from "./event-emit-select.js"
|
||||||
import expandToggle from "./expand-toggle.js"
|
// export {ExpandToggle} from "./expand-toggle.js"
|
||||||
import headerMobileToggle from "./header-mobile-toggle.js"
|
// export {HeaderMobileToggle} from "./header-mobile-toggle.js"
|
||||||
import homepageControl from "./homepage-control.js"
|
// export {HomepageControl} from "./homepage-control.js"
|
||||||
import imageManager from "./image-manager.js"
|
// export {ImageManager} from "./image-manager.js"
|
||||||
import imagePicker from "./image-picker.js"
|
// export {ImagePicker} from "./image-picker.js"
|
||||||
import listSortControl from "./list-sort-control.js"
|
// export {ListSortControl} from "./list-sort-control.js"
|
||||||
import markdownEditor from "./markdown-editor.js"
|
// export {MarkdownEditor} from "./markdown-editor.js"
|
||||||
import newUserPassword from "./new-user-password.js"
|
// export {NewUserPassword} from "./new-user-password.js"
|
||||||
import notification from "./notification.js"
|
// export {Notification} from "./notification.js"
|
||||||
import optionalInput from "./optional-input.js"
|
// export {OptionalInput} from "./optional-input.js"
|
||||||
import pageComments from "./page-comments.js"
|
// export {PageComments} from "./page-comments.js"
|
||||||
import pageDisplay from "./page-display.js"
|
// export {PageDisplay} from "./page-display.js"
|
||||||
import pageEditor from "./page-editor.js"
|
// export {PageEditor} from "./page-editor.js"
|
||||||
import pagePicker from "./page-picker.js"
|
// export {PagePicker} from "./page-picker.js"
|
||||||
import permissionsTable from "./permissions-table.js"
|
// export {PermissionsTable} from "./permissions-table.js"
|
||||||
import pointer from "./pointer.js";
|
// export {Pointer} from "./pointer.js";
|
||||||
import popup from "./popup.js"
|
// export {Popup} from "./popup.js"
|
||||||
import settingAppColorPicker from "./setting-app-color-picker.js"
|
// export {SettingAppColorPicker} from "./setting-app-color-picker.js"
|
||||||
import settingColorPicker from "./setting-color-picker.js"
|
// export {SettingColorPicker} from "./setting-color-picker.js"
|
||||||
import shelfSort from "./shelf-sort.js"
|
// export {ShelfSort} from "./shelf-sort.js"
|
||||||
import shortcuts from "./shortcuts";
|
// export {Shortcuts} from "./shortcuts";
|
||||||
import shortcutInput from "./shortcut-input";
|
// export {ShortcutInput} from "./shortcut-input";
|
||||||
import sidebar from "./sidebar.js"
|
// export {Sidebar} from "./sidebar.js"
|
||||||
import sortableList from "./sortable-list.js"
|
// export {SortableList} from "./sortable-list.js"
|
||||||
import submitOnChange from "./submit-on-change.js"
|
// export {SubmitOnChange} from "./submit-on-change.js"
|
||||||
import tabs from "./tabs.js"
|
// export {Tabs} from "./tabs.js"
|
||||||
import tagManager from "./tag-manager.js"
|
// export {TagManager} from "./tag-manager.js"
|
||||||
import templateManager from "./template-manager.js"
|
// export {TemplateManager} from "./template-manager.js"
|
||||||
import toggleSwitch from "./toggle-switch.js"
|
// export {ToggleSwitch} from "./toggle-switch.js"
|
||||||
import triLayout from "./tri-layout.js"
|
// export {TriLayout} from "./tri-layout.js"
|
||||||
import userSelect from "./user-select.js"
|
// export {UserSelect} from "./user-select.js"
|
||||||
import webhookEvents from "./webhook-events";
|
// export {WebhookEvents} from "./webhook-events";
|
||||||
import wysiwygEditor from "./wysiwyg-editor.js"
|
// export {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
|
|
||||||
*/
|
|
|
@ -90,7 +90,7 @@ class PageComments {
|
||||||
newComment.innerHTML = resp.data;
|
newComment.innerHTML = resp.data;
|
||||||
this.editingComment.innerHTML = newComment.children[0].innerHTML;
|
this.editingComment.innerHTML = newComment.children[0].innerHTML;
|
||||||
window.$events.success(this.updatedText);
|
window.$events.success(this.updatedText);
|
||||||
window.components.init(this.editingComment);
|
window.$components.init(this.editingComment);
|
||||||
this.closeUpdateForm();
|
this.closeUpdateForm();
|
||||||
this.editingComment = null;
|
this.editingComment = null;
|
||||||
}).catch(window.$events.showValidationErrors).then(() => {
|
}).catch(window.$events.showValidationErrors).then(() => {
|
||||||
|
@ -123,7 +123,7 @@ class PageComments {
|
||||||
newComment.innerHTML = resp.data;
|
newComment.innerHTML = resp.data;
|
||||||
let newElem = newComment.children[0];
|
let newElem = newComment.children[0];
|
||||||
this.container.appendChild(newElem);
|
this.container.appendChild(newElem);
|
||||||
window.components.init(newElem);
|
window.$components.init(newElem);
|
||||||
window.$events.success(this.createdText);
|
window.$events.success(this.createdText);
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
this.updateCount();
|
this.updateCount();
|
||||||
|
|
|
@ -22,7 +22,7 @@ class PageDisplay {
|
||||||
if (sidebarPageNav) {
|
if (sidebarPageNav) {
|
||||||
DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
|
DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
window.components['tri-layout'][0].showContent();
|
window.$components.first('tri-layout').showContent();
|
||||||
const contentId = child.getAttribute('href').substr(1);
|
const contentId = child.getAttribute('href').substr(1);
|
||||||
this.goToText(contentId);
|
this.goToText(contentId);
|
||||||
window.history.pushState(null, null, '#' + contentId);
|
window.history.pushState(null, null, '#' + contentId);
|
||||||
|
|
153
resources/js/services/components.js
Normal file
153
resources/js/services/components.js
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
const components = {};
|
||||||
|
const componentMap = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = componentMap[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 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 {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) {
|
||||||
|
componentMap[camelToKebab(key)] = mapping[key];
|
||||||
|
}
|
||||||
|
console.log(componentMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function camelToKebab(camelStr) {
|
||||||
|
return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
|
||||||
|
}
|
|
@ -128,6 +128,6 @@ export function removeLoading(element) {
|
||||||
export function htmlToDom(html) {
|
export function htmlToDom(html) {
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.innerHTML = html;
|
wrap.innerHTML = html;
|
||||||
window.components.init(wrap);
|
window.$components.init(wrap);
|
||||||
return wrap.children[0];
|
return wrap.children[0];
|
||||||
}
|
}
|
|
@ -9,7 +9,7 @@ function elemIsCodeBlock(elem) {
|
||||||
* @param {function(string, string)} callback (Receives (code: string,language: string)
|
* @param {function(string, string)} callback (Receives (code: string,language: string)
|
||||||
*/
|
*/
|
||||||
function showPopup(editor, code, language, callback) {
|
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)
|
callback(newCode, newLang)
|
||||||
editor.focus()
|
editor.focus()
|
||||||
});
|
});
|
||||||
|
|
|
@ -1010,4 +1010,41 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
border: 1px solid #b4b4b4;
|
border: 1px solid #b4b4b4;
|
||||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
|
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
|
||||||
color: #333;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -100,43 +100,6 @@ $loadingSize: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back to top link
|
|
||||||
$btt-size: 40px;
|
|
||||||
[back-to-top] {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
position: fixed;
|
|
||||||
bottom: $-m;
|
|
||||||
right: $-l;
|
|
||||||
padding: 5px 7px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #FFF;
|
|
||||||
fill: #FFF;
|
|
||||||
svg {
|
|
||||||
width: math.div($btt-size, 1.5);
|
|
||||||
height: math.div($btt-size, 1.5);
|
|
||||||
margin-inline-end: 4px;
|
|
||||||
}
|
|
||||||
width: $btt-size;
|
|
||||||
height: $btt-size;
|
|
||||||
border-radius: $btt-size;
|
|
||||||
transition: all ease-in-out 180ms;
|
|
||||||
opacity: 0;
|
|
||||||
z-index: 999;
|
|
||||||
overflow: hidden;
|
|
||||||
&:hover {
|
|
||||||
width: $btt-size*3.4;
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
.inner {
|
|
||||||
width: $btt-size*3.4;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
position: relative;
|
|
||||||
vertical-align: top;
|
|
||||||
line-height: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-to-content-link {
|
.skip-to-content-link {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: -52px;
|
top: -52px;
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
@include('common.footer')
|
@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">
|
<div class="inner">
|
||||||
@icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
|
@icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue