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,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module DateTimePicker
5
+ class Component < Satis::ApplicationComponent
6
+ attr_reader :form, :attribute, :inline, :options, :clearable, :format, :time_picker, :multiple, :range
7
+
8
+ def initialize(form:, attribute:, **options, &block)
9
+ super
10
+
11
+ @form = form
12
+ @attribute = attribute
13
+ @options = options
14
+ @block = block
15
+ options[:input_html] ||= {}
16
+ @time_picker = options.key?(:time_picker) ? options[:time_picker] : true
17
+ @inline = options.key?(:inline) ? options[:inline] : false
18
+ @clearable = options.key?(:clearable) ? options[:clearable] : true
19
+ @multiple = options.key?(:multiple) ? options[:multiple] : false
20
+ @range = options.key?(:range) ? options[:range] : false
21
+
22
+ @format = if options.key?(:format)
23
+ options[:format]
24
+ else
25
+ { "weekday": 'long', "month": 'short', "year": 'numeric', "day": 'numeric',
26
+ "hour": 'numeric', "minute": 'numeric', "hour12": false }
27
+ end
28
+
29
+ options[:input_html].merge!('data-satis-date-time-picker-target' => 'hiddenInput')
30
+
31
+ # FIXME: deal with ranges and multiples
32
+ hidden_value = options[:input_html][:value]
33
+ hidden_value ||= @form.object.send(attribute)
34
+ hidden_value = if hidden_value.is_a?(String)
35
+ hidden_value&.split(' - ')&.map { |d| Time.parse(d).iso8601 }.join(' - ')
36
+ else
37
+ hidden_value&.iso8601
38
+ end
39
+
40
+ options[:input_html][:value] = hidden_value
41
+ end
42
+
43
+ def week_start
44
+ Date::DAYS_INTO_WEEK[Date.beginning_of_week] || 1
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ .satis-date-time-picker {
2
+ .pt-2px {
3
+ padding-top: 2px;
4
+ }
5
+ }
@@ -0,0 +1,499 @@
1
+ import ApplicationController from "../../../../frontend/controllers/application_controller"
2
+ import { createPopper } from "@popperjs/core"
3
+ import { debounce } from "../../../../frontend/utils"
4
+
5
+ export default class extends ApplicationController {
6
+ static targets = [
7
+ "input",
8
+ "hiddenInput",
9
+ "clearButton",
10
+ "hours",
11
+ "minutes",
12
+ "month",
13
+ "year",
14
+ "days",
15
+ "weekDays",
16
+ "calendarView",
17
+ "weekDayTemplate",
18
+ "emtpyTemplate",
19
+ "dayTemplate",
20
+ ]
21
+ static values = {
22
+ locale: String, // Which locale should be used, if nothing entered, browser locale is used
23
+ weekStart: Number, // On which day do we start the week, sunday - saturday : 0 - 6
24
+ format: Object, // JSON date-format - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
25
+ clearable: Boolean, // Whether it is allowed to clear the value
26
+ inline: Boolean, // Whether the calendar should be shown inline
27
+ timePicker: Boolean, // Whether to show the timePicker
28
+ range: Boolean, // whether we allow to select a range of dates
29
+ multiple: Boolean, // whether we allow to select multiple dates
30
+ // visibleMonths: Number, // TODO: whether we show more than one calendar view
31
+ }
32
+
33
+ connect() {
34
+ super.connect()
35
+
36
+ if (!this.localeValue) {
37
+ this.localeValue = navigator.language
38
+ }
39
+
40
+ if (!this.clearableValue) {
41
+ this.clearButtonTarget.classList.add("hidden")
42
+ }
43
+
44
+ if (this.timePickerValue) {
45
+ this.rangeValue = false
46
+ this.multipleValue = false
47
+ }
48
+
49
+ this.selectedValue = []
50
+ let startDate = new Date()
51
+ if (this.hiddenInputTarget.value) {
52
+ this.hiddenInputTarget.value.split(/;| - |\s/).forEach((value) => {
53
+ this.selectedValue.push(new Date(Date.parse(value)))
54
+ })
55
+ }
56
+
57
+ if (this.selectedValue.length == 0) {
58
+ this.selectedValue.push(
59
+ new Date(
60
+ startDate.getFullYear(),
61
+ startDate.getMonth(),
62
+ startDate.getDate(),
63
+ startDate.getHours(),
64
+ startDate.getMinutes(),
65
+ 0
66
+ )
67
+ )
68
+ }
69
+
70
+ this.displayValue = new Date(this.selectedValue[0].getFullYear(), this.selectedValue[0].getMonth(), 1)
71
+ this.currentSelectNr = this.selectedValue.length
72
+
73
+ if (!this.inlineValue) {
74
+ this.popperInstance = createPopper(this.element, this.calendarViewTarget, {
75
+ offset: [-20, 2],
76
+ placement: "bottom-start",
77
+ modifiers: [
78
+ {
79
+ name: "flip",
80
+ enabled: true,
81
+ options: {
82
+ boundary: this.element.closest(".satis-card"),
83
+ },
84
+ },
85
+ {
86
+ name: "preventOverflow",
87
+ enabled: true,
88
+ },
89
+ ],
90
+ })
91
+ }
92
+
93
+ this.boundClickedOutside = this.clickedOutside.bind(this)
94
+ if (!this.inlineValue) {
95
+ window.addEventListener("click", this.boundClickedOutside)
96
+ }
97
+
98
+ this.boundKeyUp = this.keyUp.bind(this)
99
+ window.addEventListener("keyup", this.boundKeyUp)
100
+
101
+ let input = this.inputTarget
102
+ this.hiddenInputTarget.addEventListener("focus", function (event) {
103
+ input.focus()
104
+ })
105
+
106
+ // we set the calendar data and update the visual layout
107
+ if (this.hiddenInputTarget.value) {
108
+ // flag true indicates we are also updating/refreshing the input field data
109
+ this.refreshCalendar(true)
110
+ } else {
111
+ this.refreshCalendar(false)
112
+ }
113
+ }
114
+
115
+ disconnect() {
116
+ window.removeEventListener("click", this.boundClickedOutside)
117
+ window.removeEventListener("keyup", this.boundKeyUp)
118
+ }
119
+
120
+ /**************
121
+ * ACTIONS *
122
+ **************/
123
+
124
+ clear(event) {
125
+ if (this.clearableValue) {
126
+ this.selectedValue = []
127
+
128
+ let today = new Date()
129
+ this.displayValue = new Date(today.getFullYear(), today.getMonth(), 1)
130
+ this.hiddenInputTarget.value = ""
131
+ this.hiddenInputTarget.dispatchEvent(new Event("change"))
132
+ this.refreshCalendar()
133
+ this.inputTarget.value = ""
134
+ }
135
+ event.preventDefault()
136
+ }
137
+
138
+ showCalendar(event) {
139
+ this.calendarViewTarget.classList.remove("hidden")
140
+ this.calendarViewTarget.setAttribute("data-show", "")
141
+ if (!this.inlineValue) {
142
+ this.popperInstance.update()
143
+ }
144
+ }
145
+
146
+ hideCalendar(event) {
147
+ this.calendarViewTarget.classList.add("hidden")
148
+ this.calendarViewTarget.removeAttribute("data-show")
149
+ }
150
+
151
+ previousMonth(event) {
152
+ this.displayValue = new Date(new Date(this.displayValue).setMonth(this.displayValue.getMonth() - 1))
153
+ this.refreshCalendar()
154
+ }
155
+
156
+ nextMonth(event) {
157
+ this.displayValue = new Date(new Date(this.displayValue).setMonth(this.displayValue.getMonth() + 1))
158
+ this.refreshCalendar()
159
+ }
160
+
161
+ clickedOutside(event) {
162
+ if (event.target.tagName == "svg" || event.target.tagName == "path") {
163
+ return
164
+ }
165
+
166
+ let isInside = false
167
+ let controllerEl = event.target.closest('[data-controller="satis-date-time-picker"]')
168
+ if (controllerEl) {
169
+ isInside = controllerEl["satis-date-time-picker"] == this
170
+ }
171
+
172
+ if (!isInside) {
173
+ this.hideCalendar(event)
174
+ }
175
+
176
+ event.cancelBubble = true
177
+ }
178
+
179
+ keyUp(event) {
180
+ if (event.key == "Tab") {
181
+ let controllerEl = document.activeElement.closest('[data-controller="satis-date-time-picker"]')
182
+ if (controllerEl) {
183
+ if (controllerEl["satis-date-time-picker"] != this) {
184
+ this.hideCalendar(event)
185
+ }
186
+ } else {
187
+ this.hideCalendar(event)
188
+ }
189
+
190
+ event.cancelBubble = true
191
+ }
192
+ }
193
+
194
+ changeHours(event) {
195
+ this.selectedValue[0] = new Date(new Date(this.selectedValue[0]).setHours(+event.target.value))
196
+ this.refreshInputs()
197
+ event.preventDefault()
198
+ }
199
+
200
+ changeMinutes(event) {
201
+ this.selectedValue[0] = new Date(new Date(this.selectedValue[0]).setMinutes(+event.target.value))
202
+ this.refreshInputs()
203
+ event.preventDefault()
204
+ }
205
+
206
+ keyPress(event) {
207
+ switch (event.key) {
208
+ case "Escape":
209
+ this.hideCalendar(event)
210
+ break
211
+ case "Enter":
212
+ event.preventDefault()
213
+ event.cancelBubble = true
214
+ break
215
+ default:
216
+ break
217
+ }
218
+ }
219
+
220
+ dateTimeEntered(event) {
221
+ return
222
+
223
+ // FIXME: This doesn't work properly yet
224
+ let newValue
225
+ try {
226
+ newValue = new Date(this.inputTarget.value)
227
+ } catch (error) {}
228
+ if (!isNaN(newValue.getTime())) {
229
+ this.selectedValue = [newValue]
230
+ this.refreshCalendar()
231
+ }
232
+ }
233
+
234
+ selectDay(event) {
235
+ let oldCurrentValue = this.selectedValue[0]
236
+ if (!this.rangeValue && !this.multipleValue) {
237
+ this.selectedValue[0] = new Date(new Date(this.displayValue).setDate(+event.target.innerText))
238
+ if (this.timePickerValue && oldCurrentValue) {
239
+ this.selectedValue[0].setHours(oldCurrentValue.getHours())
240
+ this.selectedValue[0].setMinutes(oldCurrentValue.getMinutes())
241
+ }
242
+ this.currentSelectNr = 1
243
+ } else if (this.rangeValue) {
244
+ if (this.currentSelectNr == 1) {
245
+ this.selectedValue = []
246
+ }
247
+ this.selectedValue[this.currentSelectNr - 1] = new Date(
248
+ new Date(this.displayValue).setDate(+event.target.innerText)
249
+ )
250
+ this.currentSelectNr += 1
251
+ if (this.currentSelectNr > 2) {
252
+ this.currentSelectNr = 1
253
+ }
254
+ } else if (this.multipleValue) {
255
+ this.selectedValue[this.currentSelectNr - 1] = new Date(
256
+ new Date(this.displayValue).setDate(+event.target.innerText)
257
+ )
258
+ this.currentSelectNr += 1
259
+ }
260
+
261
+ this.refreshCalendar()
262
+
263
+ if (!this.rangeValue || this.selectedValue.length == 2) {
264
+ this.hideCalendar()
265
+ }
266
+
267
+ event.cancelBubble = true
268
+ }
269
+
270
+ /***********
271
+ * HELPERS *
272
+ ***********/
273
+
274
+ get maxSelectNr() {
275
+ let result = 1
276
+ if (this.rangeValue) {
277
+ result = 2
278
+ } else if (this.multipleValue) {
279
+ result = 0
280
+ }
281
+ return result
282
+ }
283
+
284
+ // Refreshes the hidden and visible input values
285
+ refreshInputs() {
286
+ let joinChar = ";"
287
+ if (this.rangeValue) {
288
+ joinChar = " - "
289
+ } else if (this.multipleValue) {
290
+ joinChar = ";"
291
+ }
292
+
293
+ let inputValue = this.selectedValue
294
+ .map((val) => {
295
+ return this.iso8601(val)
296
+ })
297
+ .join(joinChar)
298
+
299
+ if (inputValue.split(joinChar).length >= this.maxSelectNr) {
300
+ this.hiddenInputTarget.value = inputValue
301
+ this.hiddenInputTarget.dispatchEvent(new Event("change"))
302
+ }
303
+
304
+ let format = this.formatValue
305
+ if (!this.timePickerValue) {
306
+ delete format["hour"]
307
+ delete format["minute"]
308
+ }
309
+
310
+ this.inputTarget.value = this.selectedValue
311
+ .map((val) => {
312
+ return Intl.DateTimeFormat(this.localeValue, format).format(val)
313
+ })
314
+ .join(joinChar)
315
+ }
316
+
317
+ // Refreshes the calendar
318
+ refreshCalendar(refreshInputs) {
319
+ this.monthTarget.innerHTML = this.monthName
320
+ this.yearTarget.innerHTML = this.displayValue.getFullYear()
321
+
322
+ this.weekDaysTarget.innerHTML = ""
323
+ this.getWeekDays(this.localeValue).forEach((dayName) => {
324
+ this.weekDaysTarget.insertAdjacentHTML(
325
+ "beforeend",
326
+ this.weekDayTemplateTarget.innerHTML.replace(/\${name}/g, dayName)
327
+ )
328
+ })
329
+
330
+ // Deal with AM/PM
331
+ // new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true })
332
+
333
+ if (this.hasHoursTarget) {
334
+ if (this.selectedValue[0]) {
335
+ this.hoursTarget.value = ("" + this.selectedValue[0].getHours()).padStart(2, "0")
336
+ } else {
337
+ this.hoursTarget.value = "0" // FIXME: Should be 0:00 in locale
338
+ }
339
+ }
340
+ if (this.hasMinutesTarget) {
341
+ if (this.selectedValue[0]) {
342
+ this.minutesTarget.value = ("" + this.selectedValue[0].getMinutes()).padStart(2, "0")
343
+ } else {
344
+ this.minutesTarget.value = "00" // FIXME: Should be 0:00 in locale
345
+ }
346
+ }
347
+ this.daysTarget.innerHTML = ""
348
+
349
+ this.monthDays.forEach((day) => {
350
+ if (day == " ") {
351
+ this.daysTarget.insertAdjacentHTML("beforeend", this.emtpyTemplateTarget.innerHTML)
352
+ } else {
353
+ let date = new Date(new Date(this.displayValue).setDate(day))
354
+
355
+ let tmpDiv = document.createElement("div")
356
+ tmpDiv.innerHTML = this.dayTemplateTarget.innerHTML.replace(/\${day}/g, day)
357
+
358
+ if (this.isToday(date)) {
359
+ let div = tmpDiv.querySelector(".text-center")
360
+ div.classList.add("border-red-500", "border")
361
+ }
362
+ let div = tmpDiv.querySelector(".text-center")
363
+
364
+ if (this.isSelected(date)) {
365
+ if (this.rangeValue && this.selectedValue.length == 2) {
366
+ if (this.isDate(this.selectedValue[0], date)) {
367
+ div.classList.add("bg-primary-500", "text-white", "dark:text-gray-200")
368
+ div.classList.remove("rounded-r-full")
369
+ } else if (this.isDate(this.selectedValue[1], date)) {
370
+ div.classList.add("bg-primary-500", "text-white", "dark:text-gray-200")
371
+ div.classList.remove("rounded-l-full")
372
+ } else if (this.isSelected(date)) {
373
+ div.classList.remove("rounded-r-full")
374
+ div.classList.remove("rounded-l-full")
375
+ div.classList.add("bg-primary-200", "text-white", "dark:text-gray-200")
376
+ }
377
+ } else {
378
+ div.classList.add("bg-primary-500", "text-white", "dark:text-gray-200")
379
+ }
380
+ } else {
381
+ div.classList.add("text-gray-700", "dark:text-gray-300")
382
+ }
383
+
384
+ this.daysTarget.insertAdjacentHTML("beforeend", tmpDiv.innerHTML)
385
+ tmpDiv.remove()
386
+ }
387
+ })
388
+
389
+ if (refreshInputs != false) {
390
+ this.refreshInputs()
391
+ }
392
+ }
393
+
394
+ // Format the given Date into an ISO8601 string whilst preserving the given timezone
395
+ iso8601(date) {
396
+ let tzo = -date.getTimezoneOffset(),
397
+ dif = tzo >= 0 ? "+" : "-",
398
+ pad = function (num) {
399
+ let norm = Math.floor(Math.abs(num))
400
+ return (norm < 10 ? "0" : "") + norm
401
+ }
402
+
403
+ return (
404
+ date.getFullYear() +
405
+ "-" +
406
+ pad(date.getMonth() + 1) +
407
+ "-" +
408
+ pad(date.getDate()) +
409
+ "T" +
410
+ pad(date.getHours()) +
411
+ ":" +
412
+ pad(date.getMinutes()) +
413
+ ":" +
414
+ pad(date.getSeconds()) +
415
+ dif +
416
+ pad(tzo / 60) +
417
+ ":" +
418
+ pad(tzo % 60)
419
+ )
420
+ }
421
+
422
+ // Is date today?
423
+ isToday(date) {
424
+ const today = new Date()
425
+ return (
426
+ date.getDate() === today.getDate() &&
427
+ date.getMonth() === today.getMonth() &&
428
+ date.getFullYear() === today.getFullYear()
429
+ )
430
+ }
431
+
432
+ isDate(today, date) {
433
+ return (
434
+ date.getDate() === today.getDate() &&
435
+ date.getMonth() === today.getMonth() &&
436
+ date.getFullYear() === today.getFullYear()
437
+ )
438
+ }
439
+
440
+ // Is date the currently selected value
441
+ isSelected(date) {
442
+ if (this.rangeValue && this.selectedValue.length == 2) {
443
+ return date >= this.selectedValue[0] && date <= this.selectedValue[1]
444
+ } else {
445
+ return this.selectedValue.some((selDate) => {
446
+ return (
447
+ date.getDate() === selDate.getDate() &&
448
+ date.getMonth() === selDate.getMonth() &&
449
+ date.getFullYear() === selDate.getFullYear()
450
+ )
451
+ })
452
+ }
453
+ }
454
+
455
+ // Get name of month for current value
456
+ get monthName() {
457
+ let result = new Date(this.displayValue).toLocaleString(this.localeValue, { month: "long" })
458
+ result = result[0].toUpperCase() + result.substring(1)
459
+ return result
460
+ }
461
+
462
+ // Gets the names of week days
463
+ getWeekDays(locale) {
464
+ const baseDate = new Date(2021, 1, this.weekStartValue) // 1 Feb 2021 is a monday
465
+ let weekDays = []
466
+ for (let i = 0; i < 7; i++) {
467
+ let weekDay = baseDate.toLocaleDateString(locale, { weekday: "short" })
468
+ weekDays.push(weekDay)
469
+ baseDate.setDate(baseDate.getDate() + 1)
470
+ }
471
+ return weekDays
472
+ }
473
+
474
+ // Gets the list of days to display
475
+ get monthDays() {
476
+ let results = []
477
+
478
+ // Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6
479
+ let dayOfFirstOfMonth = new Date(new Date(this.displayValue).setDate(0)).getDay() + 1 - this.weekStartValue
480
+ let monthStart = dayOfFirstOfMonth
481
+ if (dayOfFirstOfMonth < 0) {
482
+ monthStart += 7
483
+ }
484
+
485
+ let monthEnd = new Date(
486
+ new Date(new Date(this.displayValue).setMonth(this.displayValue.getMonth() + 1)).setDate(0)
487
+ ).getDate()
488
+
489
+ for (let index = 0; index < monthStart; index++) {
490
+ results.push(" ")
491
+ }
492
+
493
+ for (let index = 1; index <= monthEnd; index++) {
494
+ results.push(index)
495
+ }
496
+
497
+ return results
498
+ }
499
+ }
@@ -0,0 +1,36 @@
1
+ div.satis-dropdown data-action="keydown->satis-dropdown#dispatch" data-controller="satis-dropdown" data-satis-dropdown-page-size-value=@page_size data-satis-dropdown-url-value=url data-satis-dropdown-url-params-value=(options[:url_params]||{}).to_json data-satis-dropdown-chain-to-value=@chain_to data-satis-dropdown-free-text-value=@free_text data-satis-dropdown-needs-exact-match-value="#{@needs_exact_match ? 'true' : 'false'}"
2
+ / Hidden (using CSS) form field containing results
3
+ = form.text_field(attribute, options[:input_html].reverse_merge(class: 'hidden'))
4
+ .flex.flex-col
5
+ .flex.flex-col.items-center
6
+ .w-full
7
+ .h-12.p-1.bg-white.dark:bg-gray-800.flex.border.border-gray-300.dark:border-gray-700.rounded
8
+ .flex.flex-auto.flex-wrap
9
+ / Input where you can search
10
+ input.p-1.px-2.appearance-none.outline-none.w-full.text-gray-800.dark:bg-gray-800 data-action="input->satis-dropdown#search" data-satis-dropdown-target="searchInput" placeholder=placeholder autofocus=options[:autofocus]
11
+ div
12
+ / Reset button
13
+ - unless @reset_button == false
14
+ button.cursor-pointer.w-6.h-full.flex.items-center.text-gray-400.outline-none.focus:outline-none data-satis-dropdown-target="resetButton" data-action="click->satis-dropdown#reset" tabindex="-1"
15
+ i.fas.fa-xmark
16
+ .text-gray-300.w-8.py-1.pl-2.pr-1.border-l.flex.items-center.border-gray-200.dark:border-gray-700
17
+ / Up/down chevrons
18
+ button.cursor-pointer.w-6.h-6.text-gray-600.outline-none.focus:outline-none data-action="click->satis-dropdown#toggleResultsList" data-satis-dropdown-target="toggleButton" tabindex="-1"
19
+ i.hidden.fas.fa-chevron-up
20
+ i.fas.fa-chevron-down
21
+
22
+ / Container for results
23
+ .hidden.container.shadow.bg-white.border.border-gray-300.dark:bg-gray-800.dark:border-gray-700.z-10.rounded.max-h-select.overflow-y-auto.w-full data-satis-dropdown-target="results" data-action="scroll->satis-dropdown#scroll" tabindex="-1"
24
+ .flex.flex-col.w-full data-satis-dropdown-target="items"
25
+ - options[:collection]&.each do |item|
26
+ - data_attrs = item.try(:third) ? item.third : {}
27
+ div data-satis-dropdown-target="item" data-satis-dropdown-item-value=item.send(value_method) data-satis-dropdown-item-text=item.send(text_method) data-action="click->satis-dropdown#select" *data_attrs
28
+ - if custom_item_html?
29
+ = item_html(item)
30
+ - else
31
+ .cursor-pointer.w-full.dark:border-gray-700.border-gray-100.border-b.hover:bg-primary-200
32
+ .flex.w-full.items-center.p-2.pl-2.border-transparent.border-l-2.hover:border-teal-100
33
+ .w-full.items-center.flex
34
+ .mx-2.-mt-1
35
+ span = item.send(text_method)
36
+
@@ -0,0 +1,48 @@
1
+ # Dropdown
2
+
3
+ ## UI
4
+
5
+ https://tailwindcomponents.com/component/select-with-custom-list
6
+
7
+ ## Usage
8
+
9
+ ### Simple, without custom HTML
10
+
11
+ ```slim
12
+ = f.input :account_id, collection: policy_scope(Account).with(@user.account_id), as: :dropdown
13
+ ```
14
+
15
+ ### Simple, with custom HTML
16
+
17
+ ```slim
18
+ = f.input :account_id, collection: policy_scope(Account).with(@user.account_id), as: :dropdown do |account|
19
+ .cursor-pointer.w-full.border-gray-100.border-b.hover:bg-primary-200
20
+ .flex.w-full.items-center.p-2.pl-2.border-transparent.border-l-2.hover:border-teal-100
21
+ .w-full.items-center.flex
22
+ .mx-2.-mt-1
23
+ span = account.name
24
+ .text-xs.truncate.w-full.normal-case.font-normal.-mt-1.text-gray-500 = account.name
25
+ ```
26
+
27
+ ### Remote select, which always needs custom HTML
28
+
29
+ ```
30
+ = f.input :location_id, url: select_locations_url(format: :html), url_params: { account_id: "[order][account_id]" }, as: :dropdown
31
+ = f.association :location, url: select_locations_url(format: :html), url_params: { account_id: "[order][account_id]" }
32
+ ```
33
+
34
+ The url will be called with format :html, and either a params[:term] or a params[:id], both searches need to be supported.
35
+
36
+ This `select.html.slim` should look like this, note the extra div with id/name information:
37
+
38
+ ```
39
+ - @locations.each do |location|
40
+ div data-satis-dropdown-item-value=location.id data-satis-dropdown-item-text=location.name
41
+ .cursor-pointer.w-full.border-gray-100.rounded-t.border-b.hover:bg-primary-200
42
+ .flex.w-full.items-center.p-2.pl-2.border-transparent.border-l-2.hover:border-teal-100
43
+ .w-full.items-center.flex
44
+ .mx-2.-mt-1
45
+ span = location.name
46
+ ```
47
+
48
+ Any attributes on above item div (other than the data-satis attributes) will be copied to the hidden input upon select.