stimulus-components 1.0.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +133 -0
  4. data/app/javascript/animated-number.js +66 -0
  5. data/app/javascript/auto-submit.js +27 -0
  6. data/app/javascript/carousel.js +28 -0
  7. data/app/javascript/character-counter.js +47 -0
  8. data/app/javascript/chartjs.js +58 -0
  9. data/app/javascript/checkbox-select-all.js +76 -0
  10. data/app/javascript/clipboard.js +48 -0
  11. data/app/javascript/color-picker.js +75 -0
  12. data/app/javascript/confirmation.js +30 -0
  13. data/app/javascript/content-loader.js +109 -0
  14. data/app/javascript/dialog.js +53 -0
  15. data/app/javascript/dropdown.js +27 -0
  16. data/app/javascript/glow.js +30 -0
  17. data/app/javascript/hotkey.js +26 -0
  18. data/app/javascript/lightbox.js +26 -0
  19. data/app/javascript/notification.js +49 -0
  20. data/app/javascript/password-visibility.js +27 -0
  21. data/app/javascript/places-autocomplete.js +123 -0
  22. data/app/javascript/popover.js +53 -0
  23. data/app/javascript/prefetch.js +56 -0
  24. data/app/javascript/rails-nested-form.js +43 -0
  25. data/app/javascript/read-more.js +38 -0
  26. data/app/javascript/remote-rails.js +27 -0
  27. data/app/javascript/reveal-controller.js +33 -0
  28. data/app/javascript/scroll-progress.js +32 -0
  29. data/app/javascript/scroll-reveal.js +62 -0
  30. data/app/javascript/scroll-to.js +56 -0
  31. data/app/javascript/sortable.js +74 -0
  32. data/app/javascript/sound.js +39 -0
  33. data/app/javascript/speech-recognition.js +92 -0
  34. data/app/javascript/stimulus-components.js +71 -0
  35. data/app/javascript/textarea-autogrow.js +45 -0
  36. data/app/javascript/timeago.js +66 -0
  37. data/lib/stimulus-components/engine.rb +9 -0
  38. data/lib/stimulus-components/version.rb +3 -0
  39. data/lib/stimulus-components.rb +43 -0
  40. metadata +98 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8c412b605bdc72f29a30d0d09516727ba0089c2dbf17aa0b530ed86499f0fd8
4
+ data.tar.gz: eb6ddc8d35cb27654bc6a2f8bb4308334f0e5a05aa3d659e3b2005da8e5159be
5
+ SHA512:
6
+ metadata.gz: 22160175d85e4a96474202ec29ddb5e640197299d102d18ca5ecc6e98bf6ce23380aae4f1f3377cea7e01829cd148ba73bb86076786d27d1fc31f78a1f6003ce
7
+ data.tar.gz: 6aa949ac5cf13530a7adb45faa95e1b3679f85936849fd32303be410a463218ddc1b9fb6f8ce51a9d762d4d01e752746a3148dcfc444cabca52fda000a69e326
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Stimulus Components
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # Stimulus Components
2
+
3
+ A Ruby gem that packages the [stimulus-components](https://github.com/stimulus-components/stimulus-components) collection of 34+ Stimulus controllers for easy integration with Rails applications.
4
+
5
+ ## Requirements
6
+
7
+ - Rails 7.1+
8
+ - Ruby 3.1+
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem "stimulus-components"
16
+ ```
17
+
18
+ Then run:
19
+
20
+ ```bash
21
+ bundle install
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Importmap (Recommended for Rails 7.1+)
27
+
28
+ Add to your `config/importmap.rb`:
29
+
30
+ ```ruby
31
+ pin "stimulus-components", to: "stimulus-components.js"
32
+ pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/gh/hotwired/stimulus@3.2.2/dist/stimulus.umd.js"
33
+ pin "@hotwired/stimulus-loading", to: "https://cdn.jsdelivr.net/gh/hotwired/stimulus-loading@1.0.5/dist/stimulus-loading.umd.js"
34
+ pin "stimulus-use", to: "https://cdn.jsdelivr.net/npm/stimulus-use@1.0.0/dist/index.js"
35
+ ```
36
+
37
+ ### Sprockets
38
+
39
+ Add to your `app/assets/javascripts/application.js`:
40
+
41
+ ```javascript
42
+ //= require stimulus-components
43
+ ```
44
+
45
+ ### Application Controller
46
+
47
+ In your JavaScript entry point (e.g., `app/javascript/controllers/index.js`):
48
+
49
+ ```javascript
50
+ import { application } from "stimulus-components"
51
+
52
+ window.Stimulus = application
53
+ ```
54
+
55
+ Or import individual controllers:
56
+
57
+ ```javascript
58
+ import ClipboardController from "stimulus-components/clipboard"
59
+ import PasswordVisibilityController from "stimulus-components/password-visibility"
60
+ ```
61
+
62
+ ## Available Controllers
63
+
64
+ | Controller | Description |
65
+ |------------|-------------|
66
+ | `animated-number` | Animates numbers on scroll |
67
+ | `auto-submit` | Auto-submits forms with debounce |
68
+ | `carousel` | Swiper-based carousel |
69
+ | `character-counter` | Counts characters in inputs |
70
+ | `chartjs` | Chart.js integration |
71
+ | `checkbox-select-all` | Select all checkbox functionality |
72
+ | `clipboard` | Copy to clipboard |
73
+ | `color-picker` | Color picker with Pickr |
74
+ | `confirmation` | Form confirmation checks |
75
+ | `content-loader` | Load remote content |
76
+ | `dialog` | Native dialog element |
77
+ | `dropdown` | Dropdown with transitions |
78
+ | `glow` | Hover glow effect |
79
+ | `hotkey` | Keyboard shortcuts |
80
+ | `lightbox` | Image lightbox gallery |
81
+ | `notification` | Toast notifications |
82
+ | `password-visibility` | Toggle password visibility |
83
+ | `places-autocomplete` | Google Places autocomplete |
84
+ | `popover` | Popover content display |
85
+ | `prefetch` | Link prefetching |
86
+ | `rails-nested-form` | Nested form management |
87
+ | `read-more` | Expandable content |
88
+ | `remote-rails` | Remote response handling |
89
+ | `reveal` | Toggle element visibility |
90
+ | `scroll-progress` | Scroll progress bar |
91
+ | `scroll-reveal` | Scroll-based animations |
92
+ | `scroll-to` | Smooth scroll to element |
93
+ | `sortable` | Sortable lists |
94
+ | `sound` | Audio playback |
95
+ | `speech-recognition` | Voice input |
96
+ | `textarea-autogrow` | Auto-growing textarea |
97
+ | `timeago` | Relative timestamps |
98
+
99
+ ## External Dependencies
100
+
101
+ Some controllers require external libraries. Add these to your `package.json` or via CDN:
102
+
103
+ ```json
104
+ {
105
+ "dependencies": {
106
+ "@hotwired/stimulus": "^3.2",
107
+ "@rails/request.js": "^0.0.8",
108
+ "chart.js": "^4.0",
109
+ "lightgallery": "^2.0",
110
+ "sortablejs": "^1.15",
111
+ "stimulus-use": "^1.0",
112
+ "swiper": "^11.0",
113
+ "@simonwep/pickr": "^1.9",
114
+ "date-fns": "^3.0"
115
+ }
116
+ }
117
+ ```
118
+
119
+ ## Documentation
120
+
121
+ For detailed usage instructions, see the [official documentation](https://www.stimulus-components.com/).
122
+
123
+ ## License
124
+
125
+ MIT License - see LICENSE.txt
126
+
127
+ ## Contributing
128
+
129
+ 1. Fork it
130
+ 2. Create your feature branch
131
+ 3. Commit your changes
132
+ 4. Push to the branch
133
+ 5. Create a new Pull Request
@@ -0,0 +1,66 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class AnimatedNumber extends Controller<HTMLElement> {
4
+ declare lazyThresholdValue: number
5
+ declare lazyRootMarginValue: string
6
+ declare startValue: number
7
+ declare endValue: number
8
+ declare durationValue: number
9
+ declare lazyValue: number
10
+
11
+ static values = {
12
+ start: Number,
13
+ end: Number,
14
+ duration: Number,
15
+ lazyThreshold: Number,
16
+ lazyRootMargin: {
17
+ type: String,
18
+ default: "0px",
19
+ },
20
+ lazy: Boolean,
21
+ }
22
+
23
+ connect(): void {
24
+ this.lazyValue ? this.lazyAnimate() : this.animate()
25
+ }
26
+
27
+ animate(): void {
28
+ let startTimestamp: number = null
29
+
30
+ const step = (timestamp: number) => {
31
+ if (!startTimestamp) startTimestamp = timestamp
32
+
33
+ const elapsed: number = timestamp - startTimestamp
34
+ const progress: number = Math.min(elapsed / this.durationValue, 1)
35
+
36
+ this.element.innerHTML = Math.floor(progress * (this.endValue - this.startValue) + this.startValue).toString()
37
+
38
+ if (progress < 1) {
39
+ window.requestAnimationFrame(step)
40
+ }
41
+ }
42
+
43
+ window.requestAnimationFrame(step)
44
+ }
45
+
46
+ lazyAnimate(): void {
47
+ const observer = new IntersectionObserver((entries, observer) => {
48
+ entries.forEach((entry: IntersectionObserverEntry) => {
49
+ if (entry.isIntersecting) {
50
+ this.animate()
51
+
52
+ observer.unobserve(entry.target)
53
+ }
54
+ })
55
+ }, this.lazyAnimateOptions)
56
+
57
+ observer.observe(this.element)
58
+ }
59
+
60
+ get lazyAnimateOptions(): IntersectionObserverInit {
61
+ return {
62
+ threshold: this.lazyThresholdValue,
63
+ rootMargin: this.lazyRootMarginValue,
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,27 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { debounce } from "stimulus-use"
3
+
4
+ export default class AutoSubmit extends Controller<HTMLFormElement> {
5
+ declare delayValue: number
6
+
7
+ static values = {
8
+ delay: {
9
+ type: Number,
10
+ default: 150,
11
+ },
12
+ }
13
+
14
+ initialize(): void {
15
+ this.submit = this.submit.bind(this)
16
+ }
17
+
18
+ connect(): void {
19
+ if (this.delayValue > 0) {
20
+ this.submit = debounce(this.submit, this.delayValue)
21
+ }
22
+ }
23
+
24
+ submit(): void {
25
+ this.element.requestSubmit()
26
+ }
27
+ }
@@ -0,0 +1,28 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import SwiperOptions from "swiper"
3
+ import Swiper from "swiper/bundle"
4
+
5
+ export default class Carousel extends Controller {
6
+ declare swiper: Swiper
7
+ declare optionsValue: SwiperOptions
8
+
9
+ static values = {
10
+ options: Object,
11
+ }
12
+
13
+ connect(): void {
14
+ this.swiper = new Swiper(this.element, {
15
+ ...this.defaultOptions,
16
+ ...this.optionsValue,
17
+ })
18
+ }
19
+
20
+ disconnect(): void {
21
+ this.swiper.destroy()
22
+ this.swiper = undefined
23
+ }
24
+
25
+ get defaultOptions(): SwiperOptions {
26
+ return {}
27
+ }
28
+ }
@@ -0,0 +1,47 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class CharacterCounter extends Controller {
4
+ declare readonly counterTarget: HTMLElement
5
+ declare readonly inputTarget: HTMLInputElement
6
+ declare readonly hasCountdownValue: boolean
7
+
8
+ static targets = ["input", "counter"]
9
+ static values = { countdown: Boolean }
10
+
11
+ initialize(): void {
12
+ this.update = this.update.bind(this)
13
+ }
14
+
15
+ connect(): void {
16
+ this.update()
17
+ this.inputTarget.addEventListener("input", this.update)
18
+ }
19
+
20
+ disconnect(): void {
21
+ this.inputTarget.removeEventListener("input", this.update)
22
+ }
23
+
24
+ update(): void {
25
+ this.counterTarget.innerHTML = this.count.toLocaleString()
26
+ }
27
+
28
+ get count(): number {
29
+ let value: number = this.inputTarget.value.length
30
+
31
+ if (this.hasCountdownValue) {
32
+ if (this.maxLength < 0) {
33
+ console.error(
34
+ `[stimulus-character-counter] You need to add a maxlength attribute on the input to use countdown mode.`
35
+ )
36
+ }
37
+
38
+ value = Math.max(this.maxLength - value, 0)
39
+ }
40
+
41
+ return value
42
+ }
43
+
44
+ get maxLength(): number {
45
+ return this.inputTarget.maxLength
46
+ }
47
+ }
@@ -0,0 +1,58 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { Chart, registerables, ChartType, ChartOptions, ChartData } from "chart.js"
3
+
4
+ Chart.register(...registerables)
5
+
6
+ export default class Chartjs extends Controller<HTMLCanvasElement> {
7
+ declare canvasTarget: HTMLCanvasElement
8
+ declare chart: Chart
9
+ declare typeValue: ChartType
10
+ declare optionsValue: ChartOptions
11
+ declare dataValue: ChartData
12
+ declare hasDataValue: boolean
13
+ declare hasCanvasTarget: boolean
14
+
15
+ static targets = ["canvas"]
16
+ static values = {
17
+ type: {
18
+ type: String,
19
+ default: "line",
20
+ },
21
+ data: Object,
22
+ options: Object,
23
+ }
24
+
25
+ connect(): void {
26
+ const element = this.hasCanvasTarget ? this.canvasTarget : this.element
27
+
28
+ this.chart = new Chart(element.getContext("2d"), {
29
+ type: this.typeValue,
30
+ data: this.chartData,
31
+ options: this.chartOptions,
32
+ })
33
+ }
34
+
35
+ disconnect(): void {
36
+ this.chart.destroy()
37
+ this.chart = undefined
38
+ }
39
+
40
+ get chartData(): ChartData {
41
+ if (!this.hasDataValue) {
42
+ console.warn("[@stimulus-components/chartjs] You need to pass data as JSON to see the chart.")
43
+ }
44
+
45
+ return this.dataValue
46
+ }
47
+
48
+ get chartOptions(): ChartOptions {
49
+ return {
50
+ ...this.defaultOptions,
51
+ ...this.optionsValue,
52
+ }
53
+ }
54
+
55
+ get defaultOptions(): ChartOptions {
56
+ return {}
57
+ }
58
+ }
@@ -0,0 +1,76 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class CheckboxSelectAll extends Controller {
4
+ declare readonly hasCheckboxAllTarget: boolean
5
+ declare readonly checkboxTargets: HTMLInputElement[]
6
+ declare readonly checkboxAllTarget: HTMLInputElement
7
+ declare readonly disableIndeterminateValue: boolean
8
+
9
+ static targets = ["checkboxAll", "checkbox"]
10
+
11
+ static values = {
12
+ disableIndeterminate: {
13
+ type: Boolean,
14
+ default: false,
15
+ },
16
+ }
17
+
18
+ initialize() {
19
+ this.toggle = this.toggle.bind(this)
20
+ this.refresh = this.refresh.bind(this)
21
+ }
22
+
23
+ checkboxAllTargetConnected(checkbox: HTMLInputElement): void {
24
+ checkbox.addEventListener("change", this.toggle)
25
+ this.refresh()
26
+ }
27
+
28
+ checkboxTargetConnected(checkbox: HTMLInputElement): void {
29
+ checkbox.addEventListener("change", this.refresh)
30
+ this.refresh()
31
+ }
32
+
33
+ checkboxAllTargetDisconnected(checkbox: HTMLInputElement): void {
34
+ checkbox.removeEventListener("change", this.toggle)
35
+ this.refresh()
36
+ }
37
+
38
+ checkboxTargetDisconnected(checkbox: HTMLInputElement): void {
39
+ checkbox.removeEventListener("change", this.refresh)
40
+ this.refresh()
41
+ }
42
+
43
+ toggle(e: Event): void {
44
+ e.preventDefault()
45
+
46
+ this.checkboxTargets.forEach((checkbox) => {
47
+ checkbox.checked = e.target.checked
48
+ this.triggerInputEvent(checkbox)
49
+ })
50
+ }
51
+
52
+ refresh(): void {
53
+ const checkboxesCount = this.checkboxTargets.length
54
+ const checkboxesCheckedCount = this.checked.length
55
+
56
+ if (this.disableIndeterminateValue) {
57
+ this.checkboxAllTarget.checked = checkboxesCheckedCount === checkboxesCount
58
+ } else {
59
+ this.checkboxAllTarget.checked = checkboxesCheckedCount > 0
60
+ this.checkboxAllTarget.indeterminate = checkboxesCheckedCount > 0 && checkboxesCheckedCount < checkboxesCount
61
+ }
62
+ }
63
+
64
+ triggerInputEvent(checkbox: HTMLInputElement): void {
65
+ const event = new Event("input", { bubbles: false, cancelable: true })
66
+ checkbox.dispatchEvent(event)
67
+ }
68
+
69
+ get checked(): HTMLInputElement[] {
70
+ return this.checkboxTargets.filter((checkbox) => checkbox.checked)
71
+ }
72
+
73
+ get unchecked(): HTMLInputElement[] {
74
+ return this.checkboxTargets.filter((checkbox) => !checkbox.checked)
75
+ }
76
+ }
@@ -0,0 +1,48 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class Clipboard extends Controller {
4
+ declare readonly hasButtonTarget: boolean
5
+ declare originalContent: string
6
+ declare successDurationValue: number
7
+ declare successContentValue: string
8
+ declare timeout: number
9
+ declare readonly buttonTarget: HTMLElement
10
+ declare readonly sourceTarget: HTMLInputElement
11
+
12
+ static targets = ["button", "source"]
13
+ static values = {
14
+ successContent: String,
15
+ successDuration: {
16
+ type: Number,
17
+ default: 2000,
18
+ },
19
+ }
20
+
21
+ connect(): void {
22
+ if (!this.hasButtonTarget) return
23
+
24
+ this.originalContent = this.buttonTarget.innerHTML
25
+ }
26
+
27
+ copy(event: Event): void {
28
+ event.preventDefault()
29
+
30
+ const text = this.sourceTarget.innerHTML || this.sourceTarget.value
31
+
32
+ navigator.clipboard.writeText(text).then(() => this.copied())
33
+ }
34
+
35
+ copied(): void {
36
+ if (!this.hasButtonTarget) return
37
+
38
+ if (this.timeout) {
39
+ clearTimeout(this.timeout)
40
+ }
41
+
42
+ this.buttonTarget.innerHTML = this.successContentValue
43
+
44
+ this.timeout = setTimeout(() => {
45
+ this.buttonTarget.innerHTML = this.originalContent
46
+ }, this.successDurationValue)
47
+ }
48
+ }
@@ -0,0 +1,75 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import Pickr from "@simonwep/pickr"
3
+
4
+ export default class ColorPicker extends Controller<HTMLElement> {
5
+ declare inputTarget: HTMLInputElement
6
+ declare buttonTarget: HTMLButtonElement
7
+ declare themeValue: Pickr.Theme
8
+ declare picker: Pickr
9
+
10
+ static targets = ["button", "input"]
11
+
12
+ static values = {
13
+ theme: {
14
+ type: String,
15
+ default: "classic",
16
+ },
17
+ }
18
+
19
+ initialize() {
20
+ this.onSave = this.onSave.bind(this)
21
+ }
22
+
23
+ connect() {
24
+ this.picker = Pickr.create({
25
+ el: this.buttonTarget,
26
+ theme: this.themeValue,
27
+ default: this.inputTarget.value,
28
+ swatches: this.swatches,
29
+ components: this.componentOptions,
30
+ })
31
+
32
+ this.picker.on("save", this.onSave)
33
+ }
34
+
35
+ disconnect() {
36
+ this.picker.destroy()
37
+ }
38
+
39
+ onSave(color: Pickr.HSVaColor) {
40
+ this.inputTarget.value = null
41
+
42
+ if (color) {
43
+ this.inputTarget.value = color.toHEXA().toString()
44
+ }
45
+
46
+ this.picker.hide()
47
+ }
48
+
49
+ get componentOptions(): object {
50
+ return {
51
+ preview: true,
52
+ hue: true,
53
+ interaction: {
54
+ input: true,
55
+ clear: true,
56
+ save: true,
57
+ },
58
+ }
59
+ }
60
+
61
+ get swatches(): string[] {
62
+ return [
63
+ "#A0AEC0",
64
+ "#F56565",
65
+ "#ED8936",
66
+ "#ECC94B",
67
+ "#48BB78",
68
+ "#38B2AC",
69
+ "#4299E1",
70
+ "#667EEA",
71
+ "#9F7AEA",
72
+ "#ED64A6",
73
+ ]
74
+ }
75
+ }
@@ -0,0 +1,30 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class Confirmation extends Controller<HTMLFormElement> {
4
+ declare inputTargets: HTMLInputElement[]
5
+ declare itemTargets: HTMLInputElement[] | HTMLButtonElement[]
6
+
7
+ static targets = ["input", "item"]
8
+
9
+ check(): void {
10
+ const disabled = this.inputTargets.some((input) => {
11
+ if (input.type === "checkbox") {
12
+ return input.checked === false
13
+ }
14
+
15
+ return input.dataset.confirmationContent !== input.value
16
+ })
17
+
18
+ this.itemTargets.forEach((target) => {
19
+ target.disabled = disabled
20
+ })
21
+ }
22
+
23
+ inputTargetConnected(): void {
24
+ this.check()
25
+ }
26
+
27
+ inputTargetDisconnected(): void {
28
+ this.check()
29
+ }
30
+ }