--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Http\Request;
+
+class UserSearchController extends Controller
+{
+ /**
+ * Search users in the system, with the response formatted
+ * for use in a select-style list.
+ */
+ public function forSelect(Request $request)
+ {
+ $search = $request->get('search', '');
+ $query = User::query()->orderBy('name', 'desc')
+ ->take(20);
+
+ if (!empty($search)) {
+ $query->where(function(Builder $query) use ($search) {
+ $query->where('email', 'like', '%' . $search . '%')
+ ->orWhere('name', 'like', '%' . $search . '%');
+ });
+ }
+
+ $users = $query->get();
+ return view('form.user-select-list', compact('users'));
+ }
+}
+++ /dev/null
-
-class BreadcrumbListing {
-
- setup() {
- this.elem = this.$el;
- this.searchInput = this.$refs.searchInput;
- this.loadingElem = this.$refs.loading;
- this.entityListElem = this.$refs.entityList;
-
- this.entityType = this.$opts.entityType;
- this.entityId = Number(this.$opts.entityId);
-
- this.elem.addEventListener('show', this.onShow.bind(this));
- this.searchInput.addEventListener('input', this.onSearch.bind(this));
- }
-
- onShow() {
- this.loadEntityView();
- }
-
- onSearch() {
- const input = this.searchInput.value.toLowerCase().trim();
- const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
- for (let listItem of listItems) {
- const match = !input || listItem.textContent.toLowerCase().includes(input);
- listItem.style.display = match ? 'flex' : 'none';
- listItem.classList.toggle('hidden', !match);
- }
- }
-
- loadEntityView() {
- this.toggleLoading(true);
-
- const params = {
- 'entity_id': this.entityId,
- 'entity_type': this.entityType,
- };
-
- window.$http.get('/search/entity/siblings', params).then(resp => {
- this.entityListElem.innerHTML = resp.data;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.toggleLoading(false);
- this.onSearch();
- });
- }
-
- toggleLoading(show = false) {
- this.loadingElem.style.display = show ? 'block' : 'none';
- }
-
-}
-
-export default BreadcrumbListing;
\ No newline at end of file
--- /dev/null
+import {debounce} from "../services/util";
+
+class DropdownSearch {
+
+ setup() {
+ this.elem = this.$el;
+ this.searchInput = this.$refs.searchInput;
+ this.loadingElem = this.$refs.loading;
+ this.listContainerElem = this.$refs.listContainer;
+
+ this.localSearchSelector = this.$opts.localSearchSelector;
+ this.url = this.$opts.url;
+
+ this.elem.addEventListener('show', this.onShow.bind(this));
+ this.searchInput.addEventListener('input', this.onSearch.bind(this));
+
+ this.runAjaxSearch = debounce(this.runAjaxSearch, 300, false);
+ }
+
+ onShow() {
+ this.loadList();
+ }
+
+ onSearch() {
+ const input = this.searchInput.value.toLowerCase().trim();
+ if (this.localSearchSelector) {
+ this.runLocalSearch(input);
+ } else {
+ this.toggleLoading(true);
+ this.runAjaxSearch(input);
+ }
+ }
+
+ runAjaxSearch(searchTerm) {
+ this.loadList(searchTerm);
+ }
+
+ runLocalSearch(searchTerm) {
+ const listItems = this.listContainerElem.querySelectorAll(this.localSearchSelector);
+ for (let listItem of listItems) {
+ const match = !searchTerm || listItem.textContent.toLowerCase().includes(searchTerm);
+ listItem.style.display = match ? 'flex' : 'none';
+ listItem.classList.toggle('hidden', !match);
+ }
+ }
+
+ async loadList(searchTerm = '') {
+ this.listContainerElem.innerHTML = '';
+ this.toggleLoading(true);
+
+ try {
+ const resp = await window.$http.get(this.getAjaxUrl(searchTerm));
+ this.listContainerElem.innerHTML = resp.data;
+ } catch (err) {
+ console.error(err);
+ }
+
+ this.toggleLoading(false);
+ if (this.localSearchSelector) {
+ this.onSearch();
+ }
+ }
+
+ getAjaxUrl(searchTerm = null) {
+ if (!searchTerm) {
+ return this.url;
+ }
+
+ const joiner = this.url.includes('?') ? '&' : '?';
+ return `${this.url}${joiner}search=${encodeURIComponent(searchTerm)}`;
+ }
+
+ toggleLoading(show = false) {
+ this.loadingElem.style.display = show ? 'block' : 'none';
+ }
+
+}
+
+export default DropdownSearch;
\ No newline at end of file
this.body = document.body;
this.showing = false;
this.setupListeners();
+ this.hide = this.hide.bind(this);
}
show(event = null) {
import autoSuggest from "./auto-suggest.js"
import backToTop from "./back-to-top.js"
import bookSort from "./book-sort.js"
-import breadcrumbListing from "./breadcrumb-listing.js"
import chapterToggle from "./chapter-toggle.js"
import codeEditor from "./code-editor.js"
import codeHighlighter from "./code-highlighter.js"
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 entityPermissionsEditor from "./entity-permissions-editor.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 wysiwygEditor from "./wysiwyg-editor.js"
const componentMapping = {
"auto-suggest": autoSuggest,
"back-to-top": backToTop,
"book-sort": bookSort,
- "breadcrumb-listing": breadcrumbListing,
"chapter-toggle": chapterToggle,
"code-editor": codeEditor,
"code-highlighter": codeHighlighter,
"custom-checkbox": customCheckbox,
"details-highlighter": detailsHighlighter,
"dropdown": dropdown,
+ "dropdown-search": dropdownSearch,
"dropzone": dropzone,
"editor-toolbox": editorToolbox,
"entity-permissions-editor": entityPermissionsEditor,
"template-manager": templateManager,
"toggle-switch": toggleSwitch,
"tri-layout": triLayout,
+ "user-select": userSelect,
"wysiwyg-editor": wysiwygEditor,
};
--- /dev/null
+import {onChildEvent} from "../services/dom";
+
+class UserSelect {
+
+ setup() {
+
+ 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));
+ }
+
+ selectUser(event, userEl) {
+ const id = userEl.getAttribute('data-id');
+ this.input.value = id;
+ this.userInfoContainer.innerHTML = userEl.innerHTML;
+ this.hide();
+ }
+
+}
+
+export default UserSelect;
\ No newline at end of file
'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
'permissions_enable' => 'Enable Custom Permissions',
'permissions_save' => 'Save Permissions',
+ 'permissions_owner' => 'Owner',
// Search
'search_results' => 'Search Results',
.template-item-actions button:first-child {
border-top: 0;
}
+}
+
+.dropdown-search-dropdown {
+ box-shadow: $bs-med;
+ overflow: hidden;
+ min-height: 100px;
+ width: 240px;
+ display: none;
+ position: absolute;
+ z-index: 80;
+ right: -$-m;
+ @include rtl {
+ right: auto;
+ left: -$-m;
+ }
+ .dropdown-search-search .svg-icon {
+ position: absolute;
+ left: $-s;
+ @include rtl {
+ right: $-s;
+ left: auto;
+ }
+ top: 11px;
+ fill: #888;
+ pointer-events: none;
+ }
+ .dropdown-search-list {
+ max-height: 400px;
+ overflow-y: scroll;
+ text-align: start;
+ }
+ .dropdown-search-item {
+ padding: $-s $-m;
+ &:hover,&:focus {
+ background-color: #F2F2F2;
+ text-decoration: none;
+ }
+ }
+ input {
+ padding-inline-start: $-xl;
+ border-radius: 0;
+ border: 0;
+ border-bottom: 1px solid #DDD;
+ }
+}
+
+@include smaller-than($m) {
+ .dropdown-search-dropdown {
+ position: fixed;
+ right: auto;
+ left: $-m;
+ }
+ .dropdown-search-dropdown .dropdown-search-list {
+ max-height: 240px;
+ }
+}
+
+.custom-select-input {
+ max-width: 280px;
+ border: 1px solid #DDD;
+ border-radius: 4px;
}
\ No newline at end of file
}
}
-.breadcrumb-listing {
+.dropdown-search {
position: relative;
- .breadcrumb-listing-toggle {
+ .dropdown-search-toggle {
padding: 6px;
border: 1px solid transparent;
border-radius: 4px;
}
}
-.breadcrumb-listing-dropdown {
- box-shadow: $bs-med;
- overflow: hidden;
- min-height: 100px;
- width: 240px;
- display: none;
- position: absolute;
- z-index: 80;
- right: -$-m;
- @include rtl {
- right: auto;
- left: -$-m;
- }
- .breadcrumb-listing-search .svg-icon {
- position: absolute;
- left: $-s;
- @include rtl {
- right: $-s;
- left: auto;
- }
- top: 11px;
- fill: #888;
- pointer-events: none;
- }
- .breadcrumb-listing-entity-list {
- max-height: 400px;
- overflow-y: scroll;
- text-align: start;
- }
- input {
- padding-inline-start: $-xl;
- border-radius: 0;
- border: 0;
- border-bottom: 1px solid #DDD;
- }
-}
-
-@include smaller-than($m) {
- .breadcrumb-listing-dropdown {
- position: fixed;
- right: auto;
- left: $-m;
- }
- .breadcrumb-listing-dropdown .breadcrumb-listing-entity-list {
- max-height: 240px;
- }
-}
-
.faded {
a, button, span, span > div {
color: #666;
.justify-center {
justify-content: center;
}
+.items-center {
+ align-items: center;
+}
/**
--- /dev/null
+@foreach($users as $user)
+ <a href="#" class="flex-container-row items-center dropdown-search-item" data-id="{{ $user->id }}">
+ <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}">
+ <span>{{ $user->name }}</span>
+ </a>
+@endforeach
\ No newline at end of file
--- /dev/null
+<div class="dropdown-search custom-select-input" components="dropdown dropdown-search user-select"
+ option:dropdown-search:url="/search/users/select"
+>
+ <input refs="user-select@input" type="hidden" name="{{ $name }}" value="{{ $user->id }}">
+ <div refs="dropdown@toggle"
+ class="dropdown-search-toggle flex-container-row items-center"
+ aria-haspopup="true" aria-expanded="false" tabindex="0">
+ <div refs="user-select@user-info" class="flex-container-row items-center px-s">
+ <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}">
+ <span>{{ $user->name }}</span>
+ </div>
+ <span style="font-size: 1.5rem; margin-left: auto;">
+ @icon('caret-down')
+ </span>
+ </div>
+ <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu">
+ <div class="dropdown-search-search">
+ @icon('search')
+ <input refs="dropdown-search@searchInput"
+ aria-label="{{ trans('common.search') }}"
+ autocomplete="off"
+ placeholder="{{ trans('common.search') }}"
+ type="text">
+ </div>
+ <div refs="dropdown-search@loading" class="text-center">
+ @include('partials.loading-icon')
+ </div>
+ <div refs="dropdown-search@listContainer" class="dropdown-search-list"></div>
+ </div>
+</div>
\ No newline at end of file
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
- <p class="mb-none">{{ trans('entities.permissions_intro') }}</p>
-
- <div class="grid half">
- <div class="form-group">
- @include('form.checkbox', [
- 'name' => 'restricted',
- 'label' => trans('entities.permissions_enable'),
- ])
+ <div class="grid half left-focus v-center">
+ <div>
+ <p class="mb-none mt-m">{{ trans('entities.permissions_intro') }}</p>
+ <div>
+ @include('form.checkbox', [
+ 'name' => 'restricted',
+ 'label' => trans('entities.permissions_enable'),
+ ])
+ </div>
</div>
- <div class="form-group">
- <label for="owner">Owner</label>
+ <div>
+ <div class="form-group">
+ <label for="owner">{{ trans('entities.permissions_owner') }}</label>
+ @include('components.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by'])
+ </div>
</div>
</div>
+ <hr>
+
<table permissions-table class="table permissions-table toggle-switch-list" style="{{ !$model->restricted ? 'display: none' : '' }}">
<tr>
<th>{{ trans('common.role') }}</th>
-<div class="breadcrumb-listing" components="dropdown breadcrumb-listing"
- option:breadcrumb-listing:entity-type="{{ $entity->getType() }}"
- option:breadcrumb-listing:entity-id="{{ $entity->id }}"
- breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}">
- <div class="breadcrumb-listing-toggle" refs="dropdown@toggle"
+<div class="dropdown-search" components="dropdown dropdown-search"
+ option:dropdown-search:url="/search/entity/siblings?entity_type={{$entity->getType()}}&entity_id={{ $entity->id }}"
+ option:dropdown-search:local-search-selector=".entity-list-item"
+>
+ <div class="dropdown-search-toggle" refs="dropdown@toggle"
aria-haspopup="true" aria-expanded="false" tabindex="0">
<div class="separator">@icon('chevron-right')</div>
</div>
- <div refs="dropdown@menu" class="breadcrumb-listing-dropdown card" role="menu">
- <div class="breadcrumb-listing-search">
+ <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu">
+ <div class="dropdown-search-search">
@icon('search')
- <input refs="breadcrumb-listing@searchInput"
+ <input refs="dropdown-search@searchInput"
aria-label="{{ trans('common.search') }}"
autocomplete="off"
- name="entity-search"
placeholder="{{ trans('common.search') }}"
type="text">
</div>
- <div refs="breadcrumb-listing@loading">
+ <div refs="dropdown-search@loading">
@include('partials.loading-icon')
</div>
- <div refs="breadcrumb-listing@entityList" class="breadcrumb-listing-entity-list px-m"></div>
+ <div refs="dropdown-search@listContainer" class="dropdown-search-list px-m"></div>
</div>
</div>
\ No newline at end of file
Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
Route::get('/search/entity/siblings', 'SearchController@searchSiblings');
+ // User Search
+ Route::get('/search/users/select', 'UserSearchController@forSelect');
+
Route::get('/templates', 'PageTemplateController@list');
Route::get('/templates/{templateId}', 'PageTemplateController@get');