Skip to content

Commit d28278b

Browse files
committed
Sorting: Added sort set form manager UI JS
Extracted much code to be shared with the shelf books management UI
1 parent bf8a84a commit d28278b

13 files changed

Lines changed: 168 additions & 103 deletions

File tree

app/Sorting/SortSet.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,21 @@
1515
class SortSet extends Model
1616
{
1717
/**
18-
* @return SortSetOption[]
18+
* @return SortSetOperation[]
1919
*/
20-
public function getOptions(): array
20+
public function getOperations(): array
2121
{
2222
$strOptions = explode(',', $this->sequence);
23-
$options = array_map(fn ($val) => SortSetOption::tryFrom($val), $strOptions);
23+
$options = array_map(fn ($val) => SortSetOperation::tryFrom($val), $strOptions);
2424
return array_filter($options);
2525
}
2626

2727
/**
28-
* @param SortSetOption[] $options
28+
* @param SortSetOperation[] $options
2929
*/
30-
public function setOptions(array $options): void
30+
public function setOperations(array $options): void
3131
{
32-
$values = array_map(fn (SortSetOption $opt) => $opt->value, $options);
32+
$values = array_map(fn (SortSetOperation $opt) => $opt->value, $options);
3333
$this->sequence = implode(',', $values);
3434
}
3535
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace BookStack\Sorting;
44

5-
enum SortSetOption: string
5+
enum SortSetOperation: string
66
{
77
case NameAsc = 'name_asc';
88
case NameDesc = 'name_desc';
@@ -34,11 +34,11 @@ public function getLabel(): string
3434
}
3535

3636
/**
37-
* @return SortSetOption[]
37+
* @return SortSetOperation[]
3838
*/
39-
public static function allExcluding(array $options): array
39+
public static function allExcluding(array $operations): array
4040
{
41-
$all = SortSetOption::cases();
42-
return array_diff($all, $options);
41+
$all = SortSetOperation::cases();
42+
return array_diff($all, $operations);
4343
}
4444
}

lang/en/settings.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@
8787
'sort_set_operations' => 'Sort Operations',
8888
'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.',
8989
'sort_set_available_operations' => 'Available Operations',
90+
'sort_set_available_operations_empty' => 'No operations remaining',
9091
'sort_set_configured_operations' => 'Configured Operations',
92+
'sort_set_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
9193
'sort_set_op_asc' => '(Asc)',
9294
'sort_set_op_desc' => '(Desc)',
9395
'sort_set_op_name' => 'Name - Alphabetical',

package-lock.json

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
},
2121
"devDependencies": {
2222
"@lezer/generator": "^1.7.2",
23+
"@types/sortablejs": "^1.15.8",
2324
"chokidar-cli": "^3.0",
2425
"esbuild": "^0.24.0",
2526
"eslint": "^8.57.1",

resources/js/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export {ShelfSort} from './shelf-sort';
5050
export {Shortcuts} from './shortcuts';
5151
export {ShortcutInput} from './shortcut-input';
5252
export {SortableList} from './sortable-list';
53+
export {SortSetManager} from './sort-set-manager'
5354
export {SubmitOnChange} from './submit-on-change';
5455
export {Tabs} from './tabs';
5556
export {TagManager} from './tag-manager';

resources/js/components/shelf-sort.js

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,6 @@
11
import Sortable from 'sortablejs';
22
import {Component} from './component';
3-
4-
/**
5-
* @type {Object<string, function(HTMLElement, HTMLElement, HTMLElement)>}
6-
*/
7-
const itemActions = {
8-
move_up(item) {
9-
const list = item.parentNode;
10-
const index = Array.from(list.children).indexOf(item);
11-
const newIndex = Math.max(index - 1, 0);
12-
list.insertBefore(item, list.children[newIndex] || null);
13-
},
14-
move_down(item) {
15-
const list = item.parentNode;
16-
const index = Array.from(list.children).indexOf(item);
17-
const newIndex = Math.min(index + 2, list.children.length);
18-
list.insertBefore(item, list.children[newIndex] || null);
19-
},
20-
remove(item, shelfBooksList, allBooksList) {
21-
allBooksList.appendChild(item);
22-
},
23-
add(item, shelfBooksList) {
24-
shelfBooksList.appendChild(item);
25-
},
26-
};
3+
import {buildListActions, sortActionClickListener} from '../services/dual-lists.ts';
274

285
export class ShelfSort extends Component {
296

@@ -55,12 +32,9 @@ export class ShelfSort extends Component {
5532
}
5633

5734
setupListeners() {
58-
this.elem.addEventListener('click', event => {
59-
const sortItemAction = event.target.closest('.scroll-box-item button[data-action]');
60-
if (sortItemAction) {
61-
this.sortItemActionClick(sortItemAction);
62-
}
63-
});
35+
const listActions = buildListActions(this.allBookList, this.shelfBookList);
36+
const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));
37+
this.elem.addEventListener('click', sortActionListener);
6438

6539
this.bookSearchInput.addEventListener('input', () => {
6640
this.filterBooksByName(this.bookSearchInput.value);
@@ -93,20 +67,6 @@ export class ShelfSort extends Component {
9367
}
9468
}
9569

96-
/**
97-
* Called when a sort item action button is clicked.
98-
* @param {HTMLElement} sortItemAction
99-
*/
100-
sortItemActionClick(sortItemAction) {
101-
const sortItem = sortItemAction.closest('.scroll-box-item');
102-
const {action} = sortItemAction.dataset;
103-
104-
const actionFunction = itemActions[action];
105-
actionFunction(sortItem, this.shelfBookList, this.allBookList);
106-
107-
this.onChange();
108-
}
109-
11070
onChange() {
11171
const shelfBookElems = Array.from(this.shelfBookList.querySelectorAll('[data-id]'));
11272
this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {Component} from "./component.js";
2+
import Sortable from "sortablejs";
3+
import {buildListActions, sortActionClickListener} from "../services/dual-lists";
4+
5+
6+
export class SortSetManager extends Component {
7+
8+
protected input!: HTMLInputElement;
9+
protected configuredList!: HTMLElement;
10+
protected availableList!: HTMLElement;
11+
12+
setup() {
13+
this.input = this.$refs.input as HTMLInputElement;
14+
this.configuredList = this.$refs.configuredOperationsList;
15+
this.availableList = this.$refs.availableOperationsList;
16+
17+
this.initSortable();
18+
19+
const listActions = buildListActions(this.availableList, this.configuredList);
20+
const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));
21+
this.$el.addEventListener('click', sortActionListener);
22+
}
23+
24+
initSortable() {
25+
const scrollBoxes = [this.configuredList, this.availableList];
26+
for (const scrollBox of scrollBoxes) {
27+
new Sortable(scrollBox, {
28+
group: 'sort-set-operations',
29+
ghostClass: 'primary-background-light',
30+
handle: '.handle',
31+
animation: 150,
32+
onSort: this.onChange.bind(this),
33+
});
34+
}
35+
}
36+
37+
onChange() {
38+
const configuredOpEls = Array.from(this.configuredList.querySelectorAll('[data-id]'));
39+
this.input.value = configuredOpEls.map(elem => elem.getAttribute('data-id')).join(',');
40+
}
41+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Service for helping manage common dual-list scenarios.
3+
* (Shelf book manager, sort set manager).
4+
*/
5+
6+
type ListActionsSet = Record<string, ((item: HTMLElement) => void)>;
7+
8+
export function buildListActions(
9+
availableList: HTMLElement,
10+
configuredList: HTMLElement,
11+
): ListActionsSet {
12+
return {
13+
move_up(item) {
14+
const list = item.parentNode as HTMLElement;
15+
const index = Array.from(list.children).indexOf(item);
16+
const newIndex = Math.max(index - 1, 0);
17+
list.insertBefore(item, list.children[newIndex] || null);
18+
},
19+
move_down(item) {
20+
const list = item.parentNode as HTMLElement;
21+
const index = Array.from(list.children).indexOf(item);
22+
const newIndex = Math.min(index + 2, list.children.length);
23+
list.insertBefore(item, list.children[newIndex] || null);
24+
},
25+
remove(item) {
26+
availableList.appendChild(item);
27+
},
28+
add(item) {
29+
configuredList.appendChild(item);
30+
},
31+
};
32+
}
33+
34+
export function sortActionClickListener(actions: ListActionsSet, onChange: () => void) {
35+
return (event: MouseEvent) => {
36+
const sortItemAction = (event.target as Element).closest('.scroll-box-item button[data-action]') as HTMLElement|null;
37+
if (sortItemAction) {
38+
const sortItem = sortItemAction.closest('.scroll-box-item') as HTMLElement;
39+
const action = sortItemAction.dataset.action;
40+
if (!action) {
41+
throw new Error('No action defined for clicked button');
42+
}
43+
44+
const actionFunction = actions[action];
45+
actionFunction(sortItem);
46+
47+
onChange();
48+
}
49+
};
50+
}
51+

resources/sass/_components.scss

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,12 +1062,16 @@ $btt-size: 40px;
10621062
cursor: pointer;
10631063
@include mixins.lightDark(background-color, #f8f8f8, #333);
10641064
}
1065+
&.items-center {
1066+
align-items: center;
1067+
}
10651068
.handle {
10661069
color: #AAA;
10671070
cursor: grab;
10681071
}
10691072
button {
10701073
opacity: .6;
1074+
line-height: 1;
10711075
}
10721076
.handle svg {
10731077
margin: 0;
@@ -1108,12 +1112,19 @@ input.scroll-box-search, .scroll-box-header-item {
11081112
border-radius: 0 0 3px 3px;
11091113
}
11101114

1111-
.scroll-box[refs="shelf-sort@shelf-book-list"] [data-action="add"] {
1115+
.scroll-box.configured-option-list [data-action="add"] {
11121116
display: none;
11131117
}
1114-
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="remove"],
1115-
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_up"],
1116-
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_down"],
1118+
.scroll-box.available-option-list [data-action="remove"],
1119+
.scroll-box.available-option-list [data-action="move_up"],
1120+
.scroll-box.available-option-list [data-action="move_down"],
11171121
{
11181122
display: none;
1123+
}
1124+
1125+
.scroll-box > li.empty-state {
1126+
display: none;
1127+
}
1128+
.scroll-box > li.empty-state:last-child {
1129+
display: list-item;
11191130
}

0 commit comments

Comments
 (0)