# JavaScript extension API

> Extend AjaxPress from your theme or plugin with window.AjaxPress: filters, actions, custom loaders and transitions, and preloading.

Source: https://arraystory.com/docs/ajaxpress/javascript-extension-api/

Since 2.4.3, AjaxPress exposes `window.AjaxPress`, the same API that AjaxPress Pro is built on. Use it from a theme or plugin to add persistent players, keep URLs out of ajax navigation, replace the loader or the page transition, or preload pages.

Read [Where your code runs](https://arraystory.com/docs/ajaxpress/javascript-events-and-markup-reference/#where-your-code-runs) first. It explains the top window and the page frame, which this API works across.

## Register a module

AjaxPress reads your filters, loader and transition while it boots. So register through the `ajaxpressModules` queue, from a script that loads **before** AjaxPress's own script:

```js
(window.ajaxpressModules = window.ajaxpressModules || []).push((api) => {
  api.addFilter('persistSelectors', (selectors) => [...selectors, '#my-player']);
});
```

AjaxPress runs every queued module against the API before it boots. Modules pushed after it has booted run straight away, but by then it has already built its loader, transition and listeners, so queue early.

In WordPress, enqueue your script in the `<head>`. AjaxPress's own script loads in the footer:

```php
add_action( 'wp_enqueue_scripts', function () {
	wp_enqueue_script(
		'my-ajaxpress-module',
		get_theme_file_uri( 'js/ajaxpress-module.js' ),
		array(),
		'1.0.0',
		false // In the <head>, before AjaxPress.
	);
} );
```

Your script is part of every page, so it runs in the top window (for the first page) and in the page frame (on every page). Each runs its own copy of AjaxPress with its own API, which is why the same registration works everywhere.

## Filters

`api.addFilter(name, callback)` changes a value AjaxPress is about to use. The callback receives the current value (plus any extra arguments) and returns the new one. Several callbacks run in the order they were added.

### persistSelectors

The CSS selectors for [persistent elements](https://arraystory.com/docs/ajaxpress/keep-audio-or-video-playing-across-pages/). It is read in the top window on every navigation.

```js
api.addFilter('persistSelectors', (selectors) => [...selectors, '#radio-bar', '.podcast-dock']);
```

### bypassPatterns

URL fragments that force a normal page load. A URL is bypassed when its path and query contain any entry. The second argument says whether this is wp-admin.

```js
api.addFilter('bypassPatterns', (patterns, { isAdmin }) => {
  return isAdmin ? patterns : [...patterns, '/members-area', '?download='];
});
```

The defaults are `/wp-admin`, `/wp-content`, `/wp-json` and `wp-admin.php`. For site owners, **Exclude Links** in the settings does the same job without code.

## Actions

`api.addAction(name, callback)` runs code at a point in AjaxPress's life. Both actions fire inside the page frame.

| Action | Argument | Fires |
| --- | --- | --- |
| `childInit` | `{ settings }` | Once on every page, as AjaxPress starts in that page |
| `childNavigate` | `{ url }` | When a link click on the page is handed to AjaxPress |

```js
api.addAction('childNavigate', ({ url }) => {
  // For example, close an open menu before the page changes.
  document.body.classList.remove('menu-open');
});
```

## Custom loader

`api.registerLoader(factory)` replaces the progress bar or spinner. The factory receives the settings and returns a loader object, or `null` to keep the built-in one. If several are registered, the last one wins.

| Property | Type | Required | Purpose |
| --- | --- | --- | --- |
| `element` | HTMLElement | Yes | The loader's element. AjaxPress appends it to the page and gives it the id `ajaxpress-loader`. |
| `start()` | function | Yes | Called when a navigation or form submission starts |
| `complete()` | function | Yes | Called when the new page is on screen |
| `isSpinner` | boolean | No | `true` if `start()` appends the element itself, instead of AjaxPress appending it up front |

```js
(window.ajaxpressModules = window.ajaxpressModules || []).push((api) => {
  api.registerLoader(() => {
    const el = document.createElement('div');
    el.style.cssText = 'position:fixed;top:0;left:0;height:3px;width:0;background:#e11d48;z-index:2147483647;transition:width .4s,opacity .3s;opacity:0';
    return {
      element: el,
      start() { el.style.opacity = '1'; el.style.width = '70%'; },
      complete() {
        el.style.width = '100%';
        setTimeout(() => { el.style.opacity = '0'; el.style.width = '0'; }, 300);
      },
    };
  });
});
```

## Custom transition

`api.registerTransition(factory)` replaces the animation between pages. The factory receives the settings and returns a transition object, or `null` to keep the built-in one.

AjaxPress calls `crossFade(oldFrame, newFrame)` when the next page has loaded. `oldFrame` is the iframe on screen, and `newFrame` is the hidden iframe holding the new page. Make `newFrame` visible and animate between them, then resolve the promise. AjaxPress then swaps the frames and resets their inline styles. The object also needs `animateOut` and `animateIn` methods, which can simply resolve.

```js
(window.ajaxpressModules = window.ajaxpressModules || []).push((api) => {
  api.registerTransition(() => ({
    animateOut: () => Promise.resolve(),
    animateIn: () => Promise.resolve(),
    crossFade(oldFrame, newFrame) {
      newFrame.style.visibility = 'visible';
      newFrame.style.zIndex = '999999';
      newFrame.style.pointerEvents = 'auto';
      const anim = newFrame.animate(
        [{ opacity: 0, transform: 'translateX(24px)' }, { opacity: 1, transform: 'none' }],
        { duration: 250, easing: 'ease-out' }
      );
      return anim.finished.then(() => {});
    },
  }));
});
```

Keep transitions short (under 300ms). The page is already loaded while it animates.

## Preload a page

`api.preload(url)` loads a page in the background, so navigating to it is instant. It works from the page frame or the top window. Pass an absolute URL: a click is matched against the preloaded page by its full address. Only one page is held at a time, and a new preload replaces the last. URLs AjaxPress would bypass are ignored.

```js
// Preload the next episode as soon as the current one starts playing.
document.querySelector('#player').addEventListener('play', () => {
  window.AjaxPress?.preload(new URL('/episodes/next/', location.origin).href);
});
```

## Check a URL

`api.shouldBypass(url)` returns `true` if AjaxPress would leave the URL to a normal page load: another site, wp-admin, a builder editor, a `mailto:` link, a bypass pattern, or an Exclude Links match.

```js
if (!window.AjaxPress?.shouldBypass(link.href)) {
  // AjaxPress will handle this link.
}
```

## Good practice

- Check that `window.AjaxPress` exists (`window.AjaxPress?.…`) before calling it directly, since AjaxPress may be deactivated.
- A callback that throws is caught and logged to the console, and AjaxPress carries on with the value it had.
