Skip to content

Commit

Permalink
Sorting: Added sort set form manager UI JS
Browse files Browse the repository at this point in the history
Extracted much code to be shared with the shelf books management UI
  • Loading branch information
ssddanbrown committed Feb 4, 2025
1 parent bf8a84a commit d28278b
Show file tree
Hide file tree
Showing 13 changed files with 168 additions and 103 deletions.
12 changes: 6 additions & 6 deletions app/Sorting/SortSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@
class SortSet extends Model
{
/**
* @return SortSetOption[]
* @return SortSetOperation[]
*/
public function getOptions(): array
public function getOperations(): array
{
$strOptions = explode(',', $this->sequence);
$options = array_map(fn ($val) => SortSetOption::tryFrom($val), $strOptions);
$options = array_map(fn ($val) => SortSetOperation::tryFrom($val), $strOptions);
return array_filter($options);
}

/**
* @param SortSetOption[] $options
* @param SortSetOperation[] $options
*/
public function setOptions(array $options): void
public function setOperations(array $options): void
{
$values = array_map(fn (SortSetOption $opt) => $opt->value, $options);
$values = array_map(fn (SortSetOperation $opt) => $opt->value, $options);
$this->sequence = implode(',', $values);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace BookStack\Sorting;

enum SortSetOption: string
enum SortSetOperation: string
{
case NameAsc = 'name_asc';
case NameDesc = 'name_desc';
Expand Down Expand Up @@ -34,11 +34,11 @@ public function getLabel(): string
}

/**
* @return SortSetOption[]
* @return SortSetOperation[]
*/
public static function allExcluding(array $options): array
public static function allExcluding(array $operations): array
{
$all = SortSetOption::cases();
return array_diff($all, $options);
$all = SortSetOperation::cases();
return array_diff($all, $operations);
}
}
2 changes: 2 additions & 0 deletions lang/en/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@
'sort_set_operations' => 'Sort Operations',
'sort_set_operations_desc' => 'Configure the sort actions to be performed in this set by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom.',
'sort_set_available_operations' => 'Available Operations',
'sort_set_available_operations_empty' => 'No operations remaining',
'sort_set_configured_operations' => 'Configured Operations',
'sort_set_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_set_op_asc' => '(Asc)',
'sort_set_op_desc' => '(Desc)',
'sort_set_op_name' => 'Name - Alphabetical',
Expand Down
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"devDependencies": {
"@lezer/generator": "^1.7.2",
"@types/sortablejs": "^1.15.8",
"chokidar-cli": "^3.0",
"esbuild": "^0.24.0",
"eslint": "^8.57.1",
Expand Down
1 change: 1 addition & 0 deletions resources/js/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export {ShelfSort} from './shelf-sort';
export {Shortcuts} from './shortcuts';
export {ShortcutInput} from './shortcut-input';
export {SortableList} from './sortable-list';
export {SortSetManager} from './sort-set-manager'
export {SubmitOnChange} from './submit-on-change';
export {Tabs} from './tabs';
export {TagManager} from './tag-manager';
Expand Down
48 changes: 4 additions & 44 deletions resources/js/components/shelf-sort.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,6 @@
import Sortable from 'sortablejs';
import {Component} from './component';

/**
* @type {Object<string, function(HTMLElement, HTMLElement, HTMLElement)>}
*/
const itemActions = {
move_up(item) {
const list = item.parentNode;
const index = Array.from(list.children).indexOf(item);
const newIndex = Math.max(index - 1, 0);
list.insertBefore(item, list.children[newIndex] || null);
},
move_down(item) {
const list = item.parentNode;
const index = Array.from(list.children).indexOf(item);
const newIndex = Math.min(index + 2, list.children.length);
list.insertBefore(item, list.children[newIndex] || null);
},
remove(item, shelfBooksList, allBooksList) {
allBooksList.appendChild(item);
},
add(item, shelfBooksList) {
shelfBooksList.appendChild(item);
},
};
import {buildListActions, sortActionClickListener} from '../services/dual-lists.ts';

export class ShelfSort extends Component {

Expand Down Expand Up @@ -55,12 +32,9 @@ export class ShelfSort extends Component {
}

setupListeners() {
this.elem.addEventListener('click', event => {
const sortItemAction = event.target.closest('.scroll-box-item button[data-action]');
if (sortItemAction) {
this.sortItemActionClick(sortItemAction);
}
});
const listActions = buildListActions(this.allBookList, this.shelfBookList);
const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));
this.elem.addEventListener('click', sortActionListener);

this.bookSearchInput.addEventListener('input', () => {
this.filterBooksByName(this.bookSearchInput.value);
Expand Down Expand Up @@ -93,20 +67,6 @@ export class ShelfSort extends Component {
}
}

/**
* Called when a sort item action button is clicked.
* @param {HTMLElement} sortItemAction
*/
sortItemActionClick(sortItemAction) {
const sortItem = sortItemAction.closest('.scroll-box-item');
const {action} = sortItemAction.dataset;

const actionFunction = itemActions[action];
actionFunction(sortItem, this.shelfBookList, this.allBookList);

this.onChange();
}

onChange() {
const shelfBookElems = Array.from(this.shelfBookList.querySelectorAll('[data-id]'));
this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
Expand Down
41 changes: 41 additions & 0 deletions resources/js/components/sort-set-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {Component} from "./component.js";
import Sortable from "sortablejs";
import {buildListActions, sortActionClickListener} from "../services/dual-lists";


export class SortSetManager extends Component {

protected input!: HTMLInputElement;
protected configuredList!: HTMLElement;
protected availableList!: HTMLElement;

setup() {
this.input = this.$refs.input as HTMLInputElement;
this.configuredList = this.$refs.configuredOperationsList;
this.availableList = this.$refs.availableOperationsList;

this.initSortable();

const listActions = buildListActions(this.availableList, this.configuredList);
const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));
this.$el.addEventListener('click', sortActionListener);
}

initSortable() {
const scrollBoxes = [this.configuredList, this.availableList];
for (const scrollBox of scrollBoxes) {
new Sortable(scrollBox, {
group: 'sort-set-operations',
ghostClass: 'primary-background-light',
handle: '.handle',
animation: 150,
onSort: this.onChange.bind(this),
});
}
}

onChange() {
const configuredOpEls = Array.from(this.configuredList.querySelectorAll('[data-id]'));
this.input.value = configuredOpEls.map(elem => elem.getAttribute('data-id')).join(',');
}
}
51 changes: 51 additions & 0 deletions resources/js/services/dual-lists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Service for helping manage common dual-list scenarios.
* (Shelf book manager, sort set manager).
*/

type ListActionsSet = Record<string, ((item: HTMLElement) => void)>;

export function buildListActions(
availableList: HTMLElement,
configuredList: HTMLElement,
): ListActionsSet {
return {
move_up(item) {
const list = item.parentNode as HTMLElement;
const index = Array.from(list.children).indexOf(item);
const newIndex = Math.max(index - 1, 0);
list.insertBefore(item, list.children[newIndex] || null);
},
move_down(item) {
const list = item.parentNode as HTMLElement;
const index = Array.from(list.children).indexOf(item);
const newIndex = Math.min(index + 2, list.children.length);
list.insertBefore(item, list.children[newIndex] || null);
},
remove(item) {
availableList.appendChild(item);
},
add(item) {
configuredList.appendChild(item);
},
};
}

export function sortActionClickListener(actions: ListActionsSet, onChange: () => void) {
return (event: MouseEvent) => {
const sortItemAction = (event.target as Element).closest('.scroll-box-item button[data-action]') as HTMLElement|null;
if (sortItemAction) {
const sortItem = sortItemAction.closest('.scroll-box-item') as HTMLElement;
const action = sortItemAction.dataset.action;
if (!action) {
throw new Error('No action defined for clicked button');
}

const actionFunction = actions[action];
actionFunction(sortItem);

onChange();
}
};
}

19 changes: 15 additions & 4 deletions resources/sass/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1062,12 +1062,16 @@ $btt-size: 40px;
cursor: pointer;
@include mixins.lightDark(background-color, #f8f8f8, #333);
}
&.items-center {
align-items: center;
}
.handle {
color: #AAA;
cursor: grab;
}
button {
opacity: .6;
line-height: 1;
}
.handle svg {
margin: 0;
Expand Down Expand Up @@ -1108,12 +1112,19 @@ input.scroll-box-search, .scroll-box-header-item {
border-radius: 0 0 3px 3px;
}

.scroll-box[refs="shelf-sort@shelf-book-list"] [data-action="add"] {
.scroll-box.configured-option-list [data-action="add"] {
display: none;
}
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="remove"],
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_up"],
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_down"],
.scroll-box.available-option-list [data-action="remove"],
.scroll-box.available-option-list [data-action="move_up"],
.scroll-box.available-option-list [data-action="move_down"],
{
display: none;
}

.scroll-box > li.empty-state {
display: none;
}
.scroll-box > li.empty-state:last-child {
display: list-item;
}
58 changes: 17 additions & 41 deletions resources/views/settings/sort-sets/parts/form.blade.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<div class="setting-list">
<div class="grid half">
<div>
Expand All @@ -13,59 +12,36 @@
</div>
</div>

<div>
<div component="sort-set-manager">
<label class="setting-list-label">{{ trans('settings.sort_set_operations') }}</label>
<p class="text-muted text-small">{{ trans('settings.sort_set_operations_desc') }}</p>


<input refs="sort-set-manager@input" type="hidden" name="books"
value="{{ $model?->sequence ?? '' }}">

<div class="grid half">
<div class="form-group">
<label for="books" id="sort-set-configured-operations">{{ trans('settings.sort_set_configured_operations') }}</label>
<ul refs="sort-set@configured-operations-list"
<label for="books"
id="sort-set-configured-operations">{{ trans('settings.sort_set_configured_operations') }}</label>
<ul refs="sort-set-manager@configured-operations-list"
aria-labelledby="sort-set-configured-operations"
class="scroll-box">
@foreach(($model?->getOptions() ?? []) as $option)
<li data-id="{{ $option->value }}"
class="scroll-box-item">
<div class="handle px-s">@icon('grip')</div>
<div>{{ $option->getLabel() }}</div>
<div class="buttons flex-container-row items-center ml-auto px-xxs py-xs">
<button type="button" data-action="move_up" class="icon-button p-xxs"
title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
<button type="button" data-action="move_down" class="icon-button p-xxs"
title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
<button type="button" data-action="remove" class="icon-button p-xxs"
title="{{ trans('common.remove') }}">@icon('remove')</button>
<button type="button" data-action="add" class="icon-button p-xxs"
title="{{ trans('common.add') }}">@icon('add-small')</button>
</div>
</li>
class="scroll-box configured-option-list">
<li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_set_configured_operations_empty') }}</li>
@foreach(($model?->getOperations() ?? []) as $option)
@include('settings.sort-sets.parts.operation')
@endforeach
</ul>
</div>

<div class="form-group">
<label for="books" id="sort-set-available-operations">{{ trans('settings.sort_set_available_operations') }}</label>
<ul refs="sort-set@available-operations-list"
<label for="books"
id="sort-set-available-operations">{{ trans('settings.sort_set_available_operations') }}</label>
<ul refs="sort-set-manager@available-operations-list"
aria-labelledby="sort-set-available-operations"
class="scroll-box">
@foreach(\BookStack\Sorting\SortSetOption::allExcluding($model?->getOptions() ?? []) as $option)
<li data-id="{{ $option->value }}"
class="scroll-box-item">
<div class="handle px-s">@icon('grip')</div>
<div>{{ $option->getLabel() }}</div>
<div class="buttons flex-container-row items-center ml-auto px-xxs py-xs">
<button type="button" data-action="move_up" class="icon-button p-xxs"
title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
<button type="button" data-action="move_down" class="icon-button p-xxs"
title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
<button type="button" data-action="remove" class="icon-button p-xxs"
title="{{ trans('common.remove') }}">@icon('remove')</button>
<button type="button" data-action="add" class="icon-button p-xxs"
title="{{ trans('common.add') }}">@icon('add-small')</button>
</div>
</li>
class="scroll-box available-option-list">
<li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_set_available_operations_empty') }}</li>
@foreach(\BookStack\Sorting\SortSetOperation::allExcluding($model?->getOperations() ?? []) as $operation)
@include('settings.sort-sets.parts.operation', ['operation' => $operation])
@endforeach
</ul>
</div>
Expand Down
Loading

0 comments on commit d28278b

Please sign in to comment.