Add supporting themes required for Lotusdocs

This commit is contained in:
Abner Coimbre
2026-01-11 16:48:19 -08:00
parent 8a4d04db58
commit f8d40c4e41
1289 changed files with 234948 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import Alpine from './../src/index'
window.Alpine = Alpine
queueMicrotask(() => {
Alpine.start()
})

View File

@@ -0,0 +1,5 @@
import Alpine from './../src/index'
export default Alpine
export { Alpine }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
{
"name": "alpinejs",
"version": "3.13.8",
"description": "The rugged, minimal JavaScript framework",
"homepage": "https://alpinejs.dev",
"repository": {
"type": "git",
"url": "https://github.com/alpinejs/alpine.git",
"directory": "packages/alpinejs"
},
"author": "Caleb Porzio",
"license": "MIT",
"main": "dist/module.cjs.js",
"module": "dist/module.esm.js",
"unpkg": "dist/cdn.min.js",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
}

View File

@@ -0,0 +1,82 @@
import { setReactivityEngine, disableEffectScheduling, reactive, effect, release, raw, watch } from './reactivity'
import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed } from './directives'
import { start, addRootSelector, addInitSelector, closestRoot, findClosest, initTree, destroyTree, interceptInit } from './lifecycle'
import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMutations, flushAndStopDeferringMutations, startObservingMutations, stopObservingMutations } from './mutation'
import { mergeProxies, closestDataStack, addScopeToNode, scope as $data } from './scope'
import { setEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions } from './evaluator'
import { transition } from './directives/x-transition'
import { clone, cloneNode, skipDuringClone, onlyDuringClone, interceptClone } from './clone'
import { interceptor } from './interceptor'
import { getBinding as bound, extractProp } from './utils/bind'
import { debounce } from './utils/debounce'
import { throttle } from './utils/throttle'
import { setStyles } from './utils/styles'
import { entangle } from './entangle'
import { nextTick } from './nextTick'
import { walk } from './utils/walk'
import { plugin } from './plugin'
import { magic } from './magics'
import { store } from './store'
import { bind } from './binds'
import { data } from './datas'
let Alpine = {
get reactive() { return reactive },
get release() { return release },
get effect() { return effect },
get raw() { return raw },
version: ALPINE_VERSION,
flushAndStopDeferringMutations,
dontAutoEvaluateFunctions,
disableEffectScheduling,
startObservingMutations,
stopObservingMutations,
setReactivityEngine,
onAttributeRemoved,
onAttributesAdded,
closestDataStack,
skipDuringClone,
onlyDuringClone,
addRootSelector,
addInitSelector,
interceptClone,
addScopeToNode,
deferMutations,
mapAttributes,
evaluateLater,
interceptInit,
setEvaluator,
mergeProxies,
extractProp,
findClosest,
onElRemoved,
closestRoot,
destroyTree,
interceptor, // INTERNAL: not public API and is subject to change without major release.
transition, // INTERNAL
setStyles, // INTERNAL
mutateDom,
directive,
entangle,
throttle,
debounce,
evaluate,
initTree,
nextTick,
prefixed,
prefix,
plugin,
magic,
store,
start,
clone, // INTERNAL
cloneNode, // INTERNAL
bound,
$data,
watch,
walk,
data,
bind,
}
export default Alpine

View File

@@ -0,0 +1,67 @@
import { attributesOnly, directives } from "./directives"
let binds = {}
export function bind(name, bindings) {
let getBindings = typeof bindings !== 'function' ? () => bindings : bindings
if (name instanceof Element) {
return applyBindingsObject(name, getBindings())
} else {
binds[name] = getBindings
}
return () => {} // Null cleanup...
}
export function injectBindingProviders(obj) {
Object.entries(binds).forEach(([name, callback]) => {
Object.defineProperty(obj, name, {
get() {
return (...args) => {
return callback(...args)
}
}
})
})
return obj
}
export function addVirtualBindings(el, bindings) {
let getBindings = typeof bindings !== 'function' ? () => bindings : bindings
el._x_virtualDirectives = getBindings()
}
export function applyBindingsObject(el, obj, original) {
let cleanupRunners = []
while (cleanupRunners.length) cleanupRunners.pop()()
let attributes = Object.entries(obj).map(([name, value]) => ({ name, value }))
let staticAttributes = attributesOnly(attributes)
// Handle binding normal HTML attributes (non-Alpine directives).
attributes = attributes.map(attribute => {
if (staticAttributes.find(attr => attr.name === attribute.name)) {
return {
name: `x-bind:${attribute.name}`,
value: `"${attribute.value}"`,
}
}
return attribute
})
directives(el, attributes, original).map(handle => {
cleanupRunners.push(handle.runCleanups)
handle()
})
return () => {
while (cleanupRunners.length) cleanupRunners.pop()()
}
}

View File

@@ -0,0 +1,89 @@
import { effect, release, overrideEffect } from "./reactivity"
import { initTree, isRoot } from "./lifecycle"
import { walk } from "./utils/walk"
export let isCloning = false
export function skipDuringClone(callback, fallback = () => {}) {
return (...args) => isCloning ? fallback(...args) : callback(...args)
}
export function onlyDuringClone(callback) {
return (...args) => isCloning && callback(...args)
}
let interceptors = []
export function interceptClone(callback) {
interceptors.push(callback)
}
export function cloneNode(from, to)
{
interceptors.forEach(i => i(from, to))
isCloning = true
// We don't need reactive effects in the new tree.
// Cloning is just used to seed new server HTML with
// Alpine before "morphing" it onto live Alpine...
dontRegisterReactiveSideEffects(() => {
initTree(to, (el, callback) => {
// We're hijacking the "walker" so that we
// only initialize the element we're cloning...
callback(el, () => {})
})
})
isCloning = false
}
export let isCloningLegacy = false
/** deprecated */
export function clone(oldEl, newEl) {
if (! newEl._x_dataStack) newEl._x_dataStack = oldEl._x_dataStack
isCloning = true
isCloningLegacy = true
dontRegisterReactiveSideEffects(() => {
cloneTree(newEl)
})
isCloning = false
isCloningLegacy = false
}
/** deprecated */
export function cloneTree(el) {
let hasRunThroughFirstEl = false
let shallowWalker = (el, callback) => {
walk(el, (el, skip) => {
if (hasRunThroughFirstEl && isRoot(el)) return skip()
hasRunThroughFirstEl = true
callback(el, skip)
})
}
initTree(el, shallowWalker)
}
function dontRegisterReactiveSideEffects(callback) {
let cache = effect
overrideEffect((callback, el) => {
let storedEffect = cache(callback)
release(storedEffect)
return () => {}
})
callback()
overrideEffect(cache)
}

View File

@@ -0,0 +1,22 @@
let datas = {}
export function data(name, callback) {
datas[name] = callback
}
export function injectDataProviders(obj, context) {
Object.entries(datas).forEach(([name, callback]) => {
Object.defineProperty(obj, name, {
get() {
return (...args) => {
return callback.bind(context)(...args)
}
},
enumerable: false,
})
})
return obj
}

View File

@@ -0,0 +1,221 @@
import { onAttributeRemoved, onElRemoved } from './mutation'
import { evaluate, evaluateLater } from './evaluator'
import { elementBoundEffect } from './reactivity'
import Alpine from './alpine'
let prefixAsString = 'x-'
export function prefix(subject = '') {
return prefixAsString + subject
}
export function setPrefix(newPrefix) {
prefixAsString = newPrefix
}
let directiveHandlers = {}
export function directive(name, callback) {
directiveHandlers[name] = callback
return {
before(directive) {
if (!directiveHandlers[directive]) {
console.warn(String.raw`Cannot find directive \`${directive}\`. \`${name}\` will use the default order of execution`);
return;
}
const pos = directiveOrder.indexOf(directive);
directiveOrder.splice(pos >= 0 ? pos : directiveOrder.indexOf('DEFAULT'), 0, name);
}
}
}
export function directives(el, attributes, originalAttributeOverride) {
attributes = Array.from(attributes)
if (el._x_virtualDirectives) {
let vAttributes = Object.entries(el._x_virtualDirectives).map(([name, value]) => ({ name, value }))
let staticAttributes = attributesOnly(vAttributes)
// Handle binding normal HTML attributes (non-Alpine directives).
vAttributes = vAttributes.map(attribute => {
if (staticAttributes.find(attr => attr.name === attribute.name)) {
return {
name: `x-bind:${attribute.name}`,
value: `"${attribute.value}"`,
}
}
return attribute
})
attributes = attributes.concat(vAttributes)
}
let transformedAttributeMap = {}
let directives = attributes
.map(toTransformedAttributes((newName, oldName) => transformedAttributeMap[newName] = oldName))
.filter(outNonAlpineAttributes)
.map(toParsedDirectives(transformedAttributeMap, originalAttributeOverride))
.sort(byPriority)
return directives.map(directive => {
return getDirectiveHandler(el, directive)
})
}
export function attributesOnly(attributes) {
return Array.from(attributes)
.map(toTransformedAttributes())
.filter(attr => ! outNonAlpineAttributes(attr))
}
let isDeferringHandlers = false
let directiveHandlerStacks = new Map
let currentHandlerStackKey = Symbol()
export function deferHandlingDirectives(callback) {
isDeferringHandlers = true
let key = Symbol()
currentHandlerStackKey = key
directiveHandlerStacks.set(key, [])
let flushHandlers = () => {
while (directiveHandlerStacks.get(key).length) directiveHandlerStacks.get(key).shift()()
directiveHandlerStacks.delete(key)
}
let stopDeferring = () => { isDeferringHandlers = false; flushHandlers() }
callback(flushHandlers)
stopDeferring()
}
export function getElementBoundUtilities(el) {
let cleanups = []
let cleanup = callback => cleanups.push(callback)
let [effect, cleanupEffect] = elementBoundEffect(el)
cleanups.push(cleanupEffect)
let utilities = {
Alpine,
effect,
cleanup,
evaluateLater: evaluateLater.bind(evaluateLater, el),
evaluate: evaluate.bind(evaluate, el),
}
let doCleanup = () => cleanups.forEach(i => i())
return [utilities, doCleanup]
}
export function getDirectiveHandler(el, directive) {
let noop = () => {}
let handler = directiveHandlers[directive.type] || noop
let [utilities, cleanup] = getElementBoundUtilities(el)
onAttributeRemoved(el, directive.original, cleanup)
let fullHandler = () => {
if (el._x_ignore || el._x_ignoreSelf) return
handler.inline && handler.inline(el, directive, utilities)
handler = handler.bind(handler, el, directive, utilities)
isDeferringHandlers ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler) : handler()
}
fullHandler.runCleanups = cleanup
return fullHandler
}
export let startingWith = (subject, replacement) => ({ name, value }) => {
if (name.startsWith(subject)) name = name.replace(subject, replacement)
return { name, value }
}
export let into = i => i
function toTransformedAttributes(callback = () => {}) {
return ({ name, value }) => {
let { name: newName, value: newValue } = attributeTransformers.reduce((carry, transform) => {
return transform(carry)
}, { name, value })
if (newName !== name) callback(newName, name)
return { name: newName, value: newValue }
}
}
let attributeTransformers = []
export function mapAttributes(callback) {
attributeTransformers.push(callback)
}
function outNonAlpineAttributes({ name }) {
return alpineAttributeRegex().test(name)
}
let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}([^:^.]+)\\b`))
function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) {
return ({ name, value }) => {
let typeMatch = name.match(alpineAttributeRegex())
let valueMatch = name.match(/:([a-zA-Z0-9\-_:]+)/)
let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []
let original = originalAttributeOverride || transformedAttributeMap[name] || name
return {
type: typeMatch ? typeMatch[1] : null,
value: valueMatch ? valueMatch[1] : null,
modifiers: modifiers.map(i => i.replace('.', '')),
expression: value,
original,
}
}
}
const DEFAULT = 'DEFAULT'
let directiveOrder = [
'ignore',
'ref',
'data',
'id',
'anchor',
'bind',
'init',
'for',
'model',
'modelable',
'transition',
'show',
'if',
DEFAULT,
'teleport',
]
function byPriority(a, b) {
let typeA = directiveOrder.indexOf(a.type) === -1 ? DEFAULT : a.type
let typeB = directiveOrder.indexOf(b.type) === -1 ? DEFAULT : b.type
return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB)
}

View File

@@ -0,0 +1,31 @@
import { directive } from '../directives'
import { warn } from '../utils/warn'
import './x-transition'
import './x-modelable'
import './x-teleport'
import './x-ignore'
import './x-effect'
import './x-model'
import './x-cloak'
import './x-init'
import './x-text'
import './x-html'
import './x-bind'
import './x-data'
import './x-show'
import './x-for'
import './x-ref'
import './x-if'
import './x-id'
import './x-on'
// Register warnings for people using plugin syntaxes and not loading the plugin itself:
warnMissingPluginDirective('Collapse', 'collapse', 'collapse')
warnMissingPluginDirective('Intersect', 'intersect', 'intersect')
warnMissingPluginDirective('Focus', 'trap', 'focus')
warnMissingPluginDirective('Mask', 'mask', 'mask')
function warnMissingPluginDirective(name, directiveName, slug) {
directive(directiveName, (el) => warn(`You can't use [x-${directiveName}] without first installing the "${name}" plugin here: https://alpinejs.dev/plugins/${slug}`, el))
}

View File

@@ -0,0 +1,60 @@
import { directive, into, mapAttributes, prefix, startingWith } from '../directives'
import { evaluateLater } from '../evaluator'
import { mutateDom } from '../mutation'
import bind from '../utils/bind'
import { applyBindingsObject, injectBindingProviders } from '../binds'
mapAttributes(startingWith(':', into(prefix('bind:'))))
let handler = (el, { value, modifiers, expression, original }, { effect, cleanup }) => {
if (! value) {
let bindingProviders = {}
injectBindingProviders(bindingProviders)
let getBindings = evaluateLater(el, expression)
getBindings(bindings => {
applyBindingsObject(el, bindings, original)
}, { scope: bindingProviders } )
return
}
if (value === 'key') return storeKeyForXFor(el, expression)
if (el._x_inlineBindings && el._x_inlineBindings[value] && el._x_inlineBindings[value].extract) {
return
}
let evaluate = evaluateLater(el, expression)
effect(() => evaluate(result => {
// If nested object key is undefined, set the default value to empty string.
if (result === undefined && typeof expression === 'string' && expression.match(/\./)) {
result = ''
}
mutateDom(() => bind(el, value, result, modifiers))
}))
cleanup(() => {
el._x_undoAddedClasses && el._x_undoAddedClasses()
el._x_undoAddedStyles && el._x_undoAddedStyles()
})
}
// @todo: see if I can take advantage of the object created here inside the
// non-inline handler above so we're not duplicating work twice...
handler.inline = (el, { value, modifiers, expression }) => {
if (! value) return;
if (! el._x_inlineBindings) el._x_inlineBindings = {}
el._x_inlineBindings[value] = { expression, extract: false }
}
directive('bind', handler)
function storeKeyForXFor(el, expression) {
el._x_keyExpression = expression
}

View File

@@ -0,0 +1,4 @@
import { directive, prefix } from '../directives'
import { mutateDom } from '../mutation'
directive('cloak', el => queueMicrotask(() => mutateDom(() => el.removeAttribute(prefix('cloak')))))

View File

@@ -0,0 +1,71 @@
import { directive, prefix } from '../directives'
import { initInterceptors } from '../interceptor'
import { injectDataProviders } from '../datas'
import { addRootSelector } from '../lifecycle'
import { interceptClone, isCloning, isCloningLegacy } from '../clone'
import { addScopeToNode } from '../scope'
import { injectMagics, magic } from '../magics'
import { reactive } from '../reactivity'
import { evaluate } from '../evaluator'
addRootSelector(() => `[${prefix('data')}]`)
directive('data', ((el, { expression }, { cleanup }) => {
if (shouldSkipRegisteringDataDuringClone(el)) return
expression = expression === '' ? '{}' : expression
let magicContext = {}
let cleanup1 = injectMagics(magicContext, el).cleanup
let dataProviderContext = {}
injectDataProviders(dataProviderContext, magicContext)
let data = evaluate(el, expression, { scope: dataProviderContext })
if (data === undefined || data === true) data = {}
let cleanup2 = injectMagics(data, el).cleanup
let reactiveData = reactive(data)
initInterceptors(reactiveData)
let undo = addScopeToNode(el, reactiveData)
reactiveData['init'] && evaluate(el, reactiveData['init'])
cleanup(() => {
reactiveData['destroy'] && evaluate(el, reactiveData['destroy'])
undo()
// MemLeak1: Issue #2140
cleanup1()
cleanup2()
})
}))
interceptClone((from, to) => {
// Transfer over existing runtime Alpine state from
// the existing dom tree over to the new one...
if (from._x_dataStack) {
to._x_dataStack = from._x_dataStack
// Set a flag to signify the new tree is using
// pre-seeded state (used so x-data knows when
// and when not to initialize state)...
to.setAttribute('data-has-alpine-state', true)
}
})
// If we are cloning a tree, we only want to evaluate x-data if another
// x-data context DOESN'T exist on the component.
// The reason a data context WOULD exist is that we graft root x-data state over
// from the live tree before hydrating the clone tree.
function shouldSkipRegisteringDataDuringClone(el) {
if (! isCloning) return false
if (isCloningLegacy) return true
return el.hasAttribute('data-has-alpine-state')
}

View File

@@ -0,0 +1,7 @@
import { skipDuringClone } from '../clone'
import { directive } from '../directives'
import { evaluate, evaluateLater } from '../evaluator'
directive('effect', skipDuringClone((el, { expression }, { effect }) => {
effect(evaluateLater(el, expression))
}))

View File

@@ -0,0 +1,292 @@
import { addScopeToNode } from '../scope'
import { evaluateLater } from '../evaluator'
import { directive } from '../directives'
import { reactive } from '../reactivity'
import { initTree } from '../lifecycle'
import { mutateDom } from '../mutation'
import { warn } from '../utils/warn'
import { dequeueJob } from '../scheduler'
import { skipDuringClone } from '../clone'
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 => {
if (keys.includes(value)) warn('Duplicate key on x-for', el)
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 => {
if (keys.includes(value)) warn('Duplicate key on x-for', el)
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]
// Remove any queued effects that might run after the DOM node has been removed.
if (!! lookup[key]._x_effects) {
lookup[key]._x_effects.forEach(dequeueJob)
}
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(() => {
if (! elForSpot) warn(`x-for ":key" is undefined or invalid`, templateEl, keyForSpot, lookup)
elForSpot.after(marker)
elInSpot.after(elForSpot)
elForSpot._x_currentIfEl && elForSpot.after(elForSpot._x_currentIfEl)
marker.before(elInSpot)
elInSpot._x_currentIfEl && elInSpot.after(elInSpot._x_currentIfEl)
marker.remove()
})
elForSpot._x_refreshXForScope(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]
// If the element is a x-if template evaluated to true,
// point lastEl to the if-generated node
if (lastEl._x_currentIfEl) lastEl = lastEl._x_currentIfEl
let scope = scopes[index]
let key = keys[index]
let clone = document.importNode(templateEl.content, true).firstElementChild
let reactiveScope = reactive(scope)
addScopeToNode(clone, reactiveScope, templateEl)
clone._x_refreshXForScope = (newScope) => {
Object.entries(newScope).forEach(([key, value]) => {
reactiveScope[key] = value
})
}
mutateDom(() => {
lastEl.after(clone)
// These nodes will be "inited" as morph walks the tree...
skipDuringClone(() => 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++) {
lookup[sames[i]]._x_refreshXForScope(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)
}

View File

@@ -0,0 +1,19 @@
import { directive } from '../directives'
import { initTree } from '../lifecycle'
import { mutateDom } from '../mutation'
directive('html', (el, { expression }, { effect, evaluateLater }) => {
let evaluate = evaluateLater(expression)
effect(() => {
evaluate(value => {
mutateDom(() => {
el.innerHTML = value
el._x_ignoreSelf = true
initTree(el)
delete el._x_ignoreSelf
})
})
})
})

View File

@@ -0,0 +1,19 @@
import { interceptClone } from "../clone"
import { directive } from "../directives"
import { setIdRoot } from '../ids'
directive('id', (el, { expression }, { evaluate }) => {
let names = evaluate(expression)
names.forEach(name => setIdRoot(el, name))
})
interceptClone((from, to) => {
// Transfer over existing ID registrations from
// the existing dom tree over to the new one
// so that there aren't ID mismatches...
if (from._x_ids) {
to._x_ids = from._x_ids
}
})

View File

@@ -0,0 +1,60 @@
import { evaluateLater } from '../evaluator'
import { addScopeToNode } from '../scope'
import { directive } from '../directives'
import { initTree } from '../lifecycle'
import { mutateDom } from '../mutation'
import { walk } from "../utils/walk"
import { dequeueJob } from '../scheduler'
import { warn } from "../utils/warn"
import { skipDuringClone } from '../clone'
directive('if', (el, { expression }, { effect, cleanup }) => {
if (el.tagName.toLowerCase() !== 'template') warn('x-if can only be used on a <template> tag', el)
let evaluate = evaluateLater(el, expression)
let show = () => {
if (el._x_currentIfEl) return el._x_currentIfEl
let clone = el.content.cloneNode(true).firstElementChild
addScopeToNode(clone, {}, el)
mutateDom(() => {
el.after(clone)
// These nodes will be "inited" as morph walks the tree...
skipDuringClone(() => initTree(clone))()
})
el._x_currentIfEl = clone
el._x_undoIf = () => {
walk(clone, (node) => {
if (!!node._x_effects) {
node._x_effects.forEach(dequeueJob)
}
})
clone.remove();
delete el._x_currentIfEl
}
return clone
}
let hide = () => {
if (! el._x_undoIf) return
el._x_undoIf()
delete el._x_undoIf
}
effect(() => evaluate(value => {
value ? show() : hide()
}))
cleanup(() => el._x_undoIf && el._x_undoIf())
})

View File

@@ -0,0 +1,17 @@
import { directive } from "../directives"
let handler = () => {}
handler.inline = (el, { modifiers }, { cleanup }) => {
modifiers.includes('self')
? el._x_ignoreSelf = true
: el._x_ignore = true
cleanup(() => {
modifiers.includes('self')
? delete el._x_ignoreSelf
: delete el._x_ignore
})
}
directive('ignore', handler)

View File

@@ -0,0 +1,13 @@
import { directive, prefix } from "../directives";
import { addInitSelector } from "../lifecycle";
import { skipDuringClone } from "../clone";
addInitSelector(() => `[${prefix('init')}]`)
directive('init', skipDuringClone((el, { expression }, { evaluate }) => {
if (typeof expression === 'string') {
return !! expression.trim() && evaluate(expression, {}, false)
}
return evaluate(expression, {}, false)
}))

View File

@@ -0,0 +1,214 @@
import { evaluateLater } from '../evaluator'
import { directive } from '../directives'
import { mutateDom } from '../mutation'
import { nextTick } from '../nextTick'
import bind, { safeParseBoolean } from '../utils/bind'
import on from '../utils/on'
import { isCloning } from '../clone'
directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
let scopeTarget = el
if (modifiers.includes('parent')) {
scopeTarget = el.parentNode
}
let evaluateGet = evaluateLater(scopeTarget, expression)
let evaluateSet
if (typeof expression === 'string') {
evaluateSet = evaluateLater(scopeTarget, `${expression} = __placeholder`)
} else if (typeof expression === 'function' && typeof expression() === 'string') {
evaluateSet = evaluateLater(scopeTarget, `${expression()} = __placeholder`)
} else {
evaluateSet = () => {}
}
let getValue = () => {
let result
evaluateGet(value => result = value)
return isGetterSetter(result) ? result.get() : result
}
let setValue = value => {
let result
evaluateGet(value => result = value)
if (isGetterSetter(result)) {
result.set(value)
} else {
evaluateSet(() => {}, {
scope: { '__placeholder': value }
})
}
}
if (typeof expression === 'string' && el.type === 'radio') {
// Radio buttons only work properly when they share a name attribute.
// People might assume we take care of that for them, because
// they already set a shared "x-model" attribute.
mutateDom(() => {
if (! el.hasAttribute('name')) el.setAttribute('name', expression)
})
}
// If the element we are binding to is a select, a radio, or checkbox
// we'll listen for the change event instead of the "input" event.
var event = (el.tagName.toLowerCase() === 'select')
|| ['checkbox', 'radio'].includes(el.type)
|| modifiers.includes('lazy')
? 'change' : 'input'
// We only want to register the event listener when we're not cloning, since the
// mutation observer handles initializing the x-model directive already when
// the element is inserted into the DOM. Otherwise we register it twice.
let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
setValue(getInputValue(el, modifiers, e, getValue()))
})
if (modifiers.includes('fill'))
if ([undefined, null, ''].includes(getValue())
|| (el.type === 'checkbox' && Array.isArray(getValue()))) {
setValue(
getInputValue(el, modifiers, { target: el }, getValue())
);
}
// Register the listener removal callback on the element, so that
// in addition to the cleanup function, x-modelable may call it.
// Also, make this a keyed object if we decide to reintroduce
// "named modelables" some time in a future Alpine version.
if (! el._x_removeModelListeners) el._x_removeModelListeners = {}
el._x_removeModelListeners['default'] = removeListener
cleanup(() => el._x_removeModelListeners['default']())
// If the input/select/textarea element is linked to a form
// we listen for the reset event on the parent form (the event
// does not trigger on the single inputs) and update
// on nextTick so the page doesn't end up out of sync
if (el.form) {
let removeResetListener = on(el.form, 'reset', [], (e) => {
nextTick(() => el._x_model && el._x_model.set(el.value))
})
cleanup(() => removeResetListener())
}
// Allow programmatic overriding of x-model.
el._x_model = {
get() {
return getValue()
},
set(value) {
setValue(value)
},
}
el._x_forceModelUpdate = (value) => {
// If nested model key is undefined, set the default value to empty string.
if (value === undefined && typeof expression === 'string' && expression.match(/\./)) value = ''
// @todo: This is nasty
window.fromModel = true
mutateDom(() => bind(el, 'value', value))
delete window.fromModel
}
effect(() => {
// We need to make sure we're always "getting" the value up front,
// so that we don't run into a situation where because of the early
// the reactive value isn't gotten and therefore disables future reactions.
let value = getValue()
// Don't modify the value of the input if it's focused.
if (modifiers.includes('unintrusive') && document.activeElement.isSameNode(el)) return
el._x_forceModelUpdate(value)
})
})
function getInputValue(el, modifiers, event, currentValue) {
return mutateDom(() => {
// Check for event.detail due to an issue where IE11 handles other events as a CustomEvent.
// Safari autofill triggers event as CustomEvent and assigns value to target
// so we return event.target.value instead of event.detail
if (event instanceof CustomEvent && event.detail !== undefined)
return event.detail !== null && event.detail !== undefined ? event.detail : event.target.value
else if (el.type === 'checkbox') {
// If the data we are binding to is an array, toggle its value inside the array.
if (Array.isArray(currentValue)) {
let newValue = null;
if (modifiers.includes('number')) {
newValue = safeParseNumber(event.target.value)
} else if (modifiers.includes('boolean')) {
newValue = safeParseBoolean(event.target.value)
} else {
newValue = event.target.value
}
return event.target.checked ? currentValue.concat([newValue]) : currentValue.filter(el => ! checkedAttrLooseCompare(el, newValue))
} else {
return event.target.checked
}
} else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
if (modifiers.includes('number')) {
return Array.from(event.target.selectedOptions).map(option => {
let rawValue = option.value || option.text
return safeParseNumber(rawValue)
})
} else if (modifiers.includes('boolean')) {
return Array.from(event.target.selectedOptions).map(option => {
let rawValue = option.value || option.text
return safeParseBoolean(rawValue)
})
}
return Array.from(event.target.selectedOptions).map(option => {
return option.value || option.text
})
} else {
let newValue
if (el.type === 'radio') {
if (event.target.checked) {
newValue = event.target.value
} else {
newValue = currentValue
}
} else {
newValue = event.target.value
}
if (modifiers.includes('number')) {
return safeParseNumber(newValue)
} else if (modifiers.includes('boolean')) {
return safeParseBoolean(newValue)
} else if (modifiers.includes('trim')) {
return newValue.trim()
} else {
return newValue
}
}
})
}
function safeParseNumber(rawValue) {
let number = rawValue ? parseFloat(rawValue) : null
return isNumeric(number) ? number : rawValue
}
function checkedAttrLooseCompare(valueA, valueB) {
return valueA == valueB
}
function isNumeric(subject){
return ! Array.isArray(subject) && ! isNaN(subject)
}
function isGetterSetter(value) {
return value !== null && typeof value === 'object' && typeof value.get === 'function' && typeof value.set === 'function'
}

View File

@@ -0,0 +1,39 @@
import { directive } from '../directives'
import { entangle } from '../entangle';
directive('modelable', (el, { expression }, { effect, evaluateLater, cleanup }) => {
let func = evaluateLater(expression)
let innerGet = () => { let result; func(i => result = i); return result; }
let evaluateInnerSet = evaluateLater(`${expression} = __placeholder`)
let innerSet = val => evaluateInnerSet(() => {}, { scope: { '__placeholder': val }})
let initialValue = innerGet()
innerSet(initialValue)
queueMicrotask(() => {
if (! el._x_model) return
// Remove native event listeners as these are now bound with x-modelable.
// The reason for this is that it's often useful to wrap <input> elements
// in x-modelable/model, but the input events from the native input
// override any functionality added by x-modelable causing confusion.
el._x_removeModelListeners['default']()
let outerGet = el._x_model.get
let outerSet = el._x_model.set
let releaseEntanglement = entangle(
{
get() { return outerGet() },
set(value) { outerSet(value) },
},
{
get() { return innerGet() },
set(value) { innerSet(value) },
},
)
cleanup(releaseEntanglement)
})
})

View File

@@ -0,0 +1,22 @@
import { directive, into, mapAttributes, prefix, startingWith } from '../directives'
import { evaluateLater } from '../evaluator'
import { skipDuringClone } from '../clone'
import on from '../utils/on'
mapAttributes(startingWith('@', into(prefix('on:'))))
directive('on', skipDuringClone((el, { value, modifiers, expression }, { cleanup }) => {
let evaluate = expression ? evaluateLater(el, expression) : () => {}
// Forward event listeners on portals.
if (el.tagName.toLowerCase() === 'template') {
if (! el._x_forwardEvents) el._x_forwardEvents = []
if (! el._x_forwardEvents.includes(value)) el._x_forwardEvents.push(value)
}
let removeListener = on(el, value, modifiers, e => {
evaluate(() => {}, { scope: { '$event': e }, params: [e] })
})
cleanup(() => removeListener())
}))

View File

@@ -0,0 +1,16 @@
import { closestRoot } from '../lifecycle'
import { directive } from '../directives'
function handler () {}
handler.inline = (el, { expression }, { cleanup }) => {
let root = closestRoot(el)
if (! root._x_refs) root._x_refs = {}
root._x_refs[expression] = el
cleanup(() => delete root._x_refs[expression])
}
directive('ref', handler)

View File

@@ -0,0 +1,68 @@
import { evaluateLater } from '../evaluator'
import { directive } from '../directives'
import { mutateDom } from '../mutation'
import { once } from '../utils/once'
directive('show', (el, { modifiers, expression }, { effect }) => {
let evaluate = evaluateLater(el, expression)
// We're going to set this function on the element directly so that
// other plugins like "Collapse" can overwrite them with their own logic.
if (! el._x_doHide) el._x_doHide = () => {
mutateDom(() => {
el.style.setProperty('display', 'none', modifiers.includes('important') ? 'important' : undefined)
})
}
if (! el._x_doShow) el._x_doShow = () => {
mutateDom(() => {
if (el.style.length === 1 && el.style.display === 'none') {
el.removeAttribute('style')
} else {
el.style.removeProperty('display')
}
})
}
let hide = () => {
el._x_doHide()
el._x_isShown = false
}
let show = () => {
el._x_doShow()
el._x_isShown = true
}
// We are wrapping this function in a setTimeout here to prevent
// a race condition from happening where elements that have a
// @click.away always view themselves as shown on the page.
let clickAwayCompatibleShow = () => setTimeout(show)
let toggle = once(
value => value ? show() : hide(),
value => {
if (typeof el._x_toggleAndCascadeWithTransitions === 'function') {
el._x_toggleAndCascadeWithTransitions(el, value, show, hide)
} else {
value ? clickAwayCompatibleShow() : hide()
}
}
)
let oldValue
let firstTime = true
effect(() => evaluate(value => {
// Let's make sure we only call this effect if the value changed.
// This prevents "blip" transitions. (1 tick out, then in)
if (! firstTime && value === oldValue) return
if (modifiers.includes('immediate')) value ? clickAwayCompatibleShow() : hide()
toggle(value)
oldValue = value
firstTime = false
}))
})

View File

@@ -0,0 +1,80 @@
import { skipDuringClone } from "../clone"
import { directive } from "../directives"
import { initTree } from "../lifecycle"
import { mutateDom } from "../mutation"
import { addScopeToNode } from "../scope"
import { warn } from "../utils/warn"
directive('teleport', (el, { modifiers, expression }, { cleanup }) => {
if (el.tagName.toLowerCase() !== 'template') warn('x-teleport can only be used on a <template> tag', el)
let target = getTarget(expression)
let clone = el.content.cloneNode(true).firstElementChild
// Add reference to element on <template x-teleport, and visa versa.
el._x_teleport = clone
clone._x_teleportBack = el
// Add the key to the DOM so they can be more easily searched for and linked up...
el.setAttribute('data-teleport-template', true)
clone.setAttribute('data-teleport-target', true)
// Forward event listeners:
if (el._x_forwardEvents) {
el._x_forwardEvents.forEach(eventName => {
clone.addEventListener(eventName, e => {
e.stopPropagation()
el.dispatchEvent(new e.constructor(e.type, e))
})
})
}
addScopeToNode(clone, {}, el)
let placeInDom = (clone, target, modifiers) => {
if (modifiers.includes('prepend')) {
// insert element before the target
target.parentNode.insertBefore(clone, target)
} else if (modifiers.includes('append')) {
// insert element after the target
target.parentNode.insertBefore(clone, target.nextSibling)
} else {
// origin
target.appendChild(clone)
}
}
mutateDom(() => {
placeInDom(clone, target, modifiers)
initTree(clone)
clone._x_ignore = true
})
el._x_teleportPutBack = () => {
let target = getTarget(expression)
mutateDom(() => {
placeInDom(el._x_teleport, target, modifiers)
})
}
cleanup(() => clone.remove())
})
let teleportContainerDuringClone = document.createElement('div')
function getTarget(expression) {
let target = skipDuringClone(() => {
return document.querySelector(expression)
}, () => {
return teleportContainerDuringClone
})()
if (! target) warn(`Cannot find x-teleport element for selector: "${expression}"`)
return target
}

View File

@@ -0,0 +1,14 @@
import { directive } from '../directives'
import { mutateDom } from '../mutation'
directive('text', (el, { expression }, { effect, evaluateLater }) => {
let evaluate = evaluateLater(expression)
effect(() => {
evaluate(value => {
mutateDom(() => {
el.textContent = value
})
})
})
})

View File

@@ -0,0 +1,335 @@
import { releaseNextTicks, holdNextTicks } from '../nextTick'
import { setClasses } from '../utils/classes'
import { setStyles } from '../utils/styles'
import { directive } from '../directives'
import { mutateDom } from '../mutation'
import { once } from '../utils/once'
directive('transition', (el, { value, modifiers, expression }, { evaluate }) => {
if (typeof expression === 'function') expression = evaluate(expression)
if (expression === false) return
if (!expression || typeof expression === 'boolean') {
registerTransitionsFromHelper(el, modifiers, value)
} else {
registerTransitionsFromClassString(el, expression, value)
}
})
function registerTransitionsFromClassString(el, classString, stage) {
registerTransitionObject(el, setClasses, '')
let directiveStorageMap = {
'enter': (classes) => { el._x_transition.enter.during = classes },
'enter-start': (classes) => { el._x_transition.enter.start = classes },
'enter-end': (classes) => { el._x_transition.enter.end = classes },
'leave': (classes) => { el._x_transition.leave.during = classes },
'leave-start': (classes) => { el._x_transition.leave.start = classes },
'leave-end': (classes) => { el._x_transition.leave.end = classes },
}
directiveStorageMap[stage](classString)
}
function registerTransitionsFromHelper(el, modifiers, stage) {
registerTransitionObject(el, setStyles)
let doesntSpecify = (! modifiers.includes('in') && ! modifiers.includes('out')) && ! stage
let transitioningIn = doesntSpecify || modifiers.includes('in') || ['enter'].includes(stage)
let transitioningOut = doesntSpecify || modifiers.includes('out') || ['leave'].includes(stage)
if (modifiers.includes('in') && ! doesntSpecify) {
modifiers = modifiers.filter((i, index) => index < modifiers.indexOf('out'))
}
if (modifiers.includes('out') && ! doesntSpecify) {
modifiers = modifiers.filter((i, index) => index > modifiers.indexOf('out'))
}
let wantsAll = ! modifiers.includes('opacity') && ! modifiers.includes('scale')
let wantsOpacity = wantsAll || modifiers.includes('opacity')
let wantsScale = wantsAll || modifiers.includes('scale')
let opacityValue = wantsOpacity ? 0 : 1
let scaleValue = wantsScale ? modifierValue(modifiers, 'scale', 95) / 100 : 1
let delay = modifierValue(modifiers, 'delay', 0) / 1000
let origin = modifierValue(modifiers, 'origin', 'center')
let property = 'opacity, transform'
let durationIn = modifierValue(modifiers, 'duration', 150) / 1000
let durationOut = modifierValue(modifiers, 'duration', 75) / 1000
let easing = `cubic-bezier(0.4, 0.0, 0.2, 1)`
if (transitioningIn) {
el._x_transition.enter.during = {
transformOrigin: origin,
transitionDelay: `${delay}s`,
transitionProperty: property,
transitionDuration: `${durationIn}s`,
transitionTimingFunction: easing,
}
el._x_transition.enter.start = {
opacity: opacityValue,
transform: `scale(${scaleValue})`,
}
el._x_transition.enter.end = {
opacity: 1,
transform: `scale(1)`,
}
}
if (transitioningOut) {
el._x_transition.leave.during = {
transformOrigin: origin,
transitionDelay: `${delay}s`,
transitionProperty: property,
transitionDuration: `${durationOut}s`,
transitionTimingFunction: easing,
}
el._x_transition.leave.start = {
opacity: 1,
transform: `scale(1)`,
}
el._x_transition.leave.end = {
opacity: opacityValue,
transform: `scale(${scaleValue})`,
}
}
}
function registerTransitionObject(el, setFunction, defaultValue = {}) {
if (! el._x_transition) el._x_transition = {
enter: { during: defaultValue, start: defaultValue, end: defaultValue },
leave: { during: defaultValue, start: defaultValue, end: defaultValue },
in(before = () => {}, after = () => {}) {
transition(el, setFunction, {
during: this.enter.during,
start: this.enter.start,
end: this.enter.end,
}, before, after)
},
out(before = () => {}, after = () => {}) {
transition(el, setFunction, {
during: this.leave.during,
start: this.leave.start,
end: this.leave.end,
}, before, after)
},
}
}
window.Element.prototype._x_toggleAndCascadeWithTransitions = function (el, value, show, hide) {
// We are running this function after one tick to prevent
// a race condition from happening where elements that have a
// @click.away always view themselves as shown on the page.
// If the tab is active, we prioritise requestAnimationFrame which plays
// nicely with nested animations otherwise we use setTimeout to make sure
// it keeps running in background. setTimeout has a lower priority in the
// event loop so it would skip nested transitions but when the tab is
// hidden, it's not relevant.
const nextTick = document.visibilityState === 'visible' ? requestAnimationFrame : setTimeout;
let clickAwayCompatibleShow = () => nextTick(show);
if (value) {
if (el._x_transition && (el._x_transition.enter || el._x_transition.leave)) {
// This fixes a bug where if you are only transitioning OUT and you are also using @click.outside
// the element when shown immediately starts transitioning out. There is a test in the manual
// transition test file for this: /tests/cypress/manual-transition-test.html
(el._x_transition.enter && (Object.entries(el._x_transition.enter.during).length || Object.entries(el._x_transition.enter.start).length || Object.entries(el._x_transition.enter.end).length))
? el._x_transition.in(show)
: clickAwayCompatibleShow()
} else {
el._x_transition
? el._x_transition.in(show)
: clickAwayCompatibleShow()
}
return
}
// Livewire depends on el._x_hidePromise.
el._x_hidePromise = el._x_transition
? new Promise((resolve, reject) => {
el._x_transition.out(() => {}, () => resolve(hide))
el._x_transitioning && el._x_transitioning.beforeCancel(() => reject({ isFromCancelledTransition: true }))
})
: Promise.resolve(hide)
queueMicrotask(() => {
let closest = closestHide(el)
if (closest) {
if (! closest._x_hideChildren) closest._x_hideChildren = []
closest._x_hideChildren.push(el)
} else {
nextTick(() => {
let hideAfterChildren = el => {
let carry = Promise.all([
el._x_hidePromise,
...(el._x_hideChildren || []).map(hideAfterChildren),
]).then(([i]) => i())
delete el._x_hidePromise
delete el._x_hideChildren
return carry
}
hideAfterChildren(el).catch((e) => {
if (! e.isFromCancelledTransition) throw e
})
})
}
})
}
function closestHide(el) {
let parent = el.parentNode
if (! parent) return
return parent._x_hidePromise ? parent : closestHide(parent)
}
export function transition(el, setFunction, { during, start, end } = {}, before = () => {}, after = () => {}) {
if (el._x_transitioning) el._x_transitioning.cancel()
if (Object.keys(during).length === 0 && Object.keys(start).length === 0 && Object.keys(end).length === 0) {
// Execute right away if there is no transition.
before(); after()
return
}
let undoStart, undoDuring, undoEnd
performTransition(el, {
start() {
undoStart = setFunction(el, start)
},
during() {
undoDuring = setFunction(el, during)
},
before,
end() {
undoStart()
undoEnd = setFunction(el, end)
},
after,
cleanup() {
undoDuring()
undoEnd()
},
})
}
export function performTransition(el, stages) {
// All transitions need to be truly "cancellable". Meaning we need to
// account for interruptions at ALL stages of the transitions and
// immediately run the rest of the transition.
let interrupted, reachedBefore, reachedEnd
let finish = once(() => {
mutateDom(() => {
interrupted = true
if (! reachedBefore) stages.before()
if (! reachedEnd) {
stages.end()
releaseNextTicks()
}
stages.after()
// Adding an "isConnected" check, in case the callback removed the element from the DOM.
if (el.isConnected) stages.cleanup()
delete el._x_transitioning
})
})
el._x_transitioning = {
beforeCancels: [],
beforeCancel(callback) { this.beforeCancels.push(callback) },
cancel: once(function () { while (this.beforeCancels.length) { this.beforeCancels.shift()() }; finish(); }),
finish,
}
mutateDom(() => {
stages.start()
stages.during()
})
holdNextTicks()
requestAnimationFrame(() => {
if (interrupted) return
// Note: Safari's transitionDuration property will list out comma separated transition durations
// for every single transition property. Let's grab the first one and call it a day.
let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000
let delay = Number(getComputedStyle(el).transitionDelay.replace(/,.*/, '').replace('s', '')) * 1000
if (duration === 0) duration = Number(getComputedStyle(el).animationDuration.replace('s', '')) * 1000
mutateDom(() => {
stages.before()
})
reachedBefore = true
requestAnimationFrame(() => {
if (interrupted) return
mutateDom(() => {
stages.end()
})
releaseNextTicks()
setTimeout(el._x_transitioning.finish, duration + delay)
reachedEnd = true
})
})
}
export function modifierValue(modifiers, key, fallback) {
// If the modifier isn't present, use the default.
if (modifiers.indexOf(key) === -1) return fallback
// If it IS present, grab the value after it: x-show.transition.duration.500ms
const rawValue = modifiers[modifiers.indexOf(key) + 1]
if (! rawValue) return fallback
if (key === 'scale') {
// Check if the very next value is NOT a number and return the fallback.
// If x-show.transition.scale, we'll use the default scale value.
// That is how a user opts out of the opacity transition.
if (isNaN(rawValue)) return fallback
}
if (key === 'duration' || key === 'delay') {
// Support x-transition.duration.500ms && duration.500
let match = rawValue.match(/([0-9]+)ms/)
if (match) return match[1]
}
if (key === 'origin') {
// Support chaining origin directions: x-show.transition.top.right
if (['top', 'right', 'left', 'center', 'bottom'].includes(modifiers[modifiers.indexOf(key) + 2])) {
return [rawValue, modifiers[modifiers.indexOf(key) + 2]].join(' ')
}
}
return rawValue
}

View File

@@ -0,0 +1,41 @@
import { effect, release } from './reactivity'
export function entangle({ get: outerGet, set: outerSet }, { get: innerGet, set: innerSet }) {
let firstRun = true
let outerHash
let innerHash
let reference = effect(() => {
let outer = outerGet()
let inner = innerGet()
if (firstRun) {
innerSet(cloneIfObject(outer))
firstRun = false
} else {
let outerHashLatest = JSON.stringify(outer)
let innerHashLatest = JSON.stringify(inner)
if (outerHashLatest !== outerHash) { // If outer changed...
innerSet(cloneIfObject(outer))
} else if (outerHashLatest !== innerHashLatest) { // If inner changed...
outerSet(cloneIfObject(inner))
} else { // If nothing changed...
// Prevent an infinite loop...
}
}
outerHash = JSON.stringify(outerGet())
innerHash = JSON.stringify(innerGet())
})
return () => {
release(reference)
}
}
function cloneIfObject(value) {
return typeof value === 'object'
? JSON.parse(JSON.stringify(value))
: value
}

View File

@@ -0,0 +1,155 @@
import { closestDataStack, mergeProxies } from './scope'
import { injectMagics } from './magics'
import { tryCatch, handleError } from './utils/error'
import { onAttributeRemoved } from './mutation'
let shouldAutoEvaluateFunctions = true
export function dontAutoEvaluateFunctions(callback) {
let cache = shouldAutoEvaluateFunctions
shouldAutoEvaluateFunctions = false
let result = callback()
shouldAutoEvaluateFunctions = cache
return result
}
export function evaluate(el, expression, extras = {}) {
let result
evaluateLater(el, expression)(value => result = value, extras)
return result
}
export function evaluateLater(...args) {
return theEvaluatorFunction(...args)
}
let theEvaluatorFunction = normalEvaluator
export function setEvaluator(newEvaluator) {
theEvaluatorFunction = newEvaluator
}
export function normalEvaluator(el, expression) {
let overriddenMagics = {}
let cleanup = injectMagics(overriddenMagics, el).cleanup
// MemLeak1: Issue #2140 (note: there are other uses of injectMagics)
onAttributeRemoved(el, "evaluator", cleanup)
let dataStack = [overriddenMagics, ...closestDataStack(el)]
let evaluator = (typeof expression === 'function')
? generateEvaluatorFromFunction(dataStack, expression)
: generateEvaluatorFromString(dataStack, expression, el)
return tryCatch.bind(null, el, expression, evaluator)
}
export function generateEvaluatorFromFunction(dataStack, func) {
return (receiver = () => {}, { scope = {}, params = [] } = {}) => {
let result = func.apply(mergeProxies([scope, ...dataStack]), params)
runIfTypeOfFunction(receiver, result)
}
}
let evaluatorMemo = {}
function generateFunctionFromString(expression, el) {
if (evaluatorMemo[expression]) {
return evaluatorMemo[expression]
}
let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
// Some expressions that are useful in Alpine are not valid as the right side of an expression.
// Here we'll detect if the expression isn't valid for an assignment and wrap it in a self-
// calling function so that we don't throw an error AND a "return" statement can b e used.
let rightSideSafeExpression = 0
// Support expressions starting with "if" statements like: "if (...) doSomething()"
|| /^[\n\s]*if.*\(.*\)/.test(expression.trim())
// Support expressions starting with "let/const" like: "let foo = 'bar'"
|| /^(let|const)\s/.test(expression.trim())
? `(async()=>{ ${expression} })()`
: expression
const safeAsyncFunction = () => {
try {
let func = new AsyncFunction(
["__self", "scope"],
`with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`
)
Object.defineProperty(func, "name", {
value: `[Alpine] ${expression}`,
})
return func
} catch ( error ) {
handleError( error, el, expression )
return Promise.resolve()
}
}
let func = safeAsyncFunction()
evaluatorMemo[expression] = func
return func
}
function generateEvaluatorFromString(dataStack, expression, el) {
let func = generateFunctionFromString(expression, el)
return (receiver = () => {}, { scope = {}, params = [] } = {}) => {
func.result = undefined
func.finished = false
// Run the function.
let completeScope = mergeProxies([ scope, ...dataStack ])
if (typeof func === 'function' ) {
let promise = func(func, completeScope).catch((error) => handleError(error, el, expression))
// Check if the function ran synchronously,
if (func.finished) {
// Return the immediate result.
runIfTypeOfFunction(receiver, func.result, completeScope, params, el)
// Once the function has run, we clear func.result so we don't create
// memory leaks. func is stored in the evaluatorMemo and every time
// it runs, it assigns the evaluated expression to result which could
// potentially store a reference to the DOM element that will be removed later on.
func.result = undefined
} else {
// If not, return the result when the promise resolves.
promise.then(result => {
runIfTypeOfFunction(receiver, result, completeScope, params, el)
}).catch( error => handleError( error, el, expression ) )
.finally( () => func.result = undefined )
}
}
}
}
export function runIfTypeOfFunction(receiver, value, scope, params, el) {
if (shouldAutoEvaluateFunctions && typeof value === 'function') {
let result = value.apply(scope, params)
if (result instanceof Promise) {
result.then(i => runIfTypeOfFunction(receiver, i, scope, params)).catch( error => handleError( error, el, value ) )
} else {
receiver(result)
}
} else if (typeof value === 'object' && value instanceof Promise) {
value.then(i => receiver(i))
} else {
receiver(value)
}
}

View File

@@ -0,0 +1,20 @@
import { findClosest } from './lifecycle'
let globalIdMemo = {}
export function findAndIncrementId(name) {
if (! globalIdMemo[name]) globalIdMemo[name] = 0
return ++globalIdMemo[name]
}
export function closestIdRoot(el, name) {
return findClosest(el, element => {
if (element._x_ids && element._x_ids[name]) return true
})
}
export function setIdRoot(el, name) {
if (! el._x_ids) el._x_ids = {}
if (! el._x_ids[name]) el._x_ids[name] = findAndIncrementId(name)
}

View File

@@ -0,0 +1,74 @@
/**
* _
* /\ | | (_) (_)
* / \ | |_ __ _ _ __ ___ _ ___
* / /\ \ | | '_ \| | '_ \ / _ \ | / __|
* / ____ \| | |_) | | | | | __/_| \__ \
* /_/ \_\_| .__/|_|_| |_|\___(_) |___/
* | | _/ |
* |_| |__/
*
* Let's build Alpine together. It's easier than you think.
* For starters, we'll import Alpine's core. This is the
* object that will expose all of Alpine's public API.
*/
import Alpine from './alpine'
/**
* _______________________________________________________
* The Evaluator
* -------------------------------------------------------
*
* Now we're ready to bootstrap Alpine's evaluation system.
* It's the function that converts raw JavaScript string
* expressions like @click="toggle()", into actual JS.
*/
import { normalEvaluator } from './evaluator'
Alpine.setEvaluator(normalEvaluator)
/**
* _______________________________________________________
* The Reactivity Engine
* -------------------------------------------------------
*
* This is the reactivity core of Alpine. It's the part of
* Alpine that triggers an element with x-text="message"
* to update its inner text when "message" is changed.
*/
import { reactive, effect, stop, toRaw } from '@vue/reactivity'
Alpine.setReactivityEngine({ reactive, effect, release: stop, raw: toRaw })
/**
* _______________________________________________________
* The Magics
* -------------------------------------------------------
*
* Yeah, we're calling them magics here like they're nouns.
* These are the properties that are magically available
* to all the Alpine expressions, within your web app.
*/
import './magics/index'
/**
* _______________________________________________________
* The Directives
* -------------------------------------------------------
*
* Now that the core is all set up, we can register Alpine
* directives like x-text or x-on that form the basis of
* how Alpine adds behavior to an app's static markup.
*/
import './directives/index'
/**
* _______________________________________________________
* The Alpine Global
* -------------------------------------------------------
*
* Now that we have set everything up internally, anything
* Alpine-related that will need to be accessed on-going
* will be made available through the "Alpine" global.
*/
export default Alpine

View File

@@ -0,0 +1,78 @@
// Warning: The concept of "interceptors" in Alpine is not public API and is subject to change
// without tagging a major release.
export function initInterceptors(data) {
let isObject = val => typeof val === 'object' && !Array.isArray(val) && val !== null
let recurse = (obj, basePath = '') => {
Object.entries(Object.getOwnPropertyDescriptors(obj)).forEach(([key, { value, enumerable }]) => {
// Skip getters.
if (enumerable === false || value === undefined) return
if (typeof value === 'object' && value !== null && value.__v_skip) return
let path = basePath === '' ? key : `${basePath}.${key}`
if (typeof value === 'object' && value !== null && value._x_interceptor) {
obj[key] = value.initialize(data, path, key)
} else {
if (isObject(value) && value !== obj && ! (value instanceof Element)) {
recurse(value, path)
}
}
})
}
return recurse(data)
}
export function interceptor(callback, mutateObj = () => {}) {
let obj = {
initialValue: undefined,
_x_interceptor: true,
initialize(data, path, key) {
return callback(this.initialValue, () => get(data, path), (value) => set(data, path, value), path, key)
}
}
mutateObj(obj)
return initialValue => {
if (typeof initialValue === 'object' && initialValue !== null && initialValue._x_interceptor) {
// Support nesting interceptors.
let initialize = obj.initialize.bind(obj)
obj.initialize = (data, path, key) => {
let innerValue = initialValue.initialize(data, path, key)
obj.initialValue = innerValue
return initialize(data, path, key)
}
} else {
obj.initialValue = initialValue
}
return obj
}
}
function get(obj, path) {
return path.split('.').reduce((carry, segment) => carry[segment], obj)
}
function set(obj, path, value) {
if (typeof path === 'string') path = path.split('.')
if (path.length === 1) obj[path[0]] = value;
else if (path.length === 0) throw error;
else {
if (obj[path[0]])
return set(obj[path[0]], path.slice(1), value);
else {
obj[path[0]] = {};
return set(obj[path[0]], path.slice(1), value);
}
}
}

View File

@@ -0,0 +1,100 @@
import { startObservingMutations, onAttributesAdded, onElAdded, onElRemoved, cleanupAttributes, cleanupElement } from "./mutation"
import { deferHandlingDirectives, directives } from "./directives"
import { dispatch } from './utils/dispatch'
import { walk } from "./utils/walk"
import { warn } from './utils/warn'
let started = false
export function start() {
if (started) warn('Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.')
started = true
if (! document.body) warn('Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine\'s `<script>` tag?')
dispatch(document, 'alpine:init')
dispatch(document, 'alpine:initializing')
startObservingMutations()
onElAdded(el => initTree(el, walk))
onElRemoved(el => destroyTree(el))
onAttributesAdded((el, attrs) => {
directives(el, attrs).forEach(handle => handle())
})
let outNestedComponents = el => ! closestRoot(el.parentElement, true)
Array.from(document.querySelectorAll(allSelectors().join(',')))
.filter(outNestedComponents)
.forEach(el => {
initTree(el)
})
dispatch(document, 'alpine:initialized')
}
let rootSelectorCallbacks = []
let initSelectorCallbacks = []
export function rootSelectors() {
return rootSelectorCallbacks.map(fn => fn())
}
export function allSelectors() {
return rootSelectorCallbacks.concat(initSelectorCallbacks).map(fn => fn())
}
export function addRootSelector(selectorCallback) { rootSelectorCallbacks.push(selectorCallback) }
export function addInitSelector(selectorCallback) { initSelectorCallbacks.push(selectorCallback) }
export function closestRoot(el, includeInitSelectors = false) {
return findClosest(el, element => {
const selectors = includeInitSelectors ? allSelectors() : rootSelectors()
if (selectors.some(selector => element.matches(selector))) return true
})
}
export function findClosest(el, callback) {
if (! el) return
if (callback(el)) return el
// Support crawling up teleports.
if (el._x_teleportBack) el = el._x_teleportBack
if (! el.parentElement) return
return findClosest(el.parentElement, callback)
}
export function isRoot(el) {
return rootSelectors().some(selector => el.matches(selector))
}
let initInterceptors = []
export function interceptInit(callback) { initInterceptors.push(callback) }
export function initTree(el, walker = walk, intercept = () => {}) {
deferHandlingDirectives(() => {
walker(el, (el, skip) => {
intercept(el, skip)
initInterceptors.forEach(i => i(el, skip))
directives(el, el.attributes).forEach(handle => handle())
el._x_ignore && skip()
})
})
}
export function destroyTree(root, walker = walk) {
walker(root, el => {
cleanupAttributes(el)
cleanupElement(el)
})
}

View File

@@ -0,0 +1,42 @@
import { getElementBoundUtilities } from './directives'
import { interceptor } from './interceptor'
import { onElRemoved } from './mutation'
let magics = {}
export function magic(name, callback) {
magics[name] = callback
}
export function injectMagics(obj, el) {
Object.entries(magics).forEach(([name, callback]) => {
let memoizedUtilities = null;
function getUtilities() {
if (memoizedUtilities) {
return memoizedUtilities;
} else {
let [utilities, cleanup] = getElementBoundUtilities(el)
memoizedUtilities = {interceptor, ...utilities}
onElRemoved(el, cleanup)
return memoizedUtilities;
}
}
Object.defineProperty(obj, `$${name}`, {
get() {
return callback(el, getUtilities());
},
enumerable: false,
})
})
return {
obj,
cleanup: () => {
// MemLeak1: Issue #2140
el = null;
}
};
}

View File

@@ -0,0 +1,4 @@
import { scope } from '../scope'
import { magic } from '../magics'
magic('data', el => scope(el))

View File

@@ -0,0 +1,4 @@
import { dispatch } from '../utils/dispatch'
import { magic } from '../magics'
magic('dispatch', el => dispatch.bind(dispatch, el))

View File

@@ -0,0 +1,3 @@
import { magic } from "../magics";
magic('el', el => el)

View File

@@ -0,0 +1,46 @@
import { magic } from '../magics'
import { closestIdRoot, findAndIncrementId } from '../ids'
import { interceptClone } from '../clone'
magic('id', (el, { cleanup }) => (name, key = null) => {
let cacheKey = `${name}${key ? `-${key}` : ''}`
return cacheIdByNameOnElement(el, cacheKey, cleanup, () => {
let root = closestIdRoot(el, name)
let id = root
? root._x_ids[name]
: findAndIncrementId(name)
return key
? `${name}-${id}-${key}`
: `${name}-${id}`
})
})
interceptClone((from, to) => {
// Transfer over existing ID registrations from
// the existing dom tree over to the new one
// so that there aren't ID mismatches...
if (from._x_id) {
to._x_id = from._x_id
}
})
function cacheIdByNameOnElement(el, cacheKey, cleanup, callback)
{
if (! el._x_id) el._x_id = {}
// We only want $id to run once per an element's lifecycle...
if (el._x_id[cacheKey]) return el._x_id[cacheKey]
let output = callback()
el._x_id[cacheKey] = output
cleanup(() => {
delete el._x_id[cacheKey]
})
return output
}

View File

@@ -0,0 +1,4 @@
import { nextTick } from '../nextTick'
import { magic } from '../magics'
magic('nextTick', () => nextTick)

View File

@@ -0,0 +1,21 @@
import { closestRoot, findClosest } from '../lifecycle'
import { mergeProxies } from '../scope'
import { magic } from '../magics'
magic('refs', el => {
if (el._x_refs_proxy) return el._x_refs_proxy
el._x_refs_proxy = mergeProxies(getArrayOfRefObject(el))
return el._x_refs_proxy
})
function getArrayOfRefObject(el) {
let refObjects = []
findClosest(el, (i) => {
if (i._x_refs) refObjects.push(i._x_refs)
})
return refObjects
}

View File

@@ -0,0 +1,4 @@
import { closestRoot } from "../lifecycle";
import { magic } from "../magics";
magic('root', el => closestRoot(el))

View File

@@ -0,0 +1,4 @@
import { getStores } from '../store'
import { magic } from '../magics'
magic('store', getStores)

View File

@@ -0,0 +1,18 @@
import { magic } from '../magics'
import { watch } from '../reactivity'
magic('watch', (el, { evaluateLater, cleanup }) => (key, callback) => {
let evaluate = evaluateLater(key)
let getter = () => {
let value
evaluate(i => value = i)
return value
}
let unwatch = watch(getter, callback)
cleanup(unwatch)
})

View File

@@ -0,0 +1,20 @@
import { warn } from '../utils/warn'
import { magic } from '../magics'
import './$nextTick'
import './$dispatch'
import './$watch'
import './$store'
import './$data'
import './$root'
import './$refs'
import './$id'
import './$el'
// Register warnings for people using plugin syntaxes and not loading the plugin itself:
warnMissingPluginMagic('Focus', 'focus', 'focus')
warnMissingPluginMagic('Persist', 'persist', 'persist')
function warnMissingPluginMagic(name, magicName, slug) {
magic(magicName, (el) => warn(`You can't use [$${magicName}] without first installing the "${name}" plugin here: https://alpinejs.dev/plugins/${slug}`, el))
}

View File

@@ -0,0 +1,217 @@
import { destroyTree } from "./lifecycle"
let onAttributeAddeds = []
let onElRemoveds = []
let onElAddeds = []
export function onElAdded(callback) {
onElAddeds.push(callback)
}
export function onElRemoved(el, callback) {
if (typeof callback === 'function') {
if (! el._x_cleanups) el._x_cleanups = []
el._x_cleanups.push(callback)
} else {
callback = el
onElRemoveds.push(callback)
}
}
export function onAttributesAdded(callback) {
onAttributeAddeds.push(callback)
}
export function onAttributeRemoved(el, name, callback) {
if (! el._x_attributeCleanups) el._x_attributeCleanups = {}
if (! el._x_attributeCleanups[name]) el._x_attributeCleanups[name] = []
el._x_attributeCleanups[name].push(callback)
}
export function cleanupAttributes(el, names) {
if (! el._x_attributeCleanups) return
Object.entries(el._x_attributeCleanups).forEach(([name, value]) => {
if (names === undefined || names.includes(name)) {
value.forEach(i => i())
delete el._x_attributeCleanups[name]
}
})
}
export function cleanupElement(el) {
if (el._x_cleanups) {
while (el._x_cleanups.length) el._x_cleanups.pop()()
}
}
let observer = new MutationObserver(onMutate)
let currentlyObserving = false
export function startObservingMutations() {
observer.observe(document, { subtree: true, childList: true, attributes: true, attributeOldValue: true })
currentlyObserving = true
}
export function stopObservingMutations() {
flushObserver()
observer.disconnect()
currentlyObserving = false
}
let queuedMutations = []
export function flushObserver() {
let records = observer.takeRecords()
queuedMutations.push(() => records.length > 0 && onMutate(records))
let queueLengthWhenTriggered = queuedMutations.length
queueMicrotask(() => {
// If these two lengths match, then we KNOW that this is the LAST
// flush in the current event loop. This way, we can process
// all mutations in one batch at the end of everything...
if (queuedMutations.length === queueLengthWhenTriggered) {
// Now Alpine can process all the mutations...
while (queuedMutations.length > 0) queuedMutations.shift()()
}
})
}
export function mutateDom(callback) {
if (! currentlyObserving) return callback()
stopObservingMutations()
let result = callback()
startObservingMutations()
return result
}
let isCollecting = false
let deferredMutations = []
export function deferMutations() {
isCollecting = true
}
export function flushAndStopDeferringMutations() {
isCollecting = false
onMutate(deferredMutations)
deferredMutations = []
}
function onMutate(mutations) {
if (isCollecting) {
deferredMutations = deferredMutations.concat(mutations)
return
}
let addedNodes = new Set
let removedNodes = new Set
let addedAttributes = new Map
let removedAttributes = new Map
for (let i = 0; i < mutations.length; i++) {
if (mutations[i].target._x_ignoreMutationObserver) continue
if (mutations[i].type === 'childList') {
mutations[i].addedNodes.forEach(node => node.nodeType === 1 && addedNodes.add(node))
mutations[i].removedNodes.forEach(node => node.nodeType === 1 && removedNodes.add(node))
}
if (mutations[i].type === 'attributes') {
let el = mutations[i].target
let name = mutations[i].attributeName
let oldValue = mutations[i].oldValue
let add = () => {
if (! addedAttributes.has(el)) addedAttributes.set(el, [])
addedAttributes.get(el).push({ name, value: el.getAttribute(name) })
}
let remove = () => {
if (! removedAttributes.has(el)) removedAttributes.set(el, [])
removedAttributes.get(el).push(name)
}
// New attribute.
if (el.hasAttribute(name) && oldValue === null) {
add()
// Changed attribute.
} else if (el.hasAttribute(name)) {
remove()
add()
// Removed attribute.
} else {
remove()
}
}
}
removedAttributes.forEach((attrs, el) => {
cleanupAttributes(el, attrs)
})
addedAttributes.forEach((attrs, el) => {
onAttributeAddeds.forEach(i => i(el, attrs))
})
for (let node of removedNodes) {
// If an element gets moved on a page, it's registered
// as both an "add" and "remove", so we want to skip those.
if (addedNodes.has(node)) continue
onElRemoveds.forEach(i => i(node))
destroyTree(node)
}
// Mutations are bundled together by the browser but sometimes
// for complex cases, there may be javascript code adding a wrapper
// and then an alpine component as a child of that wrapper in the same
// function and the mutation observer will receive 2 different mutations.
// when it comes time to run them, the dom contains both changes so the child
// element would be processed twice as Alpine calls initTree on
// both mutations. We mark all nodes as _x_ignored and only remove the flag
// when processing the node to avoid those duplicates.
addedNodes.forEach((node) => {
node._x_ignoreSelf = true
node._x_ignore = true
})
for (let node of addedNodes) {
// If the node was eventually removed as part of one of his
// parent mutations, skip it
if (removedNodes.has(node)) continue
if (! node.isConnected) continue
delete node._x_ignoreSelf
delete node._x_ignore
onElAddeds.forEach(i => i(node))
node._x_ignore = true
node._x_ignoreSelf = true
}
addedNodes.forEach((node) => {
delete node._x_ignoreSelf
delete node._x_ignore
})
addedNodes = null
removedNodes = null
addedAttributes = null
removedAttributes = null
}

View File

@@ -0,0 +1,29 @@
let tickStack = []
let isHolding = false
export function nextTick(callback = () => {}) {
queueMicrotask(() => {
isHolding || setTimeout(() => {
releaseNextTicks()
})
})
return new Promise((res) => {
tickStack.push(() => {
callback();
res();
});
})
}
export function releaseNextTicks() {
isHolding = false
while (tickStack.length) tickStack.shift()()
}
export function holdNextTicks() {
isHolding = true
}

View File

@@ -0,0 +1,7 @@
import Alpine from "./alpine";
export function plugin(callback) {
let callbacks = Array.isArray(callback) ? callback : [callback]
callbacks.forEach(i => i(Alpine))
}

View File

@@ -0,0 +1,93 @@
import { scheduler } from './scheduler'
let reactive, effect, release, raw
let shouldSchedule = true
export function disableEffectScheduling(callback) {
shouldSchedule = false
callback()
shouldSchedule = true
}
export function setReactivityEngine(engine) {
reactive = engine.reactive
release = engine.release
effect = (callback) => engine.effect(callback, { scheduler: task => {
if (shouldSchedule) {
scheduler(task)
} else {
task()
}
} })
raw = engine.raw
}
export function overrideEffect(override) { effect = override }
export function elementBoundEffect(el) {
let cleanup = () => {}
let wrappedEffect = (callback) => {
let effectReference = effect(callback)
if (! el._x_effects) {
el._x_effects = new Set
// Livewire depends on el._x_runEffects.
el._x_runEffects = () => { el._x_effects.forEach(i => i()) }
}
el._x_effects.add(effectReference)
cleanup = () => {
if (effectReference === undefined) return
el._x_effects.delete(effectReference)
release(effectReference)
}
return effectReference
}
return [wrappedEffect, () => { cleanup() }]
}
export function watch(getter, callback) {
let firstTime = true
let oldValue
let effectReference = effect(() => {
let value = getter()
// JSON.stringify touches every single property at any level enabling deep watching
JSON.stringify(value)
if (! firstTime) {
// We have to queue this watcher as a microtask so that
// the watcher doesn't pick up its own dependencies.
queueMicrotask(() => {
callback(value, oldValue)
oldValue = value
})
} else {
oldValue = value
}
firstTime = false
})
return () => release(effectReference)
}
export {
release,
reactive,
effect,
raw,
}

View File

@@ -0,0 +1,41 @@
let flushPending = false
let flushing = false
let queue = []
let lastFlushedIndex = -1
export function scheduler (callback) { queueJob(callback) }
function queueJob(job) {
if (! queue.includes(job)) queue.push(job)
queueFlush()
}
export function dequeueJob(job) {
let index = queue.indexOf(job)
if (index !== -1 && index > lastFlushedIndex) queue.splice(index, 1)
}
function queueFlush() {
if (! flushing && ! flushPending) {
flushPending = true
queueMicrotask(flushJobs)
}
}
export function flushJobs() {
flushPending = false
flushing = true
for (let i = 0; i < queue.length; i++) {
queue[i]()
lastFlushedIndex = i
}
queue.length = 0
lastFlushedIndex = -1
flushing = false
}

View File

@@ -0,0 +1,88 @@
export function scope(node) {
return mergeProxies(closestDataStack(node))
}
export function addScopeToNode(node, data, referenceNode) {
node._x_dataStack = [data, ...closestDataStack(referenceNode || node)]
return () => {
node._x_dataStack = node._x_dataStack.filter(i => i !== data)
}
}
export function hasScope(node) {
return !! node._x_dataStack
}
export function closestDataStack(node) {
if (node._x_dataStack) return node._x_dataStack
if (typeof ShadowRoot === 'function' && node instanceof ShadowRoot) {
return closestDataStack(node.host)
}
if (! node.parentNode) {
return []
}
return closestDataStack(node.parentNode)
}
export function closestDataProxy(el) {
return mergeProxies(closestDataStack(el))
}
export function mergeProxies (objects) {
return new Proxy({ objects }, mergeProxyTrap);
}
let mergeProxyTrap = {
ownKeys({ objects }) {
return Array.from(
new Set(objects.flatMap((i) => Object.keys(i)))
)
},
has({ objects }, name) {
if (name == Symbol.unscopables) return false;
return objects.some((obj) =>
Object.prototype.hasOwnProperty.call(obj, name) ||
Reflect.has(obj, name)
);
},
get({ objects }, name, thisProxy) {
if (name == "toJSON") return collapseProxies
return Reflect.get(
objects.find((obj) =>
Reflect.has(obj, name)
) || {},
name,
thisProxy
)
},
set({ objects }, name, value, thisProxy) {
const target =
objects.find((obj) =>
Object.prototype.hasOwnProperty.call(obj, name)
) || objects[objects.length - 1];
const descriptor = Object.getOwnPropertyDescriptor(target, name);
if (descriptor?.set && descriptor?.get)
return Reflect.set(target, name, value, thisProxy);
return Reflect.set(target, name, value);
},
}
function collapseProxies() {
let keys = Reflect.ownKeys(this)
return keys.reduce((acc, key) => {
acc[key] = Reflect.get(this, key)
return acc;
}, {})
}

View File

@@ -0,0 +1,23 @@
import { initInterceptors } from "./interceptor";
import { reactive } from "./reactivity"
let stores = {}
let isReactive = false
export function store(name, value) {
if (! isReactive) { stores = reactive(stores); isReactive = true; }
if (value === undefined) {
return stores[name]
}
stores[name] = value
if (typeof value === 'object' && value !== null && value.hasOwnProperty('init') && typeof value.init === 'function') {
stores[name].init()
}
initInterceptors(stores[name])
}
export function getStores() { return stores }

View File

@@ -0,0 +1,206 @@
import { dontAutoEvaluateFunctions, evaluate } from '../evaluator'
import { reactive } from '../reactivity'
import { setClasses } from './classes'
import { setStyles } from './styles'
export default function bind(el, name, value, modifiers = []) {
// Register bound data as pure observable data for other APIs to use.
if (! el._x_bindings) el._x_bindings = reactive({})
el._x_bindings[name] = value
name = modifiers.includes('camel') ? camelCase(name) : name
switch (name) {
case 'value':
bindInputValue(el, value)
break;
case 'style':
bindStyles(el, value)
break;
case 'class':
bindClasses(el, value)
break;
// 'selected' and 'checked' are special attributes that aren't necessarily
// synced with their corresponding properties when updated, so both the
// attribute and property need to be updated when bound.
case 'selected':
case 'checked':
bindAttributeAndProperty(el, name, value)
break;
default:
bindAttribute(el, name, value)
break;
}
}
function bindInputValue(el, value) {
if (el.type === 'radio') {
// Set radio value from x-bind:value, if no "value" attribute exists.
// If there are any initial state values, radio will have a correct
// "checked" value since x-bind:value is processed before x-model.
if (el.attributes.value === undefined) {
el.value = value
}
// @todo: yuck
if (window.fromModel) {
if (typeof value === 'boolean') {
el.checked = safeParseBoolean(el.value) === value
} else {
el.checked = checkedAttrLooseCompare(el.value, value)
}
}
} else if (el.type === 'checkbox') {
// If we are explicitly binding a string to the :value, set the string,
// If the value is a boolean/array/number/null/undefined, leave it alone, it will be set to "on"
// automatically.
if (Number.isInteger(value)) {
el.value = value
} else if (! Array.isArray(value) && typeof value !== 'boolean' && ! [null, undefined].includes(value)) {
el.value = String(value)
} else {
if (Array.isArray(value)) {
el.checked = value.some(val => checkedAttrLooseCompare(val, el.value))
} else {
el.checked = !!value
}
}
} else if (el.tagName === 'SELECT') {
updateSelect(el, value)
} else {
if (el.value === value) return
el.value = value === undefined ? '' : value
}
}
function bindClasses(el, value) {
if (el._x_undoAddedClasses) el._x_undoAddedClasses()
el._x_undoAddedClasses = setClasses(el, value)
}
function bindStyles(el, value) {
if (el._x_undoAddedStyles) el._x_undoAddedStyles()
el._x_undoAddedStyles = setStyles(el, value)
}
function bindAttributeAndProperty(el, name, value) {
bindAttribute(el, name, value)
setPropertyIfChanged(el, name, value)
}
function bindAttribute(el, name, value) {
if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {
el.removeAttribute(name)
} else {
if (isBooleanAttr(name)) value = name
setIfChanged(el, name, value)
}
}
function setIfChanged(el, attrName, value) {
if (el.getAttribute(attrName) != value) {
el.setAttribute(attrName, value)
}
}
function setPropertyIfChanged(el, propName, value) {
if (el[propName] !== value) {
el[propName] = value
}
}
function updateSelect(el, value) {
const arrayWrappedValue = [].concat(value).map(value => { return value + '' })
Array.from(el.options).forEach(option => {
option.selected = arrayWrappedValue.includes(option.value)
})
}
function camelCase(subject) {
return subject.toLowerCase().replace(/-(\w)/g, (match, char) => char.toUpperCase())
}
function checkedAttrLooseCompare(valueA, valueB) {
return valueA == valueB
}
export function safeParseBoolean(rawValue) {
if ([1, '1', 'true', 'on', 'yes', true].includes(rawValue)) {
return true
}
if ([0, '0', 'false', 'off', 'no', false].includes(rawValue)) {
return false
}
return rawValue ? Boolean(rawValue) : null
}
function isBooleanAttr(attrName) {
// As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
// Array roughly ordered by estimated usage
const booleanAttributes = [
'disabled','checked','required','readonly','open', 'selected',
'autofocus', 'itemscope', 'multiple', 'novalidate','allowfullscreen',
'allowpaymentrequest', 'formnovalidate', 'autoplay', 'controls', 'loop',
'muted', 'playsinline', 'default', 'ismap', 'reversed', 'async', 'defer',
'nomodule'
]
return booleanAttributes.includes(attrName)
}
function attributeShouldntBePreservedIfFalsy(name) {
return ! ['aria-pressed', 'aria-checked', 'aria-expanded', 'aria-selected'].includes(name)
}
export function getBinding(el, name, fallback) {
// First let's get it out of Alpine bound data.
if (el._x_bindings && el._x_bindings[name] !== undefined) return el._x_bindings[name]
return getAttributeBinding(el, name, fallback)
}
export function extractProp(el, name, fallback, extract = true) {
// First let's get it out of Alpine bound data.
if (el._x_bindings && el._x_bindings[name] !== undefined) return el._x_bindings[name]
if (el._x_inlineBindings && el._x_inlineBindings[name] !== undefined) {
let binding = el._x_inlineBindings[name]
binding.extract = extract
return dontAutoEvaluateFunctions(() => {
return evaluate(el, binding.expression)
})
}
return getAttributeBinding(el, name, fallback)
}
function getAttributeBinding(el, name, fallback) {
// If not, we'll return the literal attribute.
let attr = el.getAttribute(name)
// Nothing bound:
if (attr === null) return typeof fallback === 'function' ? fallback() : fallback
// The case of a custom attribute with no value. Ex: <div manual>
if (attr === '') return true
if (isBooleanAttr(name)) {
return !! [name, 'true'].includes(attr)
}
return attr
}

View File

@@ -0,0 +1,58 @@
export function setClasses(el, value) {
if (Array.isArray(value)) {
return setClassesFromString(el, value.join(' '))
} else if (typeof value === 'object' && value !== null) {
return setClassesFromObject(el, value)
} else if (typeof value === 'function') {
return setClasses(el, value())
}
return setClassesFromString(el, value)
}
function setClassesFromString(el, classString) {
let split = classString => classString.split(' ').filter(Boolean)
let missingClasses = classString => classString.split(' ').filter(i => ! el.classList.contains(i)).filter(Boolean)
let addClassesAndReturnUndo = classes => {
el.classList.add(...classes)
return () => { el.classList.remove(...classes) }
}
// This is to allow short-circuit expressions like: :class="show || 'hidden'" && "show && 'block'"
classString = (classString === true) ? classString = '' : (classString || '')
return addClassesAndReturnUndo(missingClasses(classString))
}
function setClassesFromObject(el, classObject) {
let split = classString => classString.split(' ').filter(Boolean)
let forAdd = Object.entries(classObject).flatMap(([classString, bool]) => bool ? split(classString) : false).filter(Boolean)
let forRemove = Object.entries(classObject).flatMap(([classString, bool]) => ! bool ? split(classString) : false).filter(Boolean)
let added = []
let removed = []
forRemove.forEach(i => {
if (el.classList.contains(i)) {
el.classList.remove(i)
removed.push(i)
}
})
forAdd.forEach(i => {
if (! el.classList.contains(i)) {
el.classList.add(i)
added.push(i)
}
})
return () => {
removed.forEach(i => el.classList.add(i))
added.forEach(i => el.classList.remove(i))
}
}

View File

@@ -0,0 +1,18 @@
export function debounce(func, wait) {
var timeout
return function() {
var context = this, args = arguments
var later = function () {
timeout = null
func.apply(context, args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}

View File

@@ -0,0 +1,12 @@
export function dispatch(el, name, detail = {}) {
el.dispatchEvent(
new CustomEvent(name, {
detail,
bubbles: true,
// Allows events to pass the shadow DOM barrier.
composed: true,
cancelable: true,
})
)
}

View File

@@ -0,0 +1,17 @@
export function tryCatch(el, expression, callback, ...args) {
try {
return callback(...args)
} catch (e) {
handleError( e, el, expression )
}
}
export function handleError(error , el, expression = undefined) {
error = Object.assign(
error ?? { message: 'No error message given.' },
{ el, expression } )
console.warn(`Alpine Expression Error: ${error.message}\n\n${ expression ? 'Expression: \"' + expression + '\"\n\n' : '' }`, el)
setTimeout( () => { throw error }, 0 )
}

View File

@@ -0,0 +1,181 @@
import { debounce } from './debounce'
import { throttle } from './throttle'
export default function on (el, event, modifiers, callback) {
let listenerTarget = el
let handler = e => callback(e)
let options = {}
// This little helper allows us to add functionality to the listener's
// handler more flexibly in a "middleware" style.
let wrapHandler = (callback, wrapper) => (e) => wrapper(callback, e)
if (modifiers.includes("dot")) event = dotSyntax(event)
if (modifiers.includes('camel')) event = camelCase(event)
if (modifiers.includes('passive')) options.passive = true
if (modifiers.includes('capture')) options.capture = true
if (modifiers.includes('window')) listenerTarget = window
if (modifiers.includes('document')) listenerTarget = document
// By wrapping the handler with debounce & throttle first, we ensure that the wrapping logic itself is not
// throttled/debounced, only the user's callback is. This way, if the user expects
// `e.preventDefault()` to happen, it'll still happen even if their callback gets throttled.
if (modifiers.includes('debounce')) {
let nextModifier = modifiers[modifiers.indexOf('debounce')+1] || 'invalid-wait'
let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250
handler = debounce(handler, wait)
}
if (modifiers.includes('throttle')) {
let nextModifier = modifiers[modifiers.indexOf('throttle')+1] || 'invalid-wait'
let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250
handler = throttle(handler, wait)
}
if (modifiers.includes('prevent')) handler = wrapHandler(handler, (next, e) => { e.preventDefault(); next(e) })
if (modifiers.includes('stop')) handler = wrapHandler(handler, (next, e) => { e.stopPropagation(); next(e) })
if (modifiers.includes('self')) handler = wrapHandler(handler, (next, e) => { e.target === el && next(e) })
if (modifiers.includes('away') || modifiers.includes('outside')) {
listenerTarget = document
handler = wrapHandler(handler, (next, e) => {
if (el.contains(e.target)) return
if (e.target.isConnected === false) return
if (el.offsetWidth < 1 && el.offsetHeight < 1) return
// Additional check for special implementations like x-collapse
// where the element doesn't have display: none
if (el._x_isShown === false) return
next(e)
})
}
if (modifiers.includes('once')) {
handler = wrapHandler(handler, (next, e) => {
next(e)
listenerTarget.removeEventListener(event, handler, options)
})
}
// Handle :keydown and :keyup listeners.
handler = wrapHandler(handler, (next, e) => {
if (isKeyEvent(event)) {
if (isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers)) {
return
}
}
next(e)
})
listenerTarget.addEventListener(event, handler, options)
return () => {
listenerTarget.removeEventListener(event, handler, options)
}
}
function dotSyntax(subject) {
return subject.replace(/-/g, ".")
}
function camelCase(subject) {
return subject.toLowerCase().replace(/-(\w)/g, (match, char) => char.toUpperCase())
}
function isNumeric(subject){
return ! Array.isArray(subject) && ! isNaN(subject)
}
function kebabCase(subject) {
if ([' ','_'].includes(subject
)) return subject
return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase()
}
function isKeyEvent(event) {
return ['keydown', 'keyup'].includes(event)
}
function isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers) {
let keyModifiers = modifiers.filter(i => {
return ! ['window', 'document', 'prevent', 'stop', 'once', 'capture'].includes(i)
})
if (keyModifiers.includes('debounce')) {
let debounceIndex = keyModifiers.indexOf('debounce')
keyModifiers.splice(debounceIndex, isNumeric((keyModifiers[debounceIndex+1] || 'invalid-wait').split('ms')[0]) ? 2 : 1)
}
if (keyModifiers.includes('throttle')) {
let debounceIndex = keyModifiers.indexOf('throttle')
keyModifiers.splice(debounceIndex, isNumeric((keyModifiers[debounceIndex+1] || 'invalid-wait').split('ms')[0]) ? 2 : 1)
}
// If no modifier is specified, we'll call it a press.
if (keyModifiers.length === 0) return false
// If one is passed, AND it matches the key pressed, we'll call it a press.
if (keyModifiers.length === 1 && keyToModifiers(e.key).includes(keyModifiers[0])) return false
// The user is listening for key combinations.
const systemKeyModifiers = ['ctrl', 'shift', 'alt', 'meta', 'cmd', 'super']
const selectedSystemKeyModifiers = systemKeyModifiers.filter(modifier => keyModifiers.includes(modifier))
keyModifiers = keyModifiers.filter(i => ! selectedSystemKeyModifiers.includes(i))
if (selectedSystemKeyModifiers.length > 0) {
const activelyPressedKeyModifiers = selectedSystemKeyModifiers.filter(modifier => {
// Alias "cmd" and "super" to "meta"
if (modifier === 'cmd' || modifier === 'super') modifier = 'meta'
return e[`${modifier}Key`]
})
// If all the modifiers selected are pressed, ...
if (activelyPressedKeyModifiers.length === selectedSystemKeyModifiers.length) {
// AND the remaining key is pressed as well. It's a press.
if (keyToModifiers(e.key).includes(keyModifiers[0])) return false
}
}
// We'll call it NOT a valid keypress.
return true
}
function keyToModifiers(key) {
if (! key) return []
key = kebabCase(key)
let modifierToKeyMap = {
'ctrl': 'control',
'slash': '/',
'space': ' ',
'spacebar': ' ',
'cmd': 'meta',
'esc': 'escape',
'up': 'arrow-up',
'down': 'arrow-down',
'left': 'arrow-left',
'right': 'arrow-right',
'period': '.',
'equal': '=',
'minus': '-',
'underscore': '_',
}
modifierToKeyMap[key] = key
return Object.keys(modifierToKeyMap).map(modifier => {
if (modifierToKeyMap[modifier] === key) return modifier
}).filter(modifier => modifier)
}

View File

@@ -0,0 +1,14 @@
export function once(callback, fallback = () => {}) {
let called = false
return function () {
if (! called) {
called = true
callback.apply(this, arguments)
} else {
fallback.apply(this, arguments)
}
}
}

View File

@@ -0,0 +1,50 @@
export function setStyles(el, value) {
if (typeof value === 'object' && value !== null) {
return setStylesFromObject(el, value)
}
return setStylesFromString(el, value)
}
function setStylesFromObject(el, value) {
let previousStyles = {}
Object.entries(value).forEach(([key, value]) => {
previousStyles[key] = el.style[key]
// When we use javascript object, css properties use the camelCase
// syntax but when we use setProperty, we need the css format
// so we need to convert camelCase to kebab-case.
// In case key is a CSS variable, leave it as it is.
if (! key.startsWith('--')) {
key = kebabCase(key);
}
el.style.setProperty(key, value)
})
setTimeout(() => {
if (el.style.length === 0) {
el.removeAttribute('style')
}
})
return () => {
setStyles(el, previousStyles)
}
}
function setStylesFromString(el, value) {
let cache = el.getAttribute('style', value)
el.setAttribute('style', value)
return () => {
el.setAttribute('style', cache || '')
}
}
function kebabCase(subject) {
return subject.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
}

View File

@@ -0,0 +1,16 @@
export function throttle(func, limit) {
let inThrottle
return function() {
let context = this, args = arguments
if (! inThrottle) {
func.apply(context, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}

View File

@@ -0,0 +1,21 @@
export function walk(el, callback) {
if (typeof ShadowRoot === 'function' && el instanceof ShadowRoot) {
Array.from(el.children).forEach(el => walk(el, callback))
return
}
let skip = false
callback(el, () => skip = true)
if (skip) return
let node = el.firstElementChild
while (node) {
walk(node, callback, false)
node = node.nextElementSibling
}
}

View File

@@ -0,0 +1,4 @@
export function warn(message, ...args) {
console.warn(`Alpine Warning: ${message}`, ...args)
}

View File

@@ -0,0 +1,5 @@
import anchor from '../src/index.js'
document.addEventListener('alpine:init', () => {
window.Alpine.plugin(anchor)
})

View File

@@ -0,0 +1,5 @@
import anchor from '../src/index.js'
export default anchor
export { anchor }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"name": "@alpinejs/anchor",
"version": "3.13.8",
"description": "Anchor an element's position relative to another",
"homepage": "https://alpinejs.dev/plugins/anchor",
"repository": {
"type": "git",
"url": "https://github.com/alpinejs/alpine.git",
"directory": "packages/anchor"
},
"author": "Caleb Porzio",
"license": "MIT",
"main": "dist/module.cjs.js",
"module": "dist/module.esm.js",
"unpkg": "dist/cdn.min.js",
"dependencies": {}
}

View File

@@ -0,0 +1,77 @@
import { computePosition, autoUpdate, flip, offset, shift } from '@floating-ui/dom'
export default function (Alpine) {
Alpine.magic('anchor', el => {
if (! el._x_anchor) throw 'Alpine: No x-anchor directive found on element using $anchor...'
return el._x_anchor
})
Alpine.interceptClone((from, to) => {
if (from && from._x_anchor && ! to._x_anchor) {
to._x_anchor = from._x_anchor
}
})
Alpine.directive('anchor', Alpine.skipDuringClone((el, { expression, modifiers, value }, { cleanup, evaluate }) => {
let { placement, offsetValue, unstyled } = getOptions(modifiers)
el._x_anchor = Alpine.reactive({ x: 0, y: 0 })
let reference = evaluate(expression)
if (! reference) throw 'Alpine: no element provided to x-anchor...'
let compute = () => {
let previousValue
computePosition(reference, el, {
placement,
middleware: [flip(), shift({padding: 5}), offset(offsetValue)],
}).then(({ x, y }) => {
unstyled || setStyles(el, x, y)
// Only trigger Alpine reactivity when the value actually changes...
if (JSON.stringify({ x, y }) !== previousValue) {
el._x_anchor.x = x
el._x_anchor.y = y
}
previousValue = JSON.stringify({ x, y })
})
}
let release = autoUpdate(reference, el, () => compute())
cleanup(() => release())
},
// When cloning (or "morphing"), we will graft the style and position data from the live tree...
(el, { expression, modifiers, value }, { cleanup, evaluate }) => {
let { placement, offsetValue, unstyled } = getOptions(modifiers)
if (el._x_anchor) {
unstyled || setStyles(el, el._x_anchor.x, el._x_anchor.y)
}
}))
}
function setStyles(el, x, y) {
Object.assign(el.style, {
left: x+'px', top: y+'px', position: 'absolute',
})
}
function getOptions(modifiers) {
let positions = ['top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end']
let placement = positions.find(i => modifiers.includes(i))
let offsetValue = 0
if (modifiers.includes('offset')) {
let idx = modifiers.findIndex(i => i === 'offset')
offsetValue = modifiers[idx + 1] !== undefined ? Number(modifiers[idx + 1]) : offsetValue
}
let unstyled = modifiers.includes('no-style')
return { placement, offsetValue, unstyled }
}

View File

@@ -0,0 +1,5 @@
import collapse from '../src/index.js'
document.addEventListener('alpine:init', () => {
window.Alpine.plugin(collapse)
})

View File

@@ -0,0 +1,5 @@
import collapse from '../src/index.js'
export default collapse
export { collapse }

View File

@@ -0,0 +1,99 @@
(() => {
// packages/collapse/src/index.js
function src_default(Alpine) {
Alpine.directive("collapse", collapse);
collapse.inline = (el, { modifiers }) => {
if (!modifiers.includes("min"))
return;
el._x_doShow = () => {
};
el._x_doHide = () => {
};
};
function collapse(el, { modifiers }) {
let duration = modifierValue(modifiers, "duration", 250) / 1e3;
let floor = modifierValue(modifiers, "min", 0);
let fullyHide = !modifiers.includes("min");
if (!el._x_isShown)
el.style.height = `${floor}px`;
if (!el._x_isShown && fullyHide)
el.hidden = true;
if (!el._x_isShown)
el.style.overflow = "hidden";
let setFunction = (el2, styles) => {
let revertFunction = Alpine.setStyles(el2, styles);
return styles.height ? () => {
} : revertFunction;
};
let transitionStyles = {
transitionProperty: "height",
transitionDuration: `${duration}s`,
transitionTimingFunction: "cubic-bezier(0.4, 0.0, 0.2, 1)"
};
el._x_transition = {
in(before = () => {
}, after = () => {
}) {
if (fullyHide)
el.hidden = false;
if (fullyHide)
el.style.display = null;
let current = el.getBoundingClientRect().height;
el.style.height = "auto";
let full = el.getBoundingClientRect().height;
if (current === full) {
current = floor;
}
Alpine.transition(el, Alpine.setStyles, {
during: transitionStyles,
start: { height: current + "px" },
end: { height: full + "px" }
}, () => el._x_isShown = true, () => {
if (el.getBoundingClientRect().height == full) {
el.style.overflow = null;
}
});
},
out(before = () => {
}, after = () => {
}) {
let full = el.getBoundingClientRect().height;
Alpine.transition(el, setFunction, {
during: transitionStyles,
start: { height: full + "px" },
end: { height: floor + "px" }
}, () => el.style.overflow = "hidden", () => {
el._x_isShown = false;
if (el.style.height == `${floor}px` && fullyHide) {
el.style.display = "none";
el.hidden = true;
}
});
}
};
}
}
function modifierValue(modifiers, key, fallback) {
if (modifiers.indexOf(key) === -1)
return fallback;
const rawValue = modifiers[modifiers.indexOf(key) + 1];
if (!rawValue)
return fallback;
if (key === "duration") {
let match = rawValue.match(/([0-9]+)ms/);
if (match)
return match[1];
}
if (key === "min") {
let match = rawValue.match(/([0-9]+)px/);
if (match)
return match[1];
}
return rawValue;
}
// packages/collapse/builds/cdn.js
document.addEventListener("alpine:init", () => {
window.Alpine.plugin(src_default);
});
})();

View File

@@ -0,0 +1 @@
(()=>{function g(n){n.directive("collapse",e),e.inline=(t,{modifiers:i})=>{i.includes("min")&&(t._x_doShow=()=>{},t._x_doHide=()=>{})};function e(t,{modifiers:i}){let r=l(i,"duration",250)/1e3,u=l(i,"min",0),h=!i.includes("min");t._x_isShown||(t.style.height=`${u}px`),!t._x_isShown&&h&&(t.hidden=!0),t._x_isShown||(t.style.overflow="hidden");let c=(d,s)=>{let o=n.setStyles(d,s);return s.height?()=>{}:o},f={transitionProperty:"height",transitionDuration:`${r}s`,transitionTimingFunction:"cubic-bezier(0.4, 0.0, 0.2, 1)"};t._x_transition={in(d=()=>{},s=()=>{}){h&&(t.hidden=!1),h&&(t.style.display=null);let o=t.getBoundingClientRect().height;t.style.height="auto";let a=t.getBoundingClientRect().height;o===a&&(o=u),n.transition(t,n.setStyles,{during:f,start:{height:o+"px"},end:{height:a+"px"}},()=>t._x_isShown=!0,()=>{t.getBoundingClientRect().height==a&&(t.style.overflow=null)})},out(d=()=>{},s=()=>{}){let o=t.getBoundingClientRect().height;n.transition(t,c,{during:f,start:{height:o+"px"},end:{height:u+"px"}},()=>t.style.overflow="hidden",()=>{t._x_isShown=!1,t.style.height==`${u}px`&&h&&(t.style.display="none",t.hidden=!0)})}}}}function l(n,e,t){if(n.indexOf(e)===-1)return t;let i=n[n.indexOf(e)+1];if(!i)return t;if(e==="duration"){let r=i.match(/([0-9]+)ms/);if(r)return r[1]}if(e==="min"){let r=i.match(/([0-9]+)px/);if(r)return r[1]}return i}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(g)});})();

View File

@@ -0,0 +1,125 @@
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// packages/collapse/builds/module.js
var module_exports = {};
__export(module_exports, {
collapse: () => src_default,
default: () => module_default
});
module.exports = __toCommonJS(module_exports);
// packages/collapse/src/index.js
function src_default(Alpine) {
Alpine.directive("collapse", collapse);
collapse.inline = (el, { modifiers }) => {
if (!modifiers.includes("min"))
return;
el._x_doShow = () => {
};
el._x_doHide = () => {
};
};
function collapse(el, { modifiers }) {
let duration = modifierValue(modifiers, "duration", 250) / 1e3;
let floor = modifierValue(modifiers, "min", 0);
let fullyHide = !modifiers.includes("min");
if (!el._x_isShown)
el.style.height = `${floor}px`;
if (!el._x_isShown && fullyHide)
el.hidden = true;
if (!el._x_isShown)
el.style.overflow = "hidden";
let setFunction = (el2, styles) => {
let revertFunction = Alpine.setStyles(el2, styles);
return styles.height ? () => {
} : revertFunction;
};
let transitionStyles = {
transitionProperty: "height",
transitionDuration: `${duration}s`,
transitionTimingFunction: "cubic-bezier(0.4, 0.0, 0.2, 1)"
};
el._x_transition = {
in(before = () => {
}, after = () => {
}) {
if (fullyHide)
el.hidden = false;
if (fullyHide)
el.style.display = null;
let current = el.getBoundingClientRect().height;
el.style.height = "auto";
let full = el.getBoundingClientRect().height;
if (current === full) {
current = floor;
}
Alpine.transition(el, Alpine.setStyles, {
during: transitionStyles,
start: { height: current + "px" },
end: { height: full + "px" }
}, () => el._x_isShown = true, () => {
if (el.getBoundingClientRect().height == full) {
el.style.overflow = null;
}
});
},
out(before = () => {
}, after = () => {
}) {
let full = el.getBoundingClientRect().height;
Alpine.transition(el, setFunction, {
during: transitionStyles,
start: { height: full + "px" },
end: { height: floor + "px" }
}, () => el.style.overflow = "hidden", () => {
el._x_isShown = false;
if (el.style.height == `${floor}px` && fullyHide) {
el.style.display = "none";
el.hidden = true;
}
});
}
};
}
}
function modifierValue(modifiers, key, fallback) {
if (modifiers.indexOf(key) === -1)
return fallback;
const rawValue = modifiers[modifiers.indexOf(key) + 1];
if (!rawValue)
return fallback;
if (key === "duration") {
let match = rawValue.match(/([0-9]+)ms/);
if (match)
return match[1];
}
if (key === "min") {
let match = rawValue.match(/([0-9]+)px/);
if (match)
return match[1];
}
return rawValue;
}
// packages/collapse/builds/module.js
var module_default = src_default;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
collapse
});

View File

@@ -0,0 +1,99 @@
// packages/collapse/src/index.js
function src_default(Alpine) {
Alpine.directive("collapse", collapse);
collapse.inline = (el, { modifiers }) => {
if (!modifiers.includes("min"))
return;
el._x_doShow = () => {
};
el._x_doHide = () => {
};
};
function collapse(el, { modifiers }) {
let duration = modifierValue(modifiers, "duration", 250) / 1e3;
let floor = modifierValue(modifiers, "min", 0);
let fullyHide = !modifiers.includes("min");
if (!el._x_isShown)
el.style.height = `${floor}px`;
if (!el._x_isShown && fullyHide)
el.hidden = true;
if (!el._x_isShown)
el.style.overflow = "hidden";
let setFunction = (el2, styles) => {
let revertFunction = Alpine.setStyles(el2, styles);
return styles.height ? () => {
} : revertFunction;
};
let transitionStyles = {
transitionProperty: "height",
transitionDuration: `${duration}s`,
transitionTimingFunction: "cubic-bezier(0.4, 0.0, 0.2, 1)"
};
el._x_transition = {
in(before = () => {
}, after = () => {
}) {
if (fullyHide)
el.hidden = false;
if (fullyHide)
el.style.display = null;
let current = el.getBoundingClientRect().height;
el.style.height = "auto";
let full = el.getBoundingClientRect().height;
if (current === full) {
current = floor;
}
Alpine.transition(el, Alpine.setStyles, {
during: transitionStyles,
start: { height: current + "px" },
end: { height: full + "px" }
}, () => el._x_isShown = true, () => {
if (el.getBoundingClientRect().height == full) {
el.style.overflow = null;
}
});
},
out(before = () => {
}, after = () => {
}) {
let full = el.getBoundingClientRect().height;
Alpine.transition(el, setFunction, {
during: transitionStyles,
start: { height: full + "px" },
end: { height: floor + "px" }
}, () => el.style.overflow = "hidden", () => {
el._x_isShown = false;
if (el.style.height == `${floor}px` && fullyHide) {
el.style.display = "none";
el.hidden = true;
}
});
}
};
}
}
function modifierValue(modifiers, key, fallback) {
if (modifiers.indexOf(key) === -1)
return fallback;
const rawValue = modifiers[modifiers.indexOf(key) + 1];
if (!rawValue)
return fallback;
if (key === "duration") {
let match = rawValue.match(/([0-9]+)ms/);
if (match)
return match[1];
}
if (key === "min") {
let match = rawValue.match(/([0-9]+)px/);
if (match)
return match[1];
}
return rawValue;
}
// packages/collapse/builds/module.js
var module_default = src_default;
export {
src_default as collapse,
module_default as default
};

View File

@@ -0,0 +1,17 @@
{
"name": "@alpinejs/collapse",
"version": "3.13.8",
"description": "Collapse and expand elements with robust animations",
"homepage": "https://alpinejs.dev/plugins/collapse",
"repository": {
"type": "git",
"url": "https://github.com/alpinejs/alpine.git",
"directory": "packages/collapse"
},
"author": "Caleb Porzio",
"license": "MIT",
"main": "dist/module.cjs.js",
"module": "dist/module.esm.js",
"unpkg": "dist/cdn.min.js",
"dependencies": {}
}

View File

@@ -0,0 +1,108 @@
export default function (Alpine) {
Alpine.directive('collapse', collapse)
// If we're using a "minimum height", we'll need to disable
// x-show's default behavior of setting display: 'none'.
collapse.inline = (el, { modifiers }) => {
if (! modifiers.includes('min')) return
el._x_doShow = () => {}
el._x_doHide = () => {}
}
function collapse(el, { modifiers }) {
let duration = modifierValue(modifiers, 'duration', 250) / 1000
let floor = modifierValue(modifiers, 'min', 0)
let fullyHide = ! modifiers.includes('min')
if (! el._x_isShown) el.style.height = `${floor}px`
// We use the hidden attribute for the benefit of Tailwind
// users as the .space utility will ignore [hidden] elements.
// We also use display:none as the hidden attribute has very
// low CSS specificity and could be accidentally overridden
// by a user.
if (! el._x_isShown && fullyHide) el.hidden = true
if (! el._x_isShown) el.style.overflow = 'hidden'
// Override the setStyles function with one that won't
// revert updates to the height style.
let setFunction = (el, styles) => {
let revertFunction = Alpine.setStyles(el, styles);
return styles.height ? () => {} : revertFunction
}
let transitionStyles = {
transitionProperty: 'height',
transitionDuration: `${duration}s`,
transitionTimingFunction: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
}
el._x_transition = {
in(before = () => {}, after = () => {}) {
if (fullyHide) el.hidden = false;
if (fullyHide) el.style.display = null
let current = el.getBoundingClientRect().height
el.style.height = 'auto'
let full = el.getBoundingClientRect().height
if (current === full) { current = floor }
Alpine.transition(el, Alpine.setStyles, {
during: transitionStyles,
start: { height: current+'px' },
end: { height: full+'px' },
}, () => el._x_isShown = true, () => {
if (el.getBoundingClientRect().height == full) {
el.style.overflow = null
}
})
},
out(before = () => {}, after = () => {}) {
let full = el.getBoundingClientRect().height
Alpine.transition(el, setFunction, {
during: transitionStyles,
start: { height: full+'px' },
end: { height: floor+'px' },
}, () => el.style.overflow = 'hidden', () => {
el._x_isShown = false
// check if element is fully collapsed
if (el.style.height == `${floor}px` && fullyHide) {
el.style.display = 'none'
el.hidden = true
}
})
},
}
}
}
function modifierValue(modifiers, key, fallback) {
// If the modifier isn't present, use the default.
if (modifiers.indexOf(key) === -1) return fallback
// If it IS present, grab the value after it: x-show.transition.duration.500ms
const rawValue = modifiers[modifiers.indexOf(key) + 1]
if (! rawValue) return fallback
if (key === 'duration') {
// Support x-collapse.duration.500ms && duration.500
let match = rawValue.match(/([0-9]+)ms/)
if (match) return match[1]
}
if (key === 'min') {
// Support x-collapse.min.100px && min.100
let match = rawValue.match(/([0-9]+)px/)
if (match) return match[1]
}
return rawValue
}

View File

@@ -0,0 +1,7 @@
import Alpine from './../src/index'
window.Alpine = Alpine
queueMicrotask(() => {
Alpine.start()
})

View File

@@ -0,0 +1,5 @@
import Alpine from './../src/index'
export default Alpine
export { Alpine }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
{
"name": "@alpinejs/csp",
"version": "3.13.8",
"description": "A CSP-friendly build of AlpineJS",
"author": "Caleb Porzio",
"license": "MIT",
"main": "dist/module.cjs.js",
"module": "dist/module.esm.js",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
}

View File

@@ -0,0 +1,50 @@
import { generateEvaluatorFromFunction, runIfTypeOfFunction } from 'alpinejs/src/evaluator'
import { closestDataStack, mergeProxies } from 'alpinejs/src/scope'
import { tryCatch } from 'alpinejs/src/utils/error'
import { injectMagics } from 'alpinejs/src/magics'
export function cspEvaluator(el, expression) {
let dataStack = generateDataStack(el)
// Return if the provided expression is already a function...
if (typeof expression === 'function') {
return generateEvaluatorFromFunction(dataStack, expression)
}
let evaluator = generateEvaluator(el, expression, dataStack)
return tryCatch.bind(null, el, expression, evaluator)
}
function generateDataStack(el) {
let overriddenMagics = {}
injectMagics(overriddenMagics, el)
return [overriddenMagics, ...closestDataStack(el)]
}
function generateEvaluator(el, expression, dataStack) {
return (receiver = () => {}, { scope = {}, params = [] } = {}) => {
let completeScope = mergeProxies([scope, ...dataStack])
if (completeScope[expression] === undefined) {
throwExpressionError(el, expression)
}
runIfTypeOfFunction(receiver, completeScope[expression], completeScope, params)
}
}
function throwExpressionError(el, expression) {
console.warn(
`Alpine Error: Alpine is unable to interpret the following expression using the CSP-friendly build:
"${expression}"
Read more about the Alpine's CSP-friendly build restrictions here: https://alpinejs.dev/advanced/csp
`,
el
)
}

View File

@@ -0,0 +1,37 @@
/**
* Alpine CSP Build.
*
* Alpine allows you to use JavaScript directly inside your HTML. This is an
* incredibly powerful features. However, it violates the "unsafe-eval"
* Content Security Policy. This alternate Alpine build provides a
* more constrained API for Alpine that is also CSP-friendly...
*/
import Alpine from 'alpinejs/src/alpine'
/**
* _______________________________________________________
* The Evaluator
* -------------------------------------------------------
*
* By default, Alpine's evaluator "eval"-like utilties to
* interpret strings as runtime JS. We're going to use
* a more CSP-friendly evaluator for this instead.
*/
import { cspEvaluator } from './evaluator'
Alpine.setEvaluator(cspEvaluator)
/**
* The rest of this file bootstraps Alpine the way it is
* normally bootstrapped in the default build. We will
* set and define it's directives, magics, etc...
*/
import { reactive, effect, stop, toRaw } from '@vue/reactivity'
Alpine.setReactivityEngine({ reactive, effect, release: stop, raw: toRaw })
import 'alpinejs/src/magics/index'
import 'alpinejs/src/directives/index'
export default Alpine

View File

@@ -0,0 +1,7 @@
{
"name": "@alpinejs/docs",
"version": "3.13.8-revision.1",
"description": "The documentation for Alpine",
"author": "Caleb Porzio",
"license": "MIT"
}

View File

@@ -0,0 +1,5 @@
---
order: 8
title: Advanced
type: sub-directory
---

View File

@@ -0,0 +1,40 @@
---
order: 4
title: Async
---
# Async
Alpine is built to support asynchronous functions in most places it supports standard ones.
For example, let's say you have a simple function called `getLabel()` that you use as the input to an `x-text` directive:
```js
function getLabel() {
return 'Hello World!'
}
```
```alpine
<span x-text="getLabel()"></span>
```
Because `getLabel` is synchronous, everything works as expected.
Now let's pretend that `getLabel` makes a network request to retrieve the label and can't return one instantaneously (asynchronous). By making `getLabel` an async function, you can call it from Alpine using JavaScript's `await` syntax.
```js
async function getLabel() {
let response = await fetch('/api/label')
return await response.text()
}
```
```alpine
<span x-text="await getLabel()"></span>
```
Additionally, if you prefer calling methods in Alpine without the trailing parenthesis, you can leave them out and Alpine will detect that the provided function is async and handle it accordingly. For example:
```alpine
<span x-text="getLabel"></span>
```

View File

@@ -0,0 +1,120 @@
---
order: 1
title: CSP
---
# CSP (Content-Security Policy) Build
In order for Alpine to be able to execute plain strings from HTML attributes as JavaScript expressions, for example `x-on:click="console.log()"`, it needs to rely on utilities that violate the "unsafe-eval" [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) that some applications may enforce for security purposes.
> Under the hood, Alpine doesn't actually use eval() itself because it's slow and problematic. Instead it uses Function declarations, which are much better, but still violate "unsafe-eval".
In order to accommodate environments where this CSP is necessary, Alpine offer's an alternate build that doesn't violate "unsafe-eval", but has a more restrictive syntax.
<a name="installation"></a>
## Installation
You can use this build by either including it from a `<script>` tag or installing it via NPM:
### Via CDN
You can include this build's CDN as a `<script>` tag just like you would normally with standard Alpine build:
```alpine
<!-- Alpine's CSP-friendly Core -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/csp@3.x.x/dist/cdn.min.js"></script>
```
### Via NPM
You can alternatively install this build from NPM for use inside your bundle like so:
```shell
npm install @alpinejs/csp
```
Then initialize it from your bundle:
```js
import Alpine from '@alpinejs/csp'
window.Alpine = Alpine
Alpine.start()
```
<a name="basic-example"></a>
## Basic Example
To provide a glimpse of how using the CSP build might feel, here is a copy-pastable HTML file with a working counter component using a common CSP setup:
```alpine
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-a23gbfz9e'">
<script defer nonce="a23gbfz9e" src="https://cdn.jsdelivr.net/npm/@alpinejs/csp@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
<div x-data="counter">
<button x-on:click="increment"></button>
<span x-text="count"></span>
</div>
<script nonce="a23gbfz9e">
document.addEventListener('alpine:init', () => {
Alpine.data('counter', () => {
return {
count: 1,
increment() {
this.count++;
},
}
})
})
</script>
</body>
</html>
```
<a name="api-restrictions"></a>
## API Restrictions
Since Alpine can no longer interpret strings as plain JavaScript, it has to parse and construct JavaScript functions from them manually.
Due to this limitation, you must use `Alpine.data` to register your `x-data` objects, and must reference properties and methods from it by key only.
For example, an inline component like this will not work.
```alpine
<!-- Bad -->
<div x-data="{ count: 1 }">
<button @click="count++">Increment</button>
<span x-text="count"></span>
</div>
```
However, breaking out the expressions into external APIs, the following is valid with the CSP build:
```alpine
<!-- Good -->
<div x-data="counter">
<button @click="increment">Increment</button>
<span x-text="count"></span>
</div>
```
```js
Alpine.data('counter', () => ({
count: 1,
increment() {
this.count++
},
}))
```

View File

@@ -0,0 +1,378 @@
---
order: 3
title: Extending
---
# Extending
Alpine has a very open codebase that allows for extension in a number of ways. In fact, every available directive and magic in Alpine itself uses these exact APIs. In theory you could rebuild all of Alpine's functionality using them yourself.
<a name="lifecycle-concerns"></a>
## Lifecycle concerns
Before we dive into each individual API, let's first talk about where in your codebase you should consume these APIs.
Because these APIs have an impact on how Alpine initializes the page, they must be registered AFTER Alpine is downloaded and available on the page, but BEFORE it has initialized the page itself.
There are two different techniques depending on if you are importing Alpine into a bundle, or including it directly via a `<script>` tag. Let's look at them both:
<a name="via-script-tag"></a>
### Via a script tag
If you are including Alpine via a script tag, you will need to register any custom extension code inside an `alpine:init` event listener.
Here's an example:
```alpine
<html>
<script src="/js/alpine.js" defer></script>
<div x-data x-foo></div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.directive('foo', ...)
})
</script>
</html>
```
If you want to extract your extension code into an external file, you will need to make sure that file's `<script>` tag is located BEFORE Alpine's like so:
```alpine
<html>
<script src="/js/foo.js" defer></script>
<script src="/js/alpine.js" defer></script>
<div x-data x-foo></div>
</html>
```
<a name="via-npm"></a>
### Via an NPM module
If you imported Alpine into a bundle, you have to make sure you are registering any extension code IN BETWEEN when you import the `Alpine` global object, and when you initialize Alpine by calling `Alpine.start()`. For example:
```js
import Alpine from 'alpinejs'
Alpine.directive('foo', ...)
window.Alpine = Alpine
window.Alpine.start()
```
Now that we know where to use these extension APIs, let's look more closely at how to use each one:
<a name="custom-directives"></a>
## Custom directives
Alpine allows you to register your own custom directives using the `Alpine.directive()` API.
<a name="method-signature"></a>
### Method Signature
```js
Alpine.directive('[name]', (el, { value, modifiers, expression }, { Alpine, effect, cleanup }) => {})
```
&nbsp; | &nbsp;
---|---
name | The name of the directive. The name "foo" for example would be consumed as `x-foo`
el | The DOM element the directive is added to
value | If provided, the part of the directive after a colon. Ex: `'bar'` in `x-foo:bar`
modifiers | An array of dot-separated trailing additions to the directive. Ex: `['baz', 'lob']` from `x-foo.baz.lob`
expression | The attribute value portion of the directive. Ex: `law` from `x-foo="law"`
Alpine | The Alpine global object
effect | A function to create reactive effects that will auto-cleanup after this directive is removed from the DOM
cleanup | A function you can pass bespoke callbacks to that will run when this directive is removed from the DOM
<a name="simple-example"></a>
### Simple Example
Here's an example of a simple directive we're going to create called: `x-uppercase`:
```js
Alpine.directive('uppercase', el => {
el.textContent = el.textContent.toUpperCase()
})
```
```alpine
<div x-data>
<span x-uppercase>Hello World!</span>
</div>
```
<a name="evaluating-expressions"></a>
### Evaluating expressions
When registering a custom directive, you may want to evaluate a user-supplied JavaScript expression:
For example, let's say you wanted to create a custom directive as a shortcut to `console.log()`. Something like:
```alpine
<div x-data="{ message: 'Hello World!' }">
<div x-log="message"></div>
</div>
```
You need to retrieve the actual value of `message` by evaluating it as a JavaScript expression with the `x-data` scope.
Fortunately, Alpine exposes its system for evaluating JavaScript expressions with an `evaluate()` API. Here's an example:
```js
Alpine.directive('log', (el, { expression }, { evaluate }) => {
// expression === 'message'
console.log(
evaluate(expression)
)
})
```
Now, when Alpine initializes the `<div x-log...>`, it will retrieve the expression passed into the directive ("message" in this case), and evaluate it in the context of the current element's Alpine component scope.
<a name="introducing-reactivity"></a>
### Introducing reactivity
Building on the `x-log` example from before, let's say we wanted `x-log` to log the value of `message` and also log it if the value changes.
Given the following template:
```alpine
<div x-data="{ message: 'Hello World!' }">
<div x-log="message"></div>
<button @click="message = 'yolo'">Change</button>
</div>
```
We want "Hello World!" to be logged initially, then we want "yolo" to be logged after pressing the `<button>`.
We can adjust the implementation of `x-log` and introduce two new APIs to achieve this: `evaluateLater()` and `effect()`:
```js
Alpine.directive('log', (el, { expression }, { evaluateLater, effect }) => {
let getThingToLog = evaluateLater(expression)
effect(() => {
getThingToLog(thingToLog => {
console.log(thingToLog)
})
})
})
```
Let's walk through the above code, line by line.
```js
let getThingToLog = evaluateLater(expression)
```
Here, instead of immediately evaluating `message` and retrieving the result, we will convert the string expression ("message") into an actual JavaScript function that we can run at any time. If you're going to evaluate a JavaScript expression more than once, it is highly recommended to first generate a JavaScript function and use that rather than calling `evaluate()` directly. The reason being that the process to interpret a plain string as a JavaScript function is expensive and should be avoided when unnecessary.
```js
effect(() => {
...
})
```
By passing in a callback to `effect()`, we are telling Alpine to run the callback immediately, then track any dependencies it uses (`x-data` properties like `message` in our case). Now as soon as one of the dependencies changes, this callback will be re-run. This gives us our "reactivity".
You may recognize this functionality from `x-effect`. It is the same mechanism under the hood.
You may also notice that `Alpine.effect()` exists and wonder why we're not using it here. The reason is that the `effect` function provided via the method parameter has special functionality that cleans itself up when the directive is removed from the page for any reason.
For example, if for some reason the element with `x-log` on it got removed from the page, by using `effect()` instead of `Alpine.effect()` when the `message` property is changed, the value will no longer be logged to the console.
[→ Read more about reactivity in Alpine](/advanced/reactivity)
```js
getThingToLog(thingToLog => {
console.log(thingToLog)
})
```
Now we will call `getThingToLog`, which if you recall is the actual JavaScript function version of the string expression: "message".
You might expect `getThingToCall()` to return the result right away, but instead Alpine requires you to pass in a callback to receive the result.
The reason for this is to support async expressions like `await getMessage()`. By passing in a "receiver" callback instead of getting the result immediately, you are allowing your directive to work with async expressions as well.
[→ Read more about async in Alpine](/advanced/async)
<a name="cleaning-up"></a>
### Cleaning Up
Let's say you needed to register an event listener from a custom directive. After that directive is removed from the page for any reason, you would want to remove the event listener as well.
Alpine makes this simple by providing you with a `cleanup` function when registering custom directives.
Here's an example:
```js
Alpine.directive('...', (el, {}, { cleanup }) => {
let handler = () => {}
window.addEventListener('click', handler)
cleanup(() => {
window.removeEventListener('click', handler)
})
})
```
Now if the directive is removed from this element or the element is removed itself, the event listener will be removed as well.
<a name="custom-order"></a>
### Custom order
By default, any new directive will run after the majority of the standard ones (with the exception of `x-teleport`). This is usually acceptable but some times you might need to run your custom directive before another specific one.
This can be achieved by chaining the `.before() function to `Alpine.directive()` and specifying which directive needs to run after your custom one.
```js
Alpine.directive('foo', (el, { value, modifiers, expression }) => {
Alpine.addScopeToNode(el, {foo: 'bar'})
}).before('bind')
```
```alpine
<div x-data>
<span x-foo x-bind:foo="foo"></span>
</div>
```
> Note, the directive name must be written without the `x-` prefix (or any other custom prefix you may use).
<a name="custom-magics"></a>
## Custom magics
Alpine allows you to register custom "magics" (properties or methods) using `Alpine.magic()`. Any magic you register will be available to all your application's Alpine code with the `$` prefix.
<a name="method-signature"></a>
### Method Signature
```js
Alpine.magic('[name]', (el, { Alpine }) => {})
```
&nbsp; | &nbsp;
---|---
name | The name of the magic. The name "foo" for example would be consumed as `$foo`
el | The DOM element the magic was triggered from
Alpine | The Alpine global object
<a name="magic-properties"></a>
### Magic Properties
Here's a basic example of a "$now" magic helper to easily get the current time from anywhere in Alpine:
```js
Alpine.magic('now', () => {
return (new Date).toLocaleTimeString()
})
```
```alpine
<span x-text="$now"></span>
```
Now the `<span>` tag will contain the current time, resembling something like "12:00:00 PM".
As you can see `$now` behaves like a static property, but under the hood is actually a getter that evaluates every time the property is accessed.
Because of this, you can implement magic "functions" by returning a function from the getter.
<a name="magic-functions"></a>
### Magic Functions
For example, if we wanted to create a `$clipboard()` magic function that accepts a string to copy to clipboard, we could implement it like so:
```js
Alpine.magic('clipboard', () => {
return subject => navigator.clipboard.writeText(subject)
})
```
```alpine
<button @click="$clipboard('hello world')">Copy "Hello World"</button>
```
Now that accessing `$clipboard` returns a function itself, we can immediately call it and pass it an argument like we see in the template with `$clipboard('hello world')`.
You can use the more brief syntax (a double arrow function) for returning a function from a function if you'd prefer:
```js
Alpine.magic('clipboard', () => subject => {
navigator.clipboard.writeText(subject)
})
```
<a name="writing-and-sharing-plugins"></a>
## Writing and sharing plugins
By now you should see how friendly and simple it is to register your own custom directives and magics in your application, but what about sharing that functionality with others via an NPM package or something?
You can get started quickly with Alpine's official "plugin-blueprint" package. It's as simple as cloning the repository and running `npm install && npm run build` to get a plugin authored.
For demonstration purposes, let's create a pretend Alpine plugin from scratch called `Foo` that includes both a directive (`x-foo`) and a magic (`$foo`).
We'll start producing this plugin for consumption as a simple `<script>` tag alongside Alpine, then we'll level it up to a module for importing into a bundle:
<a name="script-include"></a>
### Script include
Let's start in reverse by looking at how our plugin will be included into a project:
```alpine
<html>
<script src="/js/foo.js" defer></script>
<script src="/js/alpine.js" defer></script>
<div x-data x-init="$foo()">
<span x-foo="'hello world'">
</div>
</html>
```
Notice how our script is included BEFORE Alpine itself. This is important, otherwise, Alpine would have already been initialized by the time our plugin got loaded.
Now let's look inside of `/js/foo.js`'s contents:
```js
document.addEventListener('alpine:init', () => {
window.Alpine.directive('foo', ...)
window.Alpine.magic('foo', ...)
})
```
That's it! Authoring a plugin for inclusion via a script tag is extremely simple with Alpine.
<a name="bundle-module"></a>
### Bundle module
Now let's say you wanted to author a plugin that someone could install via NPM and include into their bundle.
Like the last example, we'll walk through this in reverse, starting with what it will look like to consume this plugin:
```js
import Alpine from 'alpinejs'
import foo from 'foo'
Alpine.plugin(foo)
window.Alpine = Alpine
window.Alpine.start()
```
You'll notice a new API here: `Alpine.plugin()`. This is a convenience method Alpine exposes to prevent consumers of your plugin from having to register multiple different directives and magics themselves.
Now let's look at the source of the plugin and what gets exported from `foo`:
```js
export default function (Alpine) {
Alpine.directive('foo', ...)
Alpine.magic('foo', ...)
}
```
You'll see that `Alpine.plugin` is incredibly simple. It accepts a callback and immediately invokes it while providing the `Alpine` global as a parameter for use inside of it.
Then you can go about extending Alpine as you please.

View File

@@ -0,0 +1,101 @@
---
order: 2
title: Reactivity
---
# Reactivity
Alpine is "reactive" in the sense that when you change a piece of data, everything that depends on that data "reacts" automatically to that change.
Every bit of reactivity that takes place in Alpine, happens because of two very important reactive functions in Alpine's core: `Alpine.reactive()`, and `Alpine.effect()`.
> Alpine uses VueJS's reactivity engine under the hood to provide these functions.
> [→ Read more about @vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity)
Understanding these two functions will give you super powers as an Alpine developer, but also just as a web developer in general.
<a name="alpine-reactive"></a>
## Alpine.reactive()
Let's first look at `Alpine.reactive()`. This function accepts a JavaScript object as its parameter and returns a "reactive" version of that object. For example:
```js
let data = { count: 1 }
let reactiveData = Alpine.reactive(data)
```
Under the hood, when `Alpine.reactive` receives `data`, it wraps it inside a custom JavaScript proxy.
A proxy is a special kind of object in JavaScript that can intercept "get" and "set" calls to a JavaScript object.
[→ Read more about JavaScript proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
At face value, `reactiveData` should behave exactly like `data`. For example:
```js
console.log(data.count) // 1
console.log(reactiveData.count) // 1
reactiveData.count = 2
console.log(data.count) // 2
console.log(reactiveData.count) // 2
```
What you see here is that because `reactiveData` is a thin wrapper around `data`, any attempts to get or set a property will behave exactly as if you had interacted with `data` directly.
The main difference here is that any time you modify or retrieve (get or set) a value from `reactiveData`, Alpine is aware of it and can execute any other logic that depends on this data.
`Alpine.reactive` is only the first half of the story. `Alpine.effect` is the other half, let's dig in.
<a name="alpine-effect"></a><a name="alpine-effect"></a>
## Alpine.effect()
`Alpine.effect` accepts a single callback function. As soon as `Alpine.effect` is called, it will run the provided function, but actively look for any interactions with reactive data. If it detects an interaction (a get or set from the aforementioned reactive proxy) it will keep track of it and make sure to re-run the callback if any of reactive data changes in the future. For example:
```js
let data = Alpine.reactive({ count: 1 })
Alpine.effect(() => {
console.log(data.count)
})
```
When this code is first run, "1" will be logged to the console. Any time `data.count` changes, it's value will be logged to the console again.
This is the mechanism that unlocks all of the reactivity at the core of Alpine.
To connect the dots further, let's look at a simple "counter" component example without using Alpine syntax at all, only using `Alpine.reactive` and `Alpine.effect`:
```alpine
<button>Increment</button>
Count: <span></span>
```
```js
let button = document.querySelector('button')
let span = document.querySelector('span')
let data = Alpine.reactive({ count: 1 })
Alpine.effect(() => {
span.textContent = data.count
})
button.addEventListener('click', () => {
data.count = data.count + 1
})
```
<!-- START_VERBATIM -->
<div x-data="{ count: 1 }" class="demo">
<button @click="count++">Increment</button>
<div>Count: <span x-text="count"></span></div>
</div>
<!-- END_VERBATIM -->
As you can see, you can make any data reactive, and you can also wrap any functionality in `Alpine.effect`.
This combination unlocks an incredibly powerful programming paradigm for web development. Run wild and free.

View File

@@ -0,0 +1,7 @@
---
order: 4
title: Directives
prefix: x-
font-type: mono
type: sub-directory
---

View File

@@ -0,0 +1,198 @@
---
order: 4
title: bind
---
# x-bind
`x-bind` allows you to set HTML attributes on elements based on the result of JavaScript expressions.
For example, here's a component where we will use `x-bind` to set the placeholder value of an input.
```alpine
<div x-data="{ placeholder: 'Type here...' }">
<input type="text" x-bind:placeholder="placeholder">
</div>
```
<a name="shorthand-syntax"></a>
## Shorthand syntax
If `x-bind:` is too verbose for your liking, you can use the shorthand: `:`. For example, here is the same input element as above, but refactored to use the shorthand syntax.
```alpine
<input type="text" :placeholder="placeholder">
```
<a name="binding-classes"></a>
## Binding classes
`x-bind` is most often useful for setting specific classes on an element based on your Alpine state.
Here's a simple example of a simple dropdown toggle, but instead of using `x-show`, we'll use a "hidden" class to toggle an element.
```alpine
<div x-data="{ open: false }">
<button x-on:click="open = ! open">Toggle Dropdown</button>
<div :class="open ? '' : 'hidden'">
Dropdown Contents...
</div>
</div>
```
Now, when `open` is `false`, the "hidden" class will be added to the dropdown.
<a name="shorthand-conditionals"></a>
### Shorthand conditionals
In cases like these, if you prefer a less verbose syntax you can use JavaScript's short-circuit evaluation instead of standard conditionals:
```alpine
<div :class="show ? '' : 'hidden'">
<!-- Is equivalent to: -->
<div :class="show || 'hidden'">
```
The inverse is also available to you. Suppose instead of `open`, we use a variable with the opposite value: `closed`.
```alpine
<div :class="closed ? 'hidden' : ''">
<!-- Is equivalent to: -->
<div :class="closed && 'hidden'">
```
<a name="class-object-syntax"></a>
### Class object syntax
Alpine offers an additional syntax for toggling classes if you prefer. By passing a JavaScript object where the classes are the keys and booleans are the values, Alpine will know which classes to apply and which to remove. For example:
```alpine
<div :class="{ 'hidden': ! show }">
```
This technique offers a unique advantage to other methods. When using object-syntax, Alpine will NOT preserve original classes applied to an element's `class` attribute.
For example, if you wanted to apply the "hidden" class to an element before Alpine loads, AND use Alpine to toggle its existence you can only achieve that behavior using object-syntax:
```alpine
<div class="hidden" :class="{ 'hidden': ! show }">
```
In case that confused you, let's dig deeper into how Alpine handles `x-bind:class` differently than other attributes.
<a name="special-behavior"></a>
### Special behavior
`x-bind:class` behaves differently than other attributes under the hood.
Consider the following case.
```alpine
<div class="opacity-50" :class="hide && 'hidden'">
```
If "class" were any other attribute, the `:class` binding would overwrite any existing class attribute, causing `opacity-50` to be overwritten by either `hidden` or `''`.
However, Alpine treats `class` bindings differently. It's smart enough to preserve existing classes on an element.
For example, if `hide` is true, the above example will result in the following DOM element:
```alpine
<div class="opacity-50 hidden">
```
If `hide` is false, the DOM element will look like:
```alpine
<div class="opacity-50">
```
This behavior should be invisible and intuitive to most users, but it is worth mentioning explicitly for the inquiring developer or any special cases that might crop up.
<a name="binding-styles"></a>
## Binding styles
Similar to the special syntax for binding classes with JavaScript objects, Alpine also offers an object-based syntax for binding `style` attributes.
Just like the class objects, this syntax is entirely optional. Only use it if it affords you some advantage.
```alpine
<div :style="{ color: 'red', display: 'flex' }">
<!-- Will render: -->
<div style="color: red; display: flex;" ...>
```
Conditional inline styling is possible using expressions just like with x-bind:class. Short circuit operators can be used here as well by using a styles object as the second operand.
```alpine
<div x-bind:style="true && { color: 'red' }">
<!-- Will render: -->
<div style="color: red;">
```
One advantage of this approach is being able to mix it in with existing styles on an element:
```alpine
<div style="padding: 1rem;" :style="{ color: 'red', display: 'flex' }">
<!-- Will render: -->
<div style="padding: 1rem; color: red; display: flex;" ...>
```
And like most expressions in Alpine, you can always use the result of a JavaScript expression as the reference:
```alpine
<div x-data="{ styles: { color: 'red', display: 'flex' }}">
<div :style="styles">
</div>
<!-- Will render: -->
<div ...>
<div style="color: red; display: flex;" ...>
</div>
```
<a name="bind-directives"></a>
## Binding Alpine Directives Directly
`x-bind` allows you to bind an object of different directives and attributes to an element.
The object keys can be anything you would normally write as an attribute name in Alpine. This includes Alpine directives and modifiers, but also plain HTML attributes. The object values are either plain strings, or in the case of dynamic Alpine directives, callbacks to be evaluated by Alpine.
```alpine
<div x-data="dropdown()">
<button x-bind="trigger">Open Dropdown</button>
<span x-bind="dialogue">Dropdown Contents</span>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
open: false,
trigger: {
['x-ref']: 'trigger',
['@click']() {
this.open = true
},
},
dialogue: {
['x-show']() {
return this.open
},
['@click.outside']() {
this.open = false
},
},
}))
})
</script>
```
There are a couple of caveats to this usage of `x-bind`:
> When the directive being "bound" or "applied" is `x-for`, you should return a normal expression string from the callback. For example: `['x-for']() { return 'item in items' }`

View File

@@ -0,0 +1,46 @@
---
order: 12
title: cloak
---
# x-cloak
Sometimes, when you're using AlpineJS for a part of your template, there is a "blip" where you might see your uninitialized template after the page loads, but before Alpine loads.
`x-cloak` addresses this scenario by hiding the element it's attached to until Alpine is fully loaded on the page.
For `x-cloak` to work however, you must add the following CSS to the page.
```css
[x-cloak] { display: none !important; }
```
The following example will hide the `<span>` tag until its `x-show` is specifically set to true, preventing any "blip" of the hidden element onto screen as Alpine loads.
```alpine
<span x-cloak x-show="false">This will not 'blip' onto screen at any point</span>
```
`x-cloak` doesn't just work on elements hidden by `x-show` or `x-if`: it also ensures that elements containing data are hidden until the data is correctly set. The following example will hide the `<span>` tag until Alpine has set its text content to the `message` property.
```alpine
<span x-cloak x-text="message"></span>
```
When Alpine loads on the page, it removes all `x-cloak` property from the element, which also removes the `display: none;` applied by CSS, therefore showing the element.
## Alternative to global syntax
If you'd like to achieve this same behavior, but avoid having to include a global style, you can use the following cool, but admittedly odd trick:
```alpine
<template x-if="true">
<span x-text="message"></span>
</template>
```
This will achieve the same goal as `x-cloak` by just leveraging the way `x-if` works.
Because `<template>` elements are "hidden" in browsers by default, you won't see the `<span>` until Alpine has had a chance to render the `x-if="true"` and show it.
Again, this solution is not for everyone, but it's worth mentioning for special cases.

Some files were not shown because too many files have changed in this diff Show More