ruby_ui_admin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
- data/app/components/ruby_ui_admin/ui/base.rb +26 -0
- data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
- data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
- data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
- data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
- data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
- data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
- data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
- data/app/components/ruby_ui_admin/ui/select.rb +29 -0
- data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
- data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
- data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
- data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
- data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
- data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
- data/app/components/ruby_ui_admin/ui.rb +12 -0
- data/app/components/ruby_ui_admin/views/action.rb +37 -0
- data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
- data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
- data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
- data/app/components/ruby_ui_admin/views/base.rb +301 -0
- data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
- data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
- data/app/components/ruby_ui_admin/views/docs.rb +91 -0
- data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
- data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
- data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
- data/app/components/ruby_ui_admin/views/form.rb +151 -0
- data/app/components/ruby_ui_admin/views/home.rb +35 -0
- data/app/components/ruby_ui_admin/views/index.rb +324 -0
- data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
- data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
- data/app/components/ruby_ui_admin/views/show.rb +77 -0
- data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
- data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
- data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
- data/app/components/ruby_ui_admin/views/translation.rb +12 -0
- data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
- data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
- data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
- data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
- data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
- data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
- data/config/locales/ruby_ui_admin.en.yml +59 -0
- data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
- data/config/routes/dynamic_routes.rb +11 -0
- data/config/routes.rb +23 -0
- data/docs/README.md +53 -0
- data/docs/actions/overview.md +112 -0
- data/docs/authorization/action-policy.md +72 -0
- data/docs/authorization/adapters.md +101 -0
- data/docs/authorization/explicit-mode.md +45 -0
- data/docs/authorization/field-authorization.md +53 -0
- data/docs/authorization/policies.md +62 -0
- data/docs/authorization/scopes.md +48 -0
- data/docs/customization/controllers.md +62 -0
- data/docs/customization/ejecting.md +55 -0
- data/docs/customization/javascript.md +161 -0
- data/docs/customization/theming-rubyui.md +80 -0
- data/docs/dashboards/overview.md +65 -0
- data/docs/fields/overview.md +94 -0
- data/docs/filters/overview.md +73 -0
- data/docs/generators-and-tasks/overview.md +42 -0
- data/docs/getting-started/authentication.md +98 -0
- data/docs/getting-started/configuration.md +174 -0
- data/docs/getting-started/installation.md +201 -0
- data/docs/getting-started/internationalization.md +50 -0
- data/docs/getting-started/practical-guide.md +310 -0
- data/docs/resources/associations.md +66 -0
- data/docs/resources/index-customization.md +68 -0
- data/docs/resources/overview.md +50 -0
- data/docs/resources/scopes.md +54 -0
- data/docs/resources/tabs-panels.md +65 -0
- data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
- data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
- data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
- data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
- data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
- data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
- data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
- data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
- data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
- data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
- data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
- data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
- data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
- data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
- data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
- data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
- data/lib/ruby_ui_admin/authorization.rb +40 -0
- data/lib/ruby_ui_admin/base_action.rb +211 -0
- data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
- data/lib/ruby_ui_admin/base_policy.rb +38 -0
- data/lib/ruby_ui_admin/base_resource.rb +503 -0
- data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
- data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
- data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
- data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
- data/lib/ruby_ui_admin/configuration.rb +177 -0
- data/lib/ruby_ui_admin/current.rb +20 -0
- data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
- data/lib/ruby_ui_admin/engine.rb +80 -0
- data/lib/ruby_ui_admin/execution_context.rb +36 -0
- data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
- data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
- data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
- data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
- data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
- data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
- data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
- data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
- data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
- data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
- data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
- data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
- data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
- data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
- data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
- data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
- data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
- data/lib/ruby_ui_admin/menu/builder.rb +94 -0
- data/lib/ruby_ui_admin/resource_manager.rb +82 -0
- data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
- data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
- data/lib/ruby_ui_admin/structure.rb +40 -0
- data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
- data/lib/ruby_ui_admin/version.rb +5 -0
- data/lib/ruby_ui_admin/view.rb +34 -0
- data/lib/ruby_ui_admin.rb +110 -0
- data/lib/tasks/ruby_ui_admin.rake +71 -0
- data/public/ruby-ui-admin-assets/application.css +2 -0
- data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
- data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
- data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
- data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
- data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
- data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
- data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
- metadata +379 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="ruby-ui--combobox"
|
|
4
|
+
// Vendored from RubyUI; the only change is `updatePopoverPosition`, which uses plain
|
|
5
|
+
// getBoundingClientRect positioning instead of @floating-ui/dom (so no bundler/extra dep).
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static values = {
|
|
8
|
+
term: String,
|
|
9
|
+
minPopoverWidth: { type: Number, default: 240 }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static targets = [
|
|
13
|
+
"input",
|
|
14
|
+
"toggleAll",
|
|
15
|
+
"popover",
|
|
16
|
+
"item",
|
|
17
|
+
"emptyState",
|
|
18
|
+
"searchInput",
|
|
19
|
+
"trigger",
|
|
20
|
+
"triggerContent"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
selectedItemIndex = null
|
|
24
|
+
|
|
25
|
+
connect() {
|
|
26
|
+
this.updateTriggerContent()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
disconnect() {
|
|
30
|
+
if (this.cleanup) { this.cleanup() }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
handlePopoverToggle(event) {
|
|
34
|
+
this.triggerTarget.ariaExpanded = event.newState === 'open' ? 'true' : 'false'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
inputChanged(e) {
|
|
38
|
+
this.updateTriggerContent()
|
|
39
|
+
|
|
40
|
+
if (e.target.type == "radio") {
|
|
41
|
+
this.closePopover()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (this.hasToggleAllTarget && !e.target.checked) {
|
|
45
|
+
this.toggleAllTarget.checked = false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
inputContent(input) {
|
|
50
|
+
return input.dataset.text || input.parentElement.textContent
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toggleAllItems() {
|
|
54
|
+
const isChecked = this.toggleAllTarget.checked
|
|
55
|
+
this.inputTargets.forEach(input => input.checked = isChecked)
|
|
56
|
+
this.updateTriggerContent()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
updateTriggerContent() {
|
|
60
|
+
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
|
61
|
+
|
|
62
|
+
if (checkedInputs.length === 0) {
|
|
63
|
+
this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder
|
|
64
|
+
} else if (this.termValue && checkedInputs.length > 1) {
|
|
65
|
+
this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`
|
|
66
|
+
} else {
|
|
67
|
+
this.triggerContentTarget.innerText = checkedInputs.map((input) => this.inputContent(input)).join(", ")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
togglePopover(event) {
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
|
|
74
|
+
if (this.triggerTarget.ariaExpanded === "true") {
|
|
75
|
+
this.closePopover()
|
|
76
|
+
} else {
|
|
77
|
+
this.openPopover(event)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
openPopover(event) {
|
|
82
|
+
if (event) event.preventDefault()
|
|
83
|
+
|
|
84
|
+
this.updatePopoverPosition()
|
|
85
|
+
this.updatePopoverWidth()
|
|
86
|
+
this.triggerTarget.ariaExpanded = "true"
|
|
87
|
+
this.selectedItemIndex = null
|
|
88
|
+
this.itemTargets.forEach(item => item.ariaCurrent = "false")
|
|
89
|
+
this.popoverTarget.showPopover()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
closePopover() {
|
|
93
|
+
this.triggerTarget.ariaExpanded = "false"
|
|
94
|
+
this.popoverTarget.hidePopover()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
filterItems(e) {
|
|
98
|
+
if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const filterTerm = this.searchInputTarget.value.toLowerCase()
|
|
103
|
+
|
|
104
|
+
if (this.hasToggleAllTarget) {
|
|
105
|
+
if (filterTerm) this.toggleAllTarget.parentElement.classList.add("hidden")
|
|
106
|
+
else this.toggleAllTarget.parentElement.classList.remove("hidden")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let resultCount = 0
|
|
110
|
+
|
|
111
|
+
this.selectedItemIndex = null
|
|
112
|
+
|
|
113
|
+
this.inputTargets.forEach((input) => {
|
|
114
|
+
const text = this.inputContent(input).toLowerCase()
|
|
115
|
+
|
|
116
|
+
if (text.indexOf(filterTerm) > -1) {
|
|
117
|
+
input.parentElement.classList.remove("hidden")
|
|
118
|
+
resultCount++
|
|
119
|
+
} else {
|
|
120
|
+
input.parentElement.classList.add("hidden")
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
this.emptyStateTarget.classList.toggle("hidden", resultCount !== 0)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
keyDownPressed() {
|
|
128
|
+
if (this.selectedItemIndex !== null) {
|
|
129
|
+
this.selectedItemIndex++
|
|
130
|
+
} else {
|
|
131
|
+
this.selectedItemIndex = 0
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.focusSelectedInput()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
keyUpPressed() {
|
|
138
|
+
if (this.selectedItemIndex !== null) {
|
|
139
|
+
this.selectedItemIndex--
|
|
140
|
+
} else {
|
|
141
|
+
this.selectedItemIndex = -1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.focusSelectedInput()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
focusSelectedInput() {
|
|
148
|
+
const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden"))
|
|
149
|
+
|
|
150
|
+
this.wrapSelectedInputIndex(visibleInputs.length)
|
|
151
|
+
|
|
152
|
+
visibleInputs.forEach((input, index) => {
|
|
153
|
+
if (index == this.selectedItemIndex) {
|
|
154
|
+
input.parentElement.ariaCurrent = "true"
|
|
155
|
+
input.parentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
|
|
156
|
+
} else {
|
|
157
|
+
input.parentElement.ariaCurrent = "false"
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
keyEnterPressed(event) {
|
|
163
|
+
event.preventDefault()
|
|
164
|
+
const option = this.itemTargets.find(item => item.ariaCurrent === "true")
|
|
165
|
+
|
|
166
|
+
if (option) {
|
|
167
|
+
option.click()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
wrapSelectedInputIndex(length) {
|
|
172
|
+
this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Position the popover under the trigger using viewport coordinates. The popover lives in
|
|
176
|
+
// the top layer (native Popover API), so a fixed position relative to the trigger's rect
|
|
177
|
+
// places it correctly without a positioning library.
|
|
178
|
+
updatePopoverPosition() {
|
|
179
|
+
const rect = this.triggerTarget.getBoundingClientRect()
|
|
180
|
+
const width = Math.max(this.triggerTarget.offsetWidth, this.minPopoverWidthValue)
|
|
181
|
+
// Keep the popover within the viewport (pull left when the trigger is near the right edge).
|
|
182
|
+
let left = rect.left
|
|
183
|
+
const maxLeft = window.innerWidth - width - 8
|
|
184
|
+
if (left > maxLeft) left = Math.max(8, maxLeft)
|
|
185
|
+
Object.assign(this.popoverTarget.style, {
|
|
186
|
+
position: "fixed",
|
|
187
|
+
top: `${rect.bottom + 4}px`,
|
|
188
|
+
left: `${left}px`,
|
|
189
|
+
margin: "0"
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
updatePopoverWidth() {
|
|
194
|
+
const width = Math.max(this.triggerTarget.offsetWidth, this.minPopoverWidthValue)
|
|
195
|
+
this.popoverTarget.style.width = `${width}px`
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["content"]
|
|
5
|
+
|
|
6
|
+
static values = { open: false }
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
if (this.openValue) this.open()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
open() {
|
|
13
|
+
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
|
4
|
+
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
5
|
+
const State = {
|
|
6
|
+
EXPANDED: "expanded",
|
|
7
|
+
COLLAPSED: "collapsed",
|
|
8
|
+
};
|
|
9
|
+
const MOBILE_BREAKPOINT = 768;
|
|
10
|
+
|
|
11
|
+
export default class extends Controller {
|
|
12
|
+
static targets = ["sidebar", "mobileSidebar"];
|
|
13
|
+
|
|
14
|
+
sidebarTargetConnected() {
|
|
15
|
+
const { state, collapsibleKind } = this.sidebarTarget.dataset;
|
|
16
|
+
|
|
17
|
+
this.open = state === State.EXPANDED;
|
|
18
|
+
this.collapsibleKind = collapsibleKind;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toggle(e) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
|
|
24
|
+
if (this.#isMobile()) {
|
|
25
|
+
this.#openMobileSidebar();
|
|
26
|
+
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.open = !this.open;
|
|
31
|
+
this.onToggle();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onToggle() {
|
|
35
|
+
this.#updateSidebarState();
|
|
36
|
+
this.#persistSidebarState();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#updateSidebarState() {
|
|
40
|
+
if (!this.hasSidebarTarget) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { dataset } = this.sidebarTarget;
|
|
45
|
+
|
|
46
|
+
dataset.state = this.open ? State.EXPANDED : State.COLLAPSED;
|
|
47
|
+
dataset.collapsible = this.open ? "" : this.collapsibleKind;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#persistSidebarState() {
|
|
51
|
+
document.cookie = `${SIDEBAR_COOKIE_NAME}=${this.open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#isMobile() {
|
|
55
|
+
return window.innerWidth < MOBILE_BREAKPOINT;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#openMobileSidebar() {
|
|
59
|
+
if (!this.hasMobileSidebarTarget) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.mobileSidebarTarget.dispatchEvent(
|
|
64
|
+
new CustomEvent("ruby--ui-sidebar:open"),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
const SWIPE_THRESHOLD = 45
|
|
4
|
+
const TIME_BEFORE_UNMOUNT = 200
|
|
5
|
+
|
|
6
|
+
// Connects to data-controller="ruby-ui--toast"
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static values = {
|
|
9
|
+
duration: { type: Number, default: 4000 },
|
|
10
|
+
dismissible: { type: Boolean, default: true },
|
|
11
|
+
invert: { type: Boolean, default: false },
|
|
12
|
+
onDismiss: String,
|
|
13
|
+
onAutoClose: String,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
connect() {
|
|
17
|
+
this._timer = null
|
|
18
|
+
this._startedAt = 0
|
|
19
|
+
this._remaining = this.durationValue
|
|
20
|
+
this._paused = false
|
|
21
|
+
this._swipe = { active: false, x: 0, y: 0, startedAt: 0 }
|
|
22
|
+
|
|
23
|
+
this._onPointerDown = this._onPointerDown.bind(this)
|
|
24
|
+
this._onPointerMove = this._onPointerMove.bind(this)
|
|
25
|
+
this._onPointerUp = this._onPointerUp.bind(this)
|
|
26
|
+
this._onPointerEnter = () => this._pause()
|
|
27
|
+
this._onPointerLeave = () => { if (!this._swipe.active) this._resume() }
|
|
28
|
+
this._onKeyDown = this._onKeyDown.bind(this)
|
|
29
|
+
this._onForceDismiss = (e) => { e.stopPropagation(); this._close() }
|
|
30
|
+
this._onRestart = () => this._restart()
|
|
31
|
+
this._onRegionPause = () => this._pause()
|
|
32
|
+
this._onRegionResume = () => this._resume()
|
|
33
|
+
|
|
34
|
+
this.element.addEventListener("pointerdown", this._onPointerDown)
|
|
35
|
+
this.element.addEventListener("pointerenter", this._onPointerEnter)
|
|
36
|
+
this.element.addEventListener("pointerleave", this._onPointerLeave)
|
|
37
|
+
this.element.addEventListener("keydown", this._onKeyDown)
|
|
38
|
+
this.element.addEventListener("ruby-ui:toast:force-dismiss", this._onForceDismiss)
|
|
39
|
+
this.element.addEventListener("ruby-ui:toast:restart", this._onRestart)
|
|
40
|
+
document.addEventListener("ruby-ui:toast:pause", this._onRegionPause)
|
|
41
|
+
document.addEventListener("ruby-ui:toast:resume", this._onRegionResume)
|
|
42
|
+
|
|
43
|
+
requestAnimationFrame(() => {
|
|
44
|
+
this.element.dataset.state = "open"
|
|
45
|
+
this._start()
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
disconnect() {
|
|
50
|
+
this._clearTimer()
|
|
51
|
+
this.element.removeEventListener("pointerdown", this._onPointerDown)
|
|
52
|
+
this.element.removeEventListener("pointerenter", this._onPointerEnter)
|
|
53
|
+
this.element.removeEventListener("pointerleave", this._onPointerLeave)
|
|
54
|
+
this.element.removeEventListener("keydown", this._onKeyDown)
|
|
55
|
+
this.element.removeEventListener("ruby-ui:toast:force-dismiss", this._onForceDismiss)
|
|
56
|
+
this.element.removeEventListener("ruby-ui:toast:restart", this._onRestart)
|
|
57
|
+
document.removeEventListener("ruby-ui:toast:pause", this._onRegionPause)
|
|
58
|
+
document.removeEventListener("ruby-ui:toast:resume", this._onRegionResume)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dismiss(e) {
|
|
62
|
+
e?.preventDefault()
|
|
63
|
+
if (!this.dismissibleValue) return
|
|
64
|
+
this._close("dismiss")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_close(reason) {
|
|
68
|
+
if (this.element.dataset.state === "closing") return
|
|
69
|
+
this.element.dataset.state = "closing"
|
|
70
|
+
this.element.dispatchEvent(new CustomEvent(reason === "auto" ? "ruby-ui:toast:auto-close" : "ruby-ui:toast:dismiss", { bubbles: true, detail: { id: this.element.id } }))
|
|
71
|
+
setTimeout(() => this.element.remove(), TIME_BEFORE_UNMOUNT)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_start() {
|
|
75
|
+
if (!Number.isFinite(this.durationValue) || this.durationValue <= 0) return
|
|
76
|
+
this._startedAt = performance.now()
|
|
77
|
+
this._remaining = this.durationValue
|
|
78
|
+
this._timer = setTimeout(() => this._close("auto"), this._remaining)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_restart() {
|
|
82
|
+
this._clearTimer()
|
|
83
|
+
this._start()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_pause() {
|
|
87
|
+
if (this._paused || !this._timer) return
|
|
88
|
+
this._paused = true
|
|
89
|
+
clearTimeout(this._timer)
|
|
90
|
+
this._timer = null
|
|
91
|
+
this._remaining -= performance.now() - this._startedAt
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_resume() {
|
|
95
|
+
if (!this._paused) return
|
|
96
|
+
this._paused = false
|
|
97
|
+
if (this._remaining <= 0) return this._close("auto")
|
|
98
|
+
this._startedAt = performance.now()
|
|
99
|
+
this._timer = setTimeout(() => this._close("auto"), this._remaining)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_clearTimer() {
|
|
103
|
+
if (this._timer) clearTimeout(this._timer)
|
|
104
|
+
this._timer = null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_onKeyDown(e) {
|
|
108
|
+
if (e.key === "Escape" && this.dismissibleValue) this.dismiss(e)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_onPointerDown(e) {
|
|
112
|
+
if (!this.dismissibleValue) return
|
|
113
|
+
if (e.target.closest("button")) return
|
|
114
|
+
try { this.element.setPointerCapture(e.pointerId) } catch {}
|
|
115
|
+
this._swipe = { active: true, x: e.clientX, y: e.clientY, startedAt: performance.now(), pointerId: e.pointerId }
|
|
116
|
+
this.element.dataset.swipe = "start"
|
|
117
|
+
this.element.addEventListener("pointermove", this._onPointerMove)
|
|
118
|
+
this.element.addEventListener("pointerup", this._onPointerUp)
|
|
119
|
+
this.element.addEventListener("pointercancel", this._onPointerUp)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_onPointerMove(e) {
|
|
123
|
+
const dx = e.clientX - this._swipe.x
|
|
124
|
+
const dy = e.clientY - this._swipe.y
|
|
125
|
+
this.element.dataset.swipe = "move"
|
|
126
|
+
this.element.style.transform = `translate(${dx}px, ${dy}px)`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_onPointerUp(e) {
|
|
130
|
+
const dx = e.clientX - this._swipe.x
|
|
131
|
+
const dy = e.clientY - this._swipe.y
|
|
132
|
+
const dist = Math.hypot(dx, dy)
|
|
133
|
+
const dt = performance.now() - this._swipe.startedAt
|
|
134
|
+
const velocity = dist / Math.max(dt, 1)
|
|
135
|
+
this.element.removeEventListener("pointermove", this._onPointerMove)
|
|
136
|
+
this.element.removeEventListener("pointerup", this._onPointerUp)
|
|
137
|
+
this.element.removeEventListener("pointercancel", this._onPointerUp)
|
|
138
|
+
this._swipe.active = false
|
|
139
|
+
if (dist > SWIPE_THRESHOLD || velocity > 0.5) {
|
|
140
|
+
this.element.style.setProperty("--swipe-end-x", `${Math.sign(dx) * 500}px`)
|
|
141
|
+
this.element.style.setProperty("--swipe-end-y", `${Math.sign(dy) * 500}px`)
|
|
142
|
+
this.element.dataset.swipe = "end"
|
|
143
|
+
this.element.style.transform = ""
|
|
144
|
+
this._close("dismiss")
|
|
145
|
+
} else {
|
|
146
|
+
this.element.dataset.swipe = "cancel"
|
|
147
|
+
this.element.style.transform = ""
|
|
148
|
+
this._resume()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|