import {slideDown, slideUp} from "../services/animations";
+import {Component} from "./component";
/**
* Collapsible
* Provides some simple logic to allow collapsible sections.
*/
-class Collapsible {
+export class Collapsible extends Component {
- constructor(elem) {
- this.elem = elem;
- this.trigger = elem.querySelector('[collapsible-trigger]');
- this.content = elem.querySelector('[collapsible-content]');
+ setup() {
+ this.container = this.$el;
+ this.trigger = this.$refs.trigger;
+ this.content = this.$refs.content;
- if (!this.trigger) return;
- this.trigger.addEventListener('click', this.toggle.bind(this));
- this.openIfContainsError();
+ if (this.trigger) {
+ this.trigger.addEventListener('click', this.toggle.bind(this));
+ this.openIfContainsError();
+ }
}
open() {
- this.elem.classList.add('open');
+ this.container.classList.add('open');
this.trigger.setAttribute('aria-expanded', 'true');
slideDown(this.content, 300);
}
close() {
- this.elem.classList.remove('open');
+ this.container.classList.remove('open');
this.trigger.setAttribute('aria-expanded', 'false');
slideUp(this.content, 300);
}
toggle() {
- if (this.elem.classList.contains('open')) {
+ if (this.container.classList.contains('open')) {
this.close();
} else {
this.open();
}
}
-}
-
-export default Collapsible;
\ No newline at end of file
+}
\ No newline at end of file
import {debounce} from "../services/util";
import {transitionHeight} from "../services/animations";
+import {Component} from "./component";
-class DropdownSearch {
+export class DropdownSearch extends Component {
setup() {
this.elem = this.$el;
this.loadingElem.style.display = show ? 'block' : 'none';
}
-}
-
-export default DropdownSearch;
\ No newline at end of file
+}
\ No newline at end of file
import {slideUp, slideDown} from "../services/animations";
+import {Component} from "./component";
-class ExpandToggle {
+export class ExpandToggle extends Component {
- constructor(elem) {
- this.elem = elem;
-
- // Component state
- this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
- this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
- this.selector = elem.getAttribute('expand-toggle');
+ setup(elem) {
+ this.targetSelector = this.$opts.targetSelector;
+ this.isOpen = this.$opts.isOpen === 'true';
+ this.updateEndpoint = this.$opts.updateEndpoint;
// Listener setup
- elem.addEventListener('click', this.click.bind(this));
+ this.$el.addEventListener('click', this.click.bind(this));
}
open(elemToToggle) {
click(event) {
event.preventDefault();
- const matchingElems = document.querySelectorAll(this.selector);
+ const matchingElems = document.querySelectorAll(this.targetSelector);
for (let match of matchingElems) {
this.isOpen ? this.close(match) : this.open(match);
}
});
}
-}
-
-export default ExpandToggle;
\ No newline at end of file
+}
\ No newline at end of file
+import {Component} from "./component";
-class HeaderMobileToggle {
+export class HeaderMobileToggle extends Component {
setup() {
this.elem = this.$el;
this.onToggle(event);
}
-}
-
-export default HeaderMobileToggle;
\ No newline at end of file
+}
\ No newline at end of file
+import {Component} from "./component";
-class ImagePicker {
+export class ImagePicker extends Component {
- constructor(elem) {
- this.elem = elem;
- this.imageElem = elem.querySelector('img');
- this.imageInput = elem.querySelector('input[type=file]');
- this.resetInput = elem.querySelector('input[data-reset-input]');
- this.removeInput = elem.querySelector('input[data-remove-input]');
+ setup() {
+ this.imageElem = this.$refs.image;
+ this.imageInput = this.$refs.imageInput;
+ this.resetInput = this.$refs.resetInput;
+ this.removeInput = this.$refs.removeInput;
+ this.resetButton = this.$refs.resetButton;
+ this.removeButton = this.$refs.removeButton || null;
- this.defaultImage = elem.getAttribute('data-default-image');
+ this.defaultImage = this.$opts.defaultImage;
- const resetButton = elem.querySelector('button[data-action="reset-image"]');
- resetButton.addEventListener('click', this.reset.bind(this));
+ this.setupListeners();
+ }
+
+ setupListeners() {
+ this.resetButton.addEventListener('click', this.reset.bind(this));
- const removeButton = elem.querySelector('button[data-action="remove-image"]');
- if (removeButton) {
- removeButton.addEventListener('click', this.removeImage.bind(this));
+ if (this.removeButton) {
+ this.removeButton.addEventListener('click', this.removeImage.bind(this));
}
this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
this.resetInput.setAttribute('disabled', 'disabled');
}
-}
-
-export default ImagePicker;
\ No newline at end of file
+}
\ No newline at end of file
// export {CodeEditor} from "./code-editor.js"
export {CodeHighlighter} from "./code-highlighter.js"
export {CodeTextarea} from "./code-textarea.js"
-// export {Collapsible} from "./collapsible.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 {DropdownSearch} from "./dropdown-search.js"
// export {Dropzone} from "./dropzone.js"
// export {EditorToolbox} from "./editor-toolbox.js"
export {EntityPermissions} from "./entity-permissions"
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 {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 {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 {NewUserPassword} from "./new-user-password.js"
export {Notification} from "./notification.js"
-// export {OptionalInput} from "./optional-input.js"
+export {OptionalInput} from "./optional-input.js"
export {PageComments} from "./page-comments.js"
-// export {PageDisplay} from "./page-display.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 {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 {ShelfSort} from "./shelf-sort.js"
export {Shortcuts} from "./shortcuts"
export {ShortcutInput} from "./shortcut-input"
-// export {Sidebar} from "./sidebar.js"
// export {SortableList} from "./sortable-list.js"
-// export {SubmitOnChange} from "./submit-on-change.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 {TriLayout} from "./tri-layout.js"
+export {UserSelect} from "./user-select.js"
+export {WebhookEvents} from "./webhook-events";
// export {WysiwygEditor} from "./wysiwyg-editor.js"
\ No newline at end of file
/**
* ListSortControl
* Manages the logic for the control which provides list sorting options.
- * @extends {Component}
*/
-class ListSortControl {
+import {Component} from "./component";
+
+export class ListSortControl extends Component {
setup() {
this.elem = this.$el;
this.form.submit();
}
-}
-
-export default ListSortControl;
\ No newline at end of file
+}
\ No newline at end of file
+import {Component} from "./component";
-class NewUserPassword {
+export class NewUserPassword extends Component {
- constructor(elem) {
- this.elem = elem;
- this.inviteOption = elem.querySelector('input[name=send_invite]');
+ setup() {
+ this.container = this.$el;
+ this.inputContainer = this.$refs.inputContainer;
+ this.inviteOption = this.container.querySelector('input[name=send_invite]');
if (this.inviteOption) {
this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
inviteOptionChange() {
const inviting = (this.inviteOption.value === 'true');
- const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
+ const passwordBoxes = this.container.querySelectorAll('input[type=password]');
for (const input of passwordBoxes) {
input.disabled = inviting;
}
- const container = this.elem.querySelector('#password-input-container');
- if (container) {
- container.style.display = inviting ? 'none' : 'block';
- }
- }
-}
+ this.inputContainer.style.display = inviting ? 'none' : 'block';
+ }
-export default NewUserPassword;
\ No newline at end of file
+}
\ No newline at end of file
import {onSelect} from "../services/dom";
+import {Component} from "./component";
-class OptionalInput {
+export class OptionalInput extends Component {
setup() {
this.removeButton = this.$refs.remove;
this.showButton = this.$refs.show;
});
}
-}
-
-export default OptionalInput;
\ No newline at end of file
+}
\ No newline at end of file
import * as DOM from "../services/dom";
import {scrollAndHighlightElement} from "../services/util";
+import {Component} from "./component";
-class PageDisplay {
+export class PageDisplay extends Component {
- constructor(elem) {
- this.elem = elem;
- this.pageId = elem.getAttribute('page-display');
+ setup() {
+ this.container = this.$el;
+ this.pageId = this.$opts.pageId;
window.importVersioned('code').then(Code => Code.highlight());
this.setupNavHighlighting();
// Check the hash on load
if (window.location.hash) {
- let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
+ const text = window.location.hash.replace(/%20/g, ' ').substring(1);
this.goToText(text);
}
}
setupNavHighlighting() {
- // Check if support is present for IntersectionObserver
- if (!('IntersectionObserver' in window) ||
- !('IntersectionObserverEntry' in window) ||
- !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
- return;
- }
-
- let pageNav = document.querySelector('.sidebar-page-nav');
+ const pageNav = document.querySelector('.sidebar-page-nav');
// fetch all the headings.
- let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
+ const headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
// if headings are present, add observers.
if (headings.length > 0 && pageNav !== null) {
addNavObserver(headings);
function addNavObserver(headings) {
// Setup the intersection observer.
- let intersectOpts = {
+ const intersectOpts = {
rootMargin: '0px 0px 0px 0px',
threshold: 1.0
};
- let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
+ const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
// observe each heading
- for (let heading of headings) {
+ for (const heading of headings) {
pageNavObserver.observe(heading);
}
}
function headingVisibilityChange(entries, observer) {
- for (let entry of entries) {
- let isVisible = (entry.intersectionRatio === 1);
+ for (const entry of entries) {
+ const isVisible = (entry.intersectionRatio === 1);
toggleAnchorHighlighting(entry.target.id, isVisible);
}
}
codeMirrors.forEach(cm => cm.CodeMirror && cm.CodeMirror.refresh());
};
- const details = [...this.elem.querySelectorAll('details')];
+ const details = [...this.container.querySelectorAll('details')];
details.forEach(detail => detail.addEventListener('toggle', onToggle));
}
-}
-
-export default PageDisplay;
+}
\ No newline at end of file
import Sortable from "sortablejs";
+import {Component} from "./component";
-class ShelfSort {
+export class ShelfSort extends Component {
setup() {
this.elem = this.$el;
initSortable() {
const scrollBoxes = this.elem.querySelectorAll('.scroll-box');
- for (let scrollBox of scrollBoxes) {
+ for (const scrollBox of scrollBoxes) {
new Sortable(scrollBox, {
group: 'shelf-books',
ghostClass: 'primary-background-light',
+++ /dev/null
-
-class Sidebar {
-
- constructor(elem) {
- this.elem = elem;
- this.toggleElem = elem.querySelector('.sidebar-toggle');
- this.toggleElem.addEventListener('click', this.toggle.bind(this));
- }
-
- toggle(show = true) {
- this.elem.classList.toggle('open');
- }
-
-}
-
-export default Sidebar;
\ No newline at end of file
+import {Component} from "./component";
+
/**
* Submit on change
* Simply submits a parent form when this input is changed.
- * @extends {Component}
*/
-class SubmitOnChange {
+export class SubmitOnChange extends Component {
setup() {
this.filter = this.$opts.filter;
});
}
-}
-
-export default SubmitOnChange;
\ No newline at end of file
+}
\ No newline at end of file
+import {Component} from "./component";
-class TriLayout {
+export class TriLayout extends Component {
setup() {
this.container = this.$refs.container;
this.lastTabShown = tabName;
}
-}
-
-export default TriLayout;
\ No newline at end of file
+}
\ No newline at end of file
import {onChildEvent} from "../services/dom";
+import {Component} from "./component";
-class UserSelect {
+export class UserSelect extends Component {
setup() {
this.input = this.$refs.input;
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();
}
-}
-
-export default UserSelect;
\ No newline at end of file
+}
\ No newline at end of file
-
/**
* Webhook Events
* Manages dynamic selection control in the webhook form interface.
- * @extends {Component}
*/
-class WebhookEvents {
+import {Component} from "./component";
+
+export class WebhookEvents extends Component {
setup() {
this.checkboxes = this.$el.querySelectorAll('input[type="checkbox"]');
}
}
-}
-
-export default WebhookEvents;
\ No newline at end of file
+}
\ No newline at end of file
}
}
-.form-group[collapsible] {
+.form-group.collapsible {
padding: 0 $-m;
border: 1px solid;
@include lightDark(border-color, #DDD, #000);
@include('form.textarea', ['name' => 'description'])
</div>
-<div class="form-group" collapsible id="logo-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label>{{ trans('common.cover_image') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
<p class="small">{{ trans('common.cover_image_description') }}</p>
@include('form.image-picker', [
</div>
</div>
-<div class="form-group" collapsible id="tags-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="tags-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label for="tag-manager">{{ trans('entities.book_tags') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
@include('entities.tag-manager', ['entity' => $book ?? null])
</div>
</div>
@include('form.textarea', ['name' => 'description'])
</div>
-<div class="form-group" collapsible id="logo-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label for="tags">{{ trans('entities.chapter_tags') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
@include('entities.tag-manager', ['entity' => $chapter ?? null])
</div>
</div>
-<div 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>
$key - Unique key for checking existing stored state.
--}}
<?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
-<button type="button" expand-toggle="{{ $target }}"
- expand-toggle-update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
- expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
- class="icon-list-item {{ $classes ?? '' }}">
+<button component="expand-toggle"
+ option:expand-toggle:target-selector="{{ $target }}"
+ option:expand-toggle:update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
+ option:expand-toggle:is-open="{{ $isOpen ? 'true' : 'false' }}"
+ type="button"
+ class="icon-list-item {{ $classes ?? '' }}">
<span>@icon('expand-text')</span>
<span>{{ trans('common.toggle_details') }}</span>
</button>
@section('body')
<div class="mt-m">
<main class="content-wrap card">
- <div class="page-content" page-display="{{ $customHomepage->id }}">
+ <div component="page-display"
+ option:page-display:page-id="{{ $page->id }}"
+ class="page-content">
@include('pages.parts.page-display', ['page' => $customHomepage])
</div>
</main>
</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])
-<div class="form-group" collapsible id="logo-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label>{{ trans('common.cover_image') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
<p class="small">{{ trans('common.cover_image_description') }}</p>
@include('form.image-picker', [
</div>
</div>
-<div class="form-group" collapsible id="tags-control">
- <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="tags-control">
+ <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
<label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
</button>
- <div class="collapse-content" collapsible-content>
+ <div refs="collapsible@content" class="collapse-content">
@include('entities.tag-manager', ['entity' => $shelf ?? null])
</div>
</div>
@endif
@if($authMethod === 'standard')
- <div new-user-password>
+ <div component="new-user-password">
<label class="setting-list-label">{{ trans('settings.users_password') }}</label>
@if(!isset($model))
'value' => old('send_invite', 'true') === 'true',
'label' => trans('settings.users_send_invite_option')
])
-
@endif
- <div id="password-input-container" @if(!isset($model)) style="display: none;" @endif>
+ <div refs="new-user-password@input-container" @if(!isset($model)) style="display: none;" @endif>
<p class="small">{{ trans('settings.users_password_desc') }}</p>
@if(isset($model))
<p class="small">