mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Merge pull request #3853 from BookStackApp/component_refactor
Started refactor and alignment of JS component system
This commit is contained in:
commit
bbf13e9242
@ -24,7 +24,7 @@ class Dropdown {
|
||||
|
||||
All usage of $refs, $manyRefs and $opts should be done at the top of the `setup` function so any requirements can be easily seen.
|
||||
|
||||
Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file. You'll need to import the component class then add it to `componentMapping` object, following the pattern of other components.
|
||||
Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file by defining an additional export, following the pattern of other components.
|
||||
|
||||
### Using a Component in HTML
|
||||
|
||||
@ -80,9 +80,9 @@ Will result with `this.$opts` being:
|
||||
}
|
||||
```
|
||||
|
||||
#### Component Properties
|
||||
#### Component Properties & Methods
|
||||
|
||||
A component has the below shown properties available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.
|
||||
A component has the below shown properties & methods available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.
|
||||
|
||||
```javascript
|
||||
// The root element that the compontent has been applied to.
|
||||
@ -98,6 +98,15 @@ this.$manyRefs
|
||||
|
||||
// Options defined for the compontent.
|
||||
this.$opts
|
||||
|
||||
// The registered name of the component, usually kebab-case.
|
||||
this.$name
|
||||
|
||||
// Emit a custom event from this component.
|
||||
// Will be bubbled up from the dom element this is registered on,
|
||||
// as a custom event with the name `<elementName>-<eventName>`,
|
||||
// with the provided data in the event detail.
|
||||
this.$emit(eventName, data = {})
|
||||
```
|
||||
|
||||
## Global JavaScript Helpers
|
||||
@ -132,7 +141,16 @@ window.trans_plural(translationString, count, replacements);
|
||||
|
||||
// Component System
|
||||
// Parse and initialise any components from the given root el down.
|
||||
window.components.init(rootEl);
|
||||
// Get the first active component of the given name
|
||||
window.components.first(name);
|
||||
window.$components.init(rootEl);
|
||||
// Register component models to be used by the component system.
|
||||
// Takes a mapping of classes/constructors keyed by component names.
|
||||
// Names will be converted to kebab-case.
|
||||
window.$components.register(mapping);
|
||||
// Get the first active component of the given name.
|
||||
window.$components.first(name);
|
||||
// Get all the active components of the given name.
|
||||
window.$components.get(name);
|
||||
// Get the first active component of the given name that's been
|
||||
// created on the given element.
|
||||
window.$components.firstOnElement(element, name);
|
||||
```
|
@ -27,5 +27,8 @@ window.trans_choice = translator.getPlural.bind(translator);
|
||||
window.trans_plural = translator.parsePlural.bind(translator);
|
||||
|
||||
// Load Components
|
||||
import components from "./components"
|
||||
components();
|
||||
import * as components from "./services/components"
|
||||
import * as componentMap from "./components";
|
||||
components.register(componentMap);
|
||||
window.$components = components;
|
||||
components.init();
|
||||
|
@ -1,13 +1,13 @@
|
||||
import {onChildEvent} from "../services/dom";
|
||||
import {uniqueId} from "../services/util";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* AddRemoveRows
|
||||
* Allows easy row add/remove controls onto a table.
|
||||
* Needs a model row to use when adding a new row.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class AddRemoveRows {
|
||||
export class AddRemoveRows extends Component {
|
||||
setup() {
|
||||
this.modelRow = this.$refs.model;
|
||||
this.addButton = this.$refs.add;
|
||||
@ -31,7 +31,7 @@ class AddRemoveRows {
|
||||
clone.classList.remove('hidden');
|
||||
this.setClonedInputNames(clone);
|
||||
this.modelRow.parentNode.insertBefore(clone, this.modelRow);
|
||||
window.components.init(clone);
|
||||
window.$components.init(clone);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,6 +49,4 @@ class AddRemoveRows {
|
||||
elem.name = elem.name.split('randrowid').join(rowId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AddRemoveRows;
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
/**
|
||||
* AjaxDelete
|
||||
* @extends {Component}
|
||||
*/
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
class AjaxDeleteRow {
|
||||
export class AjaxDeleteRow extends Component {
|
||||
setup() {
|
||||
this.row = this.$el;
|
||||
this.url = this.$opts.url;
|
||||
@ -27,6 +24,4 @@ class AjaxDeleteRow {
|
||||
this.row.style.pointerEvents = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AjaxDeleteRow;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import {onEnterPress, onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Ajax Form
|
||||
@ -8,10 +9,8 @@ import {onEnterPress, onSelect} from "../services/dom";
|
||||
*
|
||||
* Will handle a real form if that's what the component is added to
|
||||
* otherwise will act as a fake form element.
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class AjaxForm {
|
||||
export class AjaxForm extends Component {
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.responseContainer = this.container;
|
||||
@ -72,11 +71,9 @@ class AjaxForm {
|
||||
this.responseContainer.innerHTML = err.data;
|
||||
}
|
||||
|
||||
window.components.init(this.responseContainer);
|
||||
window.$components.init(this.responseContainer);
|
||||
this.responseContainer.style.opacity = null;
|
||||
this.responseContainer.style.pointerEvents = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AjaxForm;
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Attachments List
|
||||
* Adds '?open=true' query to file attachment links
|
||||
* when ctrl/cmd is pressed down.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class AttachmentsList {
|
||||
export class AttachmentsList extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -42,6 +43,4 @@ class AttachmentsList {
|
||||
link.removeAttribute('target');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AttachmentsList;
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
/**
|
||||
* Attachments
|
||||
* @extends {Component}
|
||||
*/
|
||||
import {showLoading} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
class Attachments {
|
||||
export class Attachments extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -46,10 +43,12 @@ class Attachments {
|
||||
|
||||
reloadList() {
|
||||
this.stopEdit();
|
||||
this.mainTabs.components.tabs.show('items');
|
||||
/** @var {Tabs} */
|
||||
const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
|
||||
tabs.show('items');
|
||||
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
|
||||
this.list.innerHTML = resp.data;
|
||||
window.components.init(this.list);
|
||||
window.$components.init(this.list);
|
||||
});
|
||||
}
|
||||
|
||||
@ -66,7 +65,7 @@ class Attachments {
|
||||
showLoading(this.editContainer);
|
||||
const resp = await window.$http.get(`/attachments/edit/${id}`);
|
||||
this.editContainer.innerHTML = resp.data;
|
||||
window.components.init(this.editContainer);
|
||||
window.$components.init(this.editContainer);
|
||||
}
|
||||
|
||||
stopEdit() {
|
||||
@ -74,6 +73,4 @@ class Attachments {
|
||||
this.listContainer.classList.remove('hidden');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Attachments;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class AutoSubmit {
|
||||
export class AutoSubmit extends Component {
|
||||
|
||||
setup() {
|
||||
this.form = this.$el;
|
||||
@ -7,6 +8,4 @@ class AutoSubmit {
|
||||
this.form.submit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AutoSubmit;
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
import {escapeHtml} from "../services/util";
|
||||
import {onChildEvent} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
const ajaxCache = {};
|
||||
|
||||
/**
|
||||
* AutoSuggest
|
||||
* @extends {Component}
|
||||
*/
|
||||
class AutoSuggest {
|
||||
export class AutoSuggest extends Component {
|
||||
setup() {
|
||||
this.parent = this.$el.parentElement;
|
||||
this.container = this.$el;
|
||||
@ -148,6 +148,4 @@ class AutoSuggest {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AutoSuggest;
|
||||
}
|
@ -1,34 +1,35 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class BackToTop {
|
||||
export class BackToTop extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
setup() {
|
||||
this.button = this.$el;
|
||||
this.targetElem = document.getElementById('header');
|
||||
this.showing = false;
|
||||
this.breakPoint = 1200;
|
||||
|
||||
if (document.body.classList.contains('flexbox')) {
|
||||
this.elem.style.display = 'none';
|
||||
this.button.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
this.elem.addEventListener('click', this.scrollToTop.bind(this));
|
||||
this.button.addEventListener('click', this.scrollToTop.bind(this));
|
||||
window.addEventListener('scroll', this.onPageScroll.bind(this));
|
||||
}
|
||||
|
||||
onPageScroll() {
|
||||
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
if (!this.showing && scrollTopPos > this.breakPoint) {
|
||||
this.elem.style.display = 'block';
|
||||
this.button.style.display = 'block';
|
||||
this.showing = true;
|
||||
setTimeout(() => {
|
||||
this.elem.style.opacity = 0.4;
|
||||
this.button.style.opacity = 0.4;
|
||||
}, 1);
|
||||
} else if (this.showing && scrollTopPos < this.breakPoint) {
|
||||
this.elem.style.opacity = 0;
|
||||
this.button.style.opacity = 0;
|
||||
this.showing = false;
|
||||
setTimeout(() => {
|
||||
this.elem.style.display = 'none';
|
||||
this.button.style.display = 'none';
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@ -54,6 +55,4 @@ class BackToTop {
|
||||
requestAnimationFrame(setPos.bind(this));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BackToTop;
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import Sortable from "sortablejs";
|
||||
import {Component} from "./component";
|
||||
import {htmlToDom} from "../services/dom";
|
||||
|
||||
// Auto sort control
|
||||
const sortOperations = {
|
||||
@ -35,14 +37,14 @@ const sortOperations = {
|
||||
},
|
||||
};
|
||||
|
||||
class BookSort {
|
||||
export class BookSort extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.sortContainer = elem.querySelector('[book-sort-boxes]');
|
||||
this.input = elem.querySelector('[book-sort-input]');
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.sortContainer = this.$refs.sortContainer;
|
||||
this.input = this.$refs.input;
|
||||
|
||||
const initialSortBox = elem.querySelector('.sort-box');
|
||||
const initialSortBox = this.container.querySelector('.sort-box');
|
||||
this.setupBookSortable(initialSortBox);
|
||||
this.setupSortPresets();
|
||||
|
||||
@ -90,14 +92,12 @@ class BookSort {
|
||||
* @param {Object} entityInfo
|
||||
*/
|
||||
bookSelect(entityInfo) {
|
||||
const alreadyAdded = this.elem.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
|
||||
const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
|
||||
if (alreadyAdded) return;
|
||||
|
||||
const entitySortItemUrl = entityInfo.link + '/sort-item';
|
||||
window.$http.get(entitySortItemUrl).then(resp => {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.innerHTML = resp.data;
|
||||
const newBookContainer = wrap.children[0];
|
||||
const newBookContainer = htmlToDom(resp.data);
|
||||
this.sortContainer.append(newBookContainer);
|
||||
this.setupBookSortable(newBookContainer);
|
||||
});
|
||||
@ -155,7 +155,7 @@ class BookSort {
|
||||
*/
|
||||
buildEntityMap() {
|
||||
const entityMap = [];
|
||||
const lists = this.elem.querySelectorAll('.sort-list');
|
||||
const lists = this.container.querySelectorAll('.sort-list');
|
||||
|
||||
for (let list of lists) {
|
||||
const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
|
||||
@ -202,6 +202,4 @@ class BookSort {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BookSort;
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
import {slideUp, slideDown} from "../services/animations";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* @extends {Component}
|
||||
*/
|
||||
class ChapterContents {
|
||||
export class ChapterContents extends Component {
|
||||
|
||||
setup() {
|
||||
this.list = this.$refs.list;
|
||||
@ -31,7 +29,4 @@ class ChapterContents {
|
||||
event.preventDefault();
|
||||
this.isOpen ? this.close() : this.open();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ChapterContents;
|
||||
|
@ -1,10 +1,8 @@
|
||||
import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Code Editor
|
||||
* @extends {Component}
|
||||
*/
|
||||
class CodeEditor {
|
||||
|
||||
export class CodeEditor extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$refs.container;
|
||||
@ -128,7 +126,7 @@ class CodeEditor {
|
||||
}
|
||||
|
||||
this.loadHistory();
|
||||
this.popup.components.popup.show(() => {
|
||||
this.getPopup().show(() => {
|
||||
Code.updateLayout(this.editor);
|
||||
this.editor.focus();
|
||||
}, () => {
|
||||
@ -137,10 +135,17 @@ class CodeEditor {
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.popup.components.popup.hide();
|
||||
this.getPopup().hide();
|
||||
this.addHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Popup}
|
||||
*/
|
||||
getPopup() {
|
||||
return window.$components.firstOnElement(this.popup, 'popup');
|
||||
}
|
||||
|
||||
async updateEditorMode(language) {
|
||||
const Code = await window.importVersioned('code');
|
||||
Code.setMode(this.editor, language, this.editor.getValue());
|
||||
@ -184,6 +189,4 @@ class CodeEditor {
|
||||
window.sessionStorage.setItem(this.historyKey, historyString);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CodeEditor;
|
||||
}
|
@ -1,14 +1,16 @@
|
||||
class CodeHighlighter {
|
||||
import {Component} from "./component";
|
||||
|
||||
constructor(elem) {
|
||||
const codeBlocks = elem.querySelectorAll('pre');
|
||||
export class CodeHighlighter extends Component{
|
||||
|
||||
setup() {
|
||||
const container = this.$el;
|
||||
|
||||
const codeBlocks = container.querySelectorAll('pre');
|
||||
if (codeBlocks.length > 0) {
|
||||
window.importVersioned('code').then(Code => {
|
||||
Code.highlightWithin(elem);
|
||||
Code.highlightWithin(container);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CodeHighlighter;
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
/**
|
||||
* A simple component to render a code editor within the textarea
|
||||
* this exists upon.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class CodeTextarea {
|
||||
import {Component} from "./component";
|
||||
|
||||
export class CodeTextarea extends Component {
|
||||
|
||||
async setup() {
|
||||
const mode = this.$opts.mode;
|
||||
@ -11,6 +12,4 @@ class CodeTextarea {
|
||||
Code.inlineEditor(this.$el, mode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CodeTextarea;
|
||||
}
|
@ -1,35 +1,37 @@
|
||||
import {slideDown, slideUp} from "../services/animations";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Collapsible
|
||||
* Provides some simple logic to allow collapsible sections.
|
||||
*/
|
||||
class Collapsible {
|
||||
export class Collapsible extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.trigger = elem.querySelector('[collapsible-trigger]');
|
||||
this.content = elem.querySelector('[collapsible-content]');
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.trigger = this.$refs.trigger;
|
||||
this.content = this.$refs.content;
|
||||
|
||||
if (!this.trigger) return;
|
||||
this.trigger.addEventListener('click', this.toggle.bind(this));
|
||||
this.openIfContainsError();
|
||||
if (this.trigger) {
|
||||
this.trigger.addEventListener('click', this.toggle.bind(this));
|
||||
this.openIfContainsError();
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
this.elem.classList.add('open');
|
||||
this.container.classList.add('open');
|
||||
this.trigger.setAttribute('aria-expanded', 'true');
|
||||
slideDown(this.content, 300);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.elem.classList.remove('open');
|
||||
this.container.classList.remove('open');
|
||||
this.trigger.setAttribute('aria-expanded', 'false');
|
||||
slideUp(this.content, 300);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.elem.classList.contains('open')) {
|
||||
if (this.container.classList.contains('open')) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
@ -43,6 +45,4 @@ class Collapsible {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Collapsible;
|
||||
}
|
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);
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Custom equivalent of window.confirm() using our popup component.
|
||||
* Is promise based so can be used like so:
|
||||
* `const result = await dialog.show()`
|
||||
* @extends {Component}
|
||||
*/
|
||||
class ConfirmDialog {
|
||||
export class ConfirmDialog extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -34,7 +34,7 @@ class ConfirmDialog {
|
||||
* @returns {Popup}
|
||||
*/
|
||||
getPopup() {
|
||||
return this.container.components.popup;
|
||||
return window.$components.firstOnElement(this.container, 'popup');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,6 +47,4 @@ class ConfirmDialog {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ConfirmDialog;
|
||||
}
|
@ -1,18 +1,19 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class CustomCheckbox {
|
||||
export class CustomCheckbox extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.checkbox = elem.querySelector('input[type=checkbox]');
|
||||
this.display = elem.querySelector('[role="checkbox"]');
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.checkbox = this.container.querySelector('input[type=checkbox]');
|
||||
this.display = this.container.querySelector('[role="checkbox"]');
|
||||
|
||||
this.checkbox.addEventListener('change', this.stateChange.bind(this));
|
||||
this.elem.addEventListener('keydown', this.onKeyDown.bind(this));
|
||||
this.container.addEventListener('keydown', this.onKeyDown.bind(this));
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
const isEnterOrPress = event.keyCode === 32 || event.keyCode === 13;
|
||||
if (isEnterOrPress) {
|
||||
const isEnterOrSpace = event.key === ' ' || event.key === 'Enter';
|
||||
if (isEnterOrSpace) {
|
||||
event.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
@ -29,6 +30,4 @@ class CustomCheckbox {
|
||||
this.display.setAttribute('aria-checked', checked);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CustomCheckbox;
|
||||
}
|
@ -1,21 +1,22 @@
|
||||
class DetailsHighlighter {
|
||||
import {Component} from "./component";
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
export class DetailsHighlighter extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.dealtWith = false;
|
||||
elem.addEventListener('toggle', this.onToggle.bind(this));
|
||||
|
||||
this.container.addEventListener('toggle', this.onToggle.bind(this));
|
||||
}
|
||||
|
||||
onToggle() {
|
||||
if (this.dealtWith) return;
|
||||
|
||||
if (this.elem.querySelector('pre')) {
|
||||
if (this.container.querySelector('pre')) {
|
||||
window.importVersioned('code').then(Code => {
|
||||
Code.highlightWithin(this.elem);
|
||||
Code.highlightWithin(this.container);
|
||||
});
|
||||
}
|
||||
this.dealtWith = true;
|
||||
}
|
||||
}
|
||||
|
||||
export default DetailsHighlighter;
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import {debounce} from "../services/util";
|
||||
import {transitionHeight} from "../services/animations";
|
||||
import {Component} from "./component";
|
||||
|
||||
class DropdownSearch {
|
||||
export class DropdownSearch extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
@ -78,6 +79,4 @@ class DropdownSearch {
|
||||
this.loadingElem.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DropdownSearch;
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Dropdown
|
||||
* Provides some simple logic to create simple dropdown menus.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class DropDown {
|
||||
export class Dropdown extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -74,7 +74,7 @@ class DropDown {
|
||||
}
|
||||
|
||||
hideAll() {
|
||||
for (let dropdown of window.components.dropdown) {
|
||||
for (let dropdown of window.$components.get('dropdown')) {
|
||||
dropdown.hide();
|
||||
}
|
||||
}
|
||||
@ -171,6 +171,4 @@ class DropDown {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DropDown;
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
import DropZoneLib from "dropzone";
|
||||
import {fadeOut} from "../services/animations";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Dropzone
|
||||
* @extends {Component}
|
||||
*/
|
||||
class Dropzone {
|
||||
export class Dropzone extends Component {
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.url = this.$opts.url;
|
||||
@ -73,6 +70,4 @@ class Dropzone {
|
||||
removeAll() {
|
||||
this.dz.removeAllFiles(true);
|
||||
}
|
||||
}
|
||||
|
||||
export default Dropzone;
|
||||
}
|
@ -1,51 +1,58 @@
|
||||
class EditorToolbox {
|
||||
import {Component} from "./component";
|
||||
|
||||
constructor(elem) {
|
||||
export class EditorToolbox extends Component {
|
||||
|
||||
setup() {
|
||||
// Elements
|
||||
this.elem = elem;
|
||||
this.buttons = elem.querySelectorAll('[toolbox-tab-button]');
|
||||
this.contentElements = elem.querySelectorAll('[toolbox-tab-content]');
|
||||
this.toggleButton = elem.querySelector('[toolbox-toggle]');
|
||||
this.container = this.$el;
|
||||
this.buttons = this.$manyRefs.tabButton;
|
||||
this.contentElements = this.$manyRefs.tabContent;
|
||||
this.toggleButton = this.$refs.toggle;
|
||||
|
||||
// Toolbox toggle button click
|
||||
this.toggleButton.addEventListener('click', this.toggle.bind(this));
|
||||
// Tab button click
|
||||
this.elem.addEventListener('click', event => {
|
||||
let button = event.target.closest('[toolbox-tab-button]');
|
||||
if (button === null) return;
|
||||
let name = button.getAttribute('toolbox-tab-button');
|
||||
this.setActiveTab(name, true);
|
||||
});
|
||||
this.setupListeners();
|
||||
|
||||
// Set the first tab as active on load
|
||||
this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content'));
|
||||
this.setActiveTab(this.contentElements[0].dataset.tabContent);
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
// Toolbox toggle button click
|
||||
this.toggleButton.addEventListener('click', () => this.toggle());
|
||||
// Tab button click
|
||||
this.container.addEventListener('click', event => {
|
||||
const button = event.target.closest('button');
|
||||
if (this.buttons.includes(button)) {
|
||||
const name = button.dataset.tab;
|
||||
this.setActiveTab(name, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.elem.classList.toggle('open');
|
||||
const expanded = this.elem.classList.contains('open') ? 'true' : 'false';
|
||||
this.container.classList.toggle('open');
|
||||
const expanded = this.container.classList.contains('open') ? 'true' : 'false';
|
||||
this.toggleButton.setAttribute('aria-expanded', expanded);
|
||||
}
|
||||
|
||||
setActiveTab(tabName, openToolbox = false) {
|
||||
|
||||
// Set button visibility
|
||||
for (let i = 0, len = this.buttons.length; i < len; i++) {
|
||||
this.buttons[i].classList.remove('active');
|
||||
let bName = this.buttons[i].getAttribute('toolbox-tab-button');
|
||||
if (bName === tabName) this.buttons[i].classList.add('active');
|
||||
}
|
||||
// Set content visibility
|
||||
for (let i = 0, len = this.contentElements.length; i < len; i++) {
|
||||
this.contentElements[i].style.display = 'none';
|
||||
let cName = this.contentElements[i].getAttribute('toolbox-tab-content');
|
||||
if (cName === tabName) this.contentElements[i].style.display = 'block';
|
||||
for (const button of this.buttons) {
|
||||
button.classList.remove('active');
|
||||
const bName = button.dataset.tab;
|
||||
if (bName === tabName) button.classList.add('active');
|
||||
}
|
||||
|
||||
if (openToolbox && !this.elem.classList.contains('open')) {
|
||||
// Set content visibility
|
||||
for (const contentEl of this.contentElements) {
|
||||
contentEl.style.display = 'none';
|
||||
const cName = contentEl.dataset.tabContent;
|
||||
if (cName === tabName) contentEl.style.display = 'block';
|
||||
}
|
||||
|
||||
if (openToolbox && !this.container.classList.contains('open')) {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EditorToolbox;
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
/**
|
||||
* @extends {Component}
|
||||
*/
|
||||
import {htmlToDom} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
class EntityPermissions {
|
||||
export class EntityPermissions extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -74,6 +72,4 @@ class EntityPermissions {
|
||||
row.remove();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EntityPermissions;
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Class EntitySearch
|
||||
* @extends {Component}
|
||||
*/
|
||||
class EntitySearch {
|
||||
export class EntitySearch extends Component {
|
||||
setup() {
|
||||
this.entityId = this.$opts.entityId;
|
||||
this.entityType = this.$opts.entityType;
|
||||
@ -54,6 +51,4 @@ class EntitySearch {
|
||||
this.loadingBlock.classList.add('hidden');
|
||||
this.searchInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
export default EntitySearch;
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
/**
|
||||
* Entity Selector Popup
|
||||
* @extends {Component}
|
||||
*/
|
||||
class EntitySelectorPopup {
|
||||
import {Component} from "./component";
|
||||
|
||||
export class EntitySelectorPopup extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
this.container = this.$el;
|
||||
this.selectButton = this.$refs.select;
|
||||
|
||||
window.EntitySelectorPopup = this;
|
||||
this.selectorEl = this.$refs.selector;
|
||||
|
||||
this.callback = null;
|
||||
@ -21,16 +17,26 @@ class EntitySelectorPopup {
|
||||
|
||||
show(callback) {
|
||||
this.callback = callback;
|
||||
this.elem.components.popup.show();
|
||||
this.getPopup().show();
|
||||
this.getSelector().focusSearch();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.elem.components.popup.hide();
|
||||
this.getPopup().hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Popup}
|
||||
*/
|
||||
getPopup() {
|
||||
return window.$components.firstOnElement(this.container, 'popup');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {EntitySelector}
|
||||
*/
|
||||
getSelector() {
|
||||
return this.selectorEl.components['entity-selector'];
|
||||
return window.$components.firstOnElement(this.selectorEl, 'entity-selector');
|
||||
}
|
||||
|
||||
onSelectButtonClick() {
|
||||
@ -51,6 +57,4 @@ class EntitySelectorPopup {
|
||||
this.getSelector().reset();
|
||||
if (this.callback && entity) this.callback(entity);
|
||||
}
|
||||
}
|
||||
|
||||
export default EntitySelectorPopup;
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import {onChildEvent} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Entity Selector
|
||||
* @extends {Component}
|
||||
*/
|
||||
class EntitySelector {
|
||||
export class EntitySelector extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
@ -185,6 +185,4 @@ class EntitySelector {
|
||||
this.selectedItemData = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EntitySelector;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* EventEmitSelect
|
||||
@ -10,10 +11,8 @@ import {onSelect} from "../services/dom";
|
||||
*
|
||||
* All options will be set as the "detail" of the event with
|
||||
* their values included.
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class EventEmitSelect {
|
||||
export class EventEmitSelect extends Component{
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.name = this.$opts.name;
|
||||
@ -24,6 +23,4 @@ class EventEmitSelect {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EventEmitSelect;
|
||||
}
|
@ -1,17 +1,15 @@
|
||||
import {slideUp, slideDown} from "../services/animations";
|
||||
import {Component} from "./component";
|
||||
|
||||
class ExpandToggle {
|
||||
export class ExpandToggle extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
|
||||
// Component state
|
||||
this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
|
||||
this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
|
||||
this.selector = elem.getAttribute('expand-toggle');
|
||||
setup(elem) {
|
||||
this.targetSelector = this.$opts.targetSelector;
|
||||
this.isOpen = this.$opts.isOpen === 'true';
|
||||
this.updateEndpoint = this.$opts.updateEndpoint;
|
||||
|
||||
// Listener setup
|
||||
elem.addEventListener('click', this.click.bind(this));
|
||||
this.$el.addEventListener('click', this.click.bind(this));
|
||||
}
|
||||
|
||||
open(elemToToggle) {
|
||||
@ -25,7 +23,7 @@ class ExpandToggle {
|
||||
click(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const matchingElems = document.querySelectorAll(this.selector);
|
||||
const matchingElems = document.querySelectorAll(this.targetSelector);
|
||||
for (let match of matchingElems) {
|
||||
this.isOpen ? this.close(match) : this.open(match);
|
||||
}
|
||||
@ -40,6 +38,4 @@ class ExpandToggle {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ExpandToggle;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class HeaderMobileToggle {
|
||||
export class HeaderMobileToggle extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
@ -36,6 +37,4 @@ class HeaderMobileToggle {
|
||||
this.onToggle(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HeaderMobileToggle;
|
||||
}
|
@ -1,13 +1,9 @@
|
||||
import {onChildEvent, onSelect, removeLoading, showLoading} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* ImageManager
|
||||
* @extends {Component}
|
||||
*/
|
||||
class ImageManager {
|
||||
export class ImageManager extends Component {
|
||||
|
||||
setup() {
|
||||
|
||||
// Options
|
||||
this.uploadedTo = this.$opts.uploadedTo;
|
||||
|
||||
@ -36,8 +32,6 @@ class ImageManager {
|
||||
this.resetState();
|
||||
|
||||
this.setupListeners();
|
||||
|
||||
window.ImageManager = this;
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
@ -100,7 +94,7 @@ class ImageManager {
|
||||
|
||||
this.callback = callback;
|
||||
this.type = type;
|
||||
this.popupEl.components.popup.show();
|
||||
this.getPopup().show();
|
||||
this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery');
|
||||
|
||||
if (!this.hasData) {
|
||||
@ -110,7 +104,14 @@ class ImageManager {
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.popupEl.components.popup.hide();
|
||||
this.getPopup().hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Popup}
|
||||
*/
|
||||
getPopup() {
|
||||
return window.$components.firstOnElement(this.popupEl, 'popup');
|
||||
}
|
||||
|
||||
async loadGallery() {
|
||||
@ -132,7 +133,7 @@ class ImageManager {
|
||||
addReturnedHtmlElementsToList(html) {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = html;
|
||||
window.components.init(el);
|
||||
window.$components.init(el);
|
||||
for (const child of [...el.children]) {
|
||||
this.listContainer.appendChild(child);
|
||||
}
|
||||
@ -207,9 +208,7 @@ class ImageManager {
|
||||
const params = requestDelete ? {delete: true} : {};
|
||||
const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);
|
||||
this.formContainer.innerHTML = formHtml;
|
||||
window.components.init(this.formContainer);
|
||||
window.$components.init(this.formContainer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ImageManager;
|
||||
}
|
@ -1,21 +1,25 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class ImagePicker {
|
||||
export class ImagePicker extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.imageElem = elem.querySelector('img');
|
||||
this.imageInput = elem.querySelector('input[type=file]');
|
||||
this.resetInput = elem.querySelector('input[data-reset-input]');
|
||||
this.removeInput = elem.querySelector('input[data-remove-input]');
|
||||
setup() {
|
||||
this.imageElem = this.$refs.image;
|
||||
this.imageInput = this.$refs.imageInput;
|
||||
this.resetInput = this.$refs.resetInput;
|
||||
this.removeInput = this.$refs.removeInput;
|
||||
this.resetButton = this.$refs.resetButton;
|
||||
this.removeButton = this.$refs.removeButton || null;
|
||||
|
||||
this.defaultImage = elem.getAttribute('data-default-image');
|
||||
this.defaultImage = this.$opts.defaultImage;
|
||||
|
||||
const resetButton = elem.querySelector('button[data-action="reset-image"]');
|
||||
resetButton.addEventListener('click', this.reset.bind(this));
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
const removeButton = elem.querySelector('button[data-action="remove-image"]');
|
||||
if (removeButton) {
|
||||
removeButton.addEventListener('click', this.removeImage.bind(this));
|
||||
setupListeners() {
|
||||
this.resetButton.addEventListener('click', this.reset.bind(this));
|
||||
|
||||
if (this.removeButton) {
|
||||
this.removeButton.addEventListener('click', this.removeImage.bind(this));
|
||||
}
|
||||
|
||||
this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
|
||||
@ -50,6 +54,4 @@ class ImagePicker {
|
||||
this.resetInput.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ImagePicker;
|
||||
}
|
@ -1,282 +1,58 @@
|
||||
import addRemoveRows from "./add-remove-rows.js"
|
||||
import ajaxDeleteRow from "./ajax-delete-row.js"
|
||||
import ajaxForm from "./ajax-form.js"
|
||||
import attachments from "./attachments.js"
|
||||
import attachmentsList from "./attachments-list.js"
|
||||
import autoSuggest from "./auto-suggest.js"
|
||||
import autoSubmit from "./auto-submit.js";
|
||||
import backToTop from "./back-to-top.js"
|
||||
import bookSort from "./book-sort.js"
|
||||
import chapterContents from "./chapter-contents.js"
|
||||
import codeEditor from "./code-editor.js"
|
||||
import codeHighlighter from "./code-highlighter.js"
|
||||
import codeTextarea from "./code-textarea.js"
|
||||
import collapsible from "./collapsible.js"
|
||||
import confirmDialog from "./confirm-dialog"
|
||||
import customCheckbox from "./custom-checkbox.js"
|
||||
import detailsHighlighter from "./details-highlighter.js"
|
||||
import dropdown from "./dropdown.js"
|
||||
import dropdownSearch from "./dropdown-search.js"
|
||||
import dropzone from "./dropzone.js"
|
||||
import editorToolbox from "./editor-toolbox.js"
|
||||
import entityPermissions from "./entity-permissions";
|
||||
import entitySearch from "./entity-search.js"
|
||||
import entitySelector from "./entity-selector.js"
|
||||
import entitySelectorPopup from "./entity-selector-popup.js"
|
||||
import eventEmitSelect from "./event-emit-select.js"
|
||||
import expandToggle from "./expand-toggle.js"
|
||||
import headerMobileToggle from "./header-mobile-toggle.js"
|
||||
import homepageControl from "./homepage-control.js"
|
||||
import imageManager from "./image-manager.js"
|
||||
import imagePicker from "./image-picker.js"
|
||||
import listSortControl from "./list-sort-control.js"
|
||||
import markdownEditor from "./markdown-editor.js"
|
||||
import newUserPassword from "./new-user-password.js"
|
||||
import notification from "./notification.js"
|
||||
import optionalInput from "./optional-input.js"
|
||||
import pageComments from "./page-comments.js"
|
||||
import pageDisplay from "./page-display.js"
|
||||
import pageEditor from "./page-editor.js"
|
||||
import pagePicker from "./page-picker.js"
|
||||
import permissionsTable from "./permissions-table.js"
|
||||
import pointer from "./pointer.js";
|
||||
import popup from "./popup.js"
|
||||
import settingAppColorPicker from "./setting-app-color-picker.js"
|
||||
import settingColorPicker from "./setting-color-picker.js"
|
||||
import shelfSort from "./shelf-sort.js"
|
||||
import shortcuts from "./shortcuts";
|
||||
import shortcutInput from "./shortcut-input";
|
||||
import sidebar from "./sidebar.js"
|
||||
import sortableList from "./sortable-list.js"
|
||||
import submitOnChange from "./submit-on-change.js"
|
||||
import tabs from "./tabs.js"
|
||||
import tagManager from "./tag-manager.js"
|
||||
import templateManager from "./template-manager.js"
|
||||
import toggleSwitch from "./toggle-switch.js"
|
||||
import triLayout from "./tri-layout.js"
|
||||
import userSelect from "./user-select.js"
|
||||
import webhookEvents from "./webhook-events";
|
||||
import wysiwygEditor from "./wysiwyg-editor.js"
|
||||
|
||||
const componentMapping = {
|
||||
"add-remove-rows": addRemoveRows,
|
||||
"ajax-delete-row": ajaxDeleteRow,
|
||||
"ajax-form": ajaxForm,
|
||||
"attachments": attachments,
|
||||
"attachments-list": attachmentsList,
|
||||
"auto-suggest": autoSuggest,
|
||||
"auto-submit": autoSubmit,
|
||||
"back-to-top": backToTop,
|
||||
"book-sort": bookSort,
|
||||
"chapter-contents": chapterContents,
|
||||
"code-editor": codeEditor,
|
||||
"code-highlighter": codeHighlighter,
|
||||
"code-textarea": codeTextarea,
|
||||
"collapsible": collapsible,
|
||||
"confirm-dialog": confirmDialog,
|
||||
"custom-checkbox": customCheckbox,
|
||||
"details-highlighter": detailsHighlighter,
|
||||
"dropdown": dropdown,
|
||||
"dropdown-search": dropdownSearch,
|
||||
"dropzone": dropzone,
|
||||
"editor-toolbox": editorToolbox,
|
||||
"entity-permissions": entityPermissions,
|
||||
"entity-search": entitySearch,
|
||||
"entity-selector": entitySelector,
|
||||
"entity-selector-popup": entitySelectorPopup,
|
||||
"event-emit-select": eventEmitSelect,
|
||||
"expand-toggle": expandToggle,
|
||||
"header-mobile-toggle": headerMobileToggle,
|
||||
"homepage-control": homepageControl,
|
||||
"image-manager": imageManager,
|
||||
"image-picker": imagePicker,
|
||||
"list-sort-control": listSortControl,
|
||||
"markdown-editor": markdownEditor,
|
||||
"new-user-password": newUserPassword,
|
||||
"notification": notification,
|
||||
"optional-input": optionalInput,
|
||||
"page-comments": pageComments,
|
||||
"page-display": pageDisplay,
|
||||
"page-editor": pageEditor,
|
||||
"page-picker": pagePicker,
|
||||
"permissions-table": permissionsTable,
|
||||
"pointer": pointer,
|
||||
"popup": popup,
|
||||
"setting-app-color-picker": settingAppColorPicker,
|
||||
"setting-color-picker": settingColorPicker,
|
||||
"shelf-sort": shelfSort,
|
||||
"shortcuts": shortcuts,
|
||||
"shortcut-input": shortcutInput,
|
||||
"sidebar": sidebar,
|
||||
"sortable-list": sortableList,
|
||||
"submit-on-change": submitOnChange,
|
||||
"tabs": tabs,
|
||||
"tag-manager": tagManager,
|
||||
"template-manager": templateManager,
|
||||
"toggle-switch": toggleSwitch,
|
||||
"tri-layout": triLayout,
|
||||
"user-select": userSelect,
|
||||
"webhook-events": webhookEvents,
|
||||
"wysiwyg-editor": wysiwygEditor,
|
||||
};
|
||||
|
||||
window.components = {};
|
||||
|
||||
/**
|
||||
* Initialize components of the given name within the given element.
|
||||
* @param {String} componentName
|
||||
* @param {HTMLElement|Document} parentElement
|
||||
*/
|
||||
function searchForComponentInParent(componentName, parentElement) {
|
||||
const elems = parentElement.querySelectorAll(`[${componentName}]`);
|
||||
for (let j = 0, jLen = elems.length; j < jLen; j++) {
|
||||
initComponent(componentName, elems[j]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a component instance on the given dom element.
|
||||
* @param {String} name
|
||||
* @param {Element} element
|
||||
*/
|
||||
function initComponent(name, element) {
|
||||
const componentModel = componentMapping[name];
|
||||
if (componentModel === undefined) return;
|
||||
|
||||
// Create our component instance
|
||||
let instance;
|
||||
try {
|
||||
instance = new componentModel(element);
|
||||
instance.$el = element;
|
||||
const allRefs = parseRefs(name, element);
|
||||
instance.$refs = allRefs.refs;
|
||||
instance.$manyRefs = allRefs.manyRefs;
|
||||
instance.$opts = parseOpts(name, element);
|
||||
instance.$emit = (eventName, data = {}) => {
|
||||
data.from = instance;
|
||||
const event = new CustomEvent(`${name}-${eventName}`, {
|
||||
bubbles: true,
|
||||
detail: data
|
||||
});
|
||||
instance.$el.dispatchEvent(event);
|
||||
};
|
||||
if (typeof instance.setup === 'function') {
|
||||
instance.setup();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to create component', e, name, element);
|
||||
}
|
||||
|
||||
|
||||
// Add to global listing
|
||||
if (typeof window.components[name] === "undefined") {
|
||||
window.components[name] = [];
|
||||
}
|
||||
window.components[name].push(instance);
|
||||
|
||||
// Add to element listing
|
||||
if (typeof element.components === 'undefined') {
|
||||
element.components = {};
|
||||
}
|
||||
element.components[name] = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse out the element references within the given element
|
||||
* for the given component name.
|
||||
* @param {String} name
|
||||
* @param {Element} element
|
||||
*/
|
||||
function parseRefs(name, element) {
|
||||
const refs = {};
|
||||
const manyRefs = {};
|
||||
|
||||
const prefix = `${name}@`
|
||||
const selector = `[refs*="${prefix}"]`;
|
||||
const refElems = [...element.querySelectorAll(selector)];
|
||||
if (element.matches(selector)) {
|
||||
refElems.push(element);
|
||||
}
|
||||
|
||||
for (const el of refElems) {
|
||||
const refNames = el.getAttribute('refs')
|
||||
.split(' ')
|
||||
.filter(str => str.startsWith(prefix))
|
||||
.map(str => str.replace(prefix, ''))
|
||||
.map(kebabToCamel);
|
||||
for (const ref of refNames) {
|
||||
refs[ref] = el;
|
||||
if (typeof manyRefs[ref] === 'undefined') {
|
||||
manyRefs[ref] = [];
|
||||
}
|
||||
manyRefs[ref].push(el);
|
||||
}
|
||||
}
|
||||
return {refs, manyRefs};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse out the element component options.
|
||||
* @param {String} name
|
||||
* @param {Element} element
|
||||
* @return {Object<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
|
||||
*/
|
||||
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"
|
@ -1,9 +1,10 @@
|
||||
/**
|
||||
* ListSortControl
|
||||
* Manages the logic for the control which provides list sorting options.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class ListSortControl {
|
||||
import {Component} from "./component";
|
||||
|
||||
export class ListSortControl extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
@ -44,6 +45,4 @@ class ListSortControl {
|
||||
this.form.submit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ListSortControl;
|
||||
}
|
@ -4,8 +4,9 @@ import Clipboard from "../services/clipboard";
|
||||
import {debounce} from "../services/util";
|
||||
import {patchDomFromHtmlString} from "../services/vdom";
|
||||
import DrawIO from "../services/drawio";
|
||||
import {Component} from "./component";
|
||||
|
||||
class MarkdownEditor {
|
||||
export class MarkdownEditor extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
@ -430,7 +431,9 @@ class MarkdownEditor {
|
||||
|
||||
actionInsertImage() {
|
||||
const cursorPos = this.cm.getCursor('from');
|
||||
window.ImageManager.show(image => {
|
||||
/** @type {ImageManager} **/
|
||||
const imageManager = window.$components.first('image-manager');
|
||||
imageManager.show(image => {
|
||||
const imageUrl = image.thumbs.display || image.url;
|
||||
let selectedText = this.cm.getSelection();
|
||||
let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
|
||||
@ -442,7 +445,9 @@ class MarkdownEditor {
|
||||
|
||||
actionShowImageManager() {
|
||||
const cursorPos = this.cm.getCursor('from');
|
||||
window.ImageManager.show(image => {
|
||||
/** @type {ImageManager} **/
|
||||
const imageManager = window.$components.first('image-manager');
|
||||
imageManager.show(image => {
|
||||
this.insertDrawing(image, cursorPos);
|
||||
}, 'drawio');
|
||||
}
|
||||
@ -450,7 +455,9 @@ class MarkdownEditor {
|
||||
// Show the popup link selector and insert a link when finished
|
||||
actionShowLinkSelector() {
|
||||
const cursorPos = this.cm.getCursor('from');
|
||||
window.EntitySelectorPopup.show(entity => {
|
||||
/** @type {EntitySelectorPopup} **/
|
||||
const selector = window.$components.first('entity-selector-popup');
|
||||
selector.show(entity => {
|
||||
let selectedText = this.cm.getSelection() || entity.name;
|
||||
let newText = `[${selectedText}](${entity.link})`;
|
||||
this.cm.focus();
|
||||
@ -619,5 +626,3 @@ class MarkdownEditor {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default MarkdownEditor ;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class NewUserPassword {
|
||||
export class NewUserPassword extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.inviteOption = elem.querySelector('input[name=send_invite]');
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.inputContainer = this.$refs.inputContainer;
|
||||
this.inviteOption = this.container.querySelector('input[name=send_invite]');
|
||||
|
||||
if (this.inviteOption) {
|
||||
this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
|
||||
@ -13,16 +15,12 @@ class NewUserPassword {
|
||||
|
||||
inviteOptionChange() {
|
||||
const inviting = (this.inviteOption.value === 'true');
|
||||
const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
|
||||
const passwordBoxes = this.container.querySelectorAll('input[type=password]');
|
||||
for (const input of passwordBoxes) {
|
||||
input.disabled = inviting;
|
||||
}
|
||||
const container = this.elem.querySelector('#password-input-container');
|
||||
if (container) {
|
||||
container.style.display = inviting ? 'none' : 'block';
|
||||
}
|
||||
|
||||
this.inputContainer.style.display = inviting ? 'none' : 'block';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default NewUserPassword;
|
||||
}
|
@ -1,19 +1,21 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class Notification {
|
||||
export class Notification extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.type = elem.getAttribute('notification');
|
||||
this.textElem = elem.querySelector('span');
|
||||
this.autohide = this.elem.hasAttribute('data-autohide');
|
||||
this.elem.style.display = 'grid';
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.type = this.$opts.type;
|
||||
this.textElem = this.container.querySelector('span');
|
||||
this.autoHide = this.$opts.autoHide === 'true';
|
||||
this.initialShow = this.$opts.show === 'true'
|
||||
this.container.style.display = 'grid';
|
||||
|
||||
window.$events.listen(this.type, text => {
|
||||
this.show(text);
|
||||
});
|
||||
elem.addEventListener('click', this.hide.bind(this));
|
||||
this.container.addEventListener('click', this.hide.bind(this));
|
||||
|
||||
if (elem.hasAttribute('data-show')) {
|
||||
if (this.initialShow) {
|
||||
setTimeout(() => this.show(this.textElem.textContent), 100);
|
||||
}
|
||||
|
||||
@ -21,14 +23,14 @@ class Notification {
|
||||
}
|
||||
|
||||
show(textToShow = '') {
|
||||
this.elem.removeEventListener('transitionend', this.hideCleanup);
|
||||
this.container.removeEventListener('transitionend', this.hideCleanup);
|
||||
this.textElem.textContent = textToShow;
|
||||
this.elem.style.display = 'grid';
|
||||
this.container.style.display = 'grid';
|
||||
setTimeout(() => {
|
||||
this.elem.classList.add('showing');
|
||||
this.container.classList.add('showing');
|
||||
}, 1);
|
||||
|
||||
if (this.autohide) {
|
||||
if (this.autoHide) {
|
||||
const words = textToShow.split(' ').length;
|
||||
const timeToShow = Math.max(2000, 1000 + (250 * words));
|
||||
setTimeout(this.hide.bind(this), timeToShow);
|
||||
@ -36,15 +38,13 @@ class Notification {
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.elem.classList.remove('showing');
|
||||
this.elem.addEventListener('transitionend', this.hideCleanup);
|
||||
this.container.classList.remove('showing');
|
||||
this.container.addEventListener('transitionend', this.hideCleanup);
|
||||
}
|
||||
|
||||
hideCleanup() {
|
||||
this.elem.style.display = 'none';
|
||||
this.elem.removeEventListener('transitionend', this.hideCleanup);
|
||||
this.container.style.display = 'none';
|
||||
this.container.removeEventListener('transitionend', this.hideCleanup);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Notification;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
class OptionalInput {
|
||||
export class OptionalInput extends Component {
|
||||
setup() {
|
||||
this.removeButton = this.$refs.remove;
|
||||
this.showButton = this.$refs.show;
|
||||
@ -23,6 +24,4 @@ class OptionalInput {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OptionalInput;
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
import {scrollAndHighlightElement} from "../services/util";
|
||||
import {Component} from "./component";
|
||||
import {htmlToDom} from "../services/dom";
|
||||
|
||||
/**
|
||||
* @extends {Component}
|
||||
*/
|
||||
class PageComments {
|
||||
export class PageComments extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
@ -90,7 +89,7 @@ class PageComments {
|
||||
newComment.innerHTML = resp.data;
|
||||
this.editingComment.innerHTML = newComment.children[0].innerHTML;
|
||||
window.$events.success(this.updatedText);
|
||||
window.components.init(this.editingComment);
|
||||
window.$components.init(this.editingComment);
|
||||
this.closeUpdateForm();
|
||||
this.editingComment = null;
|
||||
}).catch(window.$events.showValidationErrors).then(() => {
|
||||
@ -119,11 +118,9 @@ class PageComments {
|
||||
};
|
||||
this.showLoading(this.form);
|
||||
window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
|
||||
let newComment = document.createElement('div');
|
||||
newComment.innerHTML = resp.data;
|
||||
let newElem = newComment.children[0];
|
||||
const newElem = htmlToDom(resp.data);
|
||||
this.container.appendChild(newElem);
|
||||
window.components.init(newElem);
|
||||
window.$components.init(newElem);
|
||||
window.$events.success(this.createdText);
|
||||
this.resetForm();
|
||||
this.updateCount();
|
||||
@ -199,6 +196,4 @@ class PageComments {
|
||||
formElem.querySelector('.form-group.loading').style.display = 'none';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PageComments;
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
import * as DOM from "../services/dom";
|
||||
import {scrollAndHighlightElement} from "../services/util";
|
||||
import {Component} from "./component";
|
||||
|
||||
class PageDisplay {
|
||||
export class PageDisplay extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.pageId = elem.getAttribute('page-display');
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.pageId = this.$opts.pageId;
|
||||
|
||||
window.importVersioned('code').then(Code => Code.highlight());
|
||||
this.setupNavHighlighting();
|
||||
@ -13,7 +14,7 @@ class PageDisplay {
|
||||
|
||||
// Check the hash on load
|
||||
if (window.location.hash) {
|
||||
let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
|
||||
const text = window.location.hash.replace(/%20/g, ' ').substring(1);
|
||||
this.goToText(text);
|
||||
}
|
||||
|
||||
@ -22,7 +23,7 @@ class PageDisplay {
|
||||
if (sidebarPageNav) {
|
||||
DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
|
||||
event.preventDefault();
|
||||
window.components['tri-layout'][0].showContent();
|
||||
window.$components.first('tri-layout').showContent();
|
||||
const contentId = child.getAttribute('href').substr(1);
|
||||
this.goToText(contentId);
|
||||
window.history.pushState(null, null, '#' + contentId);
|
||||
@ -49,17 +50,10 @@ class PageDisplay {
|
||||
}
|
||||
|
||||
setupNavHighlighting() {
|
||||
// Check if support is present for IntersectionObserver
|
||||
if (!('IntersectionObserver' in window) ||
|
||||
!('IntersectionObserverEntry' in window) ||
|
||||
!('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pageNav = document.querySelector('.sidebar-page-nav');
|
||||
const pageNav = document.querySelector('.sidebar-page-nav');
|
||||
|
||||
// fetch all the headings.
|
||||
let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
const headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
// if headings are present, add observers.
|
||||
if (headings.length > 0 && pageNav !== null) {
|
||||
addNavObserver(headings);
|
||||
@ -67,21 +61,21 @@ class PageDisplay {
|
||||
|
||||
function addNavObserver(headings) {
|
||||
// Setup the intersection observer.
|
||||
let intersectOpts = {
|
||||
const intersectOpts = {
|
||||
rootMargin: '0px 0px 0px 0px',
|
||||
threshold: 1.0
|
||||
};
|
||||
let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
|
||||
const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
|
||||
|
||||
// observe each heading
|
||||
for (let heading of headings) {
|
||||
for (const heading of headings) {
|
||||
pageNavObserver.observe(heading);
|
||||
}
|
||||
}
|
||||
|
||||
function headingVisibilityChange(entries, observer) {
|
||||
for (let entry of entries) {
|
||||
let isVisible = (entry.intersectionRatio === 1);
|
||||
for (const entry of entries) {
|
||||
const isVisible = (entry.intersectionRatio === 1);
|
||||
toggleAnchorHighlighting(entry.target.id, isVisible);
|
||||
}
|
||||
}
|
||||
@ -99,9 +93,7 @@ class PageDisplay {
|
||||
codeMirrors.forEach(cm => cm.CodeMirror && cm.CodeMirror.refresh());
|
||||
};
|
||||
|
||||
const details = [...this.elem.querySelectorAll('details')];
|
||||
const details = [...this.container.querySelectorAll('details')];
|
||||
details.forEach(detail => detail.addEventListener('toggle', onToggle));
|
||||
}
|
||||
}
|
||||
|
||||
export default PageDisplay;
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
import * as Dates from "../services/dates";
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Page Editor
|
||||
* @extends {Component}
|
||||
*/
|
||||
class PageEditor {
|
||||
export class PageEditor extends Component {
|
||||
setup() {
|
||||
// Options
|
||||
this.draftsEnabled = this.$opts.draftsEnabled === 'true';
|
||||
@ -199,7 +196,8 @@ class PageEditor {
|
||||
event.preventDefault();
|
||||
|
||||
const link = event.target.closest('a').href;
|
||||
const dialog = this.switchDialogContainer.components['confirm-dialog'];
|
||||
/** @var {ConfirmDialog} **/
|
||||
const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
|
||||
const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
|
||||
|
||||
if (saved && confirmed) {
|
||||
@ -207,6 +205,4 @@ class PageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PageEditor;
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class PagePicker {
|
||||
export class PagePicker extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.input = elem.querySelector('input');
|
||||
this.resetButton = elem.querySelector('[page-picker-reset]');
|
||||
this.selectButton = elem.querySelector('[page-picker-select]');
|
||||
this.display = elem.querySelector('[page-picker-display]');
|
||||
this.defaultDisplay = elem.querySelector('[page-picker-default]');
|
||||
this.buttonSep = elem.querySelector('span.sep');
|
||||
setup() {
|
||||
this.input = this.$refs.input;
|
||||
this.resetButton = this.$refs.resetButton;
|
||||
this.selectButton = this.$refs.selectButton;
|
||||
this.display = this.$refs.display;
|
||||
this.defaultDisplay = this.$refs.defaultDisplay;
|
||||
this.buttonSep = this.$refs.buttonSeperator;
|
||||
|
||||
this.value = this.input.value;
|
||||
this.setupListeners();
|
||||
@ -24,7 +24,9 @@ class PagePicker {
|
||||
}
|
||||
|
||||
showPopup() {
|
||||
window.EntitySelectorPopup.show(entity => {
|
||||
/** @type {EntitySelectorPopup} **/
|
||||
const selectorPopup = window.$components.first('entity-selector-popup');
|
||||
selectorPopup.show(entity => {
|
||||
this.setValue(entity.id, entity.name);
|
||||
});
|
||||
}
|
||||
@ -36,7 +38,7 @@ class PagePicker {
|
||||
}
|
||||
|
||||
controlView(name) {
|
||||
let hasValue = this.value && this.value !== 0;
|
||||
const hasValue = this.value && this.value !== 0;
|
||||
toggleElem(this.resetButton, hasValue);
|
||||
toggleElem(this.buttonSep, hasValue);
|
||||
toggleElem(this.defaultDisplay, !hasValue);
|
||||
@ -55,8 +57,5 @@ class PagePicker {
|
||||
}
|
||||
|
||||
function toggleElem(elem, show) {
|
||||
let display = (elem.tagName === 'BUTTON' || elem.tagName === 'SPAN') ? 'inline-block' : 'block';
|
||||
elem.style.display = show ? display : 'none';
|
||||
}
|
||||
|
||||
export default PagePicker;
|
||||
elem.style.display = show ? null : 'none';
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class PermissionsTable {
|
||||
export class PermissionsTable extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -62,6 +63,4 @@ class PermissionsTable {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PermissionsTable;
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
import * as DOM from "../services/dom";
|
||||
import Clipboard from "clipboard/dist/clipboard.min";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* @extends Component
|
||||
*/
|
||||
class Pointer {
|
||||
|
||||
export class Pointer extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -126,6 +125,4 @@ class Pointer {
|
||||
editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Pointer;
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
import {fadeIn, fadeOut} from "../services/animations";
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Popup window that will contain other content.
|
||||
* This component provides the show/hide functionality
|
||||
* with the ability for popup@hide child references to close this.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class Popup {
|
||||
export class Popup extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -56,6 +56,4 @@ class Popup {
|
||||
this.onHide = onHide;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Popup;
|
||||
}
|
@ -1,23 +1,13 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class SettingAppColorPicker {
|
||||
export class SettingAppColorPicker extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.colorInput = elem.querySelector('input[type=color]');
|
||||
this.lightColorInput = elem.querySelector('input[name="setting-app-color-light"]');
|
||||
this.resetButton = elem.querySelector('[setting-app-color-picker-reset]');
|
||||
this.defaultButton = elem.querySelector('[setting-app-color-picker-default]');
|
||||
setup() {
|
||||
this.colorInput = this.$refs.input;
|
||||
this.lightColorInput = this.$refs.lightInput;
|
||||
|
||||
this.colorInput.addEventListener('change', this.updateColor.bind(this));
|
||||
this.colorInput.addEventListener('input', this.updateColor.bind(this));
|
||||
this.resetButton.addEventListener('click', event => {
|
||||
this.colorInput.value = this.colorInput.dataset.current;
|
||||
this.updateColor();
|
||||
});
|
||||
this.defaultButton.addEventListener('click', event => {
|
||||
this.colorInput.value = this.colorInput.dataset.default;
|
||||
this.updateColor();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,8 +34,8 @@ class SettingAppColorPicker {
|
||||
/**
|
||||
* Covert a hex color code to rgb components.
|
||||
* @attribution https://stackoverflow.com/a/5624139
|
||||
* @param hex
|
||||
* @returns {*}
|
||||
* @param {String} hex
|
||||
* @returns {{r: Number, g: Number, b: Number}}
|
||||
*/
|
||||
hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
@ -57,5 +47,3 @@ class SettingAppColorPicker {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SettingAppColorPicker;
|
||||
|
@ -1,18 +1,20 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class SettingColorPicker {
|
||||
export class SettingColorPicker extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.colorInput = elem.querySelector('input[type=color]');
|
||||
this.resetButton = elem.querySelector('[setting-color-picker-reset]');
|
||||
this.defaultButton = elem.querySelector('[setting-color-picker-default]');
|
||||
this.resetButton.addEventListener('click', event => {
|
||||
this.colorInput.value = this.colorInput.dataset.current;
|
||||
});
|
||||
this.defaultButton.addEventListener('click', event => {
|
||||
this.colorInput.value = this.colorInput.dataset.default;
|
||||
});
|
||||
setup() {
|
||||
this.colorInput = this.$refs.input;
|
||||
this.resetButton = this.$refs.resetButton;
|
||||
this.defaultButton = this.$refs.defaultButton;
|
||||
this.currentColor = this.$opts.current;
|
||||
this.defaultColor = this.$opts.default;
|
||||
|
||||
this.resetButton.addEventListener('click', () => this.setValue(this.currentColor));
|
||||
this.defaultButton.addEventListener('click', () => this.setValue(this.defaultColor));
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingColorPicker;
|
||||
setValue(value) {
|
||||
this.colorInput.value = value;
|
||||
this.colorInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class HomepageControl {
|
||||
export class SettingHomepageControl extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.typeControl = elem.querySelector('[name="setting-app-homepage-type"]');
|
||||
this.pagePickerContainer = elem.querySelector('[page-picker-container]');
|
||||
setup() {
|
||||
this.typeControl = this.$refs.typeControl;
|
||||
this.pagePickerContainer = this.$refs.pagePickerContainer;
|
||||
|
||||
this.typeControl.addEventListener('change', this.controlPagePickerVisibility.bind(this));
|
||||
this.controlPagePickerVisibility();
|
||||
@ -14,9 +14,4 @@ class HomepageControl {
|
||||
const showPagePicker = this.typeControl.value === 'page';
|
||||
this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default HomepageControl;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import Sortable from "sortablejs";
|
||||
import {Component} from "./component";
|
||||
|
||||
class ShelfSort {
|
||||
export class ShelfSort extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
@ -15,7 +16,7 @@ class ShelfSort {
|
||||
|
||||
initSortable() {
|
||||
const scrollBoxes = this.elem.querySelectorAll('.scroll-box');
|
||||
for (let scrollBox of scrollBoxes) {
|
||||
for (const scrollBox of scrollBoxes) {
|
||||
new Sortable(scrollBox, {
|
||||
group: 'shelf-books',
|
||||
ghostClass: 'primary-background-light',
|
||||
@ -78,6 +79,4 @@ class ShelfSort {
|
||||
this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ShelfSort;
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Keys to ignore when recording shortcuts.
|
||||
* @type {string[]}
|
||||
*/
|
||||
const ignoreKeys = ['Control', 'Alt', 'Shift', 'Meta', 'Super', ' ', '+', 'Tab', 'Escape'];
|
||||
|
||||
/**
|
||||
* @extends {Component}
|
||||
*/
|
||||
class ShortcutInput {
|
||||
export class ShortcutInput extends Component {
|
||||
|
||||
setup() {
|
||||
this.input = this.$el;
|
||||
@ -52,6 +51,4 @@ class ShortcutInput {
|
||||
this.input.removeEventListener('keydown', this.listenerRecordKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ShortcutInput;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
function reverseMap(map) {
|
||||
const reversed = {};
|
||||
for (const [key, value] of Object.entries(map)) {
|
||||
@ -6,10 +8,8 @@ function reverseMap(map) {
|
||||
return reversed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @extends {Component}
|
||||
*/
|
||||
class Shortcuts {
|
||||
|
||||
export class Shortcuts extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
@ -159,6 +159,4 @@ class Shortcuts {
|
||||
|
||||
this.hintsShowing = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default Shortcuts;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
|
||||
class Sidebar {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.toggleElem = elem.querySelector('.sidebar-toggle');
|
||||
this.toggleElem.addEventListener('click', this.toggle.bind(this));
|
||||
}
|
||||
|
||||
toggle(show = true) {
|
||||
this.elem.classList.toggle('open');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Sidebar;
|
@ -1,4 +1,5 @@
|
||||
import Sortable from "sortablejs";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* SortableList
|
||||
@ -6,10 +7,8 @@ import Sortable from "sortablejs";
|
||||
* Can have data set on the dragged items by setting a 'data-drag-content' attribute.
|
||||
* This attribute must contain JSON where the keys are content types and the values are
|
||||
* the data to set on the data-transfer.
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class SortableList {
|
||||
export class SortableList extends Component {
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.handleSelector = this.$opts.handleSelector;
|
||||
@ -34,6 +33,4 @@ class SortableList {
|
||||
dragoverBubble: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default SortableList;
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Submit on change
|
||||
* Simply submits a parent form when this input is changed.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class SubmitOnChange {
|
||||
export class SubmitOnChange extends Component {
|
||||
|
||||
setup() {
|
||||
this.filter = this.$opts.filter;
|
||||
@ -21,6 +22,4 @@ class SubmitOnChange {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SubmitOnChange;
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import {onSelect} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
/**
|
||||
* Tabs
|
||||
* Works by matching 'tabToggle<Key>' with 'tabContent<Key>' sections.
|
||||
* @extends {Component}
|
||||
*/
|
||||
import {onSelect} from "../services/dom";
|
||||
|
||||
class Tabs {
|
||||
export class Tabs extends Component {
|
||||
|
||||
setup() {
|
||||
this.tabContentsByName = {};
|
||||
@ -46,6 +46,4 @@ class Tabs {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Tabs;
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
/**
|
||||
* TagManager
|
||||
* @extends {Component}
|
||||
*/
|
||||
class TagManager {
|
||||
import {Component} from "./component";
|
||||
|
||||
export class TagManager extends Component {
|
||||
setup() {
|
||||
this.addRemoveComponentEl = this.$refs.addRemove;
|
||||
this.container = this.$el;
|
||||
@ -13,7 +11,8 @@ class TagManager {
|
||||
|
||||
setupListeners() {
|
||||
this.container.addEventListener('change', event => {
|
||||
const addRemoveComponent = this.addRemoveComponentEl.components['add-remove-rows'];
|
||||
/** @var {AddRemoveRows} **/
|
||||
const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows');
|
||||
if (!this.hasEmptyRows()) {
|
||||
addRemoveComponent.add();
|
||||
}
|
||||
@ -27,6 +26,4 @@ class TagManager {
|
||||
});
|
||||
return firstEmpty !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default TagManager;
|
||||
}
|
@ -1,25 +1,48 @@
|
||||
import * as DOM from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
class TemplateManager {
|
||||
export class TemplateManager extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.list = elem.querySelector('[template-manager-list]');
|
||||
this.searching = false;
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.list = this.$refs.list;
|
||||
|
||||
this.searchInput = this.$refs.searchInput;
|
||||
this.searchButton = this.$refs.searchButton;
|
||||
this.searchCancel = this.$refs.searchCancel;
|
||||
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
// Template insert action buttons
|
||||
DOM.onChildEvent(this.elem, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));
|
||||
DOM.onChildEvent(this.container, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));
|
||||
|
||||
// Template list pagination click
|
||||
DOM.onChildEvent(this.elem, '.pagination a', 'click', this.handlePaginationClick.bind(this));
|
||||
DOM.onChildEvent(this.container, '.pagination a', 'click', this.handlePaginationClick.bind(this));
|
||||
|
||||
// Template list item content click
|
||||
DOM.onChildEvent(this.elem, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
|
||||
DOM.onChildEvent(this.container, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
|
||||
|
||||
// Template list item drag start
|
||||
DOM.onChildEvent(this.elem, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
|
||||
DOM.onChildEvent(this.container, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
|
||||
|
||||
this.setupSearchBox();
|
||||
// Search box enter press
|
||||
this.searchInput.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
this.performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Search submit button press
|
||||
this.searchButton.addEventListener('click', event => this.performSearch());
|
||||
|
||||
// Search cancel button press
|
||||
this.searchCancel.addEventListener('click', event => {
|
||||
this.searchInput.value = '';
|
||||
this.performSearch();
|
||||
});
|
||||
}
|
||||
|
||||
handleTemplateItemClick(event, templateItem) {
|
||||
@ -54,45 +77,12 @@ class TemplateManager {
|
||||
this.list.innerHTML = resp.data;
|
||||
}
|
||||
|
||||
setupSearchBox() {
|
||||
const searchBox = this.elem.querySelector('.search-box');
|
||||
|
||||
// Search box may not exist if there are no existing templates in the system.
|
||||
if (!searchBox) return;
|
||||
|
||||
const input = searchBox.querySelector('input');
|
||||
const submitButton = searchBox.querySelector('button');
|
||||
const cancelButton = searchBox.querySelector('button.search-box-cancel');
|
||||
|
||||
async function performSearch() {
|
||||
const searchTerm = input.value;
|
||||
const resp = await window.$http.get(`/templates`, {
|
||||
search: searchTerm
|
||||
});
|
||||
cancelButton.style.display = searchTerm ? 'block' : 'none';
|
||||
this.list.innerHTML = resp.data;
|
||||
}
|
||||
performSearch = performSearch.bind(this);
|
||||
|
||||
// Search box enter press
|
||||
searchBox.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Submit button press
|
||||
submitButton.addEventListener('click', event => {
|
||||
performSearch();
|
||||
});
|
||||
|
||||
// Cancel button press
|
||||
cancelButton.addEventListener('click', event => {
|
||||
input.value = '';
|
||||
performSearch();
|
||||
async performSearch() {
|
||||
const searchTerm = this.searchInput.value;
|
||||
const resp = await window.$http.get(`/templates`, {
|
||||
search: searchTerm
|
||||
});
|
||||
this.searchCancel.style.display = searchTerm ? 'block' : 'none';
|
||||
this.list.innerHTML = resp.data;
|
||||
}
|
||||
}
|
||||
|
||||
export default TemplateManager;
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class ToggleSwitch {
|
||||
export class ToggleSwitch extends Component {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.input = elem.querySelector('input[type=hidden]');
|
||||
this.checkbox = elem.querySelector('input[type=checkbox]');
|
||||
setup() {
|
||||
this.input = this.$el.querySelector('input[type=hidden]');
|
||||
this.checkbox = this.$el.querySelector('input[type=checkbox]');
|
||||
|
||||
this.checkbox.addEventListener('change', this.stateChange.bind(this));
|
||||
}
|
||||
@ -18,6 +18,4 @@ class ToggleSwitch {
|
||||
this.input.dispatchEvent(changeEvent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ToggleSwitch;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import {Component} from "./component";
|
||||
|
||||
class TriLayout {
|
||||
export class TriLayout extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$refs.container;
|
||||
@ -108,6 +109,4 @@ class TriLayout {
|
||||
this.lastTabShown = tabName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default TriLayout;
|
||||
}
|
@ -1,25 +1,28 @@
|
||||
import {onChildEvent} from "../services/dom";
|
||||
import {Component} from "./component";
|
||||
|
||||
class UserSelect {
|
||||
export class UserSelect extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.input = this.$refs.input;
|
||||
this.userInfoContainer = this.$refs.userInfo;
|
||||
|
||||
this.hide = this.$el.components.dropdown.hide;
|
||||
|
||||
onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
|
||||
onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
|
||||
}
|
||||
|
||||
selectUser(event, userEl) {
|
||||
event.preventDefault();
|
||||
const id = userEl.getAttribute('data-id');
|
||||
this.input.value = id;
|
||||
this.input.value = userEl.getAttribute('data-id');
|
||||
this.userInfoContainer.innerHTML = userEl.innerHTML;
|
||||
this.input.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
this.hide();
|
||||
}
|
||||
|
||||
}
|
||||
hide() {
|
||||
/** @var {Dropdown} **/
|
||||
const dropdown = window.$components.firstOnElement(this.container, 'dropdown');
|
||||
dropdown.hide();
|
||||
}
|
||||
|
||||
export default UserSelect;
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
|
||||
/**
|
||||
* Webhook Events
|
||||
* Manages dynamic selection control in the webhook form interface.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class WebhookEvents {
|
||||
import {Component} from "./component";
|
||||
|
||||
export class WebhookEvents extends Component {
|
||||
|
||||
setup() {
|
||||
this.checkboxes = this.$el.querySelectorAll('input[type="checkbox"]');
|
||||
@ -27,6 +27,4 @@ class WebhookEvents {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default WebhookEvents;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import {build as buildEditorConfig} from "../wysiwyg/config";
|
||||
import {Component} from "./component";
|
||||
|
||||
class WysiwygEditor {
|
||||
export class WysiwygEditor extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
@ -35,6 +36,4 @@ class WysiwygEditor {
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default WysiwygEditor;
|
||||
}
|
178
resources/js/services/components.js
Normal file
178
resources/js/services/components.js
Normal file
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* A mapping of active components keyed by name, with values being arrays of component
|
||||
* instances since there can be multiple components of the same type.
|
||||
* @type {Object<String, Component[]>}
|
||||
*/
|
||||
const components = {};
|
||||
|
||||
/**
|
||||
* A mapping of component class models, keyed by name.
|
||||
* @type {Object<String, Constructor<Component>>}
|
||||
*/
|
||||
const componentModelMap = {};
|
||||
|
||||
/**
|
||||
* A mapping of active component maps, keyed by the element components are assigned to.
|
||||
* @type {WeakMap<Element, Object<String, Component>>}
|
||||
*/
|
||||
const elementComponentMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* Initialize a component instance on the given dom element.
|
||||
* @param {String} name
|
||||
* @param {Element} element
|
||||
*/
|
||||
function initComponent(name, element) {
|
||||
/** @type {Function<Component>|undefined} **/
|
||||
const componentModel = componentModelMap[name];
|
||||
if (componentModel === undefined) return;
|
||||
|
||||
// Create our component instance
|
||||
/** @type {Component} **/
|
||||
let instance;
|
||||
try {
|
||||
instance = new componentModel();
|
||||
instance.$name = name;
|
||||
instance.$el = element;
|
||||
const allRefs = parseRefs(name, element);
|
||||
instance.$refs = allRefs.refs;
|
||||
instance.$manyRefs = allRefs.manyRefs;
|
||||
instance.$opts = parseOpts(name, element);
|
||||
instance.setup();
|
||||
} catch (e) {
|
||||
console.error('Failed to create component', e, name, element);
|
||||
}
|
||||
|
||||
// Add to global listing
|
||||
if (typeof components[name] === "undefined") {
|
||||
components[name] = [];
|
||||
}
|
||||
components[name].push(instance);
|
||||
|
||||
// Add to element mapping
|
||||
const elComponents = elementComponentMap.get(element) || {};
|
||||
elComponents[name] = instance;
|
||||
elementComponentMap.set(element, elComponents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse out the element references within the given element
|
||||
* for the given component name.
|
||||
* @param {String} name
|
||||
* @param {Element} element
|
||||
*/
|
||||
function parseRefs(name, element) {
|
||||
const refs = {};
|
||||
const manyRefs = {};
|
||||
|
||||
const prefix = `${name}@`
|
||||
const selector = `[refs*="${prefix}"]`;
|
||||
const refElems = [...element.querySelectorAll(selector)];
|
||||
if (element.matches(selector)) {
|
||||
refElems.push(element);
|
||||
}
|
||||
|
||||
for (const el of refElems) {
|
||||
const refNames = el.getAttribute('refs')
|
||||
.split(' ')
|
||||
.filter(str => str.startsWith(prefix))
|
||||
.map(str => str.replace(prefix, ''))
|
||||
.map(kebabToCamel);
|
||||
for (const ref of refNames) {
|
||||
refs[ref] = el;
|
||||
if (typeof manyRefs[ref] === 'undefined') {
|
||||
manyRefs[ref] = [];
|
||||
}
|
||||
manyRefs[ref].push(el);
|
||||
}
|
||||
}
|
||||
return {refs, manyRefs};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse out the element component options.
|
||||
* @param {String} name
|
||||
* @param {Element} element
|
||||
* @return {Object<String, String>}
|
||||
*/
|
||||
function parseOpts(name, element) {
|
||||
const opts = {};
|
||||
const prefix = `option:${name}:`;
|
||||
for (const {name, value} of element.attributes) {
|
||||
if (name.startsWith(prefix)) {
|
||||
const optName = name.replace(prefix, '');
|
||||
opts[kebabToCamel(optName)] = value || '';
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a kebab-case string to camelCase
|
||||
* @param {String} kebab
|
||||
* @returns {string}
|
||||
*/
|
||||
function kebabToCamel(kebab) {
|
||||
const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
|
||||
const words = kebab.split('-');
|
||||
return words[0] + words.slice(1).map(ucFirst).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all components found within the given element.
|
||||
* @param {Element|Document} parentElement
|
||||
*/
|
||||
export function init(parentElement = document) {
|
||||
const componentElems = parentElement.querySelectorAll(`[component],[components]`);
|
||||
|
||||
for (const el of componentElems) {
|
||||
const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
|
||||
for (const name of componentNames) {
|
||||
initComponent(name, el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the given component mapping into the component system.
|
||||
* @param {Object<String, ObjectConstructor<Component>>} mapping
|
||||
*/
|
||||
export function register(mapping) {
|
||||
const keys = Object.keys(mapping);
|
||||
for (const key of keys) {
|
||||
componentModelMap[camelToKebab(key)] = mapping[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first component of the given name.
|
||||
* @param {String} name
|
||||
* @returns {Component|null}
|
||||
*/
|
||||
export function first(name) {
|
||||
return (components[name] || [null])[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the components of the given name.
|
||||
* @param {String} name
|
||||
* @returns {Component[]}
|
||||
*/
|
||||
export function get(name) {
|
||||
return components[name] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first component, of the given name, that's assigned to the given element.
|
||||
* @param {Element} element
|
||||
* @param {String} name
|
||||
* @returns {Component|null}
|
||||
*/
|
||||
export function firstOnElement(element, name) {
|
||||
const elComponents = elementComponentMap.get(element) || {};
|
||||
return elComponents[name] || null;
|
||||
}
|
||||
|
||||
function camelToKebab(camelStr) {
|
||||
return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
|
||||
}
|
@ -128,6 +128,6 @@ export function removeLoading(element) {
|
||||
export function htmlToDom(html) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.innerHTML = html;
|
||||
window.components.init(wrap);
|
||||
window.$components.init(wrap);
|
||||
return wrap.children[0];
|
||||
}
|
@ -73,7 +73,9 @@ function file_picker_callback(callback, value, meta) {
|
||||
|
||||
// field_name, url, type, win
|
||||
if (meta.filetype === 'file') {
|
||||
window.EntitySelectorPopup.show(entity => {
|
||||
/** @type {EntitySelectorPopup} **/
|
||||
const selector = window.$components.first('entity-selector-popup');
|
||||
selector.show(entity => {
|
||||
callback(entity.link, {
|
||||
text: entity.name,
|
||||
title: entity.name,
|
||||
@ -83,7 +85,9 @@ function file_picker_callback(callback, value, meta) {
|
||||
|
||||
if (meta.filetype === 'image') {
|
||||
// Show image manager
|
||||
window.ImageManager.show(function (image) {
|
||||
/** @type {ImageManager} **/
|
||||
const imageManager = window.$components.first('image-manager');
|
||||
imageManager.show(function (image) {
|
||||
callback(image.url, {alt: image.name});
|
||||
}, 'gallery');
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ function elemIsCodeBlock(elem) {
|
||||
* @param {function(string, string)} callback (Receives (code: string,language: string)
|
||||
*/
|
||||
function showPopup(editor, code, language, callback) {
|
||||
window.components.first('code-editor').open(code, language, (newCode, newLang) => {
|
||||
window.$components.first('code-editor').open(code, language, (newCode, newLang) => {
|
||||
callback(newCode, newLang)
|
||||
editor.focus()
|
||||
});
|
||||
|
@ -15,8 +15,10 @@ function isDrawing(node) {
|
||||
function showDrawingManager(mceEditor, selectedNode = null) {
|
||||
pageEditor = mceEditor;
|
||||
currentNode = selectedNode;
|
||||
// Show image manager
|
||||
window.ImageManager.show(function (image) {
|
||||
|
||||
/** @type {ImageManager} **/
|
||||
const imageManager = window.$components.first('image-manager');
|
||||
imageManager.show(function (image) {
|
||||
if (selectedNode) {
|
||||
const imgElem = selectedNode.querySelector('img');
|
||||
pageEditor.undoManager.transact(function () {
|
||||
|
@ -3,14 +3,15 @@
|
||||
* @param {String} url
|
||||
*/
|
||||
function register(editor, url) {
|
||||
|
||||
// Custom Image picker button
|
||||
editor.ui.registry.addButton('imagemanager-insert', {
|
||||
title: 'Insert image',
|
||||
icon: 'image',
|
||||
tooltip: 'Insert image',
|
||||
onAction() {
|
||||
window.ImageManager.show(function (image) {
|
||||
/** @type {ImageManager} **/
|
||||
const imageManager = window.$components.first('image-manager');
|
||||
imageManager.show(function (image) {
|
||||
const imageUrl = image.thumbs.display || image.url;
|
||||
let html = `<a href="${image.url}" target="_blank">`;
|
||||
html += `<img src="${imageUrl}" alt="${image.name}">`;
|
||||
|
@ -44,7 +44,9 @@ export function register(editor) {
|
||||
|
||||
// Link selector shortcut
|
||||
editor.shortcuts.add('meta+shift+K', '', function() {
|
||||
window.EntitySelectorPopup.show(function(entity) {
|
||||
/** @var {EntitySelectorPopup} **/
|
||||
const selectorPopup = window.$components.first('entity-selector-popup');
|
||||
selectorPopup.show(function(entity) {
|
||||
|
||||
if (editor.selection.isCollapsed()) {
|
||||
editor.insertContent(editor.dom.createHTML('a', {href: entity.link}, editor.dom.encode(entity.name)));
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
// System wide notifications
|
||||
[notification] {
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@ -1010,4 +1010,41 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
border: 1px solid #b4b4b4;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
// Back to top link
|
||||
$btt-size: 40px;
|
||||
.back-to-top {
|
||||
background-color: var(--color-primary);
|
||||
position: fixed;
|
||||
bottom: $-m;
|
||||
right: $-l;
|
||||
padding: 5px 7px;
|
||||
cursor: pointer;
|
||||
color: #FFF;
|
||||
fill: #FFF;
|
||||
svg {
|
||||
width: math.div($btt-size, 1.5);
|
||||
height: math.div($btt-size, 1.5);
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
width: $btt-size;
|
||||
height: $btt-size;
|
||||
border-radius: $btt-size;
|
||||
transition: all ease-in-out 180ms;
|
||||
opacity: 0;
|
||||
z-index: 999;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
width: $btt-size*3.4;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.inner {
|
||||
width: $btt-size*3.4;
|
||||
}
|
||||
span {
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
line-height: 2;
|
||||
}
|
||||
}
|
@ -328,7 +328,7 @@ input[type=color] {
|
||||
}
|
||||
}
|
||||
|
||||
.form-group[collapsible] {
|
||||
.form-group.collapsible {
|
||||
padding: 0 $-m;
|
||||
border: 1px solid;
|
||||
@include lightDark(border-color, #DDD, #000);
|
||||
|
@ -278,16 +278,16 @@ body.tox-fullscreen, body.markdown-fullscreen {
|
||||
&.open {
|
||||
width: 480px;
|
||||
}
|
||||
[toolbox-toggle] svg {
|
||||
.toolbox-toggle svg {
|
||||
transition: transform ease-in-out 180ms;
|
||||
}
|
||||
[toolbox-toggle] {
|
||||
.toolbox-toggle {
|
||||
transition: background-color ease-in-out 180ms;
|
||||
}
|
||||
&.open [toolbox-toggle] {
|
||||
&.open .toolbox-toggle {
|
||||
background-color: rgba(255, 0, 0, 0.29);
|
||||
}
|
||||
&.open [toolbox-toggle] svg {
|
||||
&.open .toolbox-toggle svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
> div {
|
||||
@ -357,7 +357,7 @@ body.tox-fullscreen, body.markdown-fullscreen {
|
||||
}
|
||||
}
|
||||
|
||||
[toolbox-tab-content] {
|
||||
.toolbox-tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -100,43 +100,6 @@ $loadingSize: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// Back to top link
|
||||
$btt-size: 40px;
|
||||
[back-to-top] {
|
||||
background-color: var(--color-primary);
|
||||
position: fixed;
|
||||
bottom: $-m;
|
||||
right: $-l;
|
||||
padding: 5px 7px;
|
||||
cursor: pointer;
|
||||
color: #FFF;
|
||||
fill: #FFF;
|
||||
svg {
|
||||
width: math.div($btt-size, 1.5);
|
||||
height: math.div($btt-size, 1.5);
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
width: $btt-size;
|
||||
height: $btt-size;
|
||||
border-radius: $btt-size;
|
||||
transition: all ease-in-out 180ms;
|
||||
opacity: 0;
|
||||
z-index: 999;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
width: $btt-size*3.4;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.inner {
|
||||
width: $btt-size*3.4;
|
||||
}
|
||||
span {
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
line-height: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.skip-to-content-link {
|
||||
position: fixed;
|
||||
top: -52px;
|
||||
|
@ -38,7 +38,7 @@
|
||||
|
||||
<div style="overflow: auto;">
|
||||
|
||||
<section code-highlighter class="card content-wrap auto-height">
|
||||
<section component="code-highlighter" class="card content-wrap auto-height">
|
||||
@include('api-docs.parts.getting-started')
|
||||
</section>
|
||||
|
||||
|
@ -34,14 +34,14 @@
|
||||
@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>
|
||||
|
@ -1,6 +1,9 @@
|
||||
<div style="display: block;" toolbox-tab-content="files"
|
||||
<div style="display: block;"
|
||||
refs="editor-toolbox@tab-content"
|
||||
data-tab-content="files"
|
||||
component="attachments"
|
||||
option:attachments:page-id="{{ $page->id ?? 0 }}">
|
||||
option:attachments:page-id="{{ $page->id ?? 0 }}"
|
||||
class="toolbox-tab-content">
|
||||
|
||||
<h4>{{ trans('entities.attachments') }}</h4>
|
||||
<div class="px-l files">
|
||||
|
@ -10,11 +10,11 @@
|
||||
@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', [
|
||||
@ -26,11 +26,11 @@
|
||||
</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>
|
||||
|
@ -4,11 +4,11 @@
|
||||
<span>{{ $book->name }}</span>
|
||||
</h5>
|
||||
<div class="sort-box-options pb-sm">
|
||||
<a href="#" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</a>
|
||||
<a href="#" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</a>
|
||||
<a href="#" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</a>
|
||||
<a href="#" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</a>
|
||||
<a href="#" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</a>
|
||||
<button type="button" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</button>
|
||||
<button type="button" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</button>
|
||||
<button type="button" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</button>
|
||||
<button type="button" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</button>
|
||||
<button type="button" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</button>
|
||||
</div>
|
||||
<ul class="sortable-page-list sort-list">
|
||||
|
||||
|
@ -16,16 +16,16 @@
|
||||
|
||||
<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>
|
||||
|
@ -11,11 +11,11 @@
|
||||
@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>
|
||||
|
@ -1,11 +1,29 @@
|
||||
<div notification="success" style="display: none;" data-autohide class="pos" role="alert" @if(session()->has('success')) data-show @endif>
|
||||
<div component="notification"
|
||||
option:notification:type="success"
|
||||
option:notification:auto-hide="true"
|
||||
option:notification:show="{{ session()->has('success') ? 'true' : 'false' }}"
|
||||
style="display: none;"
|
||||
class="notification pos"
|
||||
role="alert">
|
||||
@icon('check-circle') <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span><div class="dismiss">@icon('close')</div>
|
||||
</div>
|
||||
|
||||
<div notification="warning" style="display: none;" class="warning" role="alert" @if(session()->has('warning')) data-show @endif>
|
||||
<div component="notification"
|
||||
option:notification:type="warning"
|
||||
option:notification:auto-hide="false"
|
||||
option:notification:show="{{ session()->has('warning') ? 'true' : 'false' }}"
|
||||
style="display: none;"
|
||||
class="notification warning"
|
||||
role="alert">
|
||||
@icon('info') <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span><div class="dismiss">@icon('close')</div>
|
||||
</div>
|
||||
|
||||
<div notification="error" style="display: none;" class="neg" role="alert" @if(session()->has('error')) data-show @endif>
|
||||
<div component="notification"
|
||||
option:notification:type="error"
|
||||
option:notification:auto-hide="false"
|
||||
option:notification:show="{{ session()->has('error') ? 'true' : 'false' }}"
|
||||
style="display: none;"
|
||||
class="notification neg"
|
||||
role="alert">
|
||||
@icon('danger') <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span><div class="dismiss">@icon('close')</div>
|
||||
</div>
|
||||
</div>
|
@ -4,7 +4,7 @@ $value
|
||||
$checked
|
||||
$label
|
||||
--}}
|
||||
<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
|
||||
<label component="custom-checkbox" class="toggle-switch @if($errors->has($name)) text-neg @endif">
|
||||
<input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif @if($disabled ?? false) disabled="disabled" @endif>
|
||||
<span tabindex="0" role="checkbox"
|
||||
aria-checked="{{ $checked ? 'true' : 'false' }}"
|
||||
|
@ -1,26 +1,27 @@
|
||||
<div class="image-picker @if($errors->has($name)) has-error @endif"
|
||||
image-picker="{{$name}}"
|
||||
data-default-image="{{ $defaultImage }}">
|
||||
<div component="image-picker"
|
||||
option:image-picker:default-image="{{ $defaultImage }}"
|
||||
class="image-picker @if($errors->has($name)) has-error @endif">
|
||||
|
||||
<div class="grid half">
|
||||
<div class="text-center">
|
||||
<img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
|
||||
<img refs="image-picker@image"
|
||||
@if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif
|
||||
class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
|
||||
</div>
|
||||
<div class="text-center">
|
||||
|
||||
<input type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
|
||||
<input refs="image-picker@image-input" type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
|
||||
<label for="{{ $name }}" class="button outline">{{ trans('components.image_select_image') }}</label>
|
||||
<input type="hidden" data-reset-input name="{{ $name }}_reset" value="true" disabled="disabled">
|
||||
<input refs="image-picker@reset-input" type="hidden" name="{{ $name }}_reset" value="true" disabled="disabled">
|
||||
@if(isset($removeName))
|
||||
<input type="hidden" data-remove-input name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
|
||||
<input refs="image-picker@remove-input" type="hidden" name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
|
||||
@endif
|
||||
|
||||
<br>
|
||||
<button class="text-button text-muted" data-action="reset-image" type="button">{{ trans('common.reset') }}</button>
|
||||
<button refs="image-picker@reset-button" class="text-button text-muted" type="button">{{ trans('common.reset') }}</button>
|
||||
|
||||
@if(isset($removeName))
|
||||
<span class="sep">|</span>
|
||||
<button class="text-button text-muted" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
|
||||
<button refs="image-picker@remove-button" class="text-button text-muted" type="button">{{ trans('common.remove') }}</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<label toggle-switch="{{$name}}" custom-checkbox class="toggle-switch">
|
||||
<label components="custom-checkbox toggle-switch" class="toggle-switch">
|
||||
<input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
|
||||
<input type="checkbox" @if($value) checked="checked" @endif>
|
||||
<span tabindex="0" role="checkbox"
|
||||
|
@ -3,10 +3,12 @@ $target - CSS selector of items to expand
|
||||
$key - Unique key for checking existing stored state.
|
||||
--}}
|
||||
<?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
|
||||
<button type="button" expand-toggle="{{ $target }}"
|
||||
expand-toggle-update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
|
||||
expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
|
||||
class="icon-list-item {{ $classes ?? '' }}">
|
||||
<button component="expand-toggle"
|
||||
option:expand-toggle:target-selector="{{ $target }}"
|
||||
option:expand-toggle:update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
|
||||
option:expand-toggle:is-open="{{ $isOpen ? 'true' : 'false' }}"
|
||||
type="button"
|
||||
class="icon-list-item {{ $classes ?? '' }}">
|
||||
<span>@icon('expand-text')</span>
|
||||
<span>{{ trans('common.toggle_details') }}</span>
|
||||
</button>
|
||||
|
@ -3,7 +3,9 @@
|
||||
@section('body')
|
||||
<div class="mt-m">
|
||||
<main class="content-wrap card">
|
||||
<div class="page-content" page-display="{{ $customHomepage->id }}">
|
||||
<div component="page-display"
|
||||
option:page-display:page-id="{{ $page->id }}"
|
||||
class="page-content">
|
||||
@include('pages.parts.page-display', ['page' => $customHomepage])
|
||||
</div>
|
||||
</main>
|
||||
|
@ -49,7 +49,7 @@
|
||||
|
||||
@include('common.footer')
|
||||
|
||||
<div back-to-top class="primary-background print-hidden">
|
||||
<div component="back-to-top" class="back-to-top print-hidden">
|
||||
<div class="inner">
|
||||
@icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
|
||||
</div>
|
||||
|
@ -1,15 +1,15 @@
|
||||
<div editor-toolbox class="floating-toolbox">
|
||||
<div component="editor-toolbox" class="floating-toolbox">
|
||||
|
||||
<div class="tabs primary-background-light">
|
||||
<button type="button" toolbox-toggle aria-expanded="false">@icon('caret-left-circle')</button>
|
||||
<button type="button" toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
|
||||
<button type="button" refs="editor-toolbox@toggle" aria-expanded="false" class="toolbox-toggle">@icon('caret-left-circle')</button>
|
||||
<button type="button" refs="editor-toolbox@tab-button" data-tab="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
|
||||
@if(userCan('attachment-create-all'))
|
||||
<button type="button" toolbox-tab-button="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
|
||||
<button type="button" refs="editor-toolbox@tab-button" data-tab="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
|
||||
@endif
|
||||
<button type="button" toolbox-tab-button="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
|
||||
<button type="button" refs="editor-toolbox@tab-button" data-tab="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
|
||||
</div>
|
||||
|
||||
<div toolbox-tab-content="tags">
|
||||
<div refs="editor-toolbox@tab-content" data-tab-content="tags" class="toolbox-tab-content">
|
||||
<h4>{{ trans('entities.page_tags') }}</h4>
|
||||
<div class="px-l">
|
||||
@include('entities.tag-manager', ['entity' => $page])
|
||||
@ -20,7 +20,7 @@
|
||||
@include('attachments.manager', ['page' => $page])
|
||||
@endif
|
||||
|
||||
<div toolbox-tab-content="templates">
|
||||
<div refs="editor-toolbox@tab-content" data-tab-content="templates" class="toolbox-tab-content">
|
||||
<h4>{{ trans('entities.templates') }}</h4>
|
||||
|
||||
<div class="px-l">
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div template-manager>
|
||||
<div component="template-manager">
|
||||
@if(userCan('templates-manage'))
|
||||
<p class="text-muted small mb-none">
|
||||
{{ trans('entities.templates_explain_set_as_template') }}
|
||||
@ -11,15 +11,13 @@
|
||||
<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>
|
@ -17,7 +17,9 @@
|
||||
</div>
|
||||
|
||||
<main class="content-wrap card">
|
||||
<div class="page-content clearfix" page-display="{{ $page->id }}">
|
||||
<div component="page-display"
|
||||
option:page-display:page-id="{{ $page->id }}"
|
||||
class="page-content clearfix">
|
||||
@include('pages.parts.page-display')
|
||||
</div>
|
||||
@include('pages.parts.pointer', ['page' => $page])
|
||||
|
@ -59,13 +59,16 @@
|
||||
<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>
|
||||
@ -90,20 +93,22 @@
|
||||
</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>
|
||||
|
@ -1,13 +1,13 @@
|
||||
|
||||
{{--Depends on entity selector popup--}}
|
||||
<div page-picker>
|
||||
<div component="page-picker">
|
||||
<div class="input-base">
|
||||
<span @if($value) style="display: none" @endif page-picker-default class="text-muted italic">{{ $placeholder }}</span>
|
||||
<a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" rel="noopener" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
|
||||
<span @if($value) style="display: none" @endif refs="page-picker@default-display" class="text-muted italic">{{ $placeholder }}</span>
|
||||
<a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" rel="noopener" class="text-page" refs="page-picker@display">#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
|
||||
</div>
|
||||
<br>
|
||||
<input type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
|
||||
<button @if(!$value) style="display: none" @endif type="button" page-picker-reset class="text-button">{{ trans('common.reset') }}</button>
|
||||
<span @if(!$value) style="display: none" @endif class="sep">|</span>
|
||||
<button type="button" page-picker-select class="text-button">{{ trans('common.select') }}</button>
|
||||
<input refs="page-picker@input" type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
|
||||
<button @if(!$value) style="display: none" @endif type="button" refs="page-picker@reset-button" class="text-button">{{ trans('common.reset') }}</button>
|
||||
<span refs="page-picker@button-seperator" @if(!$value) style="display: none" @endif class="sep">|</span>
|
||||
<button type="button" refs="page-picker@select-button" class="text-button">{{ trans('common.select') }}</button>
|
||||
</div>
|
@ -1,17 +1,19 @@
|
||||
{{--
|
||||
@type - Name of entity type
|
||||
--}}
|
||||
<div setting-color-picker class="grid no-break half mb-l">
|
||||
<div component="setting-color-picker"
|
||||
option:setting-color-picker:default="{{ config('setting-defaults.'. $type .'-color') }}"
|
||||
option:setting-color-picker:current="{{ setting($type .'-color') }}"
|
||||
class="grid no-break half mb-l">
|
||||
<div>
|
||||
<label for="setting-{{ $type }}-color" class="text-dark">{{ trans('settings.'. str_replace('-', '_', $type) .'_color') }}</label>
|
||||
<button type="button" class="text-button text-muted" setting-color-picker-default>{{ trans('common.default') }}</button>
|
||||
<button refs="setting-color-picker@default-button" type="button" class="text-button text-muted">{{ trans('common.default') }}</button>
|
||||
<span class="sep">|</span>
|
||||
<button type="button" class="text-button text-muted" setting-color-picker-reset>{{ trans('common.reset') }}</button>
|
||||
<button refs="setting-color-picker@reset-button" type="button" class="text-button text-muted">{{ trans('common.reset') }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<input type="color"
|
||||
data-default="{{ config('setting-defaults.'. $type .'-color') }}"
|
||||
data-current="{{ setting($type .'-color') }}"
|
||||
refs="setting-color-picker@input"
|
||||
value="{{ setting($type .'-color') }}"
|
||||
name="setting-{{ $type }}-color"
|
||||
id="setting-{{ $type }}-color"
|
||||
|
@ -43,11 +43,11 @@
|
||||
|
||||
|
||||
|
||||
<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', [
|
||||
@ -59,11 +59,11 @@
|
||||
</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>
|
||||
|
@ -48,7 +48,7 @@
|
||||
@endif
|
||||
|
||||
@if($authMethod === 'standard')
|
||||
<div new-user-password>
|
||||
<div component="new-user-password">
|
||||
<label class="setting-list-label">{{ trans('settings.users_password') }}</label>
|
||||
|
||||
@if(!isset($model))
|
||||
@ -61,10 +61,9 @@
|
||||
'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">
|
||||
|
Loading…
Reference in New Issue
Block a user