diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1277f63
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,49 @@
+# Changelog
+
+
+
+## [3.0.0] - 2023-07-26
+
+- Allow returning Promises and async/await animation functions
+- Breaking: the first matching rule is used
+- Update for swup 4 compatibility
+
+## [2.0.0] - 2023-03-13
+
+- Switch to microbundle
+- Export native ESM module
+
+## [1.0.5] - 2022-08-21
+
+- Bail early if no animation found
+
+## [1.0.4] - 2022-08-15
+
+- Remove reference to global swup instance
+
+## [1.0.3] - 2019-05-23
+
+- Improve how animations are ranked
+
+## [1.0.2] - 2019-05-13
+
+- Fix plugin name
+
+## [1.0.1] - 2019-05-02
+
+- Update readme
+
+## [1.0.0] - 2019-05-02
+
+- Initial release
+
+[Unreleased]: https://github.com/swup/js-plugin/compare/3.0.0...HEAD
+
+[3.0.0]: https://github.com/swup/js-plugin/releases/tag/3.0.0
+[2.0.0]: https://github.com/swup/js-plugin/releases/tag/2.0.0
+[1.0.5]: https://github.com/swup/js-plugin/releases/tag/1.0.5
+[1.0.4]: https://github.com/swup/js-plugin/releases/tag/1.0.4
+[1.0.3]: https://github.com/swup/js-plugin/releases/tag/1.0.3
+[1.0.2]: https://github.com/swup/js-plugin/releases/tag/1.0.2
+[1.0.1]: https://github.com/swup/js-plugin/releases/tag/1.0.1
+[1.0.0]: https://github.com/swup/js-plugin/releases/tag/1.0.0
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..490bfbe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,225 @@
+# Swup JS Plugin
+
+A [swup](https://swup.js.org) plugin for managing animations in JS.
+
+- Use JavaScript for timing animations instead of CSS
+- Successor to the deprecated [swupjs](https://github.com/swup/swupjs) library
+
+## Installation
+
+Install the plugin from npm and import it into your bundle.
+
+```bash
+npm install @swup/js-plugin
+```
+
+```js
+import SwupJsPlugin from '@swup/js-plugin';
+```
+
+Or include the minified production file from a CDN:
+
+```html
+
+```
+
+## Usage
+
+To run this plugin, include an instance in the swup options.
+
+```js
+const swup = new Swup({
+ plugins: [
+ new SwupJsPlugin({ animations: [ /* your custom animation functions */ ] })
+ ]
+});
+```
+
+## Options
+
+The plugin expects an `array` of animation objects.
+The example below is the default setup and defines two animations, where `out` is the
+animation function being executed before the content is being replaced, and `in` is
+the animation being executed after the content is replaced:
+
+```js
+{
+ animations: [
+ {
+ from: '(.*)', // matches any route
+ to: '(.*)', // matches any route
+ out: done => done(), // immediately continues
+ in: done => done() // immediately continues
+ }
+ ]
+}
+```
+
+This is also the fallback animation in case no other matching animations were found.
+
+Animations are chosen based on the `from` and `to` properties of the object, which are
+compared against the current visit (urls of current and next page).
+Learn more on [choosing the animation](#choosing-the-animation) below.
+
+## Animation function
+
+The animation function is executed for each corresponding animation phase. Inside the animation
+function, you manage the animation yourself and signal when it has finished. It receives two
+arguments: a `done` function and a `data` object.
+
+```js
+out: (done, data) => {
+ // Signal the end of the animation by calling done()
+ // Access info about the animation inside the data argument
+}
+```
+
+### Signaling the end of an animation
+
+Calling the `done()` function signals to swup that the animation has finished and it can proceed
+to the next step: replacing the content or finishing the visit. You can pass along the `done()`
+function as a callback to your animation library. The example below will wait for two seconds before replacing the content.
+
+```js
+out: (done) => {
+ setTimeout(done, 2000);
+}
+```
+
+#### Promises and async/await
+
+If your animation library returns Promises, you can also return the Promise directly from your
+animation function. Swup will consider the animation to be finished when the Promise resolves.
+The `done` function is then no longer required.
+
+```js
+out: () => {
+ return myAnimationLibrary.animate(/* */).then(() => {});
+}
+```
+
+This also allows `async/await` syntax for convenience.
+
+```js
+out: async () => {
+ await myAnimationLibrary.animate(/* */);
+}
+```
+
+### Data object
+
+The second parameter is an object that contains useful data about the animation, such as the visit
+object (containing actual before/after routes), the `from` and `to` parameters of the
+animation object, and the route params.
+
+```js
+{
+ visit: { /* */ }, // swup global visit object
+ direction: 'in',
+ from: {
+ url: '/',
+ pattern: '(.*)',
+ params: {}
+ },
+ to: {
+ url: '/about',
+ pattern: '(.*)',
+ params: {}
+ }
+}
+```
+
+## Examples
+
+Basic usage examples for a fade transition implemented in popular animation libraries:
+
+### [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API)
+
+```js
+{
+ from: '(.*)',
+ to: '(.*)',
+ in: async () => {
+ const container = document.querySelector('#swup');
+ await container.animate([{ opacity: 0 }, { opacity: 1 }], 500).finished;
+ },
+ out: async () => {
+ const container = document.querySelector('#swup');
+ await container.animate([{ opacity: 1 }, { opacity: 0 }], 500).finished;
+ }
+}
+```
+
+### [GSAP](https://greensock.com/gsap/)
+
+```js
+{
+ from: '(.*)',
+ to: '(.*)',
+ out: (done) => {
+ const container = document.querySelector('#swup');
+ container.style.opacity = 1;
+ gsap.to(container, { opacity: 0, duration: 0.5, onComplete: done });
+ },
+ in: (done) => {
+ const container = document.querySelector('#swup');
+ container.style.opacity = 0;
+ gsap.to(container, { opacity: 1, duration: 0.5, onComplete: done });
+ }
+}
+```
+
+### [anime.js](https://animejs.com/)
+
+```js
+{
+ from: '(.*)',
+ to: '(.*)',
+ out: (done) => {
+ const container = document.querySelector('#swup');
+ container.style.opacity = 1;
+ anime({ targets: container, opacity: 0, duration: 500, complete: done });
+ },
+ in: (done) => {
+ const container = document.querySelector('#swup');
+ container.style.opacity = 0;
+ anime({ targets: container, opacity: 1, duration: 500, complete: done });
+ }
+}
+```
+
+## Choosing the animation
+
+As mentioned above, the animation is chosen based on the `from` and `to` properties of the animation object.
+Those properties can take several forms:
+
+- a string (matching a route exactly)
+- a regular expression
+- A route pattern like `/foo/:bar`) parsed by [path-to-regexp](https://github.com/pillarjs/path-to-regexp)
+- a custom animation name taken from the `data-swup-animation` attribute of the clicked link
+
+The most fitting route is always chosen.
+Keep in mind, that two routes can be evaluated as "same fit".
+In this case, the first one defined in the options is used, so usually you would like to define the more specific routes first.
+See the example below for more info.
+
+```js
+[
+ // animation 1
+ { from: '/', to: 'custom' },
+ // animation 2
+ { from: '/', to: '/post' },
+ // animation 3
+ { from: '/', to: '/post/:id' },
+ // animation 4
+ { from: '/', to: /pos(.*)/ },
+ // animation 5
+ { from: '(.*)', to: '(.*)' },
+];
+```
+
+- from `/` to `/post` → animation **2**
+- from `/` to `/posting` → animation **4**
+- from `/` to `/post/12` → animation **3**
+- from `/` to `/some-route` → animation **5**
+- from `/` to `/post` with `data-swup-animation="custom"` → animation **1**
diff --git a/dist/SwupJsPlugin.js b/dist/SwupJsPlugin.js
deleted file mode 100644
index 91024b4..0000000
--- a/dist/SwupJsPlugin.js
+++ /dev/null
@@ -1,751 +0,0 @@
-(function webpackUniversalModuleDefinition(root, factory) {
- if(typeof exports === 'object' && typeof module === 'object')
- module.exports = factory();
- else if(typeof define === 'function' && define.amd)
- define([], factory);
- else if(typeof exports === 'object')
- exports["SwupJsPlugin"] = factory();
- else
- root["SwupJsPlugin"] = factory();
-})(window, function() {
-return /******/ (function(modules) { // webpackBootstrap
-/******/ // The module cache
-/******/ var installedModules = {};
-/******/
-/******/ // The require function
-/******/ function __webpack_require__(moduleId) {
-/******/
-/******/ // Check if module is in cache
-/******/ if(installedModules[moduleId]) {
-/******/ return installedModules[moduleId].exports;
-/******/ }
-/******/ // Create a new module (and put it into the cache)
-/******/ var module = installedModules[moduleId] = {
-/******/ i: moduleId,
-/******/ l: false,
-/******/ exports: {}
-/******/ };
-/******/
-/******/ // Execute the module function
-/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-/******/
-/******/ // Flag the module as loaded
-/******/ module.l = true;
-/******/
-/******/ // Return the exports of the module
-/******/ return module.exports;
-/******/ }
-/******/
-/******/
-/******/ // expose the modules object (__webpack_modules__)
-/******/ __webpack_require__.m = modules;
-/******/
-/******/ // expose the module cache
-/******/ __webpack_require__.c = installedModules;
-/******/
-/******/ // define getter function for harmony exports
-/******/ __webpack_require__.d = function(exports, name, getter) {
-/******/ if(!__webpack_require__.o(exports, name)) {
-/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
-/******/ }
-/******/ };
-/******/
-/******/ // define __esModule on exports
-/******/ __webpack_require__.r = function(exports) {
-/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
-/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
-/******/ }
-/******/ Object.defineProperty(exports, '__esModule', { value: true });
-/******/ };
-/******/
-/******/ // create a fake namespace object
-/******/ // mode & 1: value is a module id, require it
-/******/ // mode & 2: merge all properties of value into the ns
-/******/ // mode & 4: return value when already ns object
-/******/ // mode & 8|1: behave like require
-/******/ __webpack_require__.t = function(value, mode) {
-/******/ if(mode & 1) value = __webpack_require__(value);
-/******/ if(mode & 8) return value;
-/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
-/******/ var ns = Object.create(null);
-/******/ __webpack_require__.r(ns);
-/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
-/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
-/******/ return ns;
-/******/ };
-/******/
-/******/ // getDefaultExport function for compatibility with non-harmony modules
-/******/ __webpack_require__.n = function(module) {
-/******/ var getter = module && module.__esModule ?
-/******/ function getDefault() { return module['default']; } :
-/******/ function getModuleExports() { return module; };
-/******/ __webpack_require__.d(getter, 'a', getter);
-/******/ return getter;
-/******/ };
-/******/
-/******/ // Object.prototype.hasOwnProperty.call
-/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
-/******/
-/******/ // __webpack_public_path__
-/******/ __webpack_require__.p = "";
-/******/
-/******/
-/******/ // Load entry module and return exports
-/******/ return __webpack_require__(__webpack_require__.s = 0);
-/******/ })
-/************************************************************************/
-/******/ ([
-/* 0 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-var _index = __webpack_require__(1);
-
-var _index2 = _interopRequireDefault(_index);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-module.exports = _index2.default; // this is here for webpack to expose SwupPlugin as window.SwupPlugin
-
-/***/ }),
-/* 1 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
-
-var _plugin = __webpack_require__(2);
-
-var _plugin2 = _interopRequireDefault(_plugin);
-
-var _pathToRegexp = __webpack_require__(3);
-
-var _pathToRegexp2 = _interopRequireDefault(_pathToRegexp);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
-
-function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
-
-function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
-
-var JsPlugin = function (_Plugin) {
- _inherits(JsPlugin, _Plugin);
-
- function JsPlugin() {
- var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
-
- _classCallCheck(this, JsPlugin);
-
- var _this = _possibleConstructorReturn(this, (JsPlugin.__proto__ || Object.getPrototypeOf(JsPlugin)).call(this));
-
- _this.name = 'JsPlugin';
- _this.currentAnimation = null;
-
- _this.getAnimationPromises = function (type) {
- var animationIndex = _this.getAnimationIndex(type);
- return [_this.createAnimationPromise(animationIndex, type)];
- };
-
- _this.createAnimationPromise = function (index, type) {
- var currentTransitionRoutes = _this.swup.transition;
- var animation = _this.options[index];
-
- if (!(animation && animation[type])) {
- console.warn('No animation found');
- return Promise.resolve();
- }
-
- return new Promise(function (resolve) {
- animation[type](resolve, {
- paramsFrom: animation.regFrom.exec(currentTransitionRoutes.from),
- paramsTo: animation.regTo.exec(currentTransitionRoutes.to),
- transition: currentTransitionRoutes,
- from: animation.from,
- to: animation.to
- });
- });
- };
-
- _this.getAnimationIndex = function (type) {
- // already saved from out animation
- if (type === 'in') {
- return _this.currentAnimation;
- }
-
- var animations = _this.options;
- var animationIndex = 0;
- var topRating = 0;
-
- Object.keys(animations).forEach(function (key, index) {
- var animation = animations[key];
- var rating = _this.rateAnimation(animation);
-
- if (rating >= topRating) {
- animationIndex = index;
- topRating = rating;
- }
- });
-
- _this.currentAnimation = animationIndex;
- return _this.currentAnimation;
- };
-
- _this.rateAnimation = function (animation) {
- var currentTransitionRoutes = _this.swup.transition;
- var rating = 0;
-
- // run regex
- var fromMatched = animation.regFrom.test(currentTransitionRoutes.from);
- var toMatched = animation.regTo.test(currentTransitionRoutes.to);
-
- // check if regex passes
- rating += fromMatched ? 1 : 0;
- rating += toMatched ? 1 : 0;
-
- // beat all other if custom parameter fits
- rating += fromMatched && animation.to === currentTransitionRoutes.custom ? 2 : 0;
-
- return rating;
- };
-
- var defaultOptions = [{
- from: '(.*)',
- to: '(.*)',
- out: function out(next) {
- return next();
- },
- in: function _in(next) {
- return next();
- }
- }];
-
- _this.options = _extends({}, defaultOptions, options);
-
- _this.generateRegex();
- return _this;
- }
-
- _createClass(JsPlugin, [{
- key: 'mount',
- value: function mount() {
- var swup = this.swup;
-
- swup._getAnimationPromises = swup.getAnimationPromises;
- swup.getAnimationPromises = this.getAnimationPromises;
- }
- }, {
- key: 'unmount',
- value: function unmount() {
- var swup = this.swup;
-
- swup.getAnimationPromises = swup._getAnimationPromises;
- swup._getAnimationPromises = null;
- }
- }, {
- key: 'generateRegex',
- value: function generateRegex() {
- var _this2 = this;
-
- var isRegex = function isRegex(str) {
- return str instanceof RegExp;
- };
-
- this.options = Object.keys(this.options).map(function (key) {
- return _extends({}, _this2.options[key], {
- regFrom: isRegex(_this2.options[key].from) ? _this2.options[key].from : (0, _pathToRegexp2.default)(_this2.options[key].from),
- regTo: isRegex(_this2.options[key].to) ? _this2.options[key].to : (0, _pathToRegexp2.default)(_this2.options[key].to)
- });
- });
- }
- }]);
-
- return JsPlugin;
-}(_plugin2.default);
-
-exports.default = JsPlugin;
-
-/***/ }),
-/* 2 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
-
-var Plugin = function () {
- function Plugin() {
- _classCallCheck(this, Plugin);
-
- this.isSwupPlugin = true;
- }
-
- _createClass(Plugin, [{
- key: "mount",
- value: function mount() {
- // this is mount method rewritten by class extending
- // and is executed when swup is enabled with plugin
- }
- }, {
- key: "unmount",
- value: function unmount() {
- // this is unmount method rewritten by class extending
- // and is executed when swup with plugin is disabled
- }
- }, {
- key: "_beforeMount",
- value: function _beforeMount() {
- // here for any future hidden auto init
- }
- }, {
- key: "_afterUnmount",
- value: function _afterUnmount() {}
- // here for any future hidden auto-cleanup
-
-
- // this is here so we can tell if plugin was created by extending this class
-
- }]);
-
- return Plugin;
-}();
-
-exports.default = Plugin;
-
-/***/ }),
-/* 3 */
-/***/ (function(module, exports) {
-
-/**
- * Expose `pathToRegexp`.
- */
-module.exports = pathToRegexp
-module.exports.match = match
-module.exports.regexpToFunction = regexpToFunction
-module.exports.parse = parse
-module.exports.compile = compile
-module.exports.tokensToFunction = tokensToFunction
-module.exports.tokensToRegExp = tokensToRegExp
-
-/**
- * Default configs.
- */
-var DEFAULT_DELIMITER = '/'
-
-/**
- * The main path matching regexp utility.
- *
- * @type {RegExp}
- */
-var PATH_REGEXP = new RegExp([
- // Match escaped characters that would otherwise appear in future matches.
- // This allows the user to escape special characters that won't transform.
- '(\\\\.)',
- // Match Express-style parameters and un-named parameters with a prefix
- // and optional suffixes. Matches appear as:
- //
- // ":test(\\d+)?" => ["test", "\d+", undefined, "?"]
- // "(\\d+)" => [undefined, undefined, "\d+", undefined]
- '(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'
-].join('|'), 'g')
-
-/**
- * Parse a string for the raw tokens.
- *
- * @param {string} str
- * @param {Object=} options
- * @return {!Array}
- */
-function parse (str, options) {
- var tokens = []
- var key = 0
- var index = 0
- var path = ''
- var defaultDelimiter = (options && options.delimiter) || DEFAULT_DELIMITER
- var whitelist = (options && options.whitelist) || undefined
- var pathEscaped = false
- var res
-
- while ((res = PATH_REGEXP.exec(str)) !== null) {
- var m = res[0]
- var escaped = res[1]
- var offset = res.index
- path += str.slice(index, offset)
- index = offset + m.length
-
- // Ignore already escaped sequences.
- if (escaped) {
- path += escaped[1]
- pathEscaped = true
- continue
- }
-
- var prev = ''
- var name = res[2]
- var capture = res[3]
- var group = res[4]
- var modifier = res[5]
-
- if (!pathEscaped && path.length) {
- var k = path.length - 1
- var c = path[k]
- var matches = whitelist ? whitelist.indexOf(c) > -1 : true
-
- if (matches) {
- prev = c
- path = path.slice(0, k)
- }
- }
-
- // Push the current path onto the tokens.
- if (path) {
- tokens.push(path)
- path = ''
- pathEscaped = false
- }
-
- var repeat = modifier === '+' || modifier === '*'
- var optional = modifier === '?' || modifier === '*'
- var pattern = capture || group
- var delimiter = prev || defaultDelimiter
-
- tokens.push({
- name: name || key++,
- prefix: prev,
- delimiter: delimiter,
- optional: optional,
- repeat: repeat,
- pattern: pattern
- ? escapeGroup(pattern)
- : '[^' + escapeString(delimiter === defaultDelimiter ? delimiter : (delimiter + defaultDelimiter)) + ']+?'
- })
- }
-
- // Push any remaining characters.
- if (path || index < str.length) {
- tokens.push(path + str.substr(index))
- }
-
- return tokens
-}
-
-/**
- * Compile a string to a template function for the path.
- *
- * @param {string} str
- * @param {Object=} options
- * @return {!function(Object=, Object=)}
- */
-function compile (str, options) {
- return tokensToFunction(parse(str, options), options)
-}
-
-/**
- * Create path match function from `path-to-regexp` spec.
- */
-function match (str, options) {
- var keys = []
- var re = pathToRegexp(str, keys, options)
- return regexpToFunction(re, keys)
-}
-
-/**
- * Create a path match function from `path-to-regexp` output.
- */
-function regexpToFunction (re, keys) {
- return function (pathname, options) {
- var m = re.exec(pathname)
- if (!m) return false
-
- var path = m[0]
- var index = m.index
- var params = {}
- var decode = (options && options.decode) || decodeURIComponent
-
- for (var i = 1; i < m.length; i++) {
- if (m[i] === undefined) continue
-
- var key = keys[i - 1]
-
- if (key.repeat) {
- params[key.name] = m[i].split(key.delimiter).map(function (value) {
- return decode(value, key)
- })
- } else {
- params[key.name] = decode(m[i], key)
- }
- }
-
- return { path: path, index: index, params: params }
- }
-}
-
-/**
- * Expose a method for transforming tokens into the path function.
- */
-function tokensToFunction (tokens, options) {
- // Compile all the tokens into regexps.
- var matches = new Array(tokens.length)
-
- // Compile all the patterns before compilation.
- for (var i = 0; i < tokens.length; i++) {
- if (typeof tokens[i] === 'object') {
- matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$', flags(options))
- }
- }
-
- return function (data, options) {
- var path = ''
- var encode = (options && options.encode) || encodeURIComponent
- var validate = options ? options.validate !== false : true
-
- for (var i = 0; i < tokens.length; i++) {
- var token = tokens[i]
-
- if (typeof token === 'string') {
- path += token
- continue
- }
-
- var value = data ? data[token.name] : undefined
- var segment
-
- if (Array.isArray(value)) {
- if (!token.repeat) {
- throw new TypeError('Expected "' + token.name + '" to not repeat, but got array')
- }
-
- if (value.length === 0) {
- if (token.optional) continue
-
- throw new TypeError('Expected "' + token.name + '" to not be empty')
- }
-
- for (var j = 0; j < value.length; j++) {
- segment = encode(value[j], token)
-
- if (validate && !matches[i].test(segment)) {
- throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '"')
- }
-
- path += (j === 0 ? token.prefix : token.delimiter) + segment
- }
-
- continue
- }
-
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
- segment = encode(String(value), token)
-
- if (validate && !matches[i].test(segment)) {
- throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but got "' + segment + '"')
- }
-
- path += token.prefix + segment
- continue
- }
-
- if (token.optional) continue
-
- throw new TypeError('Expected "' + token.name + '" to be ' + (token.repeat ? 'an array' : 'a string'))
- }
-
- return path
- }
-}
-
-/**
- * Escape a regular expression string.
- *
- * @param {string} str
- * @return {string}
- */
-function escapeString (str) {
- return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
-}
-
-/**
- * Escape the capturing group by escaping special characters and meaning.
- *
- * @param {string} group
- * @return {string}
- */
-function escapeGroup (group) {
- return group.replace(/([=!:$/()])/g, '\\$1')
-}
-
-/**
- * Get the flags for a regexp from the options.
- *
- * @param {Object} options
- * @return {string}
- */
-function flags (options) {
- return options && options.sensitive ? '' : 'i'
-}
-
-/**
- * Pull out keys from a regexp.
- *
- * @param {!RegExp} path
- * @param {Array=} keys
- * @return {!RegExp}
- */
-function regexpToRegexp (path, keys) {
- if (!keys) return path
-
- // Use a negative lookahead to match only capturing groups.
- var groups = path.source.match(/\((?!\?)/g)
-
- if (groups) {
- for (var i = 0; i < groups.length; i++) {
- keys.push({
- name: i,
- prefix: null,
- delimiter: null,
- optional: false,
- repeat: false,
- pattern: null
- })
- }
- }
-
- return path
-}
-
-/**
- * Transform an array into a regexp.
- *
- * @param {!Array} path
- * @param {Array=} keys
- * @param {Object=} options
- * @return {!RegExp}
- */
-function arrayToRegexp (path, keys, options) {
- var parts = []
-
- for (var i = 0; i < path.length; i++) {
- parts.push(pathToRegexp(path[i], keys, options).source)
- }
-
- return new RegExp('(?:' + parts.join('|') + ')', flags(options))
-}
-
-/**
- * Create a path regexp from string input.
- *
- * @param {string} path
- * @param {Array=} keys
- * @param {Object=} options
- * @return {!RegExp}
- */
-function stringToRegexp (path, keys, options) {
- return tokensToRegExp(parse(path, options), keys, options)
-}
-
-/**
- * Expose a function for taking tokens and returning a RegExp.
- *
- * @param {!Array} tokens
- * @param {Array=} keys
- * @param {Object=} options
- * @return {!RegExp}
- */
-function tokensToRegExp (tokens, keys, options) {
- options = options || {}
-
- var strict = options.strict
- var start = options.start !== false
- var end = options.end !== false
- var delimiter = options.delimiter || DEFAULT_DELIMITER
- var endsWith = [].concat(options.endsWith || []).map(escapeString).concat('$').join('|')
- var route = start ? '^' : ''
-
- // Iterate over the tokens and create our regexp string.
- for (var i = 0; i < tokens.length; i++) {
- var token = tokens[i]
-
- if (typeof token === 'string') {
- route += escapeString(token)
- } else {
- var capture = token.repeat
- ? '(?:' + token.pattern + ')(?:' + escapeString(token.delimiter) + '(?:' + token.pattern + '))*'
- : token.pattern
-
- if (keys) keys.push(token)
-
- if (token.optional) {
- if (!token.prefix) {
- route += '(' + capture + ')?'
- } else {
- route += '(?:' + escapeString(token.prefix) + '(' + capture + '))?'
- }
- } else {
- route += escapeString(token.prefix) + '(' + capture + ')'
- }
- }
- }
-
- if (end) {
- if (!strict) route += '(?:' + escapeString(delimiter) + ')?'
-
- route += endsWith === '$' ? '$' : '(?=' + endsWith + ')'
- } else {
- var endToken = tokens[tokens.length - 1]
- var isEndDelimited = typeof endToken === 'string'
- ? endToken[endToken.length - 1] === delimiter
- : endToken === undefined
-
- if (!strict) route += '(?:' + escapeString(delimiter) + '(?=' + endsWith + '))?'
- if (!isEndDelimited) route += '(?=' + escapeString(delimiter) + '|' + endsWith + ')'
- }
-
- return new RegExp(route, flags(options))
-}
-
-/**
- * Normalize the given path string, returning a regular expression.
- *
- * An empty array can be passed in for the keys, which will hold the
- * placeholder key descriptions. For example, using `/user/:id`, `keys` will
- * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
- *
- * @param {(string|RegExp|Array)} path
- * @param {Array=} keys
- * @param {Object=} options
- * @return {!RegExp}
- */
-function pathToRegexp (path, keys, options) {
- if (path instanceof RegExp) {
- return regexpToRegexp(path, keys)
- }
-
- if (Array.isArray(path)) {
- return arrayToRegexp(/** @type {!Array} */ (path), keys, options)
- }
-
- return stringToRegexp(/** @type {string} */ (path), keys, options)
-}
-
-
-/***/ })
-/******/ ]);
-});
\ No newline at end of file
diff --git a/dist/SwupJsPlugin.min.js b/dist/SwupJsPlugin.min.js
deleted file mode 100644
index 050ca10..0000000
--- a/dist/SwupJsPlugin.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function e(t,r){if(typeof exports==="object"&&typeof module==="object")module.exports=r();else if(typeof define==="function"&&define.amd)define([],r);else if(typeof exports==="object")exports["SwupJsPlugin"]=r();else t["SwupJsPlugin"]=r()})(window,function(){return function(e){var t={};function r(n){if(t[n]){return t[n].exports}var o=t[n]={i:n,l:false,exports:{}};e[n].call(o.exports,o,o.exports,r);o.l=true;return o.exports}r.m=e;r.c=t;r.d=function(e,t,n){if(!r.o(e,t)){Object.defineProperty(e,t,{enumerable:true,get:n})}};r.r=function(e){if(typeof Symbol!=="undefined"&&Symbol.toStringTag){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}Object.defineProperty(e,"__esModule",{value:true})};r.t=function(e,t){if(t&1)e=r(e);if(t&8)return e;if(t&4&&typeof e==="object"&&e&&e.__esModule)return e;var n=Object.create(null);r.r(n);Object.defineProperty(n,"default",{enumerable:true,value:e});if(t&2&&typeof e!="string")for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n};r.n=function(e){var t=e&&e.__esModule?function t(){return e["default"]}:function t(){return e};r.d(t,"a",t);return t};r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};r.p="";return r(r.s=0)}([function(e,t,r){"use strict";var n=r(1);var o=i(n);function i(e){return e&&e.__esModule?e:{default:e}}e.exports=o.default},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});var n=Object.assign||function(e){for(var t=1;t0&&arguments[0]!==undefined?arguments[0]:{};c(this,t);var r=l(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));r.name="JsPlugin";r.currentAnimation=null;r.getAnimationPromises=function(e){var t=r.getAnimationIndex(e);return[r.createAnimationPromise(t,e)]};r.createAnimationPromise=function(e,t){var n=r.swup.transition;var o=r.options[e];if(!(o&&o[t])){console.warn("No animation found");return Promise.resolve()}return new Promise(function(e){o[t](e,{paramsFrom:o.regFrom.exec(n.from),paramsTo:o.regTo.exec(n.to),transition:n,from:o.from,to:o.to})})};r.getAnimationIndex=function(e){if(e==="in"){return r.currentAnimation}var t=r.options;var n=0;var o=0;Object.keys(t).forEach(function(e,i){var a=t[e];var u=r.rateAnimation(a);if(u>=o){n=i;o=u}});r.currentAnimation=n;return r.currentAnimation};r.rateAnimation=function(e){var t=r.swup.transition;var n=0;var o=e.regFrom.test(t.from);var i=e.regTo.test(t.to);n+=o?1:0;n+=i?1:0;n+=o&&e.to===t.custom?2:0;return n};var o=[{from:"(.*)",to:"(.*)",out:function e(t){return t()},in:function e(t){return t()}}];r.options=n({},o,e);r.generateRegex();return r}o(t,[{key:"mount",value:function e(){var t=this.swup;t._getAnimationPromises=t.getAnimationPromises;t.getAnimationPromises=this.getAnimationPromises}},{key:"unmount",value:function e(){var t=this.swup;t.getAnimationPromises=t._getAnimationPromises;t._getAnimationPromises=null}},{key:"generateRegex",value:function e(){var t=this;var r=function e(t){return t instanceof RegExp};this.options=Object.keys(this.options).map(function(e){return n({},t.options[e],{regFrom:r(t.options[e].from)?t.options[e].from:(0,f.default)(t.options[e].from),regTo:r(t.options[e].to)?t.options[e].to:(0,f.default)(t.options[e].to)})})}}]);return t}(a.default);t.default=v},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});var n=function(){function e(e,t){for(var r=0;r-1:true;if(_){y=j;u=u.slice(0,P)}}if(u){o.push(u);u="";p=false}var O=w==="+"||w==="*";var A=w==="?"||w==="*";var E=b||x;var T=y||f;o.push({name:h||i++,prefix:y,delimiter:T,optional:A,repeat:O,pattern:E?c(E):"[^"+s(T===f?T:T+f)+"]+?"})}if(u||a
-```
-
-## Usage
-
-To run this plugin, include an instance in the swup options.
-
-```javascript
-const swup = new Swup({
- plugins: [new SwupJsPlugin([
- // your custom transition objects
- ])]
-});
-```
-
-## Options
-
-The plugin expects a single argument in the form of an `array` of animation objects.
-The example below is the default setup and defines two animations, where `out` is the
-animation (function) being executed before the content is being replaced, and `in` is
-the animation being executed after the content is replaced:
-
-```javascript
-const options = [
- {
- from: '(.*)', // matches any route
- to: '(.*)', // matches any route
- out: next => next(), // immediately continues
- in: next => next() // immediately continues
- }
-];
-```
-
-This is also the animation object that swup will fall-back to in case no other fitting
-animation object is found.
-
-Animations are chosen based on the `from` and `to` properties of the object, which are
-compared against the current transition (routes of current and next page).
-More on that [here](#choosing-the-animation).
-
-## Animation Function
-
-The animation function receives two parameters:
-- The `next()` function
-- an object that contains information about the current animation.
-
-The `next()` function must be called once and serves as an indicator that the animation
-is done and swup can proceed with replacing the content.
-In a real world example, `next()` would be called as a callback of the animation.
-By default no animation is being executed and `next()` is called right away.
-
-The second parameter is an object that contains some useful data, like the transition
-object (containing actual before/after routes), the `from` and `to` parameters of the
-animation object, and the result of executing the Regex with the routes (`array`).
-
-In the example below, the `next` function is called after two seconds,
-which means that swup would wait at least two seconds (or any time necessary
-to load the new page content), before continuing to replacing the content.
-
-```javascript
-///...
-out: (next) => {
- setTimeout(next, 2000);
-};
-// ...
-```
-
-Basic usage with tools like [GSAP](https://greensock.com/gsap/) would look something like this:
-
-```javascript
-const options = [
- {
- from: '(.*)',
- to: '(.*)',
- in: (next, infos) => {
- document.querySelector('#swup').style.opacity = 0;
- gsap.to(document.querySelector('#swup'), {
- duration: 0.5,
- opacity: 1,
- onComplete: next
- });
- },
- out: (next, infos) => {
- document.querySelector('#swup').style.opacity = 1;
- gsap.to(document.querySelector('#swup'), 0.5, {
- duration: 0.5,
- opacity: 0,
- onComplete: next
- });
- }
- }
-];
-
-const swup = new Swup({
- plugins: [new SwupJsPlugin(options)]
-});
-```
-
-## Choosing the animation
-
-As mentioned above, the animation is chosen based on the `from` and `to` properties of the animation object.
-Those properties can take several forms:
-
-- A String (matching a route exactly).
-- A Regex.
-- A Path route definition which you may know from things like [Express](https://expressjs.com/) (eg. `/foo/:bar`). The [Path-to-RegExp](https://github.com/pillarjs/path-to-regexp) library is used for this purpose, so refer to their documentation.
-- A String of custom transitions (taken from the `data-swup-transition` attribute of the clicked link).
-
-The most fitting route is always chosen.
-Keep in mind, that two routes can be evaluated as "same fit".
-In this case, the later one defined in the options is used, so usually you would like to define the more specific routes later.
-See the example below for more info.
-
-```javascript
-[
- // animation 1
- { from: '(.*)', to: '(.*)' },
-
- // animation 2
- { from: '/', to: /pos(.*)/ },
-
- // animation 3
- { from: '/', to: '/post/:id' },
-
- // animation 4
- { from: '/', to: '/post' },
-
- // animation 5
- { from: '/', to: 'custom-transition' }
-];
-```
-
-- from `/` to `/post` → animation **4**
-- from `/` to `/posting` → animation **2**
-- from `/` to `/post/12` → animation **3**
-- from `/` to `/some-route` → animation **1**
-- from `/` to `/post` with click having `data-swup-transition="custom-transition"` → animation **5**
diff --git a/src/index.js b/src/index.js
index 990d845..31112f3 100755
--- a/src/index.js
+++ b/src/index.js
@@ -1,124 +1,131 @@
import Plugin from '@swup/plugin';
-import pathToRegexp from 'path-to-regexp';
+import { matchPath, isPromise } from 'swup';
-export default class JsPlugin extends Plugin {
- name = 'JsPlugin';
+export default class SwupJsPlugin extends Plugin {
+ name = 'SwupJsPlugin';
- currentAnimation = null;
+ requires = { swup: '>=4' };
- constructor(options = {}) {
- super();
- const defaultOptions = [
+ defaults = {
+ animations: [
{
from: '(.*)',
to: '(.*)',
- out: (next) => next(),
- in: (next) => next()
+ out: (done) => done(),
+ in: (done) => done()
}
- ];
+ ]
+ };
- this.options = {
- ...defaultOptions,
- ...options
- };
+ animations = [];
- this.generateRegex();
- }
+ constructor(options = {}) {
+ super();
- mount() {
- const swup = this.swup;
+ // Backward compatibility
+ if (Array.isArray(options)) {
+ options = { animations: options };
+ }
- swup._getAnimationPromises = swup.getAnimationPromises;
- swup.getAnimationPromises = this.getAnimationPromises;
+ this.options = { ...this.defaults, ...options };
+ this.animations = this.compileAnimations();
}
- unmount() {
- const swup = this.swup;
-
- swup.getAnimationPromises = swup._getAnimationPromises;
- swup._getAnimationPromises = null;
+ mount() {
+ this.replace('animation:in:await', this.awaitInAnimation, { priority: -1 });
+ this.replace('animation:out:await', this.awaitOutAnimation, { priority: -1 });
}
- generateRegex() {
- const isRegex = (str) => str instanceof RegExp;
-
- this.options = Object.keys(this.options).map((key) => {
- return {
- ...this.options[key],
- regFrom: isRegex(this.options[key].from)
- ? this.options[key].from
- : pathToRegexp(this.options[key].from),
- regTo: isRegex(this.options[key].to)
- ? this.options[key].to
- : pathToRegexp(this.options[key].to)
- };
+ // Compile path patterns to match functions and transitions
+ compileAnimations() {
+ return this.options.animations.map((animation) => {
+ const matchesFrom = matchPath(animation.from, this.options.matchOptions);
+ const matchesTo = matchPath(animation.to, this.options.matchOptions);
+ return { ...animation, matchesFrom, matchesTo };
});
}
- getAnimationPromises = (type) => {
- const animationIndex = this.getAnimationIndex(type);
- return [this.createAnimationPromise(animationIndex, type)];
- };
+ async awaitInAnimation(visit, { skip }) {
+ if (skip) return;
+ const animation = this.getBestAnimationMatch(visit);
+ await this.createAnimationPromise(animation, visit, 'in');
+ }
- createAnimationPromise = (index, type) => {
- const currentTransitionRoutes = this.swup.transition;
- const animation = this.options[index];
+ async awaitOutAnimation(visit, { skip }) {
+ if (skip) return;
+ const animation = this.getBestAnimationMatch(visit);
+ await this.createAnimationPromise(animation, visit, 'out');
+ }
- if (!(animation && animation[type])) {
+ createAnimationPromise(animation, visit, direction) {
+ const animationFn = animation?.[direction];
+ if (!animationFn) {
console.warn('No animation found');
return Promise.resolve();
}
+ const matchFrom = animation.matchesFrom(visit.from.url);
+ const matchTo = animation.matchesTo(visit.to.url);
+
+ const data = {
+ visit,
+ direction,
+ from: {
+ url: visit.from.url,
+ pattern: animation.from,
+ params: matchFrom?.params
+ },
+ to: {
+ url: visit.to.url,
+ pattern: animation.to,
+ params: matchTo?.params
+ }
+ };
+
return new Promise((resolve) => {
- animation[type](resolve, {
- paramsFrom: animation.regFrom.exec(currentTransitionRoutes.from),
- paramsTo: animation.regTo.exec(currentTransitionRoutes.to),
- transition: currentTransitionRoutes,
- from: animation.from,
- to: animation.to
- });
+ const result = animationFn(resolve, data);
+ if (isPromise(result)) {
+ result.then(resolve);
+ }
});
- };
-
- getAnimationIndex = (type) => {
- // already saved from out animation
- if (type === 'in') {
- return this.currentAnimation;
- }
+ }
- const animations = this.options;
- let animationIndex = 0;
+ getBestAnimationMatch(visit) {
let topRating = 0;
- Object.keys(animations).forEach((key, index) => {
- const animation = animations[key];
- const rating = this.rateAnimation(animation);
-
+ return this.animations.reduceRight((bestMatch, animation) => {
+ const rating = this.rateAnimation(visit, animation);
if (rating >= topRating) {
- animationIndex = index;
topRating = rating;
+ return animation;
+ } else {
+ return bestMatch;
}
- });
+ }, null);
+ }
- this.currentAnimation = animationIndex;
- return this.currentAnimation;
- };
+ rateAnimation(visit, animation) {
+ const from = visit.from.url;
+ const to = visit.to.url;
+ const name = visit.animation.name;
- rateAnimation = (animation) => {
- const currentTransitionRoutes = this.swup.transition;
let rating = 0;
- // run regex
- const fromMatched = animation.regFrom.test(currentTransitionRoutes.from);
- const toMatched = animation.regTo.test(currentTransitionRoutes.to);
-
- // check if regex passes
- rating += fromMatched ? 1 : 0;
- rating += toMatched ? 1 : 0;
+ // check if route patterns match
+ const fromMatched = animation.matchesFrom(from);
+ const toMatched = animation.matchesTo(to);
+ if (fromMatched) {
+ rating += 1;
+ }
+ if (toMatched) {
+ rating += 1;
+ }
- // beat all other if custom parameter fits
- rating += fromMatched && animation.to === currentTransitionRoutes.custom ? 2 : 0;
+ // beat all others if custom name fits
+ if (fromMatched && animation.to === name) {
+ rating += 2;
+ }
return rating;
- };
+ }
}