feat(interactions): Elementor v4 Interactions CRUD (element animations) — 4 tools#14
Conversation
Add four MCP tools to author Elementor 4 Interactions — the per-element
scroll/hover/click animations attached to atomic elements:
- list-interactions (edit_posts) — unwrap the element's `interactions`
field to ergonomic rows
- add-interaction (manage_options) — build the $$type tree, write a
temp-<hex> id, save, re-read to
surface the canonical id
- edit-interaction (manage_options) — id-addressable in-place patch
(the differentiator), preserving id
+ untouched fields
- delete-interaction (manage_options) — remove by interaction_id
Storage model: each element carries a TOP-LEVEL `interactions` field (a
JSON-encoded `{version:1, items:[…]}` string of nested atomic $$type trees:
interaction-item → animation-preset-props → timing-config / config-v2). The
tools walk the page tree and mutate `$element['interactions']` directly
(update_element_settings only touches `settings`), then save the page.
Gated on BOTH the `e_interactions` and Atomic Widgets experiments (permissive
only when the experiments API is unreachable) plus an atomic target element.
Pro triggers/effects/easing are rejected on non-Pro via Utils::has_pro().
Wiring: require in elementor-mcp.php, register after Variables in the
registrar, add the catalog group in the admin Tools tab (delete badged
destructive, list read-only), bump to 1.16.0, CHANGELOG entry.
Tests: +28 (7 capability, 21 functional). Suite 598 → 626, green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4621b1e22c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| public function check_read_permission(): bool { | ||
| return current_user_can( 'edit_posts' ); |
There was a problem hiding this comment.
Gate interaction reads by the requested post
When list-interactions is called by a user who has edit_posts but cannot edit the requested post_id (for example a contributor querying someone else's private page), this callback still authorizes the request and execute_list() reads that page's Elementor data. Other page-scoped tools in this repo check current_user_can( 'edit_post', $post_id ) from the input; this read callback should accept the input and enforce the same per-post capability before exposing element interactions.
Useful? React with 👍 / 👎.
| /** | ||
| * Triggers that require Elementor Pro. | ||
| */ | ||
| const TRIGGERS_PRO = array( 'scrollOut', 'scrollOn', 'hover', 'click' ); |
There was a problem hiding this comment.
Reject scrollOn until its range config is written
Allowing the scrollOn/while-scrolling trigger here creates interactions that build_item() serializes with only the generic preset timing/easing fields and no scroll start/end range, even though while-scrolling interactions require those start/end points to define when progress begins and stops. In the Pro scenario where a caller chooses trigger: "scrollOn", the tool reports success but saves an incomplete interaction that cannot behave correctly; either reject this trigger for now or add schema/build support for the required scroll range fields.
Useful? React with 👍 / 👎.
…lOn (Codex P2 x2)
- Permission: check_read_permission/check_write_permission now take $input and
enforce current_user_can('edit_post', $post_id) (matching the other
page-scoped tools), so a contributor with edit_posts can't read/write
interactions on a page they can't edit. Callbacks now return true|WP_Error.
- scrollOn: removed from the accepted pro triggers — the while-scrolling /
scroll-progress trigger needs a scroll start/end range in the Pro config that
build_item() doesn't write, so it would save an incomplete, non-functional
interaction. Rejected until range support is added; the other pro triggers are
event-based (no range). +1 per-post test; capability tests updated for the
WP_Error return.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f93b614f33
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // that build_item() does not write, so a scrollOn interaction created here would | ||
| // save "successfully" but never behave correctly. Reject it until range support | ||
| // is added. The other pro triggers are event-based (no range). | ||
| const TRIGGERS_PRO = array( 'scrollOut', 'hover', 'click' ); |
There was a problem hiding this comment.
Reject unsupported scrollOut triggers
When Elementor Pro is active, validate_enum() accepts trigger: "scrollOut" and execute_add/execute_edit will persist it successfully, but Elementor's V4 interaction trigger values are page load (load), scroll into view (scrollIn), while scrolling (scrollOn), hover, and click; there is no scroll-out trigger for the frontend to run. This makes the tool report success while creating an interaction that silently never fires.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Verified false positive against Elementor core. scrollOut is a real, frontend-handled trigger:
modules/interactions/validators/trigger-value.phpVALID_TRIGGERS=[load, scrollIn, scrollOut, scrollOn, hover, click](line 15).modules/interactions/assets/js/interactions.jsreferencesscrollOut(it isscrollIns viewport-exit counterpart — an event, not scroll-progress).
It is distinct fromscrollOn(while-scrolling / progress), which this PR does reject because it needs a scroll start/end range we do not write.scrollOutis event-based and needs no range, so it stays.
| if ( '' !== $id && ! isset( $before[ $id ] ) ) { | ||
| $new[] = $id; |
There was a problem hiding this comment.
Return the canonical id when prior temp ids are rewritten
If this element already has temp-... ids from an earlier raw-meta fallback, the next native Elementor save will canonicalize both those old temp ids and the newly-added temp id. Because this comparison keys only by the pre-save ids, every canonicalized old interaction is also treated as "new", so count($new) is no longer 1 and the method falls back to returning the new temp id even though it no longer exists; callers that immediately edit/delete using the returned id will get not_found.
Useful? React with 👍 / 👎.
…ids (Codex P2) If an element already carried temp- interaction ids (from an earlier raw-meta-write fallback), the next native save canonicalizes BOTH those and the newly-added item, so the post-add id diff couldn't isolate the new one and add-interaction returned a stale temp id (follow-up edit/delete → not_found). execute_add now canonicalizes any pre-existing temp ids first (save unchanged + re-read) so only the newly-added temp id is new, and the returned id is the usable canonical one. +1 test (stub now assigns unique canonical ids). (The paired 'scrollOut unsupported' finding was a verified false positive — scrollOut is in Elementor's VALID_TRIGGERS and handled in interactions.js; it's scrollIn's event-based viewport-exit counterpart, distinct from the range-based scrollOn this PR does reject. Replied on the thread.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 996c82588e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /** | ||
| * Animation effects that require Elementor Pro. | ||
| */ | ||
| const EFFECTS_PRO = array( 'custom' ); |
There was a problem hiding this comment.
Reject custom effects until their payload is written
On Pro sites, effect=custom now passes validation because it is listed as a supported Pro effect, but the add/edit paths only write the common preset fields and never collect or persist a custom_effect/keyframe payload. A request that adds a custom interaction can therefore save successfully while leaving the interaction without the custom animation data needed to render; either reject custom like the unsupported scroll range trigger or add the missing custom-effect fields before accepting it.
Useful? React with 👍 / 👎.
| if ( ! isset( $item['value'] ) || ! is_array( $item['value'] ) ) { | ||
| $item['value'] = array(); |
There was a problem hiding this comment.
Preserve IDs when patching bare interaction items
When an existing interactions list contains a bare item shape such as {interaction_id: ...}—a shape this class can decode and find via item_id()—execute_edit() reaches patch_item(), but this branch replaces the missing value with an empty array and never copies the existing interaction_id into it. Editing one of those tolerated items saves it back without its ID, so subsequent list/edit/delete calls can no longer address it.
Useful? React with 👍 / 👎.
| 'properties' => array( | ||
| 'post_id' => array( 'type' => 'integer', 'description' => __( 'The post/page ID containing the element.', 'elementor-mcp' ) ), | ||
| 'element_id' => array( 'type' => 'string', 'description' => __( 'The target atomic element ID.', 'elementor-mcp' ) ), | ||
| 'trigger' => array( 'type' => 'string', 'description' => __( 'When the animation fires. Free: load, scrollIn. Pro: scrollOut, scrollOn, hover, click. Default load.', 'elementor-mcp' ) ), |
There was a problem hiding this comment.
Stop advertising the unsupported scrollOn trigger
The tool schema tells clients that scrollOn is an accepted Pro trigger, but TRIGGERS_PRO intentionally omits it and validation returns invalid_trigger for that exact value. Agents following the schema will generate failing add/edit requests whenever they try to use while-scrolling interactions, so the description should match the supported trigger set until scroll range support is implemented.
Useful? React with 👍 / 👎.
…ems; fix schema advertising (Codex P2/P2/P3)
- effect=custom removed from the accepted set: a custom effect needs a
custom_effect/keyframes payload build_item() doesn't write, so it would save
an interaction with no animation data. Rejected until keyframe support (same
class as the range-based scrollOn trigger).
- patch_item() now captures the interaction_id before normalizing to the wrapped
shape and re-inserts it: a tolerated 'bare' item ({interaction_id:...} with no
value wrapper) was being saved back without its id, making it unaddressable.
- schema/descriptions no longer advertise scrollOn (rejected) or custom.
+2 tests; repointed the pro-gating test to a pro trigger (no pro effects remain).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 64e85ba033
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| $temp_id = 'temp-' . bin2hex( random_bytes( 8 ) ); | ||
| $items[] = $this->build_item( $temp_id, $fields ); |
There was a problem hiding this comment.
Reject adds beyond Elementor's interaction cap
When the target element already has five interactions, this still appends a sixth item. Elementor's Interactions validator caps each element at 5 interactions; the normal document save can reject this, and the plugin's WP-CLI/REST raw-meta fallback can persist an invalid list that Elementor later sanitizes or fails to save. Please check count($items) before appending and return a clear limit error.
Useful? React with 👍 / 👎.
…Codex P2) Elementor's Interactions Validation caps each element at 5 interactions (max_number_of_interactions = 5; a native save of a 6th throws, and the raw-meta fallback would persist an invalid list Elementor later rejects). add-interaction now refuses when the element already has MAX_INTERACTIONS (5) with a clear error. +1 test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a81ef7f4eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ( array_key_exists( 'duration_ms', $fields ) && (int) $fields['duration_ms'] < 0 ) { | ||
| return new \WP_Error( 'invalid_duration', __( 'duration_ms must be a non-negative integer.', 'elementor-mcp' ) ); | ||
| } | ||
| if ( array_key_exists( 'delay_ms', $fields ) && (int) $fields['delay_ms'] < 0 ) { | ||
| return new \WP_Error( 'invalid_delay', __( 'delay_ms must be a non-negative integer.', 'elementor-mcp' ) ); |
There was a problem hiding this comment.
Bound interaction timing to Elementor's supported ranges
When duration_ms or delay_ms is provided above Elementor's supported controls (duration 100–3000ms, delay 0–2000ms), this validation still accepts it and builds a timing_config that Elementor's own save validation rejects; on save paths that fall back to raw meta this can also persist an interaction the editor/runtime won't accept. Please reject out-of-range timing values here instead of only checking for negatives.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Verified false positive against Elementor core. Elementor's interactions save validation bounds duration/delay only by a MIN of 0, with no max:
// modules/interactions/validation.php is_valid_timing_config()
if ( ! $this->is_valid_timing_value( $timing_value, 'duration', 0 ) ) { ... } // min 0, max = null
if ( ! $this->is_valid_timing_value( $timing_value, 'delay', 0 ) ) { ... } // min 0, max = null(Contrast start/end, which ARE range-checked 0..100.) So a duration like 5000ms passes save validation and renders (motion.js animates whatever it is) — the 100–3000 / 0–2000 figures are the editor slider UX bounds, not save constraints. validate_fields() already rejects negatives, which matches the save contract exactly; adding an upper bound would reject valid, renderable interactions. Not changing.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a81ef7f4eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ); | ||
| } | ||
|
|
||
| $save = $this->data->save_page_data( $post_id, $page ); |
There was a problem hiding this comment.
Refresh the interactions cache after fallback saves
When save_page_data() falls back to the raw _elementor_data meta write (the REST/CLI path this tool explicitly supports), Elementor's elementor/document/after_save hook does not run, so the elementor-interactions-cache post meta remains stale. On pages that already have a non-empty interactions cache, add/edit/delete returns success here but the frontend continues loading the old cached interactions instead of the updated element data; this write path should also invalidate or rebuild that interactions cache.
Useful? React with 👍 / 👎.
…s (Codex P2) Elementor's frontend reads interactions from the `elementor-interactions-cache` postmeta, rebuilt on `elementor/document/after_save`. On the raw `_elementor_data` meta-write fallback (the REST/CLI path this tool supports), that hook never fires, so add/edit/delete would return success while the frontend kept loading the stale cached interactions. write_items() now rebuilds the cache via Interactions_Postmeta::process_content($post_id, ['elements'=>$page]) after every save (idempotent when the document-save path already refreshed it). Guarded + non-fatal. +1 test (+ a cache stub). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4c08110fb0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // `elementor/document/after_save` never fires, so the cache the frontend | ||
| // reads (`elementor-interactions-cache`) would otherwise stay stale. | ||
| // Idempotent when the document-save path already rebuilt it. | ||
| $this->refresh_interactions_cache( $post_id, $page ); |
There was a problem hiding this comment.
Refresh the cache from saved data
When the native Elementor save path succeeds, Document::save() runs Elementor's save/data filter to sanitize the document and replace temp-* interaction IDs, and its after_save hook rebuilds elementor-interactions-cache from that saved payload. This unconditional refresh immediately overwrites that cache with the pre-save $page passed into write_items(); after add-interaction the frontend cache can keep the temporary ID (or otherwise unsanitized interaction data) until another save. Re-read the page after save, or only do this manual refresh on the raw-meta fallback path.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| return array( | ||
| 'interaction_id' => $interaction_id, |
There was a problem hiding this comment.
Return the canonical ID after editing temp IDs
If an element still has a temp-* interaction ID from an earlier raw-meta fallback, editing that interaction in a later request that uses Elementor's native save will cause Parser::assign_interaction_ids() to rewrite the ID to a canonical one. Returning the input $interaction_id here then hands callers an ID that no longer exists, so a follow-up edit/delete by the returned ID reports not_found. For temp IDs, re-read after save and surface the current ID, similar to execute_add().
Useful? React with 👍 / 👎.
… id (Codex P2 x2) - Cache refresh regression: refreshing the interactions postmeta cache from the PRE-save $page clobbered the sanitized cache the native document save had just rebuilt (canonical ids) with stale temp-id data. write_items() now re-reads the saved page and refreshes from THAT (idempotent on the document-save path; keeps the raw-fallback path in sync). - edit-interaction returned the input id even when it was a temp- id that the native save canonicalized, handing callers a stale not-found id. When the edited id was temp, edit now re-reads the item at the same position and returns its current (canonical) id — mirroring add. +2 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Third and final v4 design-system plank (after Global Classes #12/v1.14.0 and Variables #13/v1.15.0). Four MCP tools to author Elementor 4 Interactions — per-element scroll/hover/click animations.
list-interactions{interaction_id, trigger, effect, type, direction, duration_ms, delay_ms, easing}add-interactionedit-interactiondelete-interactionBeat vs Novamira: the reference plugin only supports
list/add/delete-by-indexfor interactions — it has no edit, because id assignment forces a save round-trip. This ships anedit-interactionkeyed on the canonicalinteraction_idthat patches only the changed fields and preserves the rest (including Pro nodes we don't model, likebreakpoints/custom_effect).Built against Elementor's actual source (
modules/interactions/*)interactionsfield — a JSON string{version:1, items:[…]}. Each item is the nested atomic treeinteraction-item → {interaction_id, trigger, animation},animation-preset-props → {effect, type, direction, timing-config → {duration, delay (size props, unit ms)}, config-v2 → {easing}}.$$typekeys + the free/pro enum split (triggersload/scrollIn; effectsfade/slide/scale; easingeaseIn) verified per prop-typeget_key()andPresets.addwrites atemp-<hex>id and saves through the document; Elementor'selementor/document/save/datafilter (Parser::assign_interaction_ids) swaps it for a canonical{post}-{el}-{hash}; the tool re-reads and diffs to surface it (falls back to the temp id if a raw-meta write bypassed the filter).interactionsis a top-level element field (notsettings), so a recursive by-reference setter mutates it andsave_page_data()persists.Gating / defaults
Registers only when the
e_interactions+ Atomic Widgets experiments are active; interactions attach only to atomice-elements (non-atomic targets rejected with their compact schema). Pro triggers/effects/easing require Elementor Pro (Utils::has_pro()), rejected withrequires_proon Free. Writesmanage_options, readsedit_posts; enabled by default. Version → 1.16.0.Tests
+28 (7 capability, 21 functional). Functional stub emulates
assign_interaction_ids()on save to prove the temp→canonical re-read. Suite 626 green.Note
Subagent-drafted from a source-verified spec, then reviewed against the module. One live-site verification item: the temp→canonical conversion depends on Elementor's real
$document->save()firing the id-assignment filter (the code handles both outcomes).🤖 Generated with Claude Code