utils.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. // Thanks @stimulus:
  2. // https://github.com/stimulusjs/stimulus/blob/master/packages/%40stimulus/core/src/application.ts
  3. export function domReady() {
  4. return new Promise(resolve => {
  5. if (document.readyState == "loading") {
  6. document.addEventListener("DOMContentLoaded", resolve)
  7. } else {
  8. resolve()
  9. }
  10. })
  11. }
  12. export function isTesting() {
  13. return navigator.userAgent, navigator.userAgent.includes("Node.js")
  14. || navigator.userAgent.includes("jsdom")
  15. }
  16. export function kebabCase(subject) {
  17. return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase()
  18. }
  19. export function walkSkippingNestedComponents(el, callback, isRoot = true) {
  20. if (el.hasAttribute('x-data') && ! isRoot) return
  21. callback(el)
  22. let node = el.firstElementChild
  23. while (node) {
  24. walkSkippingNestedComponents(node, callback, false)
  25. node = node.nextElementSibling
  26. }
  27. }
  28. export function debounce(func, wait, immediate) {
  29. var timeout;
  30. return function () {
  31. var context = this, args = arguments;
  32. var later = function () {
  33. timeout = null;
  34. if (!immediate) func.apply(context, args);
  35. };
  36. var callNow = immediate && !timeout;
  37. clearTimeout(timeout);
  38. timeout = setTimeout(later, wait);
  39. if (callNow) func.apply(context, args);
  40. };
  41. };
  42. export function onlyUnique(value, index, self) {
  43. return self.indexOf(value) === index;
  44. }
  45. export function saferEval(expression, dataContext, additionalHelperVariables = {}) {
  46. return (new Function(['$data', ...Object.keys(additionalHelperVariables)], `var result; with($data) { result = ${expression} }; return result`))(
  47. dataContext, ...Object.values(additionalHelperVariables)
  48. )
  49. }
  50. export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
  51. return (new Function(['dataContext', ...Object.keys(additionalHelperVariables)], `with(dataContext) { ${expression} }`))(
  52. dataContext, ...Object.values(additionalHelperVariables)
  53. )
  54. }
  55. export function isXAttr(attr) {
  56. const name = replaceAtAndColonWithStandardSyntax(attr.name)
  57. const xAttrRE = /x-(on|bind|data|text|html|model|if|show|cloak|transition|ref)/
  58. return xAttrRE.test(name)
  59. }
  60. export function getXAttrs(el, type) {
  61. return Array.from(el.attributes)
  62. .filter(isXAttr)
  63. .map(attr => {
  64. const name = replaceAtAndColonWithStandardSyntax(attr.name)
  65. const typeMatch = name.match(/x-(on|bind|data|text|html|model|if|show|cloak|transition|ref)/)
  66. const valueMatch = name.match(/:([a-zA-Z\-]+)/)
  67. const modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []
  68. return {
  69. type: typeMatch ? typeMatch[1] : null,
  70. value: valueMatch ? valueMatch[1] : null,
  71. modifiers: modifiers.map(i => i.replace('.', '')),
  72. expression: attr.value,
  73. }
  74. })
  75. .filter(i => {
  76. // If no type is passed in for filtering, bypassfilter
  77. if (! type) return true
  78. return i.type === type
  79. })
  80. }
  81. export function replaceAtAndColonWithStandardSyntax(name) {
  82. if (name.startsWith('@')) {
  83. return name.replace('@', 'x-on:')
  84. } else if (name.startsWith(':')) {
  85. return name.replace(':', 'x-bind:')
  86. }
  87. return name
  88. }
  89. export function transitionIn(el, callback, forceSkip = false) {
  90. if (forceSkip) callback()
  91. const attrs = getXAttrs(el, 'transition')
  92. if (attrs.length < 1) callback()
  93. const enter = (attrs.find(i => i.value === 'enter') || { expression: '' }).expression.split(' ').filter(i => i !== '')
  94. const enterStart = (attrs.find(i => i.value === 'enter-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
  95. const enterEnd = (attrs.find(i => i.value === 'enter-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')
  96. transition(el, enter, enterStart, enterEnd, callback, () => {})
  97. }
  98. export function transitionOut(el, callback, forceSkip = false) {
  99. if (forceSkip) callback()
  100. const attrs = getXAttrs(el, 'transition')
  101. if (attrs.length < 1) callback()
  102. const leave = (attrs.find(i => i.value === 'leave') || { expression: '' }).expression.split(' ').filter(i => i !== '')
  103. const leaveStart = (attrs.find(i => i.value === 'leave-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
  104. const leaveEnd = (attrs.find(i => i.value === 'leave-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')
  105. transition(el, leave, leaveStart, leaveEnd, () => {}, callback)
  106. }
  107. export function transition(el, classesDuring, classesStart, classesEnd, hook1, hook2) {
  108. el.classList.add(...classesStart)
  109. el.classList.add(...classesDuring)
  110. requestAnimationFrame(() => {
  111. const duration = Number(getComputedStyle(el).transitionDuration.replace('s', '')) * 1000
  112. hook1()
  113. requestAnimationFrame(() => {
  114. el.classList.remove(...classesStart)
  115. el.classList.add(...classesEnd)
  116. setTimeout(() => {
  117. hook2()
  118. // Adding an "isConnected" check, in case the callback
  119. // removed the element from the DOM.
  120. if (el.isConnected) {
  121. el.classList.remove(...classesDuring)
  122. el.classList.remove(...classesEnd)
  123. }
  124. }, duration);
  125. })
  126. });
  127. }