Skip to content

Commit aca2cf6

Browse files
committed
[feature] typeahead for command list
- adds typeahead component for selecting command on edit task form
1 parent 4c62f52 commit aca2cf6

File tree

9 files changed

+182
-35
lines changed

9 files changed

+182
-35
lines changed

public/css/app.css

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

public/css/app.css.map

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

public/js/app.js

Lines changed: 24 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/assets/js/app.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import TaskOutput from './tasks/components/TaskOutput.vue';
99
import StatusButton from './tasks/components/StatusButton.vue';
1010
import ExecuteButton from './tasks/components/ExecuteButton.vue';
1111
import ImportButton from './tasks/components/ImportButton'
12+
import CommandList from './tasks/components/CommandList'
13+
import ClickToClose from "./components/ClickToClose";
1214

1315
Promise.delay = function (time) {
1416
return new Promise((resolve, reject) => {
@@ -50,7 +52,9 @@ new Vue({
5052
'execute-button': ExecuteButton,
5153
'import-button': ImportButton,
5254
'task-type' : TaskType,
53-
'task-output' : TaskOutput
55+
'task-output' : TaskOutput,
56+
'click-to-close' : ClickToClose,
57+
'command-list' : CommandList
5458
},
5559
mounted() {
5660
UIkit.use(Icons);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script>
2+
export default {
3+
name: 'ClickToClose',
4+
props: ['do'],
5+
mounted() {
6+
const listener = (e) => {
7+
if (e.target === this.$el || this.$el.contains(e.target)) {
8+
return;
9+
}
10+
11+
this.do();
12+
};
13+
14+
document.addEventListener('click', listener);
15+
16+
this.$once('hook:beforeDestroy', () => document.removeEventListener('click', listener));
17+
},
18+
render() {
19+
return this.$slots.default[0];
20+
},
21+
};
22+
</script>

resources/assets/js/components/UIKitModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
},
2828
mounted() {
2929
document.addEventListener("keydown", (e) => {
30-
if (this.show && e.keyCode == 27) {
30+
if (this.show && e.keyCode === 27) {
3131
this.close();
3232
}
3333
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<template>
2+
<click-to-close :do="close">
3+
<div class="search-select">
4+
<input type="text" name="command" ref="input" readonly @click="open" class="uk-input" v-model="selected" placeholder="Select a command"/>
5+
<div ref="dropdown" v-show="isOpen" class="uk-card uk-card-default uk-padding-small uk-box-shadow-large">
6+
<div class="uk-search uk-search-default uk-width-1-1">
7+
<span class="uk-search-icon-flip" uk-search-icon></span>
8+
<label>
9+
<input class="uk-input"
10+
type="search"
11+
v-model="search"
12+
ref="search"
13+
@keydown.esc="close"
14+
@keydown.down="highlightNext"
15+
@keydown.up="highlightPrev"
16+
@keydown.enter.prevent="selectHighlighted"
17+
@keydown.tab.prevent>
18+
</label>
19+
</div>
20+
21+
<ul ref="options" v-show="filteredOptions.length > 0" class="uk-list uk-list-striped uk-height-max-medium uk-position-relative uk-overflow-auto">
22+
<li class="search-select-option" :class="{ 'uk-text-bold': index === highlightedIndex }"
23+
v-for="(option, index) in filteredOptions"
24+
:key="option.name"
25+
@click="select(option)">
26+
{{ option.name }}
27+
<em class="uk-padding-small uk-padding-remove-top uk-padding-remove-bottom uk-padding-remove-right">
28+
{{option.description}}
29+
</em>
30+
</li>
31+
</ul>
32+
<div v-show="filteredOptions.length <= 0" class="uk-padding-small">
33+
No results found for "{{ search }}"
34+
</div>
35+
</div>
36+
</div>
37+
</click-to-close>
38+
</template>
39+
40+
<style scoped>
41+
.search-select-option {
42+
background: transparent;
43+
}
44+
.search-select-option:hover {
45+
cursor: pointer;
46+
}
47+
</style>
48+
49+
<script>
50+
import ClickToClose from "../../components/ClickToClose";
51+
export default {
52+
name: 'CommandList',
53+
components: {ClickToClose},
54+
props: ['command', 'commands'],
55+
data() {
56+
return {
57+
selected: decodeURI(this.command),
58+
options: Object.values(this.commands),
59+
isOpen: false,
60+
search: '',
61+
highlightedIndex: 0
62+
};
63+
},
64+
computed: {
65+
filteredOptions() {
66+
return this.options.filter(option => option.name.toLowerCase().includes(this.search.toLowerCase()));
67+
}
68+
},
69+
methods: {
70+
open() {
71+
if (this.isOpen) {
72+
return;
73+
}
74+
75+
this.isOpen = true;
76+
this.highlightedIndex = this.options.findIndex(option => option.name === this.selected);
77+
this.$nextTick(() => {
78+
this.$refs.search.focus();
79+
this.scrollToHighlighted();
80+
});
81+
},
82+
close() {
83+
if (!this.isOpen) {
84+
return;
85+
}
86+
87+
this.isOpen = false;
88+
this.$refs.input.focus();
89+
},
90+
select(option) {
91+
this.selected = option.name;
92+
this.search = '';
93+
this.highlightedIndex = 0;
94+
this.close();
95+
},
96+
selectHighlighted() {
97+
this.select(this.filteredOptions[this.highlightedIndex]);
98+
},
99+
scrollToHighlighted() {
100+
this.$refs.options.children[this.highlightedIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
101+
},
102+
highlight(index) {
103+
this.highlightedIndex = index;
104+
105+
if (this.highlightedIndex < 0) {
106+
this.highlightedIndex = this.filteredOptions.length - 1;
107+
}
108+
109+
if (this.highlightedIndex > this.filteredOptions.length - 1) {
110+
this.highlightedIndex = 0;
111+
}
112+
113+
this.scrollToHighlighted();
114+
},
115+
highlightNext() {
116+
this.highlight(this.highlightedIndex + 1);
117+
},
118+
highlightPrev() {
119+
this.highlight(this.highlightedIndex - 1);
120+
}
121+
}
122+
};
123+
</script>

resources/views/tasks/form.blade.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,7 @@
3131
<div class="uk-text-meta">Select an artisan command to schedule</div>
3232
</div>
3333
<div class="uk-width-1-1@s uk-width-2-3@m">
34-
<select id="command" name="command" class="uk-select" placeholder="Click here to select one of the available commands">
35-
<option value="">Select a command</option>
36-
@foreach ($commands as $command)
37-
<optgroup label="{{$command->getDescription()}}">
38-
<option value="{{$command->getName()}}" {{old('command', $task->command) == $command->getName() ? 'selected' : ''}}>
39-
{{$command->getName()}}
40-
</option>
41-
</optgroup>
42-
@endforeach
43-
</select>
34+
<command-list command="{{ $task->command }}" :commands="{{ json_encode($commands) }}"></command-list>
4435
@if($errors->has('command'))
4536
<p class="uk-text-danger">{{$errors->first('command')}}</p>
4637
@endif

src/Http/Controllers/TasksController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,12 @@ public function view(Task $task)
9797
*/
9898
public function edit(Task $task)
9999
{
100+
$commands = Totem::getCommands()->map(function($command) {
101+
return ['name' => $command->getName(), 'description' => $command->getDescription()];
102+
});
100103
return view('totem::tasks.form', [
101104
'task' => $task,
102-
'commands' => Totem::getCommands(),
105+
'commands' => $commands,
103106
'timezones' => timezone_identifiers_list(),
104107
'frequencies' => Totem::frequencies(),
105108
]);

0 commit comments

Comments
 (0)