directives.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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. 'bind',
  137. 'init',
  138. 'for',
  139. 'mask',
  140. 'model',
  141. 'modelable',
  142. 'transition',
  143. 'show',
  144. 'if',
  145. DEFAULT,
  146. 'teleport',
  147. ]
  148. function byPriority(a, b) {
  149. let typeA = directiveOrder.indexOf(a.type) === -1 ? DEFAULT : a.type
  150. let typeB = directiveOrder.indexOf(b.type) === -1 ? DEFAULT : b.type
  151. return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB)
  152. }