Compare commits

..

3 Commits

Author SHA1 Message Date
Abner Coimbre f8d40c4e41 Add supporting themes required for Lotusdocs 2026-01-11 16:48:19 -08:00
Abner Coimbre 8a4d04db58 Add Lotusdocs theme 2026-01-11 16:46:05 -08:00
Abner Coimbre d051d46d1f Remove broken themes 2026-01-11 16:44:40 -08:00
1870 changed files with 275352 additions and 4 deletions

@ -1 +0,0 @@
Subproject commit 70a9d8178bcc16ac76341fc2d975ce0229763315

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 GoHugo.io
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,91 @@
[![Netlify Status](https://api.netlify.com/api/v1/badges/1afd337b-0273-4c6e-aa6f-e08bdde9833b/deploy-status)](https://app.netlify.com/sites/hugo-mod-bootstrap-scss/deploys)
  [Test Site](https://hugo-mod-bootstrap-scss.netlify.app/)
This is a [Hugo module](https://gohugo.io/hugo-modules/) that packages the [Bootstrap v5](https://getbootstrap.com/) SCSS and JavaScript source ready to be used in Hugo.
For Bootstrap v4, see [the v4 branch](https://github.com/gohugoio/hugo-mod-bootstrap-scss/tree/v4).
You need the Hugo extended version and [Go](https://golang.org/dl/) to use this component.
## Use
Add the component to your Hugo site's config:
```toml
[module]
[[module.imports]]
path = "github.com/gohugoio/hugo-mod-bootstrap-scss/v5"
```
### SCSS
The Bootstrap SCSS will be mounted in `assets/scss/bootstrap`, so you can then import either all:
```scss
@import "bootstrap/bootstrap";
```
Or only what you need:
```scss
// Configuration
@import "bootstrap/functions";
@import "bootstrap/variables";
@import "bootstrap/mixins";
@import "bootstrap/utilities";
// Layout & components
@import "bootstrap/root";
@import "bootstrap/reboot";
@import "bootstrap/type";
@import "bootstrap/images";
@import "bootstrap/containers";
@import "bootstrap/grid";
@import "bootstrap/tables";
@import "bootstrap/forms";
@import "bootstrap/buttons";
@import "bootstrap/transitions";
@import "bootstrap/dropdown";
@import "bootstrap/button-group";
@import "bootstrap/nav";
@import "bootstrap/navbar";
@import "bootstrap/card";
@import "bootstrap/accordion";
@import "bootstrap/breadcrumb";
@import "bootstrap/pagination";
@import "bootstrap/badge";
@import "bootstrap/alert";
@import "bootstrap/progress";
@import "bootstrap/list-group";
@import "bootstrap/close";
@import "bootstrap/toasts";
@import "bootstrap/modal";
@import "bootstrap/tooltip";
@import "bootstrap/popover";
@import "bootstrap/carousel";
@import "bootstrap/spinners";
@import "bootstrap/offcanvas";
// Helpers
@import "bootstrap/helpers";
// Utilities
@import "bootstrap/utilities/api";
```
### JavaScript
See the [Example Site](./exampleSite).
## Versions
This repository will be versioned following https://github.com/bep/semverpair
## How to Upgrade Bootstrap
1. Checkout the relevant branch (`main`=latest=Bootstrap 5, `v4`=Bootstrap 4)
1. Create a PR branch
1. Run `hugo mod get -u github.com/twbs/bootstrap`
1. Verify that `go.mod` is updated with correct version (run `hugo mod graph`).
1. Do `cd exampleSite` and run `hugo server` and make sure it works (and that `github.com/twbs/bootstrap` version is as expected in the table).
1. Create a Pull Request and verify that it builds and that the Netlify preview works.

View File

@ -0,0 +1,348 @@
// stylelint-disable scss/dimension-no-non-numeric-values
// SCSS RFS mixin
//
// Automated responsive values for font sizes, paddings, margins and much more
//
// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)
// Configuration
// Base value
$rfs-base-value: 1.25rem !default;
$rfs-unit: rem !default;
@if $rfs-unit != rem and $rfs-unit != px {
@error "`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.";
}
// Breakpoint at where values start decreasing if screen width is smaller
$rfs-breakpoint: 1200px !default;
$rfs-breakpoint-unit: px !default;
@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {
@error "`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.";
}
// Resize values based on screen height and width
$rfs-two-dimensional: false !default;
// Factor of decrease
$rfs-factor: 10 !default;
@if type-of($rfs-factor) != number or $rfs-factor <= 1 {
@error "`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.";
}
// Mode. Possibilities: "min-media-query", "max-media-query"
$rfs-mode: min-media-query !default;
// Generate enable or disable classes. Possibilities: false, "enable" or "disable"
$rfs-class: false !default;
// 1 rem = $rfs-rem-value px
$rfs-rem-value: 16 !default;
// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14
$rfs-safari-iframe-resize-bug-fix: false !default;
// Disable RFS by setting $enable-rfs to false
$enable-rfs: true !default;
// Cache $rfs-base-value unit
$rfs-base-value-unit: unit($rfs-base-value);
@function divide($dividend, $divisor, $precision: 10) {
$sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);
$dividend: abs($dividend);
$divisor: abs($divisor);
@if $dividend == 0 {
@return 0;
}
@if $divisor == 0 {
@error "Cannot divide by 0";
}
$remainder: $dividend;
$result: 0;
$factor: 10;
@while ($remainder > 0 and $precision >= 0) {
$quotient: 0;
@while ($remainder >= $divisor) {
$remainder: $remainder - $divisor;
$quotient: $quotient + 1;
}
$result: $result * 10 + $quotient;
$factor: $factor * .1;
$remainder: $remainder * 10;
$precision: $precision - 1;
@if ($precision < 0 and $remainder >= $divisor * 5) {
$result: $result + 1;
}
}
$result: $result * $factor * $sign;
$dividend-unit: unit($dividend);
$divisor-unit: unit($divisor);
$unit-map: (
"px": 1px,
"rem": 1rem,
"em": 1em,
"%": 1%
);
@if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {
$result: $result * map-get($unit-map, $dividend-unit);
}
@return $result;
}
// Remove px-unit from $rfs-base-value for calculations
@if $rfs-base-value-unit == px {
$rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);
}
@else if $rfs-base-value-unit == rem {
$rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));
}
// Cache $rfs-breakpoint unit to prevent multiple calls
$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);
// Remove unit from $rfs-breakpoint for calculations
@if $rfs-breakpoint-unit-cache == px {
$rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);
}
@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == "em" {
$rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));
}
// Calculate the media query value
$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});
$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);
$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);
// Internal mixin used to determine which media query needs to be used
@mixin _rfs-media-query {
@if $rfs-two-dimensional {
@if $rfs-mode == max-media-query {
@media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {
@content;
}
}
@else {
@media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {
@content;
}
}
}
@else {
@media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {
@content;
}
}
}
// Internal mixin that adds disable classes to the selector if needed.
@mixin _rfs-rule {
@if $rfs-class == disable and $rfs-mode == max-media-query {
// Adding an extra class increases specificity, which prevents the media query to override the property
&,
.disable-rfs &,
&.disable-rfs {
@content;
}
}
@else if $rfs-class == enable and $rfs-mode == min-media-query {
.enable-rfs &,
&.enable-rfs {
@content;
}
} @else {
@content;
}
}
// Internal mixin that adds enable classes to the selector if needed.
@mixin _rfs-media-query-rule {
@if $rfs-class == enable {
@if $rfs-mode == min-media-query {
@content;
}
@include _rfs-media-query () {
.enable-rfs &,
&.enable-rfs {
@content;
}
}
}
@else {
@if $rfs-class == disable and $rfs-mode == min-media-query {
.disable-rfs &,
&.disable-rfs {
@content;
}
}
@include _rfs-media-query () {
@content;
}
}
}
// Helper function to get the formatted non-responsive value
@function rfs-value($values) {
// Convert to list
$values: if(type-of($values) != list, ($values,), $values);
$val: "";
// Loop over each value and calculate value
@each $value in $values {
@if $value == 0 {
$val: $val + " 0";
}
@else {
// Cache $value unit
$unit: if(type-of($value) == "number", unit($value), false);
@if $unit == px {
// Convert to rem if needed
$val: $val + " " + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);
}
@else if $unit == rem {
// Convert to px if needed
$val: $val + " " + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);
} @else {
// If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value
$val: $val + " " + $value;
}
}
}
// Remove first space
@return unquote(str-slice($val, 2));
}
// Helper function to get the responsive value calculated by RFS
@function rfs-fluid-value($values) {
// Convert to list
$values: if(type-of($values) != list, ($values,), $values);
$val: "";
// Loop over each value and calculate value
@each $value in $values {
@if $value == 0 {
$val: $val + " 0";
} @else {
// Cache $value unit
$unit: if(type-of($value) == "number", unit($value), false);
// If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value
@if not $unit or $unit != px and $unit != rem {
$val: $val + " " + $value;
} @else {
// Remove unit from $value for calculations
$value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));
// Only add the media query if the value is greater than the minimum value
@if abs($value) <= $rfs-base-value or not $enable-rfs {
$val: $val + " " + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);
}
@else {
// Calculate the minimum value
$value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);
// Calculate difference between $value and the minimum value
$value-diff: abs($value) - $value-min;
// Base value formatting
$min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);
// Use negative value if needed
$min-width: if($value < 0, -$min-width, $min-width);
// Use `vmin` if two-dimensional is enabled
$variable-unit: if($rfs-two-dimensional, vmin, vw);
// Calculate the variable width between 0 and $rfs-breakpoint
$variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};
// Return the calculated value
$val: $val + " calc(" + $min-width + if($value < 0, " - ", " + ") + $variable-width + ")";
}
}
}
}
// Remove first space
@return unquote(str-slice($val, 2));
}
// RFS mixin
@mixin rfs($values, $property: font-size) {
@if $values != null {
$val: rfs-value($values);
$fluid-val: rfs-fluid-value($values);
// Do not print the media query if responsive & non-responsive values are the same
@if $val == $fluid-val {
#{$property}: $val;
}
@else {
@include _rfs-rule () {
#{$property}: if($rfs-mode == max-media-query, $val, $fluid-val);
// Include safari iframe resize fix if needed
min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);
}
@include _rfs-media-query-rule () {
#{$property}: if($rfs-mode == max-media-query, $fluid-val, $val);
}
}
}
}
// Shorthand helper mixins
@mixin font-size($value) {
@include rfs($value);
}
@mixin padding($value) {
@include rfs($value, padding);
}
@mixin padding-top($value) {
@include rfs($value, padding-top);
}
@mixin padding-right($value) {
@include rfs($value, padding-right);
}
@mixin padding-bottom($value) {
@include rfs($value, padding-bottom);
}
@mixin padding-left($value) {
@include rfs($value, padding-left);
}
@mixin margin($value) {
@include rfs($value, margin);
}
@mixin margin-top($value) {
@include rfs($value, margin-top);
}
@mixin margin-right($value) {
@include rfs($value, margin-right);
}
@mixin margin-bottom($value) {
@include rfs($value, margin-bottom);
}
@mixin margin-left($value) {
@include rfs($value, margin-left);
}

View File

@ -0,0 +1,21 @@
[module]
[module.hugoVersion]
extended = true
[[module.mounts]]
# Workaround for https://github.com/gohugoio/hugo/issues/6945
source = "assets/scss/bootstrap/_vendor"
target = "assets/scss/bootstrap/vendor"
[[module.mounts]]
source = "assets"
target = "assets"
[[module.imports]]
ignoreConfig = true
path = "github.com/twbs/bootstrap"
[[module.imports.mounts]]
source = "scss"
target = "assets/scss/bootstrap"
[[module.imports.mounts]]
source = "js"
target = "assets/js/bootstrap"
[[module.imports]]
path = "github.com/gohugoio/hugo-mod-jslibs-dist/popperjs"

View File

@ -0,0 +1,22 @@
// Import the Bootstrap components we want to use.
// See https://github.com/twbs/bootstrap/blob/main/js/index.umd.js
import Toast from 'js/bootstrap/src/toast';
import Popover from 'js/bootstrap/src/popover';
import Button from 'js/bootstrap/src/button.js';
import Carousel from 'js/bootstrap/src/carousel.js';
(function () {
let toastElList = [].slice.call(document.querySelectorAll('.toast'));
let toastList = toastElList.map(function (toastEl) {
return new Toast(toastEl);
});
toastList.forEach(function (toast) {
toast.show();
});
let popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function (popoverTriggerEl) {
return new Popover(popoverTriggerEl);
});
})();

View File

@ -0,0 +1,5 @@
@import "bootstrap/bootstrap";
.bd-placeholder-img-lg {
@include font-size(5.5rem);
}

View File

@ -0,0 +1,14 @@
baseURL = "https://example.com"
disableKinds = ["page", "section", "taxonomy", "term"]
[outputs]
home = ["HTML"]
[module]
[module.hugoVersion]
# We use hugo.Deps to list dependencies, which was added in Hugo 0.92.0
min = "0.92.0"
[[module.imports]]
path="github.com/gohugoio/hugo-mod-bootstrap-scss/v5"

View File

@ -0,0 +1,9 @@
module github.com/gohugoio/hugo-mod-bootstrap-scss/exampleSite/v5
go 1.17
require (
github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.90100.90300 // indirect
)
replace github.com/gohugoio/hugo-mod-bootstrap-scss/v5 => ../

View File

@ -0,0 +1,8 @@
github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000 h1:GZxx4Hc+yb0/t3/rau1j8XlAxLE4CyXns2fqQbyqWfs=
github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000/go.mod h1:mFberT6ZtcchrsDtfvJM7aAH2bDKLdOnruUHl0hlapI=
github.com/twbs/bootstrap v5.1.3+incompatible h1:19+1/69025oghttdacCOGvs1wv9D5lZnpfoCvKUsPCs=
github.com/twbs/bootstrap v5.1.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
github.com/twbs/bootstrap v5.2.1+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
github.com/twbs/bootstrap v5.3.0-alpha1+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
github.com/twbs/bootstrap v5.3.0-alpha3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
github.com/twbs/bootstrap v5.3.2+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=

View File

@ -0,0 +1,165 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
{{ .Title }}
</title>
{{/* styles */}}
<style>
html {
font-size: 12px;
}
</style>
{{/* Load Bootstrap SCSS. */}}
{{ $options := dict "enableSourceMap" true }}
{{ if hugo.IsProduction }}
{{ $options := dict "enableSourceMap" false "outputStyle" "compressed" }}
{{ end }}
{{ $styles := resources.Get "scss/styles.scss" }}
{{ $styles = $styles | css.Sass $options }}
{{ if hugo.IsProduction }}
{{ $styles = $styles | fingerprint }}
{{ end }}
<link href="{{ $styles.RelPermalink }}" rel="stylesheet" />
{{/* Load Bootstrap JS. */}}
{{ $js := resources.Get "js/index.js" }}
{{ $params := dict }}
{{ $sourceMap := cond hugo.IsProduction "" "inline" }}
{{ $opts := dict "sourceMap" $sourceMap "minify" hugo.IsProduction "target" "es2018" "params" $params }}
{{ $js = $js | js.Build $opts }}
{{ if hugo.IsProduction }}
{{ $js = $js | fingerprint }}
{{ end }}
<script
src="{{ $js.RelPermalink }}"
{{ if hugo.IsProduction }}integrity="{{ $js.Data.Integrity }}"{{ end }}
defer></script>
</head>
<body>
<div class="container mt-5 mb-5">
<h1>Bootstrap v5 Hugo Module</h1>
<h2 class="mt-4">Dependencies</h2>
<p class="mt-4">
<strong>Note:</strong> We have a replacement of
github.com/gohugoio/hugo-mod-bootstrap-scss/v4 to point to the directory
one level up (we do this to get correct PR previews when we update
Bootstrap). The version number reflects the version in
<code>go.mod</code>.
</p>
<table class="table table-striped table-responsive mt-2">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Owner</th>
<th scope="col">Path</th>
<th scope="col">Version</th>
<th scope="col">Time</th>
<th scope="col">Vendor</th>
</tr>
</thead>
<tbody>
{{ range $index, $element := hugo.Deps }}
<tr>
<th scope="row">{{ add $index 1 }}</th>
<td>{{ with $element.Owner }}{{ .Path }}{{ end }}</td>
<td>
{{ $element.Path }}
{{ with $element.Replace }}
=>
{{ .Path }}
{{ end }}
</td>
<td>{{ $element.Version }}</td>
<td>{{ with $element.Time }}{{ . }}{{ end }}</td>
<td>{{ $element.Vendor }}</td>
</tr>
{{ end }}
</tbody>
</table>
<h2 class="my-4">Toast (JS plugin)</h2>
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">Bootstrap</strong>
<small>11 mins ago</small>
<button
type="button"
class="btn-close"
data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
<div class="toast-body">Hello, world! This is a toast message.</div>
</div>
<h2 class="my-4">Popover (JS plugin)</h2>
<button
type="button"
class="btn btn-lg btn-danger"
data-bs-toggle="popover"
title="Popover title"
data-bs-content="And here's some amazing content. It's very engaging. Right?">
Click to toggle popover
</button>
<h2 class="my-4">Buttons</h2>
<div class="d-grid gap-2 d-md-block">
<button
type="button"
class="btn btn-primary"
data-bs-toggle="button"
autocomplete="off">
Toggle button
</button>
<button
type="button"
class="btn btn-primary active"
data-bs-toggle="button"
autocomplete="off"
aria-pressed="true">
Active toggle button
</button>
<button
type="button"
class="btn btn-primary"
disabled
data-bs-toggle="button"
autocomplete="off">
Disabled toggle button
</button>
</div>
<h2 class="my-4">Carousel</h2>
<div
id="carouselExampleControls"
class="carousel slide"
data-bs-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active">
{{ partial "placeholder.html" (dict "width" "800" "height" "400" "class" "bd-placeholder-img-lg d-block w-100" "color" "#555" "background" "#777" "text" "First slide") }}
</div>
<div class="carousel-item">
{{ partial "placeholder.html" (dict "width" "800" "height" "400" "class" "bd-placeholder-img-lg d-block w-100" "color" "#555" "background" "#777" "text" "Second slide") }}
</div>
<div class="carousel-item">
{{ partial "placeholder.html" (dict "width" "800" "height" "400" "class" "bd-placeholder-img-lg d-block w-100" "color" "#555" "background" "#777" "text" "Third slide") }}
</div>
</div>
<button
class="carousel-control-prev"
type="button"
data-bs-target="#carouselExampleControls"
data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button
class="carousel-control-next"
type="button"
data-bs-target="#carouselExampleControls"
data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,31 @@
{{- $title := .title | default "Placeholder" -}}
{{- $class := .class -}}
{{- $default_color := "#6c757d" -}}
{{- $default_background := "#6c757d" -}}
{{- $color := .color | default $default_color -}}
{{- $background := .background | default $default_background -}}
{{- $width := .width | default "100%" -}}
{{- $height := .height | default "180" -}}
{{- $text := .text | default (printf "%sx%s" $width $height) -}}
{{- $show_title := not (eq $title "false") -}}
{{- $show_text := not (eq $text "false") -}}
<svg
class="{{ with $class }}{{ . }}{{ end }}"
width="{{ $width }}"
height="{{ $height }}"
xmlns="http://www.w3.org/2000/svg"
{{ if (or $show_title $show_text) }}
role="img"
aria-label="{{ if $show_title }}
{{ $title }}{{ if $show_text }}:{{ end }}
{{ end }}{{ if ($show_text) }}{{ $text }}{{ end }}"
{{ else }}
aria-hidden="true"
{{ end }}
preserveAspectRatio="xMidYMid slice">
{{- if $show_title }}<title>{{ $title }}</title>{{ end -}}
<rect width="100%" height="100%" fill="{{ $background }}" />
{{- if $show_text }}
<text x="40%" y="50%" fill="{{ $color }}" dy=".3em">{{ $text }}</text>
{{ end -}}
</svg>

View File

@ -0,0 +1,8 @@
module github.com/gohugoio/hugo-mod-bootstrap-scss/v5
go 1.16
require (
github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000 // indirect
github.com/twbs/bootstrap v5.3.3+incompatible // indirect
)

View File

@ -0,0 +1,4 @@
github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000 h1:GZxx4Hc+yb0/t3/rau1j8XlAxLE4CyXns2fqQbyqWfs=
github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000/go.mod h1:mFberT6ZtcchrsDtfvJM7aAH2bDKLdOnruUHl0hlapI=
github.com/twbs/bootstrap v5.3.3+incompatible h1:goFoqinzdHfkeegpFP7pvhbd0g+A3O2hbU3XCjuNrEQ=
github.com/twbs/bootstrap v5.3.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=

View File

@ -0,0 +1,12 @@
[build]
publish = "exampleSite/public"
command = "hugo --gc -s exampleSite --minify"
[build.environment]
HUGO_VERSION = "0.143.1"
[context.deploy-preview]
command = "hugo -s exampleSite --minify -D -F -b $DEPLOY_PRIME_URL"
[context.branch-deploy]
command = "hugo -s exampleSite --minify --gc -b $DEPLOY_PRIME_URL"

@ -1 +0,0 @@
Subproject commit 18762bc856492ade7e3626a7fca2b214b547669d

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 GoHugo.io
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,6 @@
# hugo-mod-jslibs-dist
Thin Hugo Module wrappers around some popular JS libs' distribution source code.

View File

@ -0,0 +1,41 @@
This module mounts [AlpineJS](https://github.com/alpinejs/alpine)'s `packages/.../dist` folders.
See [Releases](https://github.com/gohugoio/hugo-mod-jslibs-dist/releases) for version information. We use the [Semver Pair](https://github.com/bep/semverpair) versioning scheme.
The `packages` folder is mounted in `assets/jslibs/alpinejs/v3`.
That means that you can just import it into your Hugo config:
```toml
[[module.imports]]
path = "github.com/gohugoio/hugo-mod-jslibs-dist/alpinejs/v3"
```
And then use it in your JS files:
```js
import Alpine from 'jslibs/alpinejs/v3/alpinejs/dist/module.esm.js';
import intersect from 'jslibs/alpinejs/v3/intersect/dist/module.esm.js';
import persist from 'jslibs/alpinejs/v3/persist/dist/module.esm.js';
// Set up and start Alpine.
(function() {
// Register AlpineJS plugins.
Alpine.plugin(intersect);
Alpine.plugin(persist);
// Start Alpine.
Alpine.start();
})();
```
Note that AlpineJS now requires ES target 2017 or later to work:
```handlebars
{{ $params := dict }}
{{ $opts := dict "sourceMap" $sourceMap "minify" (ne hugo.Environment "development") "target" "es2017" "params" $params }}
{{ $js := $js | js.Build $opts }}
```
Note that this works great in combination with [Turbo](https://github.com/gohugoio/hugo-mod-jslibs/tree/master/turbo), but you would need to set up something like [these listeners](https://gist.github.com/bep/a9809f0cb119e44e8ddbe37dd1e58b50) to make it work properly.

View File

@ -0,0 +1,4 @@
[module]
[[module.mounts]]
source = "packages"
target = "assets/jslibs/alpinejs/v3"

View File

@ -0,0 +1,3 @@
module github.com/gohugoio/hugo-mod-jslibs-dist/alpinejs/v3
go 1.17

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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