Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo-mod-jslibs-dist.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'alpinejs/packages/alpinejs/src/directives/x-for.js')
-rw-r--r--alpinejs/packages/alpinejs/src/directives/x-for.js262
1 files changed, 262 insertions, 0 deletions
diff --git a/alpinejs/packages/alpinejs/src/directives/x-for.js b/alpinejs/packages/alpinejs/src/directives/x-for.js
new file mode 100644
index 0000000..504306d
--- /dev/null
+++ b/alpinejs/packages/alpinejs/src/directives/x-for.js
@@ -0,0 +1,262 @@
+import { addScopeToNode, refreshScope } from '../scope'
+import { evaluateLater } from '../evaluator'
+import { directive } from '../directives'
+import { reactive } from '../reactivity'
+import { initTree } from '../lifecycle'
+import { mutateDom } from '../mutation'
+import { flushJobs } from '../scheduler'
+import { warn } from '../utils/warn'
+
+directive('for', (el, { expression }, { effect, cleanup }) => {
+ let iteratorNames = parseForExpression(expression)
+
+ let evaluateItems = evaluateLater(el, iteratorNames.items)
+ let evaluateKey = evaluateLater(el,
+ // the x-bind:key expression is stored for our use instead of evaluated.
+ el._x_keyExpression || 'index'
+ )
+
+ el._x_prevKeys = []
+ el._x_lookup = {}
+
+ effect(() => loop(el, iteratorNames, evaluateItems, evaluateKey))
+
+ cleanup(() => {
+ Object.values(el._x_lookup).forEach(el => el.remove())
+
+ delete el._x_prevKeys
+ delete el._x_lookup
+ })
+})
+
+let shouldFastRender = true
+
+function loop(el, iteratorNames, evaluateItems, evaluateKey) {
+ let isObject = i => typeof i === 'object' && ! Array.isArray(i)
+ let templateEl = el
+
+ evaluateItems(items => {
+ // Prepare yourself. There's a lot going on here. Take heart,
+ // every bit of complexity in this function was added for
+ // the purpose of making Alpine fast with large datas.
+
+ // Support number literals. Ex: x-for="i in 100"
+ if (isNumeric(items) && items >= 0) {
+ items = Array.from(Array(items).keys(), i => i + 1)
+ }
+
+ if (items === undefined) items = []
+
+ let lookup = el._x_lookup
+ let prevKeys = el._x_prevKeys
+ let scopes = []
+ let keys = []
+
+ // In order to preserve DOM elements (move instead of replace)
+ // we need to generate all the keys for every iteration up
+ // front. These will be our source of truth for diffing.
+ if (isObject(items)) {
+ items = Object.entries(items).map(([key, value]) => {
+ let scope = getIterationScopeVariables(iteratorNames, value, key, items)
+
+ evaluateKey(value => keys.push(value), { scope: { index: key, ...scope} })
+
+ scopes.push(scope)
+ })
+ } else {
+ for (let i = 0; i < items.length; i++) {
+ let scope = getIterationScopeVariables(iteratorNames, items[i], i, items)
+
+ evaluateKey(value => keys.push(value), { scope: { index: i, ...scope} })
+
+ scopes.push(scope)
+ }
+ }
+
+ // Rather than making DOM manipulations inside one large loop, we'll
+ // instead track which mutations need to be made in the following
+ // arrays. After we're finished, we can batch them at the end.
+ let adds = []
+ let moves = []
+ let removes = []
+ let sames = []
+
+ // First, we track elements that will need to be removed.
+ for (let i = 0; i < prevKeys.length; i++) {
+ let key = prevKeys[i]
+
+ if (keys.indexOf(key) === -1) removes.push(key)
+ }
+
+ // Notice we're mutating prevKeys as we go. This makes it
+ // so that we can efficiently make incremental comparisons.
+ prevKeys = prevKeys.filter(key => ! removes.includes(key))
+
+ let lastKey = 'template'
+
+ // This is the important part of the diffing algo. Identifying
+ // which keys (future DOM elements) are new, which ones have
+ // or haven't moved (noting where they moved to / from).
+ for (let i = 0; i < keys.length; i++) {
+ let key = keys[i]
+
+ let prevIndex = prevKeys.indexOf(key)
+
+ if (prevIndex === -1) {
+ // New key found.
+ prevKeys.splice(i, 0, key)
+
+ adds.push([lastKey, i])
+ } else if (prevIndex !== i) {
+ // A key has moved.
+ let keyInSpot = prevKeys.splice(i, 1)[0]
+ let keyForSpot = prevKeys.splice(prevIndex - 1, 1)[0]
+
+ prevKeys.splice(i, 0, keyForSpot)
+ prevKeys.splice(prevIndex, 0, keyInSpot)
+
+ moves.push([keyInSpot, keyForSpot])
+ } else {
+ // This key hasn't moved, but we'll still keep track
+ // so that we can refresh it later on.
+ sames.push(key)
+ }
+
+ lastKey = key
+ }
+
+ // Now that we've done the diffing work, we can apply the mutations
+ // in batches for both separating types work and optimizing
+ // for browser performance.
+
+ // We'll remove all the nodes that need to be removed,
+ // letting the mutation observer pick them up and
+ // clean up any side effects they had.
+ for (let i = 0; i < removes.length; i++) {
+ let key = removes[i]
+
+ lookup[key].remove()
+
+ lookup[key] = null
+ delete lookup[key]
+ }
+
+ // Here we'll move elements around, skipping
+ // mutation observer triggers by using "mutateDom".
+ for (let i = 0; i < moves.length; i++) {
+ let [keyInSpot, keyForSpot] = moves[i]
+
+ let elInSpot = lookup[keyInSpot]
+ let elForSpot = lookup[keyForSpot]
+
+ let marker = document.createElement('div')
+
+ mutateDom(() => {
+ elForSpot.after(marker)
+ elInSpot.after(elForSpot)
+ marker.before(elInSpot)
+ marker.remove()
+ })
+
+ refreshScope(elForSpot, scopes[keys.indexOf(keyForSpot)])
+ }
+
+ // We can now create and add new elements.
+ for (let i = 0; i < adds.length; i++) {
+ let [lastKey, index] = adds[i]
+
+ let lastEl = (lastKey === 'template') ? templateEl : lookup[lastKey]
+
+ let scope = scopes[index]
+ let key = keys[index]
+
+ let clone = document.importNode(templateEl.content, true).firstElementChild
+
+ addScopeToNode(clone, reactive(scope), templateEl)
+
+ mutateDom(() => {
+ lastEl.after(clone)
+
+ initTree(clone)
+ })
+
+ if (typeof key === 'object') {
+ warn('x-for key cannot be an object, it must be a string or an integer', templateEl)
+ }
+
+ lookup[key] = clone
+ }
+
+ // If an element hasn't changed, we still want to "refresh" the
+ // data it depends on in case the data has changed in an
+ // "unobservable" way.
+ for (let i = 0; i < sames.length; i++) {
+ refreshScope(lookup[sames[i]], scopes[keys.indexOf(sames[i])])
+ }
+
+ // Now we'll log the keys (and the order they're in) for comparing
+ // against next time.
+ templateEl._x_prevKeys = keys
+ })
+}
+
+// This was taken from VueJS 2.* core. Thanks Vue!
+function parseForExpression(expression) {
+ let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
+ let stripParensRE = /^\s*\(|\)\s*$/g
+ let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
+ let inMatch = expression.match(forAliasRE)
+
+ if (! inMatch) return
+
+ let res = {}
+ res.items = inMatch[2].trim()
+ let item = inMatch[1].replace(stripParensRE, '').trim()
+ let iteratorMatch = item.match(forIteratorRE)
+
+ if (iteratorMatch) {
+ res.item = item.replace(forIteratorRE, '').trim()
+ res.index = iteratorMatch[1].trim()
+
+ if (iteratorMatch[2]) {
+ res.collection = iteratorMatch[2].trim()
+ }
+ } else {
+ res.item = item
+ }
+
+ return res
+}
+
+function getIterationScopeVariables(iteratorNames, item, index, items) {
+ // We must create a new object, so each iteration has a new scope
+ let scopeVariables = {}
+
+ // Support array destructuring ([foo, bar]).
+ if (/^\[.*\]$/.test(iteratorNames.item) && Array.isArray(item)) {
+ let names = iteratorNames.item.replace('[', '').replace(']', '').split(',').map(i => i.trim())
+
+ names.forEach((name, i) => {
+ scopeVariables[name] = item[i]
+ })
+ // Support object destructuring ({ foo: 'oof', bar: 'rab' }).
+ } else if (/^\{.*\}$/.test(iteratorNames.item) && ! Array.isArray(item) && typeof item === 'object') {
+ let names = iteratorNames.item.replace('{', '').replace('}', '').split(',').map(i => i.trim())
+
+ names.forEach(name => {
+ scopeVariables[name] = item[name]
+ })
+ } else {
+ scopeVariables[iteratorNames.item] = item
+ }
+
+ if (iteratorNames.index) scopeVariables[iteratorNames.index] = index
+
+ if (iteratorNames.collection) scopeVariables[iteratorNames.collection] = items
+
+ return scopeVariables
+}
+
+function isNumeric(subject){
+ return ! Array.isArray(subject) && ! isNaN(subject)
+}