# JavaScript Components
-This document details the format for JavaScript components in BookStack.
+This document details the format for JavaScript components in BookStack. This is a really simple class-based setup with a few helpers provided.
#### Defining a Component in JS
```js
class Dropdown {
setup() {
+ this.toggle = this.$refs.toggle;
+ this.menu = this.$refs.menu;
+
+ this.speed = parseInt(this.$opts.speed);
}
}
```
+All usage of $refs, $manyRefs and $opts should be done at the top of the `setup` function so any requirements can be easily seen.
+
#### Using a Component in HTML
A component is used like so:
--- /dev/null
+import {onSelect} from "../services/dom";
+
+/**
+ * Class EntitySearch
+ * @extends {Component}
+ */
+class EntitySearch {
+ setup() {
+ this.entityId = this.$opts.entityId;
+ this.entityType = this.$opts.entityType;
+
+ this.contentView = this.$refs.contentView;
+ this.searchView = this.$refs.searchView;
+ this.searchResults = this.$refs.searchResults;
+ this.searchInput = this.$refs.searchInput;
+ this.searchForm = this.$refs.searchForm;
+ this.clearButton = this.$refs.clearButton;
+ this.loadingBlock = this.$refs.loadingBlock;
+
+ this.setupListeners();
+ }
+
+ setupListeners() {
+ this.searchInput.addEventListener('change', this.runSearch.bind(this));
+ this.searchForm.addEventListener('submit', e => {
+ e.preventDefault();
+ this.runSearch();
+ });
+
+ onSelect(this.clearButton, this.clearSearch.bind(this));
+ }
+
+ runSearch() {
+ const term = this.searchInput.value.trim();
+ if (term.length === 0) {
+ return this.clearSearch();
+ }
+
+ this.searchView.classList.remove('hidden');
+ this.contentView.classList.add('hidden');
+ this.loadingBlock.classList.remove('hidden');
+
+ const url = window.baseUrl(`/search/${this.entityType}/${this.entityId}`);
+ window.$http.get(url, {term}).then(resp => {
+ this.searchResults.innerHTML = resp.data;
+ }).catch(console.error).then(() => {
+ this.loadingBlock.classList.add('hidden');
+ });
+ }
+
+ clearSearch() {
+ this.searchView.classList.add('hidden');
+ this.contentView.classList.remove('hidden');
+ this.loadingBlock.classList.add('hidden');
+ this.searchInput.value = '';
+ }
+}
+
+export default EntitySearch;
\ No newline at end of file
+++ /dev/null
-let data = {
- id: null,
- type: '',
- searching: false,
- searchTerm: '',
- searchResults: '',
-};
-
-let computed = {
-
-};
-
-let methods = {
-
- searchBook() {
- if (this.searchTerm.trim().length === 0) return;
- this.searching = true;
- this.searchResults = '';
- let url = window.baseUrl(`/search/${this.type}/${this.id}`);
- url += `?term=${encodeURIComponent(this.searchTerm)}`;
- this.$http.get(url).then(resp => {
- this.searchResults = resp.data;
- });
- },
-
- checkSearchForm() {
- this.searching = this.searchTerm > 0;
- },
-
- clearSearch() {
- this.searching = false;
- this.searchTerm = '';
- }
-
-};
-
-function mounted() {
- this.id = Number(this.$el.getAttribute('entity-id'));
- this.type = this.$el.getAttribute('entity-type');
-}
-
-export default {
- data, computed, methods, mounted
-};
\ No newline at end of file
return document.getElementById(id) !== null;
}
-import entityDashboard from "./entity-dashboard";
import imageManager from "./image-manager";
import tagManager from "./tag-manager";
import attachmentManager from "./attachment-manager";
import pageEditor from "./page-editor";
let vueMapping = {
- 'entity-dashboard': entityDashboard,
'image-manager': imageManager,
'tag-manager': tagManager,
'attachment-manager': attachmentManager,
@extends('tri-layout')
@section('container-attrs')
- id="entity-dashboard"
- entity-id="{{ $book->id }}"
- entity-type="book"
+ component="entity-search"
+ option:entity-search:entity-id="{{ $book->id }}"
+ option:entity-search:entity-type="book"
@stop
@section('body')
</div>
<main class="content-wrap card">
- <h1 class="break-text" v-pre>{{$book->name}}</h1>
- <div class="book-content" v-show="!searching">
- <p class="text-muted" v-pre>{!! nl2br(e($book->description)) !!}</p>
+ <h1 class="break-text">{{$book->name}}</h1>
+ <div refs="entity-search@contentView" class="book-content">
+ <p class="text-muted">{!! nl2br(e($book->description)) !!}</p>
@if(count($bookChildren) > 0)
- <div class="entity-list book-contents" v-pre>
+ <div class="entity-list book-contents">
@foreach($bookChildren as $childElement)
@if($childElement->isA('chapter'))
@include('chapters.list-item', ['chapter' => $childElement])
@endforeach
</div>
@else
- <div class="mt-xl" v-pre>
+ <div class="mt-xl">
<hr>
<p class="text-muted italic mb-m mt-xl">{{ trans('entities.books_empty_contents') }}</p>
@endif
</div>
- @include('partials.entity-dashboard-search-results')
+ @include('partials.entity-search-results')
</main>
@stop
@section('left')
- @include('partials.entity-dashboard-search-box')
+ @include('partials.entity-search-form', ['label' => trans('entities.books_search_this')])
@if($book->tags->count() > 0)
<div class="mb-xl">
@extends('tri-layout')
@section('container-attrs')
- id="entity-dashboard"
- entity-id="{{ $chapter->id }}"
- entity-type="chapter"
+ component="entity-search"
+ option:entity-search:entity-id="{{ $chapter->id }}"
+ option:entity-search:entity-type="chapter"
@stop
@section('body')
</div>
<main class="content-wrap card">
- <h1 class="break-text" v-pre>{{ $chapter->name }}</h1>
- <div class="chapter-content" v-show="!searching">
- <p v-pre class="text-muted break-text">{!! nl2br(e($chapter->description)) !!}</p>
+ <h1 class="break-text">{{ $chapter->name }}</h1>
+ <div refs="entity-search@contentView" class="chapter-content">
+ <p class="text-muted break-text">{!! nl2br(e($chapter->description)) !!}</p>
@if(count($pages) > 0)
- <div v-pre class="entity-list book-contents">
+ <div class="entity-list book-contents">
@foreach($pages as $page)
@include('pages.list-item', ['page' => $page])
@endforeach
</div>
@else
- <div class="mt-xl" v-pre>
+ <div class="mt-xl">
<hr>
<p class="text-muted italic mb-m mt-xl">{{ trans('entities.chapters_empty') }}</p>
@endif
</div>
- @include('partials.entity-dashboard-search-results')
+ @include('partials.entity-search-results')
</main>
@stop
@section('left')
- @include('partials.entity-dashboard-search-box')
+ @include('partials.entity-search-form', ['label' => trans('entities.chapters_search_this')])
@if($chapter->tags->count() > 0)
<div class="mb-xl">
+++ /dev/null
-<div class="mb-xl">
- <form v-on:submit.prevent="searchBook" class="search-box flexible" role="search">
- <input v-model="searchTerm" v-on:change="checkSearchForm" type="text" aria-label="{{ trans('entities.books_search_this') }}" name="term" placeholder="{{ trans('entities.books_search_this') }}">
- <button type="submit" aria-label="{{ trans('common.search') }}">@icon('search')</button>
- <button v-if="searching" v-cloak class="search-box-cancel text-neg" v-on:click="clearSearch"
- type="button" aria-label="{{ trans('common.search_clear') }}">@icon('close')</button>
- </form>
-</div>
\ No newline at end of file
+++ /dev/null
-<div class="search-results" v-cloak v-show="searching">
- <div class="grid half v-center">
- <h3 class="text-muted px-none">
- {{ trans('entities.search_results') }}
- </h3>
- <div class="text-right">
- <a v-if="searching" v-on:click="clearSearch" class="button outline">{{ trans('entities.search_clear') }}</a>
- </div>
- </div>
-
- <div v-if="!searchResults">
- @include('partials.loading-icon')
- </div>
- <div class="book-contents" v-html="searchResults"></div>
-</div>
\ No newline at end of file
--- /dev/null
+{{--
+@label - Placeholder/aria-label text
+--}}
+<div class="mb-xl">
+ <form refs="entity-search@searchForm" class="search-box flexible" role="search">
+ <input refs="entity-search@searchInput" type="text"
+ aria-label="{{ $label }}" name="term" placeholder="{{ $label }}">
+ <button type="submit" aria-label="{{ trans('common.search') }}">@icon('search')</button>
+ </form>
+</div>
\ No newline at end of file
--- /dev/null
+<div refs="entity-search@searchView" class="search-results hidden">
+ <div class="grid half v-center">
+ <h3 class="text-muted px-none">
+ {{ trans('entities.search_results') }}
+ </h3>
+ <div class="text-right">
+ <a refs="entity-search@clearButton" class="button outline">{{ trans('entities.search_clear') }}</a>
+ </div>
+ </div>
+
+ <div refs="entity-search@loadingBlock">
+ @include('partials.loading-icon')
+ </div>
+ <div class="book-contents" refs="entity-search@searchResults"></div>
+</div>
\ No newline at end of file