# Frontend SPA Engine

The frontend engine is a vanilla JavaScript module that converts a WordPress site into a Single Page Application using an iframe-based architecture. It loads at ~14KB and handles navigation, prefetching, transitions, and element persistence.

## Initialization

**Entry point:** `src/frontend/index.js`

```
1. License()         -- Validate license (async, non-blocking)
2. IframeContainer() -- Initialize SPA if enable_navigation is true
```

## Iframe Architecture

AjaxPress uses a dual-iframe system managed by a parent frame:

```
Browser Window (Parent)
├── #ajaxpress-container    (visible, z-index: 999999)  -- Current page
├── #ajaxpress-background   (hidden, z-index: 999998)   -- Preloading next page
├── #ajaxpress-persist      (fixed, z-index: 1000000)   -- Persistent elements
└── Loader element          -- Progress bar or spinner
```

### Why Iframes?

- **Full DOM isolation** -- Each page gets a clean document context
- **Script execution** -- Page scripts run naturally without manual re-execution
- **Style isolation** -- No CSS conflicts between old and new content
- **Compatibility** -- Works with any theme/plugin without special integration

### Parent Frame (`initParent()`)

On first load, the parent frame:

1. Creates the main iframe and loads the current page into it
2. Creates a hidden background iframe for prefetching
3. Creates a fixed container for persistent elements
4. Hides the original page body content
5. Injects custom CSS from settings
6. Listens for `message` events from child iframes
7. Listens for `popstate` events for browser back/forward

### Child Frame (`initIframeChild()`)

Each page loaded in an iframe initializes as a child:

1. Intercepts all link clicks within the iframe
2. Intercepts form submissions
3. Sends navigation requests to the parent via `postMessage`
4. Reports URL and title to parent for history updates
5. Triggers prefetch requests on link hover

## Navigation Flow

### Link Click

```
User clicks link in child iframe
       |
       v
Child checks bypass rules
       |
  [bypassed?] --yes--> Normal navigation (full reload)
       |
       no
       |
       v
Child sends AJAXPRESS_NAVIGATE { url }
       |
       v
Parent receives message
       |
  [prefetched & ready?] --yes--> Immediate crossfade
       |
       no
       |
       v
Parent loads URL in background iframe
       |
       v
Background iframe loads, sends AJAXPRESS_NAV
       |
       v
Parent performs crossfade animation
       |
       v
Background becomes visible, old iframe hides
       |
       v
history.pushState() updates URL
       |
       v
Scroll to top (if enabled, skipped for back/forward)
```

### Form Submission

```
User submits form in child iframe
       |
       v
Child sends AJAXPRESS_FORM_SUBMIT { action, method, data }
       |
       v
Parent creates form element in background iframe
       |
       v
Form submits, background iframe loads response
       |
       v
Same crossfade flow as link click
```

### Browser Back/Forward

```
User clicks back/forward button
       |
       v
Parent popstate handler fires
       |
       v
Loads URL in background iframe (isHistory = true)
       |
       v
Crossfade animation
       |
       v
Scroll position NOT reset (isHistory flag)
```

## Prefetch System

Prefetch pre-loads pages before the user clicks, making navigation feel instant.

### Triggers

| Setting | Trigger | Behavior |
|---------|---------|----------|
| `enable_prefetch` | Mouse hover | Start loading on hover |
| `prefetch_on_mousedown` | Mouse button down | Start loading on mousedown |
| `prefetch_ignore_visited` | -- | Skip already-visited URLs |

### Prefetch Flow

1. Child iframe detects hover/mousedown on a link
2. Sends `AJAXPRESS_PREFETCH_REQUEST { url }` to parent
3. Parent loads URL in background iframe
4. Background iframe reports ready via `AJAXPRESS_NAV`
5. Parent sets `prefetchReady = true`
6. On actual click, crossfade happens immediately (no load wait)

### Prefetch Guards

Prefetch is skipped when:
- Same URL is already being prefetched
- Same URL is already the active navigation target
- URL matches a bypass pattern
- URL is to a different origin

## Bypass Rules

Certain URLs always trigger full page loads instead of SPA navigation.

### URL Patterns

**Frontend context:**
- `/wp-admin`, `/wp-login`, `/wp-content`, `/wp-json`
- `wp-admin.php`, `wp-login.php`

**Admin SPA context (additional):**
- `update.php`, `plugins.php?action=*`, `options.php`
- `customize.php`, `plugin-editor.php`, `theme-install.php`

### Scheme Bypass

Links with these schemes bypass SPA: `mailto:`, `tel:`, `sms:`, `javascript:`, `data:`, `blob:`, `ftp:`, `file:`

### User-Defined Exclusions

The `ignore_links` setting accepts URL patterns (one per line, regex supported) that bypass SPA navigation.

## Transitions

Transitions use the Web Animation API (`element.animate()`) for hardware-accelerated animations.

### Available Animations

| Name | Out Effect | In Effect |
|------|-----------|----------|
| `fade` | Opacity 1 -> 0 | Opacity 0 -> 1 |
| `slide` | Translate Y + fade out | Translate Y + fade in |
| `scale` | Scale down + fade out | Scale up + fade in |
| `flip` | 3D rotate + fade out | 3D rotate + fade in |

### Crossfade Process

1. Match background iframe scroll position to current iframe
2. Make background iframe visible
3. Run out-animation on old iframe + in-animation on new iframe in parallel
4. Hide old iframe on completion
5. Swap iframe references (background becomes main)
6. Smooth scroll to top (if enabled, 400ms cubic easing)

### Configuration

- `content_animation_name` -- Animation type (fade, slide, scale, flip)
- `content_animation_duration` -- Duration in seconds (0.2, 0.3, 0.5)
- `content_animation` -- Enable/disable animations

## Persistent Elements

Elements marked for persistence survive iframe navigation. They are extracted from the iframe and placed in a fixed container on the parent frame.

### Default Selectors

- `[data-ajaxpress-persist]`
- `audio[data-persist]`
- `video[data-persist]`
- `.ajaxpress-persist`

### User-Defined Selectors

The `ignore_elements` setting accepts CSS selectors for additional persistent elements.

### Persistence Flow

1. Child iframe detects element matching a persist selector
2. Sends `AJAXPRESS_PERSIST_ELEMENT` with element HTML and position
3. Parent stores element in `#ajaxpress-persist` container
4. On subsequent navigations, child checks if element is already persisted via `AJAXPRESS_CHECK_PERSISTED`
5. Position updates via `AJAXPRESS_UPDATE_PERSIST_RECT`

## Event Listener & Timer Cleanup

To prevent memory leaks across navigations, the frontend tracks and cleans up:

### Event Listeners

- `window.ajaxpress_tracked_listeners` tracks listeners added to `document` and `window`
- `startTrackingListeners()` wraps `addEventListener` after initial page load
- `cleanupTrackedListeners()` removes tracked listeners before navigation
- `markAsAjaxPressListener()` marks listeners that should be preserved

### Timers & Observers

- `window.ajaxpress_tracked_timers` tracks `setTimeout`, `setInterval`, and observers
- `startTrackingTimers()` wraps timer APIs after initial load
- `cleanupTrackedTimers()` clears all tracked timers before navigation

### Script Tracking

- `window.ajaxpress_core_scripts` -- Core scripts (jQuery, etc.) that never re-execute
- `window.ajaxpress_executed_scripts` -- All executed scripts, persists across navigations

## License Validation

Located at `src/frontend/features/license.js`.

- Runs on initial page load
- Checks license status via remote endpoint
- Caches result in `localStorage` with 12-hour TTL
- Only checks if browser is online
- Sets `window.licenseStatus = { isValid, isChecking }`
- Prevents multiple concurrent initializations
