# Kirki Publish Button Fix

**Event-Driven Lifecycle System for WordPress Customizer Dirty State Management**

---

## The Problem

When using Kirki Framework to extend the WordPress Customizer, the **Publish button is always active** on page load, even when no changes have been made. This breaks standard WordPress Customizer behavior.

- **Expected**: Publish button disabled until user makes a change
- **Actual (Bug)**: Publish button enabled immediately on load

---

## Root Cause

The issue originates in Kirki's `dynamic-control.js`:

1. `element.sync()` and `element.set()` calls during control initialization trigger `Value.prototype.set()`
2. `Value.prototype.set()` unconditionally sets `_dirty = true` when value changes
3. During initialization, old value is `undefined`, so values appear "changed"

---

## Solution: Event-Driven Lifecycle System

### Architecture

```
PHASE_SETUP → PHASE_BIND → PHASE_READY
     ↓              ↓            ↓
 Controls      Elements      Dirty
 register      sync        tracking
                           enabled
```

### Key Components

| Component | Purpose |
|-----------|---------|
| `kirkiLifecycle` object | Manages lifecycle phases, tracks pending controls |
| `isInitializing()` | Returns `true` during SETUP/BIND phases |
| `controlInitStarted()` / `controlInitCompleted()` | Track control initialization |
| `finalizeInitialization()` | Resets dirty states, transitions to READY |

### Code Structure

The lifecycle system runs **FIRST** in `dynamic-control.js`, before control definitions:

```javascript
// RUNS FIRST - Guard in place before any initialization
(function (api) {
    var kirkiLifecycle = {
        PHASE_SETUP: 'setup',
        PHASE_BIND: 'bind', 
        PHASE_READY: 'ready',
        currentPhase: 'setup',
        pendingControls: 0,
        finalized: false,
        
        isInitializing: function() {
            return this.currentPhase !== this.PHASE_READY;
        },
        // ... other methods
    };
    
    api.kirkiLifecycle = kirkiLifecycle;
    
    // Override Value.prototype.set to respect lifecycle
    api.Value.prototype.set = function (to) {
        // ... validation ...
        this._value = to;
        
        // Only set dirty AFTER initialization completes
        if (!api.kirkiLifecycle.isInitializing()) {
            this._dirty = true;
        }
        // ...
    };
}(wp.customize));

// RUNS SECOND - Control definitions (protected by guard)
(function () {
    wp.customize.kirkiDynamicControl = wp.customize.Control.extend({
        // ...
    });
}());
```

---

## Files Modified

| File | Change |
|------|--------|
| `control-base/src/dynamic-control.js` | Lifecycle system (runs first) |
| `control-base/dist/control.js` | Minified version |
| `control-slider/src/KirkiSliderControl.js` | Lifecycle signals |
| `control-react-colorful/src/KirkiReactColorfulControl.js` | Lifecycle signals |
| `control-repeater/src/control.js` | Lifecycle signals |

---

## Known Limitation

### Brief Flash on Page Load

The Publish button may **briefly flash** as enabled before becoming disabled. This is caused by WordPress's script load order:

```
1. WordPress loads customize-controls.js (FIRST)
   → Contains original Value.prototype.set
   → May set _dirty = true during initialization

2. Kirki's kirki-control-base loads (SECOND)
   → Has dependency: array('customize-controls')
   → Our override and cleanup happen here

3. kirkiResetDirtyState() cleans up
   → Button becomes disabled (causes brief flash)
```

**This is a WordPress core limitation** - Kirki's script must depend on `customize-controls` to access the Customizer API.

### Current Behavior

- ✅ Brief flash may occur (acceptable)
- ✅ Button correctly becomes disabled after initialization
- ✅ Only activates when user makes actual changes
- ✅ Works across all Kirki control types

---

## Verification

1. **Clear browser cache**
2. **Open WordPress Customizer**
3. **Check Publish button**: Should be disabled on load (may briefly flash)
4. **Make a change**: Button should become enabled
5. **Browser Console**: Run `wp.customize.kirkiLifecycle` to verify state

```javascript
// Expected output after initialization:
{
    currentPhase: "ready",
    pendingControls: 0,
    finalized: true
}
```

---

## Debugging

The lifecycle object is exposed globally for debugging:

```javascript
// Check current phase
wp.customize.kirkiLifecycle.currentPhase

// Check if still initializing
wp.customize.kirkiLifecycle.isInitializing()

// Check pending controls
wp.customize.kirkiLifecycle.pendingControls
```

---

## WordPress Hooks

The following hooks are fired during the lifecycle:

| Hook | When |
|------|------|
| `kirki.lifecycle.phaseChange` | On phase transitions |
| `kirki.initialized` | When initialization completes |

```javascript
// Example: Listen for initialization complete
wp.hooks.addAction('kirki.initialized', 'myPlugin', function() {
    console.log('Kirki controls fully initialized');
});
```

---

## Technical Details

- **WordPress Version**: Tested with WordPress 6.x
- **Kirki Version**: Custom build based on Kirki 4.x/5.x
- **Fix Type**: Architectural event-driven lifecycle system

---

*Last Updated: December 2024*
