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
@@ -0,0 +1,33 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class Reveal extends Controller {
4
+ declare hasHiddenClass: boolean
5
+ declare hiddenClass: string
6
+ declare itemTargets: HTMLElement[]
7
+ declare class: string
8
+
9
+ static targets = ["item"]
10
+ static classes = ["hidden"]
11
+
12
+ connect(): void {
13
+ this.class = this.hasHiddenClass ? this.hiddenClass : "hidden"
14
+ }
15
+
16
+ toggle(): void {
17
+ this.itemTargets.forEach((item) => {
18
+ item.classList.toggle(this.class)
19
+ })
20
+ }
21
+
22
+ show(): void {
23
+ this.itemTargets.forEach((item) => {
24
+ item.classList.remove(this.class)
25
+ })
26
+ }
27
+
28
+ hide(): void {
29
+ this.itemTargets.forEach((item) => {
30
+ item.classList.add(this.class)
31
+ })
32
+ }
33
+ }
@@ -0,0 +1,32 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class ScrollProgress extends Controller<HTMLElement> {
4
+ declare readonly throttleDelayValue: number
5
+
6
+ static values = {
7
+ throttleDelay: {
8
+ type: Number,
9
+ default: 15,
10
+ },
11
+ }
12
+
13
+ initialize(): void {
14
+ this.scroll = this.scroll.bind(this)
15
+ }
16
+
17
+ connect(): void {
18
+ window.addEventListener("scroll", this.scroll, { passive: true })
19
+ this.scroll()
20
+ }
21
+
22
+ disconnect(): void {
23
+ window.removeEventListener("scroll", this.scroll)
24
+ }
25
+
26
+ scroll(): void {
27
+ const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
28
+ const width = (window.scrollY / height) * 100
29
+
30
+ this.element.style.width = `${width}%`
31
+ }
32
+ }
@@ -0,0 +1,62 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class ScrollReveal extends Controller {
4
+ declare classValue: string
5
+ declare thresholdValue: number
6
+ declare rootMarginValue: string
7
+ declare class: string
8
+ declare threshold: number
9
+ declare rootMargin: string
10
+ declare observer: IntersectionObserver
11
+ declare readonly itemTargets: HTMLElement[]
12
+
13
+ static targets = ["item"]
14
+ static values = {
15
+ class: String,
16
+ threshold: Number,
17
+ rootMargin: String,
18
+ }
19
+
20
+ initialize(): void {
21
+ this.intersectionObserverCallback = this.intersectionObserverCallback.bind(this)
22
+ }
23
+
24
+ connect(): void {
25
+ this.class = this.classValue || this.defaultOptions.class || "in"
26
+ this.threshold = this.thresholdValue || this.defaultOptions.threshold || 0.1
27
+ this.rootMargin = this.rootMarginValue || this.defaultOptions.rootMargin || "0px"
28
+
29
+ this.observer = new IntersectionObserver(this.intersectionObserverCallback, this.intersectionObserverOptions)
30
+ this.itemTargets.forEach((item) => this.observer.observe(item))
31
+ }
32
+
33
+ disconnect(): void {
34
+ this.itemTargets.forEach((item) => this.observer.unobserve(item))
35
+ }
36
+
37
+ intersectionObserverCallback(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void {
38
+ entries.forEach((entry) => {
39
+ if (entry.intersectionRatio > this.threshold) {
40
+ const target = entry.target as HTMLElement
41
+ target.classList.add(...this.class.split(" "))
42
+
43
+ if (target.dataset.delay) {
44
+ target.style.transitionDelay = target.dataset.delay
45
+ }
46
+
47
+ observer.unobserve(target)
48
+ }
49
+ })
50
+ }
51
+
52
+ get intersectionObserverOptions(): IntersectionObserverInit {
53
+ return {
54
+ threshold: this.threshold,
55
+ rootMargin: this.rootMargin,
56
+ }
57
+ }
58
+
59
+ get defaultOptions(): { class?: string; threshold?: number; rootMargin?: string } {
60
+ return {}
61
+ }
62
+ }
@@ -0,0 +1,56 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class ScrollTo extends Controller<HTMLAnchorElement> {
4
+ declare offsetValue: number
5
+ declare behaviorValue: ScrollBehavior
6
+ declare hasOffsetValue: boolean
7
+
8
+ static values = {
9
+ offset: Number,
10
+ behavior: String,
11
+ }
12
+
13
+ initialize(): void {
14
+ this.scroll = this.scroll.bind(this)
15
+ }
16
+
17
+ connect(): void {
18
+ this.element.addEventListener("click", this.scroll)
19
+ }
20
+
21
+ disconnect(): void {
22
+ this.element.removeEventListener("click", this.scroll)
23
+ }
24
+
25
+ scroll(event: Event): void {
26
+ event.preventDefault()
27
+
28
+ const id = this.element.hash.replace(/^#/, "")
29
+ const target = document.getElementById(id)
30
+
31
+ if (!target) {
32
+ console.warn(`[@stimulus-components/scroll-to] The element with the id: "${id}" does not exist on the page.`)
33
+ return
34
+ }
35
+
36
+ const elementPosition = target.getBoundingClientRect().top + window.pageYOffset
37
+ const offsetPosition = elementPosition - this.offset
38
+
39
+ window.scrollTo({
40
+ top: offsetPosition,
41
+ behavior: this.behavior,
42
+ })
43
+ }
44
+
45
+ get offset(): number {
46
+ if (this.hasOffsetValue) {
47
+ return this.offsetValue
48
+ }
49
+
50
+ return 10
51
+ }
52
+
53
+ get behavior(): ScrollBehavior {
54
+ return this.behaviorValue || "smooth"
55
+ }
56
+ }
@@ -0,0 +1,74 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import Sortable from "sortablejs"
3
+ import { FetchRequest, type ResponseKind } from "@rails/request.js"
4
+
5
+ export default class Sortable extends Controller<HTMLElement> {
6
+ declare animationValue: number
7
+ declare resourceNameValue: string
8
+ declare paramNameValue: string
9
+ declare responseKindValue: ResponseKind
10
+ declare sortable: Sortable
11
+ declare handleValue: string
12
+ declare methodValue: string
13
+
14
+ static values = {
15
+ resourceName: String,
16
+ paramName: {
17
+ type: String,
18
+ default: "position",
19
+ },
20
+ responseKind: {
21
+ type: String,
22
+ default: "html",
23
+ },
24
+ animation: Number,
25
+ handle: String,
26
+ method: {
27
+ type: String,
28
+ default: "patch",
29
+ },
30
+ }
31
+
32
+ initialize() {
33
+ this.onUpdate = this.onUpdate.bind(this)
34
+ }
35
+
36
+ connect() {
37
+ this.sortable = new Sortable(this.element, {
38
+ ...this.defaultOptions,
39
+ ...this.options,
40
+ })
41
+ }
42
+
43
+ disconnect() {
44
+ this.sortable.destroy()
45
+ this.sortable = undefined
46
+ }
47
+
48
+ async onUpdate({ item, newIndex }) {
49
+ if (!item.dataset.sortableUpdateUrl) return
50
+
51
+ const param = this.resourceNameValue ? `${this.resourceNameValue}[${this.paramNameValue}]` : this.paramNameValue
52
+
53
+ const data = new FormData()
54
+ data.append(param, newIndex + 1)
55
+
56
+ const request = new FetchRequest(this.methodValue, item.dataset.sortableUpdateUrl, {
57
+ body: data,
58
+ responseKind: this.responseKindValue,
59
+ })
60
+ return await request.perform()
61
+ }
62
+
63
+ get options(): Sortable.Options {
64
+ return {
65
+ animation: this.animationValue || this.defaultOptions.animation || 150,
66
+ handle: this.handleValue || this.defaultOptions.handle || undefined,
67
+ onUpdate: this.onUpdate,
68
+ }
69
+ }
70
+
71
+ get defaultOptions(): Sortable.Options {
72
+ return {}
73
+ }
74
+ }
@@ -0,0 +1,39 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class Sound extends Controller {
4
+ declare readonly urlValue: string
5
+ declare sound: HTMLAudioElement
6
+
7
+ static values = {
8
+ url: String,
9
+ }
10
+
11
+ connect() {
12
+ this.sound = new Audio(this.urlValue)
13
+ }
14
+
15
+ play() {
16
+ this.sound.play()
17
+ }
18
+
19
+ pause() {
20
+ this.sound.pause()
21
+ }
22
+
23
+ reset() {
24
+ this.sound.pause()
25
+ this.sound.load()
26
+ }
27
+
28
+ volume({ params }) {
29
+ this.sound.volume = params.volume
30
+ }
31
+
32
+ muted({ params }) {
33
+ this.sound.muted = params.muted
34
+ }
35
+
36
+ loop({ params }) {
37
+ this.sound.loop = params.loop
38
+ }
39
+ }
@@ -0,0 +1,92 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class SpeechRecognitionController extends Controller {
4
+ declare readonly startButtonTarget: HTMLButtonElement
5
+ declare readonly stopButtonTarget: HTMLButtonElement
6
+ declare readonly inputTarget: HTMLTextAreaElement | HTMLInputElement
7
+ declare readonly indicatorTarget: HTMLElement
8
+ declare readonly hasIndicatorTarget: boolean
9
+ declare readonly hiddenClass: string
10
+ declare readonly hasHiddenClass: boolean
11
+
12
+ static targets = ["startButton", "stopButton", "indicator", "input"]
13
+ static classes = ["hidden"]
14
+
15
+ private recognition: SpeechRecognition | null = null
16
+ private isListening = false
17
+ private hiddenClassName!: string
18
+
19
+ connect(): void {
20
+ this.hiddenClassName = this.hasHiddenClass ? this.hiddenClass : "hidden"
21
+
22
+ if (!this.isSupported) {
23
+ this.startButtonTarget.classList.add(this.hiddenClassName)
24
+ this.stopButtonTarget.classList.add(this.hiddenClassName)
25
+ if (this.hasIndicatorTarget) this.indicatorTarget.classList.add(this.hiddenClassName)
26
+ return
27
+ }
28
+
29
+ this.setupRecognition()
30
+ this.updateUI()
31
+ }
32
+
33
+ disconnect(): void {
34
+ this.recognition?.abort()
35
+ this.recognition = null
36
+ }
37
+
38
+ start(): void {
39
+ if (!this.recognition || this.isListening) return
40
+
41
+ this.recognition.start()
42
+ this.isListening = true
43
+ this.updateUI()
44
+ }
45
+
46
+ stop(): void {
47
+ if (!this.recognition || !this.isListening) return
48
+
49
+ this.recognition.stop()
50
+ this.isListening = false
51
+ this.updateUI()
52
+ }
53
+
54
+ private get isSupported(): boolean {
55
+ return "SpeechRecognition" in window || "webkitSpeechRecognition" in window
56
+ }
57
+
58
+ private setupRecognition(): void {
59
+ const SpeechRecognitionAPI = window.SpeechRecognition ?? window.webkitSpeechRecognition
60
+ this.recognition = new SpeechRecognitionAPI()
61
+ this.recognition.continuous = true
62
+ this.recognition.interimResults = true
63
+
64
+ this.recognition.onresult = (event): void => {
65
+ this.inputTarget.value = Array.from(event.results)
66
+ .map((result) => result[0].transcript)
67
+ .join("")
68
+ }
69
+
70
+ this.recognition.onend = (): void => {
71
+ if (this.isListening) {
72
+ this.isListening = false
73
+ this.updateUI()
74
+ }
75
+ }
76
+
77
+ this.recognition.onerror = (event): void => {
78
+ console.error("Speech recognition error:", event.error)
79
+ this.isListening = false
80
+ this.updateUI()
81
+ }
82
+ }
83
+
84
+ private updateUI(): void {
85
+ this.startButtonTarget.classList.toggle(this.hiddenClassName, this.isListening)
86
+ this.stopButtonTarget.classList.toggle(this.hiddenClassName, !this.isListening)
87
+
88
+ if (this.hasIndicatorTarget) {
89
+ this.indicatorTarget.classList.toggle(this.hiddenClassName, !this.isListening)
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,71 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ import AnimatedNumberController from "./stimulus-components/animated-number"
4
+ import AutoSubmitController from "./stimulus-components/auto-submit"
5
+ import CarouselController from "./stimulus-components/carousel"
6
+ import CharacterCounterController from "./stimulus-components/character-counter"
7
+ import ChartjsController from "./stimulus-components/chartjs"
8
+ import CheckboxSelectAllController from "./stimulus-components/checkbox-select-all"
9
+ import ClipboardController from "./stimulus-components/clipboard"
10
+ import ColorPickerController from "./stimulus-components/color-picker"
11
+ import ConfirmationController from "./stimulus-components/confirmation"
12
+ import ContentLoaderController from "./stimulus-components/content-loader"
13
+ import DialogController from "./stimulus-components/dialog"
14
+ import DropdownController from "./stimulus-components/dropdown"
15
+ import GlowController from "./stimulus-components/glow"
16
+ import HotkeyController from "./stimulus-components/hotkey"
17
+ import LightboxController from "./stimulus-components/lightbox"
18
+ import NotificationController from "./stimulus-components/notification"
19
+ import PasswordVisibilityController from "./stimulus-components/password-visibility"
20
+ import PlacesAutocompleteController from "./stimulus-components/places-autocomplete"
21
+ import PopoverController from "./stimulus-components/popover"
22
+ import PrefetchController from "./stimulus-components/prefetch"
23
+ import RailsNestedFormController from "./stimulus-components/rails-nested-form"
24
+ import ReadMoreController from "./stimulus-components/read-more"
25
+ import RemoteRailsController from "./stimulus-components/remote-rails"
26
+ import RevealController from "./stimulus-components/reveal-controller"
27
+ import ScrollProgressController from "./stimulus-components/scroll-progress"
28
+ import ScrollRevealController from "./stimulus-components/scroll-reveal"
29
+ import ScrollToController from "./stimulus-components/scroll-to"
30
+ import SortableController from "./stimulus-components/sortable"
31
+ import SoundController from "./stimulus-components/sound"
32
+ import SpeechRecognitionController from "./stimulus-components/speech-recognition"
33
+ import TextareaAutogrowController from "./stimulus-components/textarea-autogrow"
34
+ import TimeagoController from "./stimulus-components/timeago"
35
+
36
+ const application = Application.start()
37
+
38
+ application.register("animated-number", AnimatedNumberController)
39
+ application.register("auto-submit", AutoSubmitController)
40
+ application.register("carousel", CarouselController)
41
+ application.register("character-counter", CharacterCounterController)
42
+ application.register("chartjs", ChartjsController)
43
+ application.register("checkbox-select-all", CheckboxSelectAllController)
44
+ application.register("clipboard", ClipboardController)
45
+ application.register("color-picker", ColorPickerController)
46
+ application.register("confirmation", ConfirmationController)
47
+ application.register("content-loader", ContentLoaderController)
48
+ application.register("dialog", DialogController)
49
+ application.register("dropdown", DropdownController)
50
+ application.register("glow", GlowController)
51
+ application.register("hotkey", HotkeyController)
52
+ application.register("lightbox", LightboxController)
53
+ application.register("notification", NotificationController)
54
+ application.register("password-visibility", PasswordVisibilityController)
55
+ application.register("places-autocomplete", PlacesAutocompleteController)
56
+ application.register("popover", PopoverController)
57
+ application.register("prefetch", PrefetchController)
58
+ application.register("rails-nested-form", RailsNestedFormController)
59
+ application.register("read-more", ReadMoreController)
60
+ application.register("remote-rails", RemoteRailsController)
61
+ application.register("reveal", RevealController)
62
+ application.register("scroll-progress", ScrollProgressController)
63
+ application.register("scroll-reveal", ScrollRevealController)
64
+ application.register("scroll-to", ScrollToController)
65
+ application.register("sortable", SortableController)
66
+ application.register("sound", SoundController)
67
+ application.register("speech-recognition", SpeechRecognitionController)
68
+ application.register("textarea-autogrow", TextareaAutogrowController)
69
+ application.register("timeago", TimeagoController)
70
+
71
+ export { application }
@@ -0,0 +1,45 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ function debounce(callback: Function, delay: number) {
4
+ let timeout: number
5
+ return (...args) => {
6
+ clearTimeout(timeout)
7
+ timeout = setTimeout(() => callback.apply(this, args), delay)
8
+ }
9
+ }
10
+
11
+ export default class TextareaAutogrow extends Controller<HTMLInputElement> {
12
+ declare onResize: EventListenerOrEventListenerObject
13
+ declare resizeDebounceDelayValue: number
14
+
15
+ static values = {
16
+ resizeDebounceDelay: {
17
+ type: Number,
18
+ default: 100,
19
+ },
20
+ }
21
+
22
+ initialize(): void {
23
+ this.autogrow = this.autogrow.bind(this)
24
+ }
25
+
26
+ connect(): void {
27
+ this.element.style.overflow = "hidden"
28
+ const delay = this.resizeDebounceDelayValue
29
+
30
+ this.onResize = delay > 0 ? debounce(this.autogrow, delay) : this.autogrow
31
+
32
+ this.autogrow()
33
+ this.element.addEventListener("input", this.autogrow)
34
+ window.addEventListener("resize", this.onResize)
35
+ }
36
+
37
+ disconnect(): void {
38
+ window.removeEventListener("resize", this.onResize)
39
+ }
40
+
41
+ autogrow(): void {
42
+ this.element.style.height = "auto"
43
+ this.element.style.height = `${this.element.scrollHeight}px`
44
+ }
45
+ }
@@ -0,0 +1,66 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { formatDistanceToNow } from "date-fns"
3
+
4
+ export default class Timeago extends Controller<HTMLTimeElement> {
5
+ declare isValid: boolean
6
+ declare refreshTimer: number
7
+ declare hasRefreshIntervalValue: boolean
8
+ declare datetimeValue: string
9
+ declare addSuffixValue: boolean
10
+ declare includeSecondsValue: boolean
11
+ declare refreshIntervalValue: number
12
+
13
+ static values = {
14
+ datetime: String,
15
+ refreshInterval: Number,
16
+ includeSeconds: Boolean,
17
+ addSuffix: Boolean,
18
+ }
19
+
20
+ initialize(): void {
21
+ this.isValid = true
22
+ }
23
+
24
+ connect(): void {
25
+ this.load()
26
+
27
+ if (this.hasRefreshIntervalValue && this.isValid) {
28
+ this.startRefreshing()
29
+ }
30
+ }
31
+
32
+ disconnect(): void {
33
+ this.stopRefreshing()
34
+ }
35
+
36
+ load(): void {
37
+ const datetime = this.datetimeValue
38
+ const date = Date.parse(datetime)
39
+ const options = {
40
+ includeSeconds: this.includeSecondsValue,
41
+ addSuffix: this.addSuffixValue,
42
+ }
43
+
44
+ if (Number.isNaN(date)) {
45
+ this.isValid = false
46
+ console.error(
47
+ `[@stimulus-components/timeago] Value given in 'data-timeago-datetime' is not a valid date (${datetime}).`
48
+ )
49
+ }
50
+
51
+ this.element.dateTime = datetime
52
+ this.element.innerHTML = this.isValid ? formatDistanceToNow(date, options) : datetime
53
+ }
54
+
55
+ startRefreshing(): void {
56
+ this.refreshTimer = setInterval(() => {
57
+ this.load()
58
+ }, this.refreshIntervalValue)
59
+ }
60
+
61
+ stopRefreshing(): void {
62
+ if (this.refreshTimer) {
63
+ clearInterval(this.refreshTimer)
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,9 @@
1
+ module StimulusComponents
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace StimulusComponents
4
+
5
+ initializer 'stimulus-components.assets' do |app|
6
+ app.config.assets.paths << root.join('app/javascript')
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module StimulusComponents
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,43 @@
1
+ require 'stimulus-components/version'
2
+ require 'stimulus-components/engine'
3
+
4
+ module StimulusComponents
5
+ COMPONENTS = %w[
6
+ animated-number
7
+ auto-submit
8
+ carousel
9
+ character-counter
10
+ chartjs
11
+ checkbox-select-all
12
+ clipboard
13
+ color-picker
14
+ confirmation
15
+ content-loader
16
+ dialog
17
+ dropdown
18
+ glow
19
+ hotkey
20
+ lightbox
21
+ notification
22
+ password-visibility
23
+ places-autocomplete
24
+ popover
25
+ prefetch
26
+ rails-nested-form
27
+ read-more
28
+ remote-rails
29
+ reveal-controller
30
+ scroll-progress
31
+ scroll-reveal
32
+ scroll-to
33
+ sortable
34
+ sound
35
+ speech-recognition
36
+ textarea-autogrow
37
+ timeago
38
+ ].freeze
39
+
40
+ def self.components
41
+ COMPONENTS
42
+ end
43
+ end