Add supporting themes required for Lotusdocs
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import Alpine from './../src/index'
|
||||
|
||||
window.Alpine = Alpine
|
||||
|
||||
queueMicrotask(() => {
|
||||
Alpine.start()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import Alpine from './../src/index'
|
||||
|
||||
export default Alpine
|
||||
|
||||
export { Alpine }
|
||||
3364
themes/hugo-mod-jslibs-dist/alpinejs/packages/alpinejs/dist/cdn.js
vendored
Normal file
3364
themes/hugo-mod-jslibs-dist/alpinejs/packages/alpinejs/dist/cdn.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5
themes/hugo-mod-jslibs-dist/alpinejs/packages/alpinejs/dist/cdn.min.js
vendored
Normal file
5
themes/hugo-mod-jslibs-dist/alpinejs/packages/alpinejs/dist/cdn.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4072
themes/hugo-mod-jslibs-dist/alpinejs/packages/alpinejs/dist/module.cjs.js
vendored
Normal file
4072
themes/hugo-mod-jslibs-dist/alpinejs/packages/alpinejs/dist/module.cjs.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3363
themes/hugo-mod-jslibs-dist/alpinejs/packages/alpinejs/dist/module.esm.js
vendored
Normal file
3363
themes/hugo-mod-jslibs-dist/alpinejs/packages/alpinejs/dist/module.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { directive, prefix } from '../directives'
|
||||
import { mutateDom } from '../mutation'
|
||||
|
||||
directive('cloak', el => queueMicrotask(() => mutateDom(() => el.removeAttribute(prefix('cloak')))))
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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))
|
||||
}))
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}))
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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())
|
||||
}))
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}))
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { scope } from '../scope'
|
||||
import { magic } from '../magics'
|
||||
|
||||
magic('data', el => scope(el))
|
||||
@@ -0,0 +1,4 @@
|
||||
import { dispatch } from '../utils/dispatch'
|
||||
import { magic } from '../magics'
|
||||
|
||||
magic('dispatch', el => dispatch.bind(dispatch, el))
|
||||
@@ -0,0 +1,3 @@
|
||||
import { magic } from "../magics";
|
||||
|
||||
magic('el', el => el)
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { nextTick } from '../nextTick'
|
||||
import { magic } from '../magics'
|
||||
|
||||
magic('nextTick', () => nextTick)
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { closestRoot } from "../lifecycle";
|
||||
import { magic } from "../magics";
|
||||
|
||||
magic('root', el => closestRoot(el))
|
||||
@@ -0,0 +1,4 @@
|
||||
import { getStores } from '../store'
|
||||
import { magic } from '../magics'
|
||||
|
||||
magic('store', getStores)
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import Alpine from "./alpine";
|
||||
|
||||
export function plugin(callback) {
|
||||
let callbacks = Array.isArray(callback) ? callback : [callback]
|
||||
|
||||
callbacks.forEach(i => i(Alpine))
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}, {})
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -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 )
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
export function warn(message, ...args) {
|
||||
console.warn(`Alpine Warning: ${message}`, ...args)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import anchor from '../src/index.js'
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.plugin(anchor)
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import anchor from '../src/index.js'
|
||||
|
||||
export default anchor
|
||||
|
||||
export { anchor }
|
||||
1258
themes/hugo-mod-jslibs-dist/alpinejs/packages/anchor/dist/cdn.js
vendored
Normal file
1258
themes/hugo-mod-jslibs-dist/alpinejs/packages/anchor/dist/cdn.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
themes/hugo-mod-jslibs-dist/alpinejs/packages/anchor/dist/cdn.min.js
vendored
Normal file
1
themes/hugo-mod-jslibs-dist/alpinejs/packages/anchor/dist/cdn.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1284
themes/hugo-mod-jslibs-dist/alpinejs/packages/anchor/dist/module.cjs.js
vendored
Normal file
1284
themes/hugo-mod-jslibs-dist/alpinejs/packages/anchor/dist/module.cjs.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1258
themes/hugo-mod-jslibs-dist/alpinejs/packages/anchor/dist/module.esm.js
vendored
Normal file
1258
themes/hugo-mod-jslibs-dist/alpinejs/packages/anchor/dist/module.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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": {}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import collapse from '../src/index.js'
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.plugin(collapse)
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import collapse from '../src/index.js'
|
||||
|
||||
export default collapse
|
||||
|
||||
export { collapse }
|
||||
99
themes/hugo-mod-jslibs-dist/alpinejs/packages/collapse/dist/cdn.js
vendored
Normal file
99
themes/hugo-mod-jslibs-dist/alpinejs/packages/collapse/dist/cdn.js
vendored
Normal 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);
|
||||
});
|
||||
})();
|
||||
1
themes/hugo-mod-jslibs-dist/alpinejs/packages/collapse/dist/cdn.min.js
vendored
Normal file
1
themes/hugo-mod-jslibs-dist/alpinejs/packages/collapse/dist/cdn.min.js
vendored
Normal 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)});})();
|
||||
125
themes/hugo-mod-jslibs-dist/alpinejs/packages/collapse/dist/module.cjs.js
vendored
Normal file
125
themes/hugo-mod-jslibs-dist/alpinejs/packages/collapse/dist/module.cjs.js
vendored
Normal 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
|
||||
});
|
||||
99
themes/hugo-mod-jslibs-dist/alpinejs/packages/collapse/dist/module.esm.js
vendored
Normal file
99
themes/hugo-mod-jslibs-dist/alpinejs/packages/collapse/dist/module.esm.js
vendored
Normal 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
|
||||
};
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import Alpine from './../src/index'
|
||||
|
||||
window.Alpine = Alpine
|
||||
|
||||
queueMicrotask(() => {
|
||||
Alpine.start()
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import Alpine from './../src/index'
|
||||
|
||||
export default Alpine
|
||||
|
||||
export { Alpine }
|
||||
3401
themes/hugo-mod-jslibs-dist/alpinejs/packages/csp/dist/cdn.js
vendored
Normal file
3401
themes/hugo-mod-jslibs-dist/alpinejs/packages/csp/dist/cdn.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
11
themes/hugo-mod-jslibs-dist/alpinejs/packages/csp/dist/cdn.min.js
vendored
Normal file
11
themes/hugo-mod-jslibs-dist/alpinejs/packages/csp/dist/cdn.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4109
themes/hugo-mod-jslibs-dist/alpinejs/packages/csp/dist/module.cjs.js
vendored
Normal file
4109
themes/hugo-mod-jslibs-dist/alpinejs/packages/csp/dist/module.cjs.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3400
themes/hugo-mod-jslibs-dist/alpinejs/packages/csp/dist/module.esm.js
vendored
Normal file
3400
themes/hugo-mod-jslibs-dist/alpinejs/packages/csp/dist/module.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@alpinejs/docs",
|
||||
"version": "3.13.8-revision.1",
|
||||
"description": "The documentation for Alpine",
|
||||
"author": "Caleb Porzio",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
order: 8
|
||||
title: Advanced
|
||||
type: sub-directory
|
||||
---
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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++
|
||||
},
|
||||
}))
|
||||
```
|
||||
@@ -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 }) => {})
|
||||
```
|
||||
|
||||
|
|
||||
---|---
|
||||
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 }) => {})
|
||||
```
|
||||
|
||||
|
|
||||
---|---
|
||||
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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
order: 4
|
||||
title: Directives
|
||||
prefix: x-
|
||||
font-type: mono
|
||||
type: sub-directory
|
||||
---
|
||||
@@ -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' }`
|
||||
@@ -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
Reference in New Issue
Block a user