directives.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import { onAttributeRemoved, onElRemoved } from './mutation'
  2. import { evaluate, evaluateLater } from './evaluator'
  3. import { elementBoundEffect } from './reactivity'
  4. import Alpine from './alpine'
  5. let prefixAsString = 'x-'
  6. export function prefix(subject = '') {
  7. return prefixAsString + subject
  8. }
  9. export function setPrefix(newPrefix) {
  10. prefixAsString = newPrefix
  11. }
  12. let directiveHandlers = {}
  13. export function directive(name, callback) {
  14. directiveHandlers[name] = callback
  15. }
  16. export function directives(el, attributes, originalAttributeOverride) {
  17. attributes = Array.from(attributes)
  18. if (el._x_virtualDirectives) {
  19. let vAttributes = Object.entries(el._x_virtualDirectives).map(([name, value]) => ({ name, value }))
  20. let staticAttributes = attributesOnly(vAttributes)
  21. // Handle binding normal HTML attributes (non-Alpine directives).
  22. vAttributes = vAttributes.map(attribute => {
  23. if (staticAttributes.find(attr => attr.name === attribute.name)) {
  24. return {
  25. name: `x-bind:${attribute.name}`,
  26. value: `"${attribute.value}"`,
  27. }
  28. }
  29. return attribute
  30. })
  31. attributes = attributes.concat(vAttributes)
  32. }
  33. let transformedAttributeMap = {}
  34. let directives = attributes
  35. .map(toTransformedAttributes((newName, oldName) => transformedAttributeMap[newName] = oldName))
  36. .filter(outNonAlpineAttributes)
  37. .map(toParsedDirectives(transformedAttributeMap, originalAttributeOverride))
  38. .sort(byPriority)
  39. return directives.map(directive => {
  40. return getDirectiveHandler(el, directive)
  41. })
  42. }
  43. export function attributesOnly(attributes) {
  44. return Array.from(attributes)
  45. .map(toTransformedAttributes())
  46. .filter(attr => ! outNonAlpineAttributes(attr))
  47. }
  48. let isDeferringHandlers = false
  49. let directiveHandlerStacks = new Map
  50. let currentHandlerStackKey = Symbol()
  51. export function deferHandlingDirectives(callback) {
  52. isDeferringHandlers = true
  53. let key = Symbol()
  54. currentHandlerStackKey = key
  55. directiveHandlerStacks.set(key, [])
  56. let flushHandlers = () => {
  57. while (directiveHandlerStacks.get(key).length) directiveHandlerStacks.get(key).shift()()
  58. directiveHandlerStacks.delete(key)
  59. }
  60. let stopDeferring = () => { isDeferringHandlers = false; flushHandlers() }
  61. callback(flushHandlers)
  62. stopDeferring()
  63. }
  64. export function getElementBoundUtilities(el) {
  65. let cleanups = []
  66. let cleanup = callback => cleanups.push(callback)
  67. let [effect, cleanupEffect] = elementBoundEffect(el)
  68. cleanups.push(cleanupEffect)
  69. let utilities = {
  70. Alpine,
  71. effect,
  72. cleanup,
  73. evaluateLater: evaluateLater.bind(evaluateLater, el),
  74. evaluate: evaluate.bind(evaluate, el),
  75. }
  76. let doCleanup = () => cleanups.forEach(i => i())
  77. return [utilities, doCleanup]
  78. }
  79. export function getDirectiveHandler(el, directive) {
  80. let noop = () => {}
  81. let handler = directiveHandlers[directive.type] || noop
  82. let [utilities, cleanup] = getElementBoundUtilities(el)
  83. onAttributeRemoved(el, directive.original, cleanup)
  84. let fullHandler = () => {
  85. if (el._x_ignore || el._x_ignoreSelf) return
  86. handler.inline && handler.inline(el, directive, utilities)
  87. handler = handler.bind(handler, el, directive, utilities)
  88. isDeferringHandlers ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler) : handler()
  89. }
  90. fullHandler.runCleanups = cleanup
  91. return fullHandler
  92. }
  93. export let startingWith = (subject, replacement) => ({ name, value }) => {
  94. if (name.startsWith(subject)) name = name.replace(subject, replacement)
  95. return { name, value }
  96. }
  97. export let into = i => i
  98. function toTransformedAttributes(callback = () => {}) {
  99. return ({ name, value }) => {
  100. let { name: newName, value: newValue } = attributeTransformers.reduce((carry, transform) => {
  101. return transform(carry)
  102. }, { name, value })
  103. if (newName !== name) callback(newName, name)
  104. return { name: newName, value: newValue }
  105. }
  106. }
  107. let attributeTransformers = []
  108. export function mapAttributes(callback) {
  109. attributeTransformers.push(callback)
  110. }
  111. function outNonAlpineAttributes({ name }) {
  112. return alpineAttributeRegex().test(name)
  113. }
  114. let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}([^:^.]+)\\b`))
  115. function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) {
  116. return ({ name, value }) => {
  117. let typeMatch = name.match(alpineAttributeRegex())
  118. let valueMatch = name.match(/:([a-zA-Z0-9\-:]+)/)
  119. let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []
  120. let original = originalAttributeOverride || transformedAttributeMap[name] || name
  121. return {
  122. type: typeMatch ? typeMatch[1] : null,
  123. value: valueMatch ? valueMatch[1] : null,
  124. modifiers: modifiers.map(i => i.replace('.', '')),
  125. expression: value,
  126. original,
  127. }
  128. }
  129. }
  130. const DEFAULT = 'DEFAULT'
  131. let directiveOrder = [
  132. 'ignore',
  133. 'ref',
  134. 'data',
  135. 'id',
  136. // @todo: provide better directive ordering mechanisms so
  137. // that I don't have to manually add things like "tabs"
  138. // to the order list...
  139. 'tabs',
  140. 'radio',
  141. 'switch',
  142. 'disclosure',
  143. 'menu',
  144. 'bind',
  145. 'init',
  146. 'for',
  147. 'mask',
  148. 'model',
  149. 'modelable',
  150. 'transition',
  151. 'show',
  152. 'if',
  153. DEFAULT,
  154. 'teleport',
  155. ]
  156. function byPriority(a, b) {
  157. let typeA = directiveOrder.indexOf(a.type) === -1 ? DEFAULT : a.type
  158. let typeB = directiveOrder.indexOf(b.type) === -1 ? DEFAULT : b.type
  159. return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB)
  160. }