satis 1.0.66

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.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +92 -0
  4. data/Rakefile +23 -0
  5. data/app/assets/config/satis_manifest.js +1 -0
  6. data/app/assets/stylesheets/satis/application.css +15 -0
  7. data/app/components/satis/appearance_switcher/component.html.slim +6 -0
  8. data/app/components/satis/appearance_switcher/component.rb +11 -0
  9. data/app/components/satis/appearance_switcher/component.scss +34 -0
  10. data/app/components/satis/appearance_switcher/component_controller.js +62 -0
  11. data/app/components/satis/application_component.rb +50 -0
  12. data/app/components/satis/avatar/component.html.slim +7 -0
  13. data/app/components/satis/avatar/component.rb +52 -0
  14. data/app/components/satis/breadcrumbs/component.html.slim +8 -0
  15. data/app/components/satis/breadcrumbs/component.rb +23 -0
  16. data/app/components/satis/breadcrumbs/component.scss +19 -0
  17. data/app/components/satis/breadcrumbs/crumb.slim +8 -0
  18. data/app/components/satis/card/component.html.slim +54 -0
  19. data/app/components/satis/card/component.md +14 -0
  20. data/app/components/satis/card/component.rb +41 -0
  21. data/app/components/satis/card/component.scss +15 -0
  22. data/app/components/satis/date_time_picker/component.html.slim +48 -0
  23. data/app/components/satis/date_time_picker/component.md +11 -0
  24. data/app/components/satis/date_time_picker/component.rb +48 -0
  25. data/app/components/satis/date_time_picker/component.scss +5 -0
  26. data/app/components/satis/date_time_picker/component_controller.js +499 -0
  27. data/app/components/satis/dropdown/component.html.slim +36 -0
  28. data/app/components/satis/dropdown/component.md +48 -0
  29. data/app/components/satis/dropdown/component.rb +77 -0
  30. data/app/components/satis/dropdown/component.scss +10 -0
  31. data/app/components/satis/dropdown/component_controller.js +547 -0
  32. data/app/components/satis/flash_messages/component.html.slim +3 -0
  33. data/app/components/satis/flash_messages/component.rb +31 -0
  34. data/app/components/satis/flash_messages/component.scss +18 -0
  35. data/app/components/satis/flash_messages/message.html.slim +8 -0
  36. data/app/components/satis/info/component.html.slim +4 -0
  37. data/app/components/satis/info/component.rb +22 -0
  38. data/app/components/satis/info_item/component.html.slim +7 -0
  39. data/app/components/satis/info_item/component.rb +19 -0
  40. data/app/components/satis/input/component.html.slim +11 -0
  41. data/app/components/satis/input/component.rb +38 -0
  42. data/app/components/satis/input/component.scss +50 -0
  43. data/app/components/satis/input/element.html.slim +2 -0
  44. data/app/components/satis/map/component.html.slim +2 -0
  45. data/app/components/satis/map/component.rb +17 -0
  46. data/app/components/satis/map/component.scss +9 -0
  47. data/app/components/satis/map/component_controller.js +37 -0
  48. data/app/components/satis/menu/component.html.slim +13 -0
  49. data/app/components/satis/menu/component.md +1 -0
  50. data/app/components/satis/menu/component.rb +16 -0
  51. data/app/components/satis/menu/component_controller.js +62 -0
  52. data/app/components/satis/menu_item/component.html.slim +16 -0
  53. data/app/components/satis/menu_item/component.rb +14 -0
  54. data/app/components/satis/page/component.html.slim +45 -0
  55. data/app/components/satis/page/component.rb +15 -0
  56. data/app/components/satis/page/component_controller.js +86 -0
  57. data/app/components/satis/sidebar_menu/component.html.slim +3 -0
  58. data/app/components/satis/sidebar_menu/component.rb +17 -0
  59. data/app/components/satis/sidebar_menu/component.scss +0 -0
  60. data/app/components/satis/sidebar_menu/component_controller.js +9 -0
  61. data/app/components/satis/sidebar_menu/mobile/component.html.slim +3 -0
  62. data/app/components/satis/sidebar_menu/mobile/component.rb +10 -0
  63. data/app/components/satis/sidebar_menu_item/component.html.slim +15 -0
  64. data/app/components/satis/sidebar_menu_item/component.rb +20 -0
  65. data/app/components/satis/sidebar_menu_item/component.scss +27 -0
  66. data/app/components/satis/sidebar_menu_item/component_controller.js +62 -0
  67. data/app/components/satis/sidebar_menu_item/mobile/component.html.slim +17 -0
  68. data/app/components/satis/sidebar_menu_item/mobile/component.rb +10 -0
  69. data/app/components/satis/switch/component.html.slim +14 -0
  70. data/app/components/satis/switch/component.rb +24 -0
  71. data/app/components/satis/switch/component_controller.js +49 -0
  72. data/app/components/satis/tab/component.rb +35 -0
  73. data/app/components/satis/tabs/component.html.slim +23 -0
  74. data/app/components/satis/tabs/component.md +21 -0
  75. data/app/components/satis/tabs/component.rb +16 -0
  76. data/app/components/satis/tabs/component.scss +33 -0
  77. data/app/components/satis/tabs/component_controller.js +123 -0
  78. data/app/controllers/satis/application_controller.rb +4 -0
  79. data/app/helpers/satis/application_helper.rb +15 -0
  80. data/app/jobs/satis/application_job.rb +4 -0
  81. data/app/mailers/satis/application_mailer.rb +6 -0
  82. data/app/models/satis/application_record.rb +5 -0
  83. data/app/views/shared/_fields_for.html.slim +35 -0
  84. data/config/routes.rb +5 -0
  85. data/lib/satis/action_controller_helpers.rb +29 -0
  86. data/lib/satis/configuration.rb +61 -0
  87. data/lib/satis/engine.rb +27 -0
  88. data/lib/satis/forms/builder.rb +440 -0
  89. data/lib/satis/forms/concerns/buttons.rb +49 -0
  90. data/lib/satis/forms/concerns/file.rb +35 -0
  91. data/lib/satis/forms/concerns/options.rb +44 -0
  92. data/lib/satis/forms/concerns/required.rb +68 -0
  93. data/lib/satis/forms/concerns/select.rb +95 -0
  94. data/lib/satis/helpers/container.rb +83 -0
  95. data/lib/satis/menus/builder.rb +13 -0
  96. data/lib/satis/menus/item.rb +34 -0
  97. data/lib/satis/menus/menu.rb +23 -0
  98. data/lib/satis/version.rb +3 -0
  99. data/lib/satis.rb +36 -0
  100. data/lib/tasks/satis_tasks.rake +4 -0
  101. metadata +213 -0
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module Dropdown
5
+ class Component < ViewComponent::Base
6
+ attr_reader :url, :form, :attribute, :title, :options
7
+
8
+ def initialize(form:, attribute:, **options, &block)
9
+ super
10
+
11
+ @form = form
12
+ @attribute = attribute
13
+ @title = title
14
+ @options = options
15
+ @url = options[:url]
16
+ @chain_to = options[:chain_to]
17
+ @free_text = options[:free_text]
18
+ @needs_exact_match = options[:needs_exact_match]
19
+ @reset_button = options[:reset_button] || options[:include_blank]
20
+
21
+ options[:input_html] ||= {}
22
+ options[:input_html][:value] = hidden_value
23
+
24
+ options[:input_html][:autofocus] ||= false
25
+ if options[:input_html][:autofocus]
26
+ options[:autofocus] = 'autofocus'
27
+ options[:input_html].delete(:autofocus)
28
+ end
29
+
30
+ actions = [options[:input_html]['data-action'], 'change->satis-dropdown#display',
31
+ 'focus->satis-dropdown#focus'].join(' ')
32
+
33
+ options[:input_html].merge!('data-satis-dropdown-target' => 'hiddenInput',
34
+ 'data-action' => actions)
35
+
36
+ @block = block
37
+ @page_size = options[:page_size] || 10
38
+ end
39
+
40
+ # Deal with context
41
+ def hidden_value
42
+ value = @options[:selected]
43
+ value ||= @options.dig(:input_html, :value)
44
+ value ||= form.object&.send(attribute)
45
+ value = value.id if value.respond_to? :id
46
+ value = value.second if value.is_a?(Array) && value.size == 2 && value.first.casecmp?(value.second)
47
+ value
48
+ end
49
+
50
+ def placeholder
51
+ return title if title.present?
52
+
53
+ if form.object.class.respond_to?(:human_attribute_name)
54
+ form.object.class.human_attribute_name(attribute)
55
+ else
56
+ attribute.to_s.humanize
57
+ end
58
+ end
59
+
60
+ def value_method
61
+ options[:value_method] || :id
62
+ end
63
+
64
+ def text_method
65
+ options[:text_method] || :name
66
+ end
67
+
68
+ def custom_item_html?
69
+ !!@block
70
+ end
71
+
72
+ def item_html(item)
73
+ form.template.capture { @block.call(item) }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,10 @@
1
+ .satis-dropdown {
2
+ .max-h-select {
3
+ min-height: 50px;
4
+ max-height: 200px;
5
+ }
6
+
7
+ input {
8
+ @apply dark:text-gray-300
9
+ }
10
+ }
@@ -0,0 +1,547 @@
1
+ import ApplicationController from "../../../../frontend/controllers/application_controller"
2
+
3
+ // FIXME: Is this full path really needed?
4
+ import { debounce, popperSameWidth } from "../../../../frontend/utils"
5
+ import { createPopper } from "@popperjs/core"
6
+
7
+ export default class extends ApplicationController {
8
+ static targets = ["results", "items", "item", "searchInput", "resetButton", "toggleButton", "hiddenInput"]
9
+ static values = {
10
+ chainTo: String,
11
+ freeText: Boolean,
12
+ needsExactMatch: Boolean,
13
+ pageSize: Number,
14
+ url: String,
15
+ urlParams: Object,
16
+ }
17
+
18
+ connect() {
19
+ super.connect()
20
+
21
+ this.debouncedFetchResults = debounce(this.fetchResults.bind(this), 250)
22
+ this.debouncedLocalResults = debounce(this.localResults.bind(this), 250)
23
+ this.selectedIndex = -1
24
+
25
+ this.boundClickedOutside = this.clickedOutside.bind(this)
26
+ this.boundResetSearchInput = this.resetSearchInput.bind(this)
27
+ this.boundHandleHiddenInputChange = this.handleHiddenInputChange.bind(this)
28
+ this.boundBlur = this.handleBlur.bind(this)
29
+
30
+ // To remember what the current page and last page were, we queried
31
+ this.currentPage = 1
32
+ this.lastPage = null
33
+ this.endPage = null
34
+
35
+ // To remember what the last search was we did
36
+ this.lastSearch = null
37
+
38
+ this.display()
39
+
40
+ this.popperInstance = createPopper(this.element, this.resultsTarget, {
41
+ offset: [-20, 2],
42
+ placement: "bottom-start",
43
+ modifiers: [
44
+ {
45
+ name: "flip",
46
+ enabled: true,
47
+ options: {
48
+ boundary: this.element.closest(".satis-card"),
49
+ },
50
+ },
51
+ {
52
+ name: "preventOverflow",
53
+ enabled: true,
54
+ },
55
+ popperSameWidth,
56
+ ],
57
+ })
58
+
59
+ this.searchInputTarget.addEventListener("blur", this.boundBlur)
60
+ this.toggleButtonTarget.addEventListener("blur", this.boundBlur)
61
+ this.resultsTarget.addEventListener("blur", this.boundBlur)
62
+
63
+ window.addEventListener("click", this.boundClickedOutside)
64
+
65
+ this.hiddenInputTarget.addEventListener("change", this.boundHandleHiddenInputChange)
66
+ }
67
+
68
+ disconnect() {
69
+ this.debouncedFetchResults = null
70
+ this.debouncedLocalResults = null
71
+ window.removeEventListener("click", this.boundClickedOutside)
72
+ }
73
+
74
+ focus(event) {
75
+ this.searchInputTarget.focus()
76
+ }
77
+
78
+ blur(event) {
79
+ this.handleBlur(event)
80
+ }
81
+
82
+ handleBlur(event) {
83
+ if (!this.element.contains(event.relatedTarget) && this.resultsShown) {
84
+ this.hideResultsList()
85
+ if (event.target == this.searchInputTarget) {
86
+ this.boundResetSearchInput(event)
87
+ }
88
+ }
89
+ }
90
+
91
+ handleHiddenInputChange(event) {
92
+ if (event?.detail?.src == "satis-dropdown") {
93
+ return
94
+ }
95
+
96
+ if (this.hiddenInputTarget.value == "") {
97
+ this.searchInputTarget.value = null
98
+ } else {
99
+ this.resetSearchInput()
100
+ }
101
+ }
102
+
103
+ // Called on connect
104
+ // FIXME: Has code duplication with select
105
+ display(event) {
106
+ // Ignore if we triggered this change event
107
+ if (event?.detail?.src == "satis-dropdown") {
108
+ return
109
+ }
110
+
111
+ // Put current selection in search field
112
+ if (this.hiddenInputTarget.value) {
113
+ if (this.itemTargets.length == 0) {
114
+ let ourUrl = this.normalizedUrl
115
+ ourUrl.searchParams.append("id", this.hiddenInputTarget.value)
116
+ ourUrl.searchParams.append("page", this.currentPage)
117
+ ourUrl.searchParams.append("page_size", this.pageSizeValue)
118
+
119
+ this.fetchResultsWith(ourUrl).then(() => {
120
+ this.setHiddenInput()
121
+ })
122
+ } else {
123
+ this.setHiddenInput()
124
+ }
125
+
126
+ if (!this.searchInputTarget.value && this.freeTextValue) {
127
+ this.searchInputTarget.value = this.hiddenInputTarget.value
128
+ }
129
+ }
130
+ }
131
+
132
+ // Called when scrolling in the resultsTarget
133
+ scroll(event) {
134
+ if (this.elementScrolled(this.resultsTarget)) {
135
+ this.fetchResults(event)
136
+ }
137
+ }
138
+
139
+ // User presses keys
140
+ dispatch(event) {
141
+ if (event.target.closest('[data-controller="satis-dropdown"]') != this.element) {
142
+ return
143
+ }
144
+
145
+ this.filterResultsChainTo()
146
+
147
+ switch (event.key) {
148
+ case "ArrowDown":
149
+ if (this.hasResults) {
150
+ this.showResultsList(event)
151
+
152
+ this.moveDown()
153
+ }
154
+ break
155
+ case "ArrowUp":
156
+ if (this.hasResults) {
157
+ this.moveUp()
158
+ }
159
+ break
160
+ case "Enter":
161
+ event.preventDefault()
162
+ this.select(event)
163
+
164
+ break
165
+ case "Escape":
166
+ if (this.resultsShown) {
167
+ this.hideResultsList(event)
168
+ } else {
169
+ this.reset(event)
170
+ }
171
+
172
+ break
173
+ default:
174
+ break
175
+ }
176
+
177
+ return true
178
+ }
179
+
180
+ // User enters text in the search field
181
+ search(event) {
182
+ if (this.hasUrlValue) {
183
+ this.debouncedFetchResults(event)
184
+ } else {
185
+ this.debouncedLocalResults(event)
186
+ }
187
+ }
188
+
189
+ // User presses reset button
190
+ reset(event) {
191
+ this.hiddenInputTarget.value = null
192
+ this.hiddenInputTarget.dispatchEvent(new Event("change"))
193
+ this.searchInputTarget.value = null
194
+ this.lastSearch = null
195
+ this.lastPage = null
196
+ this.endPage = null
197
+
198
+ if (this.selectedItem) {
199
+ this.selectedItem.classList.remove("bg-primary-200")
200
+ }
201
+ this.selectedIndex = -1
202
+ if (this.hasUrlValue) {
203
+ this.itemsTarget.innerHTML = ""
204
+ }
205
+ this.hideResultsList()
206
+ this.itemTargets.forEach((item) => {
207
+ item.classList.remove("hidden")
208
+ })
209
+
210
+ if (event) {
211
+ event.preventDefault()
212
+ }
213
+
214
+ return false
215
+ }
216
+
217
+ // User selects an item using mouse
218
+ select(event) {
219
+ let dataDiv = event.target.closest('[data-satis-dropdown-target="item"]')
220
+ if (dataDiv == null) {
221
+ dataDiv = this.selectedItem
222
+ }
223
+
224
+ if (dataDiv == null) {
225
+ return
226
+ }
227
+
228
+ this.selectItem(dataDiv)
229
+
230
+ event.preventDefault()
231
+ }
232
+
233
+ selectItem(dataDiv) {
234
+ this.hideResultsList()
235
+
236
+ // Copy over data attributes on the item div to the hidden input
237
+ Array.prototype.slice.call(dataDiv.attributes).forEach((attr) => {
238
+ if (attr.name.startsWith("data") && !attr.name.startsWith("data-satis") && !attr.name.startsWith("data-action")) {
239
+ this.hiddenInputTarget.setAttribute(attr.name, attr.value)
240
+ }
241
+ })
242
+
243
+ this.searchInputTarget.value = dataDiv.getAttribute("data-satis-dropdown-item-text")
244
+ this.hiddenInputTarget.value = dataDiv.getAttribute("data-satis-dropdown-item-value")
245
+ this.lastSearch = this.searchInputTarget.value
246
+
247
+ this.hiddenInputTarget.dispatchEvent(new Event("change"))
248
+ }
249
+
250
+ // --- Helpers
251
+
252
+ setHiddenInput() {
253
+ const currentItem = this.itemTargets.find((item) => {
254
+ return this.hiddenInputTarget.value == item.getAttribute("data-satis-dropdown-item-value")
255
+ })
256
+ if (currentItem) {
257
+ this.searchInputTarget.value = currentItem.getAttribute("data-satis-dropdown-item-text")
258
+
259
+ Array.prototype.slice.call(currentItem.attributes).forEach((attr) => {
260
+ if (attr.name.startsWith("data") && !attr.name.startsWith("data-satis") && !attr.name.startsWith("data-action")) {
261
+ this.hiddenInputTarget.setAttribute(attr.name, attr.value)
262
+ }
263
+ })
264
+ this.hiddenInputTarget.dispatchEvent(new CustomEvent("change", { detail: { src: "satis-dropdown" } }))
265
+ }
266
+ }
267
+
268
+ toggleResultsList(event) {
269
+ if (this.resultsShown) {
270
+ this.hideResultsList(event)
271
+
272
+ // Not sure what the intent is, but this causes Safari not to open a ticket
273
+ // } else if (this.element.contains(document.activeElement)) {
274
+ } else {
275
+ this.filterResultsChainTo()
276
+ if (this.hasResults) {
277
+ this.showResultsList(event)
278
+ } else {
279
+ this.fetchResults(event)
280
+ }
281
+ }
282
+
283
+ event.preventDefault()
284
+ return false
285
+ }
286
+
287
+ showResultsList(event) {
288
+ this.resultsTarget.classList.remove("hidden")
289
+ this.resultsTarget.setAttribute("data-show", "")
290
+ this.popperInstance.update()
291
+ this.toggleButtonTarget.querySelector(".fa-chevron-up").classList.remove("hidden")
292
+ this.toggleButtonTarget.querySelector(".fa-chevron-down").classList.add("hidden")
293
+ }
294
+
295
+ hideResultsList(event) {
296
+ this.resultsTarget.classList.add("hidden")
297
+ this.resultsTarget.removeAttribute("data-show")
298
+ this.toggleButtonTarget.querySelector(".fa-chevron-up").classList.add("hidden")
299
+ this.toggleButtonTarget.querySelector(".fa-chevron-down").classList.remove("hidden")
300
+ }
301
+
302
+ filterResultsChainTo() {
303
+ if (!this.chainToValue) {
304
+ return
305
+ }
306
+
307
+ let chainToValue
308
+ let chainTo = this.hiddenInputTarget.form.querySelector(`[name="${this.chainToValue}"]`)
309
+ if (chainTo) {
310
+ chainToValue = chainTo.value
311
+ }
312
+
313
+ this.itemTargets.forEach((item) => {
314
+ let itemChainToValue = item.getAttribute("data-chain")
315
+ let chainMatch = true
316
+ if (this.chainToValue || itemChainToValue) {
317
+ chainMatch = chainToValue == itemChainToValue
318
+ }
319
+
320
+ if (chainMatch) {
321
+ item.classList.remove("hidden")
322
+ } else {
323
+ item.classList.add("hidden")
324
+ }
325
+ })
326
+ }
327
+
328
+ localResults(event) {
329
+ if (this.searchInputTarget.value == this.lastSearch) {
330
+ return
331
+ }
332
+
333
+ if (this.searchInputTarget.value.length < 2) {
334
+ return
335
+ }
336
+
337
+ this.lastSearch = this.searchInputTarget.value
338
+
339
+ this.itemTargets.forEach((item) => {
340
+ item.classList.remove("hidden")
341
+ })
342
+
343
+ this.filterResultsChainTo()
344
+
345
+ let matches = []
346
+ this.itemTargets.forEach((item) => {
347
+ let text = item.getAttribute("data-satis-dropdown-item-text").toLowerCase()
348
+ let value = item.getAttribute("data-satis-dropdown-item-value").toLowerCase()
349
+
350
+ if (!item.classList.contains("hidden")) {
351
+ if (this.needsExactMatchValue && text === this.searchInputTarget.value.toLowerCase()) {
352
+ matches = matches.concat(item)
353
+ } else if (!this.needsExactMatchValue && text.indexOf(this.searchInputTarget.value.toLowerCase()) >= 0) {
354
+ matches = matches.concat(item)
355
+ } else {
356
+ item.classList.add("hidden")
357
+ }
358
+ }
359
+ })
360
+
361
+ if (this.freeTextValue && matches.length != 1) {
362
+ this.hiddenInputTarget.value = this.lastSearch
363
+ }
364
+
365
+ if (matches.length == 1 && matches[0].getAttribute("data-satis-dropdown-item-text").toLowerCase().indexOf(this.lastSearch.toLowerCase()) >= 0) {
366
+ this.selectItem(matches[0].closest('[data-satis-dropdown-target="item"]'))
367
+ } else if (matches.length > 1) {
368
+ this.showResultsList(event)
369
+ }
370
+ }
371
+
372
+ // Remote search
373
+ fetchResults(event) {
374
+ const promise = new Promise((resolve, reject) => {
375
+ if ((this.searchInputTarget.value == this.lastSearch && (this.currentPage == this.lastPage || this.currentPage == this.endPage)) || !this.hasUrlValue) {
376
+ return
377
+ }
378
+
379
+ if (this.searchInputTarget.value != this.lastSearch) {
380
+ this.currentPage = 1
381
+ this.endPage = null
382
+ }
383
+
384
+ this.lastSearch = this.searchInputTarget.value
385
+ this.lastPage = this.currentPage
386
+
387
+ let ourUrl = this.normalizedUrl
388
+ let pageSize = this.pageSizeValue
389
+ if (this.searchInputTarget.value.length >= 2) {
390
+ ourUrl.searchParams.append("term", this.searchInputTarget.value)
391
+ }
392
+ ourUrl.searchParams.append("page", this.currentPage)
393
+ ourUrl.searchParams.append("page_size", pageSize)
394
+ if (this.needsExactMatchValue) {
395
+ ourUrl.searchParams.append("needs_exact_match", this.needsExactMatchValue)
396
+ }
397
+
398
+ this.fetchResultsWith(ourUrl).then((itemCount) => {
399
+ if (this.hasResults) {
400
+ this.filterResultsChainTo()
401
+ this.highLightSelected()
402
+ this.showResultsList()
403
+
404
+ if (this.nrOfItems == 1 && this.itemTargets[0].getAttribute("data-satis-dropdown-item-text").toLowerCase().indexOf(this.searchInputTarget.value.toLowerCase()) >= 0) {
405
+ this.selectItem(this.itemTargets[0].closest('[data-satis-dropdown-target="item"]'))
406
+ } else if (this.nrOfItems == 1) {
407
+ this.moveDown()
408
+ }
409
+
410
+ if (itemCount > 0) {
411
+ this.currentPage += 1
412
+ }
413
+ if (itemCount < pageSize) {
414
+ this.endPage = this.currentPage
415
+ }
416
+
417
+ resolve()
418
+ }
419
+ })
420
+ })
421
+ return promise
422
+ }
423
+
424
+ fetchResultsWith(ourUrl) {
425
+ const promise = new Promise((resolve, reject) => {
426
+ fetch(ourUrl.href, {}).then((response) => {
427
+ response.text().then((data) => {
428
+ let tmpDiv = document.createElement("div")
429
+ tmpDiv.innerHTML = data
430
+
431
+ // Add needed items
432
+ Array.from(tmpDiv.children).forEach((item) => {
433
+ item.setAttribute("data-satis-dropdown-target", "item")
434
+ item.setAttribute("data-action", "click->satis-dropdown#select")
435
+ })
436
+
437
+ if (this.currentPage == 1) {
438
+ this.itemsTarget.innerHTML = tmpDiv.innerHTML
439
+ } else {
440
+ if (tmpDiv.innerHTML.length > 0) {
441
+ this.itemsTarget.insertAdjacentHTML("beforeend", tmpDiv.innerHTML)
442
+ }
443
+ }
444
+
445
+ resolve(tmpDiv.children.length)
446
+ })
447
+ })
448
+ })
449
+ return promise
450
+ }
451
+
452
+ get normalizedUrl() {
453
+ let ourUrl
454
+ try {
455
+ ourUrl = new URL(this.urlValue)
456
+ } catch (error) {
457
+ ourUrl = new URL(this.urlValue, window.location.href)
458
+ }
459
+
460
+ // Add searchParams based on url_params
461
+ const form = this.element.closest("form")
462
+ Object.entries(this.urlParamsValue).forEach((item) => {
463
+ let elm = form.querySelector(`[name='${item[1]}']`)
464
+ if (elm) {
465
+ ourUrl.searchParams.append(item[0], elm.value)
466
+ } else {
467
+ ourUrl.searchParams.append(item[0], item[1])
468
+ }
469
+ })
470
+
471
+ return ourUrl
472
+ }
473
+
474
+ get resultsShown() {
475
+ return this.resultsTarget.hasAttribute("data-show")
476
+ }
477
+
478
+ get nrOfItems() {
479
+ return this.itemTargets.filter((item) => {
480
+ return !item.classList.contains("hidden")
481
+ }).length
482
+ }
483
+
484
+ get hasResults() {
485
+ return this.nrOfItems > 0
486
+ }
487
+
488
+ increaseSelectedIndex() {
489
+ this.selectedIndex = this.selectedIndex + 1
490
+ if (this.selectedIndex >= this.nrOfItems) {
491
+ this.selectedIndex = this.nrOfItems - 1
492
+ }
493
+ }
494
+
495
+ decreaseSelectedIndex() {
496
+ this.selectedIndex = this.selectedIndex - 1
497
+ if (this.selectedIndex < 0) {
498
+ this.selectedIndex = 0
499
+ }
500
+ }
501
+
502
+ get selectedItem() {
503
+ return this.itemTargets.filter((item) => {
504
+ return !item.classList.contains("hidden")
505
+ })[this.selectedIndex]
506
+ }
507
+
508
+ lowLightSelected() {
509
+ if (this.selectedItem) {
510
+ this.selectedItem.classList.remove("bg-primary-200")
511
+ }
512
+ }
513
+
514
+ highLightSelected() {
515
+ if (this.selectedItem) {
516
+ this.selectedItem.classList.add("bg-primary-200")
517
+ this.selectedItem.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" })
518
+ }
519
+ }
520
+
521
+ moveDown() {
522
+ this.lowLightSelected()
523
+ this.increaseSelectedIndex()
524
+ this.highLightSelected()
525
+ }
526
+
527
+ moveUp() {
528
+ this.lowLightSelected()
529
+ this.decreaseSelectedIndex()
530
+ this.highLightSelected()
531
+ }
532
+
533
+ resetSearchInput(event) {
534
+ this.setHiddenInput()
535
+ }
536
+
537
+ clickedOutside(event) {
538
+ if (event.target.tagName == "svg" || event.target.tagName == "path") {
539
+ return
540
+ }
541
+ if (!this.element.contains(event.target)) {
542
+ if (this.resultsShown) {
543
+ this.hideResultsList()
544
+ }
545
+ }
546
+ }
547
+ }
@@ -0,0 +1,3 @@
1
+ .sts-flash-messages
2
+ - messages.each do |message|
3
+ = message
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module FlashMessages
5
+ class Message < Satis::ApplicationComponent
6
+ attr_reader :message, :level, :icon
7
+
8
+ def initialize(message:, level: :alert, icon: nil)
9
+ super
10
+ @message = message
11
+ @level = level
12
+ @icon = icon
13
+ end
14
+
15
+ def color_class
16
+ case level.to_sym
17
+ when :alert
18
+ :'bg-red-500'
19
+ when :notice
20
+ :'bg-green-500'
21
+ else
22
+ :'bg-yellow-500'
23
+ end
24
+ end
25
+ end
26
+
27
+ class Component < Satis::ApplicationComponent
28
+ renders_many :messages, Message
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ // Have these here for purging
2
+ // .bg-red-500
3
+ // .bg-green-500
4
+ // .bg-yellow-500
5
+
6
+ .sts-flash-messages {
7
+ &__message_container {
8
+ @apply inset-x-0 pb-2 sm:pb-5;
9
+ }
10
+
11
+ &__message {
12
+ @apply ml-3 font-medium text-white truncate;
13
+
14
+ i {
15
+ margin-right: 0.5rem;
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,8 @@
1
+ .sts-flash-messages__message_container
2
+ .p-2.rounded-lg class=color_class
3
+ .flex.items-center.justify-between.flex-wrap
4
+ .w-0.flex-1.flex.items-center
5
+ .sts-flash-messages__message
6
+ - if icon
7
+ i class=icon
8
+ = message
@@ -0,0 +1,4 @@
1
+ dl class=options[:class]
2
+ - items.each do |item|
3
+ = item
4
+