x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<div class="alert alert-warning">
Since this page is an iframe, you need to click here first, and then press the shortcut.
</div>
<div data-controller="command-list" class="command-list">
<a href="#" class="nav-item me-3 p-1 text-decoration-none">
<div class="input-group command-list-button ms-2" data-bs-toggle="modal" data-bs-target="#command-list-modal">
<span class="input-group-text" id="basic-addon1"><i class="fa fa-search"></i></span>
<div class="form-control form-control-sm text-center py-2">
<span class="translation_missing" title="translation missing: en.command_list.press">Press</span>
<kbd class="command-list-icon" data-command-list-target="metaKey"></kbd> + <kbd class="command-list-icon">K</kbd>
</div>
</div>
</a>
<div id="command-list-modal" class="modal" tabindex="-1" data-command-list-target="list">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1"><i class="fa fa-search"></i></span>
<input autocomplete="off"
type="text"
class="form-control"
data-command-list-target="searchField"
data-action="keydown.up->command-list#previousItem
keydown.enter->command-list#performCommand
keydown.down->command-list#nextItem">
</div>
<div class="list-group" data-command-list-target="listGroup">
<div class="form-label mt-2 fw-medium"><span class="translation_missing" title="translation missing: en.command_list.search">Search</span></div>
<a class="list-group-item" href="#"
data-action="click->command-list#selectCommand"
data-command-list-target="listItem"
data-placeholder="Search text"
data-command="https://www.google.com/search?q=_1_"
data-target="_blank">
Search on Google
</a>
<div class="form-label mt-2 fw-medium"><span class="translation_missing" title="translation missing: en.command_list.link">Link</span></div>
<a class="list-group-item" href="#"
data-action="click->command-list#selectCommand"
data-command-list-target="listItem"
data-placeholder="badge, button, avatar, etc."
data-command="/lookbook/inspect/_1_/themes"
data-target="">
Go to component
</a>
</div>
</div>
<div class="modal-footer d-flex command-list-footer">
<small class="pe-1"><kbd class="command-list-icon"></kbd> <span class="translation_missing" title="translation missing: en.command_list.to_select">To Select</span></small>
<small class="pe-1">
<kbd class="command-list-icon"></kbd>
<kbd class="command-list-icon"></kbd>
<span class="translation_missing" title="translation missing: en.command_list.to_navigate">To Navigate</span>
</small>
<small class="pe-1"><kbd class="command-list-icon">ESC</kbd> <span class="translation_missing" title="translation missing: en.command_list.to_exit">To Exit</span></small>
</div>
</div>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<div class="alert alert-warning">
Since this page is an iframe, you need to click here first, and then press the shortcut.
</div>
<%
command_list_actions = [
{ label: 'Search on Google',
command: 'https://www.google.com/search?q=_1_',
placeholder: 'Search text',
category: 'search',
target: '_blank' },
{ label: 'Go to component',
command: '/lookbook/inspect/_1_/themes',
placeholder: 'badge, button, avatar, etc.',
category: 'link' }
].group_by { |action| action[:category] }
%>
<div data-controller="command-list" class="command-list">
<a href="#" class="nav-item me-3 p-1 text-decoration-none">
<div class="input-group command-list-button ms-2" data-bs-toggle="modal" data-bs-target="#command-list-modal">
<span class="input-group-text" id="basic-addon1"><i class="fa fa-search"></i></span>
<div class="form-control form-control-sm text-center py-2">
<%= t('command_list.press') %>
<kbd class="command-list-icon" data-command-list-target="metaKey"></kbd> + <kbd class="command-list-icon">K</kbd>
</div>
</div>
</a>
<div id="command-list-modal" class="modal" tabindex="-1" data-command-list-target="list">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1"><i class="fa fa-search"></i></span>
<input autocomplete="off"
type="text"
class="form-control"
data-command-list-target="searchField"
data-action="keydown.up->command-list#previousItem
keydown.enter->command-list#performCommand
keydown.down->command-list#nextItem">
</div>
<div class="list-group" data-command-list-target="listGroup">
<% command_list_actions.each do |category, actions| %>
<div class="form-label mt-2 fw-medium"><%= t("command_list.#{category}") %></div>
<% actions.each do |action| %>
<a class="list-group-item" href="#"
data-action="click->command-list#selectCommand"
data-command-list-target="listItem"
data-placeholder="<%= action[:placeholder] || '' %>"
data-command="<%= action[:command] %>"
data-target="<%= action[:target] || '' %>">
<%= action[:label] %>
</a>
<% end %>
<% end %>
</div>
</div>
<div class="modal-footer d-flex command-list-footer">
<small class="pe-1"><kbd class="command-list-icon"></kbd> <%= t('command_list.to_select') %></small>
<small class="pe-1">
<kbd class="command-list-icon"></kbd>
<kbd class="command-list-icon"></kbd>
<%= t('command_list.to_navigate') %>
</small>
<small class="pe-1"><kbd class="command-list-icon">ESC</kbd> <%= t('command_list.to_exit') %></small>
</div>
</div>
</div>
</div>
</div>

_command_list.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.command-list {
.command-list-button {
min-width: 170px;
}
.modal-dialog {
--bs-modal-width: 600px;
--bs-modal-margin: 5rem;
--bs-modal-padding: 1.5rem;
}
.list-group-item {
color: $navbar-light-color;
&.active {
color: white;
}
}
.command-list-footer {
color: $navbar-light-color;
}
.command-list-icon {
border: 1px outset $navbar-light-color;
}
}

command_list_controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { Controller } from '@hotwired/stimulus';
export default class CommandListController extends Controller {
static targets = ['list', 'metaKey', 'searchField', 'listGroup', 'listItem'];
connect() {
this.modal = new bootstrap.Modal(this.listTarget, {});
this.children = Array.from(this.listGroupTargets[0].children);
if (this.listItemTargets.length > 0) {
this.switchActive(this.listItemTargets[0]);
}
this.metaKeyTarget.innerText = this.getMetaKey();
document.addEventListener('keydown', this.handleKeydown.bind(this));
}
disconnect() {
document.removeEventListener('keydown', this.handleKeydown.bind(this));
}
handleKeydown(event) {
console.log('handling keydown', event.metaKey, event.key);
if ((event.metaKey) && event.key === 'k') {
this.openModal();
}
}
openModal() {
this.modal.show();
this.searchField().focus();
}
performCommand(event) {
event.preventDefault();
const searchParts = this.searchField().value.split(',').map((part) => part.trim());
const command = this.activeCommand.dataset.command
.replace('_1_', searchParts[0])
.replace('_2_', searchParts[1])
.replace('_3_', searchParts[2]);
if (this.activeCommand.dataset.target === '_blank') {
window.open(command, '_blank');
} else {
Turbo.visit(command);
}
}
getMetaKey() {
const userAgent = window.navigator.userAgent.toLowerCase();
if (userAgent.includes(' mac ')) {
return '';
}
return 'CTRL';
}
switchActive(element) {
if (this.activeCommand) {
this.activeCommand.classList.remove('active');
}
element.classList.add('active');
this.activeCommand = element;
this.searchField().placeholder = element.dataset.placeholder ?? '';
}
selectCommand(event) {
event.preventDefault();
this.switchActive(event.currentTarget);
this.searchField().focus();
}
previousItem() {
const previous = this.listItemTargets[this.listItemTargets.indexOf(this.activeCommand) - 1];
if (previous) {
this.switchActive(previous);
}
}
nextItem() {
const next = this.listItemTargets[this.listItemTargets.indexOf(this.activeCommand) + 1];
if (next) {
this.switchActive(next);
}
}
searchField() {
return this.searchFieldTarget;
}
}