# Shared Patterns — Pro Extensions

Conventions that apply across most or all Pro extensions. Documented once here; per-extension docs cite this file rather than repeating.

## Class anatomy

Every Pro extension follows the same shape:

```php
namespace Essential_Addons_Elementor\Pro\Extensions;

if ( ! defined( 'ABSPATH' ) ) { exit; }

use Elementor\Controls_Manager;
// ...other Elementor imports

class {Extension_Name} {

    public function __construct() {
        // All hook registration lives here. No constructor args.
        add_action( 'elementor/element/common/_section_style/after_section_end', [ $this, 'register_controls' ] );
        add_action( 'elementor/element/section/section_advanced/after_section_end', [ $this, 'register_controls' ] );
        add_action( 'elementor/element/column/section_advanced/after_section_end', [ $this, 'register_controls' ] );
        add_action( 'elementor/element/container/section_layout/after_section_end', [ $this, 'register_controls' ] );
        add_action( 'elementor/frontend/before_render', [ $this, 'before_render' ] );
        // ...more
    }

    public function register_controls( $element ) {
        $element->start_controls_section( 'eael_ext_<feature>_section', [
            'label' => __( '<i class="eaicon-logo"></i> Feature Name', 'essential-addons-elementor' ),
            'tab'   => Controls_Manager::TAB_ADVANCED,
        ] );
        // ... add_control calls
        $element->end_controls_section();
    }
}
```

Three invariants:

1. **No constructor args.** Lite's `register_extensions()` loop calls `new $extension['class']` with no parameters.
2. **All hook registration in `__construct`.** Extensions have no lifecycle hooks beyond instantiation.
3. **`register_controls( $element )`** receives the Elementor element instance; call `$element->add_control(...)`, not `$this->add_control(...)`.

## Activation gates

A Pro extension is instantiated only when:

1. The slug is present in `config.php`'s `'extensions'` map (Pro side)
2. Pro's `inject_new_extensions` filter has merged it into Lite's registry (automatic via the bridge — see [`docs/architecture/pro-lite-bridge.md`](../architecture/pro-lite-bridge.md))
3. The slug appears in the EA dashboard "active" list (user toggle, persisted in `eael_save_settings` WP option)
4. Pro's `set_default_values()` in `Traits/Core.php` includes the slug — so the user gets it active on fresh installs

If you add a new extension and skip step 4, it won't be active on fresh installs.

## Multi-element registration (Section + Column + Container + Common)

Pro extensions that apply to "any element" register controls against multiple element types in the constructor:

```php
add_action( 'elementor/element/common/_section_style/after_section_end',         [ $this, 'register_controls' ] );  // every widget
add_action( 'elementor/element/section/section_advanced/after_section_end',      [ $this, 'register_controls' ] );  // legacy section
add_action( 'elementor/element/column/section_advanced/after_section_end',       [ $this, 'register_controls' ] );  // legacy column
add_action( 'elementor/element/container/section_layout/after_section_end',      [ $this, 'register_controls' ] );  // Elementor 3.x container
```

**Why the duplication:** Elementor 3.x introduced the Flexbox Container as a separate element type from Section/Column. Pro extensions need to apply to both layouts during the migration period. A future refactor could share the registration via a `protected $element_types = [ 'section', 'column', 'container', 'common' ]` array.

**Gotcha:** when editing controls in one of these registration paths, **all four must be updated together**. A control config change in `section` that's not mirrored in `container` causes the extension to behave differently depending on which layout the user picked.

## Hook namespacing

Pro extensions hook into Elementor's hook namespace (`elementor/*`) for control registration and rendering. **Pro-emitted hooks** use the `eael/*` namespace (Lite's namespace — Pro is part of the same plugin family, do not introduce `eael_pro/*`).

| Hook type | Namespace |
| --- | --- |
| Consumed (Elementor) | `elementor/*` |
| Emitted (Pro internal) | `eael/*` |
| Consumed (Lite bridge) | `eael/pro_enabled`, `eael/before_init`, `eael/registered_extensions` |

## Control naming

Every control id must start with `eael_ext_{slug-with-underscores}_` (kebab → snake for the slug):

```text
eael_ext_content_protection_enable
eael_ext_custom_cursor_type
eael_cl_enable                          // (older convention: 'cl' = conditional_display)
```

A few older Pro extensions use shorter prefixes (`eael_cl_*` for Conditional Display, `eael_cc_*` for Custom Cursor). New extensions should use the full `eael_ext_{slug}_*` form.

**Section label icon prefix:** every Pro extension's control section header uses `<i class="eaicon-logo"></i> Feature Name` so the EA logo appears in the controls panel.

## Text domain

Always `essential-addons-elementor`. Never `essential-addons-for-elementor-lite`. A Lite-domain string in a Pro extension splits across two `.pot` files and never gets translated. The `widget-review` skill flags this as Critical.

## Render-time gating

Pro extensions modify element rendering via:

- `elementor/widget/render_content` (filter) — modify the rendered widget HTML
- `elementor/frontend/section/should_render` / `elementor/frontend/container/should_render` (filter) — short-circuit rendering entirely (return false to hide)
- `elementor/frontend/before_render` (action) — inject pre-render output / attributes
- `elementor/frontend/after_render` (action) — inject post-render output

Choose the right hook by intent:

| Intent | Hook |
| --- | --- |
| Modify the rendered HTML (wrap, append) | `render_content` filter |
| Prevent the element from rendering at all | `should_render` filter — return false |
| Add an attribute to the wrapper before render | `before_render` action |
| Inject sibling markup before/after | `before_render` / `after_render` actions |

`Content_Protection` uses `should_render` to hide entire sections/containers (and `render_content` for widgets). `Custom_Cursor` uses `before_render` to attach data attributes. `Smooth_Animation` uses `before_render` to attach animation classes.

## Asset enqueue

Extensions declare assets in `config.php`'s `'extensions'` map with the same `'dependency'` structure widgets use:

```php
'custom-cursor' => [
    'class'      => '\Essential_Addons_Elementor\Pro\Extensions\Custom_Cursor',
    'dependency' => [
        'css' => [
            [ 'file' => EAEL_PRO_PLUGIN_PATH . 'assets/front-end/css/lib-view/cursor/ghost-following.min.css', 'type' => 'lib', 'context' => 'view' ],
            // ...
        ],
        'js' => [
            [ 'file' => EAEL_PLUGIN_PATH . '/assets/front-end/js/lib-view/dom-purify/purify.min.js',  'type' => 'lib', 'context' => 'view' ],  // Lite's copy
            [ 'file' => EAEL_PRO_PLUGIN_PATH . 'assets/front-end/js/lib-view/gsap/gsap.min.js',      'type' => 'lib', 'context' => 'view' ],
            [ 'file' => EAEL_PRO_PLUGIN_PATH . 'assets/front-end/js/view/custom-cursor.min.js',     'type' => 'self', 'context' => 'view' ],
        ],
    ],
],
```

See [`docs/architecture/asset-loading.md`](../architecture/asset-loading.md) for the full dependency-shape contract.

## Conditional logic

Pro extensions use Elementor's standard `'condition' => [ 'key' => 'value' ]` form for control visibility. The `condition` form is preferred over `conditions` (OR/nested AND) unless the dependency genuinely needs OR semantics.

A control hidden by `condition` **still saves its value** — `render()` callbacks must read defensively.

## Pro extender hook into Lite extensions

Three Pro extensions overlap with Lite features:

| Pro extension | Lite counterpart |
| --- | --- |
| `custom-cursor` | Lite has the Promotion teaser pattern; Pro has the real feature |
| `smooth-animation` (Interactive Animations) | Lite has the Promotion teaser; Pro provides GSAP/ScrollTrigger animations |
| `content-protection`, `conditional-display`, `section-parallax`, `section-particles`, `tooltip-section` | Same — Lite shows teaser, Pro implements |

When Pro is active, the Lite-side `Promotion` extension short-circuits (`apply_filters( 'eael/pro_enabled', false ) === true` → constructor returns early), and Pro's real extensions occupy the same control panel slots.

## Pure-PHP vs JS-driven

Pro extensions split into two camps:

| Pure PHP (no JS) | Has frontend JS |
| --- | --- |
| `content-protection` | `section-parallax` (jarallax + GSAP) |
| `conditional-display` | `section-particles` (particles.js) |
| `advanced-dynamic-tags` | `tooltip-section` (tippy + popper) |
| | `smooth-animation` (GSAP + ScrollTrigger + SplitText) |
| | `custom-cursor` (cursor effect libs) |

Pure-PHP extensions do all their work in render-filter callbacks. JS-driven extensions write data attributes during render, then a frontend script reads those attributes and applies the effect.

## Settings storage

Pro extensions store per-element settings inside Elementor's element data — same as any Elementor control. The keys are part of `_elementor_data` post meta, persisted automatically by Elementor's save flow. No extension owns its own post meta or option keys (except `section-particles` whose preset JSON lives in the localizer — see asset-loading.md).
