switch.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. export default function (Alpine) {
  2. Alpine.directive('switch', (el, directive) => {
  3. if (directive.value === 'group') handleGroup(el, Alpine)
  4. else if (directive.value === 'label') handleLabel(el, Alpine)
  5. else if (directive.value === 'description') handleDescription(el, Alpine)
  6. else handleRoot(el, Alpine)
  7. })
  8. Alpine.magic('switch', el => {
  9. let $data = Alpine.$data(el)
  10. return {
  11. get isChecked() {
  12. return $data.__value === true
  13. },
  14. }
  15. })
  16. }
  17. function handleGroup(el, Alpine) {
  18. Alpine.bind(el, {
  19. 'x-id'() { return ['alpine-switch-label', 'alpine-switch-description'] },
  20. 'x-data'() {
  21. return {
  22. __hasLabel: false,
  23. __hasDescription: false,
  24. __switchEl: undefined,
  25. }
  26. }
  27. })
  28. }
  29. function handleRoot(el, Alpine) {
  30. Alpine.bind(el, {
  31. 'x-modelable': '__value',
  32. 'x-data'() {
  33. return {
  34. init() {
  35. queueMicrotask(() => {
  36. this.__value = Alpine.bound(this.$el, 'default-checked', false)
  37. this.__inputName = Alpine.bound(this.$el, 'name', false)
  38. this.__inputValue = Alpine.bound(this.$el, 'value', 'on')
  39. this.__inputId = 'alpine-switch-'+Date.now()
  40. })
  41. },
  42. __value: undefined,
  43. __inputName: undefined,
  44. __inputValue: undefined,
  45. __inputId: undefined,
  46. __toggle() {
  47. this.__value = ! this.__value;
  48. },
  49. }
  50. },
  51. 'x-effect'() {
  52. let value = this.__value
  53. // Only render a hidden input if the "name" prop is passed...
  54. if (! this.__inputName) return
  55. // First remove a previously appended hidden input (if it exists)...
  56. let nextEl = this.$el.nextElementSibling
  57. if (nextEl && String(nextEl.id) === String(this.__inputId)) {
  58. nextEl.remove()
  59. }
  60. // If the value is true, create the input and append it, otherwise,
  61. // we already removed it in the previous step...
  62. if (value) {
  63. let input = document.createElement('input')
  64. input.type = 'hidden'
  65. input.value = this.__inputValue
  66. input.name = this.__inputName
  67. input.id = this.__inputId
  68. this.$el.after(input)
  69. }
  70. },
  71. 'x-init'() {
  72. if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
  73. this.$data.__switchEl = this.$el
  74. },
  75. 'role': 'switch',
  76. 'tabindex': "0",
  77. ':aria-checked'() { return !!this.__value },
  78. ':aria-labelledby'() { return this.$data.__hasLabel && this.$id('alpine-switch-label') },
  79. ':aria-describedby'() { return this.$data.__hasDescription && this.$id('alpine-switch-description') },
  80. '@click.prevent'() { this.__toggle() },
  81. '@keyup'(e) {
  82. if (e.key !== 'Tab') e.preventDefault()
  83. if (e.key === ' ') this.__toggle()
  84. },
  85. // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
  86. '@keypress.prevent'() { },
  87. })
  88. }
  89. function handleLabel(el, Alpine) {
  90. Alpine.bind(el, {
  91. 'x-init'() { this.$data.__hasLabel = true },
  92. ':id'() { return this.$id('alpine-switch-label') },
  93. '@click'() {
  94. this.$data.__switchEl.click()
  95. this.$data.__switchEl.focus({ preventScroll: true })
  96. },
  97. })
  98. }
  99. function handleDescription(el, Alpine) {
  100. Alpine.bind(el, {
  101. 'x-init'() { this.$data.__hasDescription = true },
  102. ':id'() { return this.$id('alpine-switch-description') },
  103. })
  104. }