mutation.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. let onAttributeAddeds = []
  2. let onElRemoveds = []
  3. let onElAddeds = []
  4. export function onElAdded(callback) {
  5. onElAddeds.push(callback)
  6. }
  7. export function onElRemoved(callback) {
  8. onElRemoveds.push(callback)
  9. }
  10. export function onAttributesAdded(callback) {
  11. onAttributeAddeds.push(callback)
  12. }
  13. export function onAttributeRemoved(el, name, callback) {
  14. if (! el._x_attributeCleanups) el._x_attributeCleanups = {}
  15. if (! el._x_attributeCleanups[name]) el._x_attributeCleanups[name] = []
  16. el._x_attributeCleanups[name].push(callback)
  17. }
  18. export function cleanupAttributes(el, names) {
  19. if (! el._x_attributeCleanups) return
  20. Object.entries(el._x_attributeCleanups).forEach(([name, value]) => {
  21. (names === undefined || names.includes(name)) && value.forEach(i => i())
  22. delete el._x_attributeCleanups[name]
  23. })
  24. }
  25. let observer = new MutationObserver(onMutate)
  26. let currentlyObserving = false
  27. export function startObservingMutations() {
  28. observer.observe(document, { subtree: true, childList: true, attributes: true, attributeOldValue: true })
  29. currentlyObserving = true
  30. }
  31. export function stopObservingMutations() {
  32. observer.disconnect()
  33. currentlyObserving = false
  34. }
  35. let recordQueue = []
  36. let willProcessRecordQueue = false
  37. export function flushObserver() {
  38. recordQueue = recordQueue.concat(observer.takeRecords())
  39. if (recordQueue.length && ! willProcessRecordQueue) {
  40. willProcessRecordQueue = true
  41. queueMicrotask(() => {
  42. processRecordQueue()
  43. willProcessRecordQueue = false
  44. })
  45. }
  46. }
  47. function processRecordQueue() {
  48. onMutate(recordQueue)
  49. recordQueue.length = 0
  50. }
  51. export function mutateDom(callback) {
  52. if (! currentlyObserving) return callback()
  53. flushObserver()
  54. stopObservingMutations()
  55. let result = callback()
  56. startObservingMutations()
  57. return result
  58. }
  59. function onMutate(mutations) {
  60. let addedNodes = []
  61. let removedNodes = []
  62. let addedAttributes = new Map
  63. let removedAttributes = new Map
  64. for (let i = 0; i < mutations.length; i++) {
  65. if (mutations[i].target._x_ignoreMutationObserver) continue
  66. if (mutations[i].type === 'childList') {
  67. mutations[i].addedNodes.forEach(node => node.nodeType === 1 && addedNodes.push(node))
  68. mutations[i].removedNodes.forEach(node => node.nodeType === 1 && removedNodes.push(node))
  69. }
  70. if (mutations[i].type === 'attributes') {
  71. let el = mutations[i].target
  72. let name = mutations[i].attributeName
  73. let oldValue = mutations[i].oldValue
  74. let add = () => {
  75. if (! addedAttributes.has(el)) addedAttributes.set(el, [])
  76. addedAttributes.get(el).push({ name, value: el.getAttribute(name) })
  77. }
  78. let remove = () => {
  79. if (! removedAttributes.has(el)) removedAttributes.set(el, [])
  80. removedAttributes.get(el).push(name)
  81. }
  82. // New attribute.
  83. if (el.hasAttribute(name) && oldValue === null) {
  84. add()
  85. // Changed atttribute.
  86. } else if (el.hasAttribute(name)) {
  87. remove()
  88. add()
  89. // Removed atttribute.
  90. } else {
  91. remove()
  92. }
  93. }
  94. }
  95. removedAttributes.forEach((attrs, el) => {
  96. cleanupAttributes(el, attrs)
  97. })
  98. addedAttributes.forEach((attrs, el) => {
  99. onAttributeAddeds.forEach(i => i(el, attrs))
  100. })
  101. for (let node of addedNodes) {
  102. // If an element gets moved on a page, it's registered
  103. // as both an "add" and "remove", so we wan't to skip those.
  104. if (removedNodes.includes(node)) continue
  105. onElAddeds.forEach(i => i(node))
  106. }
  107. for (let node of removedNodes) {
  108. // If an element gets moved on a page, it's registered
  109. // as both an "add" and "remove", so we want to skip those.
  110. if (addedNodes.includes(node)) continue
  111. onElRemoveds.forEach(i => i(node))
  112. }
  113. addedNodes = null
  114. removedNodes = null
  115. addedAttributes = null
  116. removedAttributes = null
  117. }