Skip to content

Added expand/collapse functionalities to relationship formwidget#1374

Open
damsfx wants to merge 11 commits intowintercms:developfrom
damsfx:collapsing-relation-formwidget
Open

Added expand/collapse functionalities to relationship formwidget#1374
damsfx wants to merge 11 commits intowintercms:developfrom
damsfx:collapsing-relation-formwidget

Conversation

@damsfx
Copy link
Contributor

@damsfx damsfx commented Jun 24, 2025

This allow relation model with nested tree trait to display a expandable/collapsable list of checkboxes.

relation_with_tree

The field config allow two new options :

  • displayTree: (bool) display the related model as a tree structure
  • quickTreeActions: (bool) show tree quick actions (expand all nodes, collapse all nodes and expand only selected nodes)

Summary by CodeRabbit

  • New Features
    • Added tree-structured relation fields with expand/collapse controls for hierarchical data organization.
    • Introduced quick tree action controls: expand all, collapse all, and expand selected items.
    • Enhanced checkbox list widget to support nested hierarchy display when enabled.
    • Multi-language support added for tree action labels.

@bennothommo
Copy link
Member

@damsfx ooo, I like this. And in Snowboard too! 😍

Couple of quick comments/questions:

  • How performant is it with a whole heap of items (like, say, 100s of items)?
  • Could the animation be a touch faster? Might be just on the border of being a bit too slow to feel "snappy".

@damsfx
Copy link
Contributor Author

damsfx commented Jun 27, 2025

@bennothommo

And in Snowboard too! 😍

Definitely yes!
The more I practice, the more I love it! It's good to be back in a vanilla javascript environment.

Despite this, a function written in jQuery is still called to update the scroll bar. :o(

How performant is it with a whole heap of items (like, say, 100s of items)?

The tree structure used for my tests and visible in the animation above contains 550 nodes spread over 4 levels of depth.
I can always do a test with more nodes.

Could the animation be a touch faster? Might be just on the border of being a bit too slow to feel "snappy".

For sure.

@damsfx
Copy link
Contributor Author

damsfx commented Jun 27, 2025

@bennothommo

Latest update :

  • Use promise to adjust srcollbar after expand/collaspe node(s)
  • CloseAll() close only opened nodes instead of all nodes
  • Reduce animation timmings
  • Use CSS animation end to trigger scrollbar update

@bennothommo
Copy link
Member

@damsfx don't be too concerned if you need to fall back on jQuery to do something in the Backend. A lot of the Backend still uses it and the goal of Snowboard was to remove the dependency of jQuery for the CMS side in particular.

@LukeTowers LukeTowers requested a review from bennothommo July 16, 2025 22:15
@LukeTowers
Copy link
Member

@bennothommo are you happy with this as it is?

@damsfx is there a related docs PR?

@LukeTowers LukeTowers added needs review Issues/PRs that require a review from a maintainer needs docs Issues/PRs that require changes to the documentation to be completed labels Jul 16, 2025
@damsfx
Copy link
Contributor Author

damsfx commented Jul 16, 2025

@damsfx is there a related docs PR?

Not yet, but I can add a pending PR for this.

damsfx and others added 2 commits July 17, 2025 01:19
@damsfx
Copy link
Contributor Author

damsfx commented Jul 18, 2025

@damsfx is there a related docs PR?

@LukeTowers Done : wintercms/docs#243

damsfx added a commit to Hounddd/wn-enhancedrelation-plugin that referenced this pull request Aug 8, 2025
@damsfx
Copy link
Contributor Author

damsfx commented Sep 5, 2025

@bennothommo I know your time is precious, but if you have 5 mins to take a look and give me a little feedback if you see any improvements to be made.
Thank you for your time.

@bennothommo
Copy link
Member

@damsfx sorry for the delay. Looks great, and the timing of the animation is much better.

Only issue I can see now is that it appears that some plugins and themes were added in your most recent commit:
90fbd40

Could you make sure these are not included in your branch? Once that's done, @LukeTowers should be able to merge if he's happy with it.

@bennothommo bennothommo removed the needs review Issues/PRs that require a review from a maintainer label Sep 9, 2025
Copy link
Member

@bennothommo bennothommo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved, pending removal of plugins and themes in recent commit

@damsfx
Copy link
Contributor Author

damsfx commented Sep 9, 2025

pending removal of plugins and themes in recent commit

😱🙄 Oh, I was really distracted!
Thanks for reminding me.

Done !

@damsfx
Copy link
Contributor Author

damsfx commented Feb 19, 2026

@LukeTowers @bennothommo Is this still an interesting PR for WinterCMS !?

@LukeTowers
Copy link
Member

@damsfx yes, I had completely forgotten about this. Can you resolve the comments and ping me when it's ready to review?

@LukeTowers LukeTowers added this to the 1.3.0 milestone Feb 20, 2026
@LukeTowers LukeTowers added needs review Issues/PRs that require a review from a maintainer and removed needs docs Issues/PRs that require changes to the documentation to be completed labels Feb 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • modules/backend/assets/ui/js/build/vendor.js

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Introduces a tree-structured relation form widget with expand/collapse functionality. A new displayTree property controls whether relation fields render as collapsible tree structures. Frontend JavaScript handles tree interactions (expand/collapse, scrollbar updates), styling provides tree UI visuals, and translations support new control labels.

Changes

Cohort / File(s) Summary
Relation Widget Backend
modules/backend/formwidgets/Relation.php, modules/backend/formwidgets/relation/partials/_relation.php
Added displayTree property and loadAssets() method to control tree rendering. Rendering logic now conditionally uses toNestedArray for tree relations when displayTree is true, falls back to listsNested or lists. Added data-control="relation" attribute to widget markup.
Frontend JavaScript Plugin
modules/backend/formwidgets/relation/assets/js/src/Relation.js
New Snowboard plugin implementing tree checkbox widget with expand/collapse controls, level toggling, node state querying, and scrollbar management. Registered as backend.formwidget.relation plugin and relation widget handler.
Styling
modules/backend/formwidgets/relation/assets/less/relation.less
New stylesheet defining tree widget appearance: nested checkbox styling, chevron icons, expand/collapse animations, transitions, and active state backgrounds.
Localization
modules/backend/lang/en/lang.php, modules/backend/lang/fr/lang.php
Added three new translation keys: expand_all, collapse_all, expand_checked with English and French strings.
Checkbox List Enhancement
modules/backend/widgets/form/partials/_field_checkboxlist.php
Extended to support tree rendering via new renderCheckboxLine recursive function. Added displayTree and quickTreeActions config flags. Conditional UI controls for tree actions (expand all, expand checked, collapse all) displayed when tree mode enabled.
Build Configuration
modules/backend/winter.mix.js
Added compilation step for Relation.js frontend asset.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature addition: expand/collapse functionalities for the relation formwidget, which aligns with the core changes across PHP, JavaScript, styling, and configuration files.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/backend/formwidgets/Relation.php (1)

184-196: ⚠️ Potential issue | 🟠 Major

Duplicate $field->options assignment — the first one (lines 184–186) is dead code.

Lines 184–186 set $field->options, then lines 188–196 immediately overwrite it with the new tree-aware logic. The original assignment should be removed.

Proposed fix
-            $field->options = $usesTree
-                ? $result->listsNested($nameFrom, $primaryKeyName)
-                : $result->lists($nameFrom, $primaryKeyName);
-
             if ($usesTree) {
                 if ($this->displayTree) {
                     $field->options = $result->toNestedArray($nameFrom, $primaryKeyName);
                 } else {
                     $field->options = $result->listsNested($nameFrom, $primaryKeyName);
                 }
             } else {
                 $field->options = $result->lists($nameFrom, $primaryKeyName);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/Relation.php` around lines 184 - 196, Remove the
dead initial assignment to $field->options that uses the ternary based on
$usesTree; instead keep the subsequent conditional block which correctly handles
$usesTree and $this->displayTree and calls $result->toNestedArray,
$result->listsNested, or $result->lists as appropriate. Specifically, delete the
first assignment (the ternary setting $field->options) and rely on the existing
if ($usesTree) { ... } else { ... } logic in Relation.php so $field->options is
only set once with the correct tree-aware method.
🧹 Nitpick comments (7)
modules/backend/formwidgets/relation/assets/less/relation.less (1)

16-19: Prefer flex-start over start for justify-content.

justify-content: start is a CSS Box Alignment value, while flex-start is the original Flexbox-specific value with broader browser support. Given the project's browser targets, flex-start is the safer choice.

Proposed fix
-            justify-content: start;
+            justify-content: flex-start;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/less/relation.less` around lines
16 - 19, Replace the CSS value "start" with the Flexbox-specific "flex-start"
for the justify-content declaration in relation.less (the block that sets
display: flex; flex-direction: row; align-items: center; justify-content:
start;), changing justify-content to "flex-start" to ensure broader browser
support.
modules/backend/lang/en/lang.php (1)

326-328: Minor terminology inconsistency with existing dashboard translations.

The dashboard section (lines 107–108) uses "Collapse all" / "Expand all", while these new form keys use "Close all" / "Open all". This isn't a bug, but you might want to unify the wording for a consistent user experience across the backend UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/lang/en/lang.php` around lines 326 - 328, The wording in the
form translation keys is inconsistent with the dashboard translations: update
the values for 'expand_all', 'collapse_all', and 'expand_checked' in the
lang.php translations so they match the dashboard terminology (use "Expand all"
instead of "Open all", "Collapse all" instead of "Close all", and "Expand
selected" instead of "Open selected") to ensure consistent UX across the backend
UI.
modules/backend/formwidgets/relation/assets/js/src/Relation.js (3)

183-199: Parameter el in onToggle is actually an Event, not an HTMLElement.

The addEventListener('click', this.events.toggle) passes the Event object. The parameter is named el (and documented as @param {HTMLElement} el), but el.target.parentElement is used, confirming it's an Event. Rename for clarity.

Proposed fix
-        onToggle(el) {
-            const tooglePromise = new Promise((resolve, reject) => {
-                let parent = el.target.parentElement;
+        onToggle(event) {
+            const togglePromise = new Promise((resolve, reject) => {
+                let parent = event.target.parentElement;

Also fixes the tooglePromise typo → togglePromise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` around lines
183 - 199, The onToggle method currently treats its parameter as an HTMLElement
and names it `el`, but it receives an Event from addEventListener('click',
this.events.toggle); rename the parameter (e.g., `event` or `evt`) and update
usages to `event.target.parentElement` (or safely `event.currentTarget` if
appropriate) and correct the variable typo `tooglePromise` to `togglePromise` in
the onToggle function so the code is clearer and accurate; ensure JSDoc/@param
is updated from HTMLElement to Event as well.

88-95: querySelectorAll matches all descendants, not just direct children.

el.querySelectorAll('.checkboxlist-children')[0] will match deeply nested .checkboxlist-children in addition to the direct child. Use :scope > .checkboxlist-children to restrict to direct children and avoid unintended matches in deeply nested trees.

Proposed fix for openLevel and closeLevel
         openLevel(el) {
             el.classList.add('open');
 
-            let child = el.querySelectorAll('.checkboxlist-children')[0];
+            let child = el.querySelector(':scope > .checkboxlist-children');
             if (child) {
                 child.classList.add('open');
             }
         }
 
         closeLevel(el) {
             el.classList.remove('open');
 
-            let child = el.querySelectorAll('.checkboxlist-children')[0];
+            let child = el.querySelector(':scope > .checkboxlist-children');
             if (child) {
                 child.classList.remove('open');
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` around lines
88 - 95, openLevel (and the sibling closeLevel) currently use
el.querySelectorAll('.checkboxlist-children')[0], which can return deeply nested
descendants; change these to select only direct children by using
el.querySelector(':scope > .checkboxlist-children') (or an equivalent
direct-child selection) so only the immediate .checkboxlist-children element is
toggled in openLevel and closeLevel; update both functions (openLevel and
closeLevel) to use the :scope > selector and keep the existing
classList.add/remove logic.

116-130: Promises add no value here — all operations are synchronous.

onExpandAll, onCollapseAll, onExpandChecked, and onToggle all wrap synchronous DOM class manipulations in new Promise(...). Since the executor runs synchronously, the .then() callback fires in the same microtask batch. Plain sequential calls would be clearer.

Example simplification for onExpandAll
         onExpandAll() {
-            const openPromise = new Promise((resolve, reject) => {
-                let animatedNodes = this.getExpandableNodes();
-
-                animatedNodes.forEach((item) => {
-                    this.openLevel(item);
-                });
-
-                resolve([].slice.call(animatedNodes).pop());
-            });
-
-            openPromise.then((el) => {
-                this.updateScollBar(el);
-            });
+            const animatedNodes = this.getExpandableNodes();
+            animatedNodes.forEach((item) => this.openLevel(item));
+            this.updateScrollBar(animatedNodes[animatedNodes.length - 1]);
         }

Same pattern applies to onCollapseAll, onExpandChecked, and onToggle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` around lines
116 - 130, The Promise wrapper is unnecessary because getExpandableNodes(),
openLevel(), etc. are synchronous; in onExpandAll remove the new Promise(...)
and simply iterate over this.getExpandableNodes(), call this.openLevel(item) for
each, then call this.updateScollBar(...) directly with the last element; apply
the same simplification to onCollapseAll, onExpandChecked, and onToggle (remove
new Promise, perform DOM class changes synchronously, then call the
corresponding this.updateScollBar/updateScrollBar invocation). Use the existing
method names getExpandableNodes, openLevel, onCollapseAll, onExpandChecked,
onToggle, and updateScollBar when locating code to change.
modules/backend/formwidgets/Relation.php (2)

52-55: Minor docblock typo.

"rendered has a displayTree" → "rendered as a tree".

Proposed fix
     /**
-     * `@var` bool Define if the widget must be rendered has a displayTree.
+     * `@var` bool Define if the widget must be rendered as a tree.
      */
     public $displayTree;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/Relation.php` around lines 52 - 55, Docblock for
the Relation widget property $displayTree contains a typo ("rendered has a
displayTree"); update the comment for the public $displayTree property in
Relation.php to read clearly (e.g., "Define if the widget must be rendered as a
tree") so the docblock accurately describes the boolean flag and its purpose.

163-164: class_uses() doesn't traverse parent classes — use class_uses_recursive() instead.

class_uses($relationModel) only returns traits directly used by the class, not inherited ones. If a model extends a base that uses NestedTree, this check will silently fail and displayTree will have no effect. This is pre-existing code, but the new displayTree feature now depends on it. The codebase already uses class_uses_recursive() elsewhere (InspectableContainer.php), establishing the correct pattern.

Proposed fix
-            $usesTree = count(array_intersect($treeTraits, class_uses($relationModel))) > 0;
+            $usesTree = count(array_intersect($treeTraits, class_uses_recursive($relationModel))) > 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/Relation.php` around lines 163 - 164, The check
that determines whether the relation model uses a tree trait uses
class_uses($relationModel), which only returns traits declared on the class
itself; replace that call with class_uses_recursive($relationModel) so inherited
traits like Winter\Storm\Database\Traits\NestedTree or SimpleTree are detected;
update the $usesTree assignment (which compares $treeTraits against the
class_uses result) to call class_uses_recursive($relationModel) instead of
class_uses($relationModel) so displayTree behavior works for models inheriting
tree traits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modules/backend/formwidgets/Relation.php`:
- Around line 188-190: The code calls a non-existent method toNestedArray on the
collection ($result->toNestedArray($nameFrom, $primaryKeyName)) when
$this->displayTree is true; replace that call with the existing, tested
collection helper listsNested (i.e. use $result->listsNested($nameFrom,
$primaryKeyName)) or implement an equivalent toNestedArray on the collection,
and keep the fallback logic for non-tree relations intact so Relation.php uses
listsNested instead of toNestedArray when building $field->options.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js`:
- Around line 73-81: destruct() currently assumes the tree action controls exist
and calls removeEventListener unconditionally, causing a TypeError when
expandAllControl/collapseAllControl/expandCheckedControl are null; update
destruct() to mirror attachEvents() by null-checking each of expandAllControl,
collapseAllControl, and expandCheckedControl before calling removeEventListener,
and keep the existing loop for this.toggles that removes events.toggle;
reference the destruct() and attachEvents() methods and the properties
expandAllControl, collapseAllControl, expandCheckedControl, toggles, and
events.toggle when making the change.
- Around line 206-216: The method updateScollBar has a typo and can throw a
TypeError when openedLevel is null; rename the method to updateScrollBar and
update every call site that uses updateScollBar, then inside the (now)
updateScrollBar method compute openedLevel as before but guard it with a null
check (if openedLevel is null/undefined return) before calling
openedLevel.addEventListener; keep the existing transitionend handler and
options unchanged. Ensure all references in this file that called updateScollBar
(the earlier calls around the checkbox/children handling) are renamed to
updateScrollBar so usages and exports stay consistent.

In `@modules/backend/formwidgets/relation/assets/less/relation.less`:
- Around line 4-9: Remove the empty line between the rule opening brace and the
comment in relation.less: locate the selector ".field-checkboxlist
.checkboxlist-controls > div:nth-child(even)" and the comment "// Top widget
controls" and place the comment immediately after the opening brace (no blank
line) so the scss/double-slash-comment-empty-line-before lint rule is satisfied.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php`:
- Line 136: The call to e(renderCheckboxLine(...)) is incorrect because
renderCheckboxLine() echoes HTML and returns void; remove the unnecessary e()
wrapper and call renderCheckboxLine($field, $checkedValues, $children,
$nameFrom, $readOnly, $index) directly in the template so the output is rendered
as intended, and if any escaping is actually required wrap the producing
function to return a string instead of echoing before escaping.
- Around line 96-143: The named function renderCheckboxLine causes a fatal
redeclaration when the partial is included multiple times; replace it with a
recursive closure: define $renderCheckboxLine = function($field, array
$checkedValues, array $fieldOptions, string $nameFrom, bool $readOnly, int
&$index) use (&$renderCheckboxLine) { ... } containing the same body, and update
the recursive call and any invocations to call $renderCheckboxLine(...) (remove
the wrapping e(...) used for renderCheckboxLine since the closure echoes
directly) so the template uses the closure instead of a global function.
- Line 2: The partial currently unconditionally reads $this->nameFrom which
doesn't exist on Form widget; change the assignment so it only reads
$this->nameFrom when available and otherwise uses a passed-in fallback (e.g. a
local $nameFrom variable) or null — do not access $this->nameFrom on Form
contexts. Ensure the conditional access is applied before any use (notably where
displayTree is handled around the block that references nameFrom later), and
update the partial to accept/use a provided $nameFrom when rendered from widgets
that don't expose $this->nameFrom.

---

Outside diff comments:
In `@modules/backend/formwidgets/Relation.php`:
- Around line 184-196: Remove the dead initial assignment to $field->options
that uses the ternary based on $usesTree; instead keep the subsequent
conditional block which correctly handles $usesTree and $this->displayTree and
calls $result->toNestedArray, $result->listsNested, or $result->lists as
appropriate. Specifically, delete the first assignment (the ternary setting
$field->options) and rely on the existing if ($usesTree) { ... } else { ... }
logic in Relation.php so $field->options is only set once with the correct
tree-aware method.

---

Nitpick comments:
In `@modules/backend/formwidgets/Relation.php`:
- Around line 52-55: Docblock for the Relation widget property $displayTree
contains a typo ("rendered has a displayTree"); update the comment for the
public $displayTree property in Relation.php to read clearly (e.g., "Define if
the widget must be rendered as a tree") so the docblock accurately describes the
boolean flag and its purpose.
- Around line 163-164: The check that determines whether the relation model uses
a tree trait uses class_uses($relationModel), which only returns traits declared
on the class itself; replace that call with class_uses_recursive($relationModel)
so inherited traits like Winter\Storm\Database\Traits\NestedTree or SimpleTree
are detected; update the $usesTree assignment (which compares $treeTraits
against the class_uses result) to call class_uses_recursive($relationModel)
instead of class_uses($relationModel) so displayTree behavior works for models
inheriting tree traits.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js`:
- Around line 183-199: The onToggle method currently treats its parameter as an
HTMLElement and names it `el`, but it receives an Event from
addEventListener('click', this.events.toggle); rename the parameter (e.g.,
`event` or `evt`) and update usages to `event.target.parentElement` (or safely
`event.currentTarget` if appropriate) and correct the variable typo
`tooglePromise` to `togglePromise` in the onToggle function so the code is
clearer and accurate; ensure JSDoc/@param is updated from HTMLElement to Event
as well.
- Around line 88-95: openLevel (and the sibling closeLevel) currently use
el.querySelectorAll('.checkboxlist-children')[0], which can return deeply nested
descendants; change these to select only direct children by using
el.querySelector(':scope > .checkboxlist-children') (or an equivalent
direct-child selection) so only the immediate .checkboxlist-children element is
toggled in openLevel and closeLevel; update both functions (openLevel and
closeLevel) to use the :scope > selector and keep the existing
classList.add/remove logic.
- Around line 116-130: The Promise wrapper is unnecessary because
getExpandableNodes(), openLevel(), etc. are synchronous; in onExpandAll remove
the new Promise(...) and simply iterate over this.getExpandableNodes(), call
this.openLevel(item) for each, then call this.updateScollBar(...) directly with
the last element; apply the same simplification to onCollapseAll,
onExpandChecked, and onToggle (remove new Promise, perform DOM class changes
synchronously, then call the corresponding this.updateScollBar/updateScrollBar
invocation). Use the existing method names getExpandableNodes, openLevel,
onCollapseAll, onExpandChecked, onToggle, and updateScollBar when locating code
to change.

In `@modules/backend/formwidgets/relation/assets/less/relation.less`:
- Around line 16-19: Replace the CSS value "start" with the Flexbox-specific
"flex-start" for the justify-content declaration in relation.less (the block
that sets display: flex; flex-direction: row; align-items: center;
justify-content: start;), changing justify-content to "flex-start" to ensure
broader browser support.

In `@modules/backend/lang/en/lang.php`:
- Around line 326-328: The wording in the form translation keys is inconsistent
with the dashboard translations: update the values for 'expand_all',
'collapse_all', and 'expand_checked' in the lang.php translations so they match
the dashboard terminology (use "Expand all" instead of "Open all", "Collapse
all" instead of "Close all", and "Expand selected" instead of "Open selected")
to ensure consistent UX across the backend UI.

Comment on lines +73 to +81
destruct() {
this.expandAllControl.removeEventListener('click', this.events.expandAll);
this.collapseAllControl.removeEventListener('click', this.events.collapseAll);
this.expandCheckedControl.removeEventListener('click', this.events.expandChecked);

this.toggles.forEach((toggle) => {
toggle.removeEventListener('click', this.events.toggle)
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

destruct() will throw if tree action controls don't exist.

attachEvents() (lines 55–63) null-checks expandAllControl, collapseAllControl, and expandCheckedControl before adding listeners, but destruct() unconditionally calls removeEventListener on them. When quickTreeActions is false, these are null and destruct() will throw a TypeError.

Proposed fix
         destruct() {
-            this.expandAllControl.removeEventListener('click', this.events.expandAll);
-            this.collapseAllControl.removeEventListener('click', this.events.collapseAll);
-            this.expandCheckedControl.removeEventListener('click', this.events.expandChecked);
+            if (this.expandAllControl) {
+                this.expandAllControl.removeEventListener('click', this.events.expandAll);
+            }
+            if (this.collapseAllControl) {
+                this.collapseAllControl.removeEventListener('click', this.events.collapseAll);
+            }
+            if (this.expandCheckedControl) {
+                this.expandCheckedControl.removeEventListener('click', this.events.expandChecked);
+            }
 
             this.toggles.forEach((toggle) => {
                 toggle.removeEventListener('click', this.events.toggle)
             });
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` around lines
73 - 81, destruct() currently assumes the tree action controls exist and calls
removeEventListener unconditionally, causing a TypeError when
expandAllControl/collapseAllControl/expandCheckedControl are null; update
destruct() to mirror attachEvents() by null-checking each of expandAllControl,
collapseAllControl, and expandCheckedControl before calling removeEventListener,
and keep the existing loop for this.toggles that removes events.toggle;
reference the destruct() and attachEvents() methods and the properties
expandAllControl, collapseAllControl, expandCheckedControl, toggles, and
events.toggle when making the change.

Comment on lines +206 to +216
updateScollBar(el) {
if (el === undefined) {
return;
}

let openedLevel = el.classList.contains("checkboxlist-children") ? el : el.querySelector('.checkboxlist-children');

openedLevel.addEventListener("transitionend", () => {
$('[data-control=scrollbar]').data('oc.scrollbar').update();
}, {once: true});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential TypeError if openedLevel is null, plus typo in method name.

Line 211: if el doesn't have class checkboxlist-children and has no such descendant, openedLevel will be null, and line 213 will throw. Also, updateScollBar should be updateScrollBar.

Proposed fix
-        updateScollBar(el) {
+        updateScrollBar(el) {
             if (el === undefined) {
                 return;
             }
 
             let openedLevel = el.classList.contains("checkboxlist-children") ? el : el.querySelector('.checkboxlist-children');
 
+            if (!openedLevel) {
+                return;
+            }
+
             openedLevel.addEventListener("transitionend", () => {
                 $('[data-control=scrollbar]').data('oc.scrollbar').update();
             }, {once: true});
         }

Also update all call sites (updateScollBarupdateScrollBar) on lines 128, 149, 172, and 197.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
updateScollBar(el) {
if (el === undefined) {
return;
}
let openedLevel = el.classList.contains("checkboxlist-children") ? el : el.querySelector('.checkboxlist-children');
openedLevel.addEventListener("transitionend", () => {
$('[data-control=scrollbar]').data('oc.scrollbar').update();
}, {once: true});
}
updateScrollBar(el) {
if (el === undefined) {
return;
}
let openedLevel = el.classList.contains("checkboxlist-children") ? el : el.querySelector('.checkboxlist-children');
if (!openedLevel) {
return;
}
openedLevel.addEventListener("transitionend", () => {
$('[data-control=scrollbar]').data('oc.scrollbar').update();
}, {once: true});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` around lines
206 - 216, The method updateScollBar has a typo and can throw a TypeError when
openedLevel is null; rename the method to updateScrollBar and update every call
site that uses updateScollBar, then inside the (now) updateScrollBar method
compute openedLevel as before but guard it with a null check (if openedLevel is
null/undefined return) before calling openedLevel.addEventListener; keep the
existing transitionend handler and options unchanged. Ensure all references in
this file that called updateScollBar (the earlier calls around the
checkbox/children handling) are renamed to updateScrollBar so usages and exports
stay consistent.

Comment on lines +4 to +9

// Top widget controls
.field-checkboxlist .checkboxlist-controls > div:nth-child(even) {
margin-left: auto;
margin-right: 0;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix empty line before comment (Stylelint).

Stylelint reports scss/double-slash-comment-empty-line-before on line 5. Remove the blank line 4 between the selector opening brace and the comment.

Proposed fix
 div[data-control="relation"] {
-
     // Top widget controls
     .field-checkboxlist .checkboxlist-controls > div:nth-child(even) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Top widget controls
.field-checkboxlist .checkboxlist-controls > div:nth-child(even) {
margin-left: auto;
margin-right: 0;
}
div[data-control="relation"] {
// Top widget controls
.field-checkboxlist .checkboxlist-controls > div:nth-child(even) {
margin-left: auto;
margin-right: 0;
}
}
🧰 Tools
🪛 Stylelint (17.3.0)

[error] 5-5: Unexpected empty line before comment (scss/double-slash-comment-empty-line-before)

(scss/double-slash-comment-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/less/relation.less` around lines
4 - 9, Remove the empty line between the rule opening brace and the comment in
relation.less: locate the selector ".field-checkboxlist .checkboxlist-controls >
div:nth-child(even)" and the comment "// Top widget controls" and place the
comment immediately after the opening brace (no blank line) so the
scss/double-slash-comment-empty-line-before lint rule is satisfied.

@@ -1,8 +1,11 @@
<?php
$nameFrom = $this->nameFrom;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all places that render _field_checkboxlist.php
echo "=== Renderers of _field_checkboxlist.php ==="
rg -n '_field_checkboxlist' --type=php -C3

Repository: wintercms/winter

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Search for nameFrom property definitions in widget classes
echo "=== Files defining nameFrom property ==="
rg -n 'nameFrom' --type=php -C2 | head -100

Repository: wintercms/winter

Length of output: 7575


🏁 Script executed:

#!/bin/bash
# Check Form widget class structure
echo "=== Searching for Form widget class ==="
fd -e php -path '*/form/*' | grep -i widget

Repository: wintercms/winter

Length of output: 285


🏁 Script executed:

#!/bin/bash
# Search for makePartial and other render calls mentioning field_checkboxlist
rg 'field_checkboxlist' --type=php

Repository: wintercms/winter

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for Form widget class and its properties
rg -n 'class.*Form.*Widget|class Form' --type=php | head -20

Repository: wintercms/winter

Length of output: 1864


🏁 Script executed:

#!/bin/bash
# Check if Form widget class defines nameFrom
rg -n 'class.*Form' --type=php -A 100 | rg 'nameFrom|public \$'

Repository: wintercms/winter

Length of output: 10268


🏁 Script executed:

#!/bin/bash
# Search for checkboxlist field type usage and partials
rg -n "checkboxlist" --type=php | head -50

Repository: wintercms/winter

Length of output: 2356


🏁 Script executed:

#!/bin/bash
# Search for makePartial calls in widget files
rg -n "makePartial.*field" --type=php

Repository: wintercms/winter

Length of output: 1620


🏁 Script executed:

#!/bin/bash
# Look at the Form widget's renderFormField method to see how it renders partials
rg -n "renderFormField|makePartial" modules/backend/widgets/Form.php -A 5

Repository: wintercms/winter

Length of output: 1826


🏁 Script executed:

#!/bin/bash
# Check the Relation widget's _relation.php partial to see how it renders the field
cat -n modules/backend/formwidgets/relation/partials/_relation.php

Repository: wintercms/winter

Length of output: 331


🏁 Script executed:

#!/bin/bash
# Check if Form widget's renderField method also renders _field_checkboxlist.php
cat -n modules/backend/widgets/Form.php | sed -n '230,280p'

Repository: wintercms/winter

Length of output: 1884


🏁 Script executed:

#!/bin/bash
# Check which widgets extend from Form widget or use checkboxlist fields with nameFrom
rg -n "type.*checkboxlist|field.*type.*checkboxlist" --type=php -B 5 -A 5

Repository: wintercms/winter

Length of output: 2123


🏁 Script executed:

#!/bin/bash
# Look at the Form test to see how checkboxlist is used without nameFrom
cat -n modules/backend/tests/widgets/FormTest.php | sed -n '130,160p'

Repository: wintercms/winter

Length of output: 1347


🏁 Script executed:

#!/bin/bash
# Check if the partial accesses nameFrom in a way that would fail without it
cat -n modules/backend/widgets/form/partials/_field_checkboxlist.php | sed -n '1,30p'

Repository: wintercms/winter

Length of output: 1359


🏁 Script executed:

#!/bin/bash
# Check if there's a default nameFrom in the partial or if it's always expected
rg -n "nameFrom\|options\(\)" modules/backend/widgets/form/partials/_field_checkboxlist.php | head -10

Repository: wintercms/winter

Length of output: 42


🏁 Script executed:

#!/bin/bash
# See the complete usage of nameFrom variable in the partial
cat -n modules/backend/widgets/form/partials/_field_checkboxlist.php | sed -n '1,10p'

Repository: wintercms/winter

Length of output: 589


🏁 Script executed:

#!/bin/bash
# Check what happens when Form widget renders - does it pass any nameFrom context?
rg -n "renderFieldElement|field_checkboxlist" modules/backend/widgets/Form.php -B 5 -A 10

Repository: wintercms/winter

Length of output: 564


🏁 Script executed:

#!/bin/bash
# Verify if makePartial in Form widget context has access to nameFrom or passes it
rg -n "makePartial.*field.*checkboxlist|renderFieldElement" modules/backend/widgets/Form.php -A 15 | head -40

Repository: wintercms/winter

Length of output: 458


🏁 Script executed:

#!/bin/bash
# Check how $nameFrom is used throughout the partial - especially the dangerous line
rg -n "\$nameFrom" modules/backend/widgets/form/partials/_field_checkboxlist.php

Repository: wintercms/winter

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check if Form widget class actually inherits nameFrom from a parent class
rg -n "class Form|nameFrom" modules/backend/widgets/Form.php | head -20

Repository: wintercms/winter

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Check what the parent class WidgetBase provides
fd -e php -name "WidgetBase.php" | xargs rg -n "public \$nameFrom|private \$nameFrom"

Repository: wintercms/winter

Length of output: 292


🏁 Script executed:

#!/bin/bash
# Find WidgetBase class
fd -e php -name "WidgetBase.php"

Repository: wintercms/winter

Length of output: 290


🏁 Script executed:

#!/bin/bash
# Check the parent WidgetBase class for nameFrom property
find . -name "WidgetBase.php" -type f | xargs cat | head -100

Repository: wintercms/winter

Length of output: 2488


🏁 Script executed:

#!/bin/bash
# Check complete usage of nameFrom in the checkboxlist partial
grep -n "nameFrom" modules/backend/widgets/form/partials/_field_checkboxlist.php

Repository: wintercms/winter

Length of output: 466


🏁 Script executed:

#!/bin/bash
# Check if there are any default values or fallbacks for nameFrom
rg -n "nameFrom" modules/backend/widgets/Form.php

Repository: wintercms/winter

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check what happens when Form widget renders checkboxlist - verify if it actually uses nameFrom
cat -n modules/backend/widgets/form/partials/_field_checkboxlist.php | sed -n '1,50p'

Repository: wintercms/winter

Length of output: 2247


🏁 Script executed:

#!/bin/bash
# Look at the complete section where nameFrom is used to understand when it matters
cat -n modules/backend/widgets/form/partials/_field_checkboxlist.php | sed -n '100,150p'

Repository: wintercms/winter

Length of output: 2951


🏁 Script executed:

#!/bin/bash
# Check if there's any error suppression or try/catch when accessing undefined properties
rg -n "error_reporting|@\$this" modules/backend/widgets/form/partials/_field_checkboxlist.php

Repository: wintercms/winter

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Verify by running the Form widget test to see if it actually passes
git log --oneline modules/backend/tests/widgets/FormTest.php | head -5

Repository: wintercms/winter

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Let's verify: Does Form widget actually get used with complex checkboxlist options with children?
rg -n "displayTree|children" modules/backend/tests/widgets/FormTest.php

Repository: wintercms/winter

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check if there are integration tests that actually render Form with checkboxlist having nameFrom-like options
rg -n "nameFrom|checkboxlist" modules/backend/tests/ -l

Repository: wintercms/winter

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Final check: What happens when PHP accesses undefined property?
# Let's see if there's any property_exists check or default handling
cat -n modules/backend/classes/FormWidgetBase.php | head -50

Repository: wintercms/winter

Length of output: 1460


🏁 Script executed:

#!/bin/bash
# Final verification: does the Form widget test actually pass and render the checkbox content?
cat -n modules/backend/tests/widgets/FormTest.php | sed -n '131,160p'

Repository: wintercms/winter

Length of output: 1339


🏁 Script executed:

#!/bin/bash
# Check what error_reporting or error handling exists in Winter CMS
rg -n "error_reporting|E_ALL" modules/backend/ -l | head -5

Repository: wintercms/winter

Length of output: 311


🏁 Script executed:

#!/bin/bash
# Verify if undefined property access is being suppressed or if there's a __get magic method
rg -n "__get|__isset|error" modules/backend/classes/FormWidgetBase.php

Repository: wintercms/winter

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Final check: Verify if Extendable base class or traits provide __get magic method
rg -n "class Extendable|__get|__isset" --type=php | grep -E "Extendable|__get|__isset" | head -20

Repository: wintercms/winter

Length of output: 1041


🏁 Script executed:

#!/bin/bash
# Check if the test actually triggers the nameFrom code path
cat -n modules/backend/widgets/form/partials/_field_checkboxlist.php | sed -n '80,100p'

Repository: wintercms/winter

Length of output: 862


🏁 Script executed:

#!/bin/bash
# Verify the condition - does renderCheckboxLine get called from Form widget test?
# Check if the test uses displayTree or structured options
cat -n modules/backend/tests/widgets/FormTest.php | sed -n '131,160p'

Repository: wintercms/winter

Length of output: 1339


🏁 Script executed:

#!/bin/bash
# Final verification: does accessing undefined property throw error or just warning?
# Check PHP error handling in the codebase
rg -n "error_reporting\|ini_set.*error" modules/backend/ --type=php | head -3

Repository: wintercms/winter

Length of output: 42


🏁 Script executed:

#!/bin/bash
# See the exact code path: when does nameFrom actually get used?
cat -n modules/backend/widgets/form/partials/_field_checkboxlist.php | sed -n '90,150p'

Repository: wintercms/winter

Length of output: 3285


🏁 Script executed:

#!/bin/bash
# Final check: confirm PHP will generate an error when accessing undefined property
# Check if displayTree defaults to false and when simple options don't trigger the nameFrom usage
cat -n modules/backend/widgets/form/partials/_field_checkboxlist.php | sed -n '1,15p'

Repository: wintercms/winter

Length of output: 741


🏁 Script executed:

#!/bin/bash
# Verify if the test actually exercises the nameFrom code path
# Look at what field->options() returns
rg -n "options\(\)|displayTree" modules/backend/classes/FormField.php | head -10

Repository: wintercms/winter

Length of output: 42


Fix undefined property access: $this->nameFrom doesn't exist on Form widget.

The partial unconditionally accesses $this->nameFrom on line 2, but this property only exists on Relation, TagList, and RecordFinder widgets—not on Form widget. When Form renders a checkboxlist field, this will generate a PHP notice. The property is actually used in lines 126+ (when displayTree is true) and should either be conditionally accessed with a fallback or passed as a variable when the partial is rendered from different widget contexts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php` at line 2, The
partial currently unconditionally reads $this->nameFrom which doesn't exist on
Form widget; change the assignment so it only reads $this->nameFrom when
available and otherwise uses a passed-in fallback (e.g. a local $nameFrom
variable) or null — do not access $this->nameFrom on Form contexts. Ensure the
conditional access is applied before any use (notably where displayTree is
handled around the block that references nameFrom later), and update the partial
to accept/use a provided $nameFrom when rendered from widgets that don't expose
$this->nameFrom.

Comment on lines +96 to 143
function renderCheckboxLine(
$field,
array $checkedValues,
array $fieldOptions,
string $nameFrom,
bool $readOnly,
int &$index
) {
foreach ($fieldOptions as $value => $option):
$index++;
$checkboxId = 'checkbox_'. $field->getId() .'_'. $index;

if (!is_array($option)) {
$option = [$option];
}
$children = array_get($option, 'children', []);
?>

<div class="checkboxlist-item">

<div class="checkbox custom-checkbox">
<input
type="checkbox"
id="<?= $checkboxId ?>"
name="<?= $field->getName() ?>[]"
value="<?= e($value) ?>"
<?= $readOnly ? 'disabled="disabled"' : '' ?>
<?= in_array($value, $checkedValues) ? 'checked="checked"' : '' ?>>

<label for="<?= $checkboxId ?>">
<?= e(trans($option[$nameFrom])) ?>
</label>
</div>

<?php if (count($children) > 0): ?>
<a href="javascript:;" class="checkboxlist-item-toggle">
<i class="icon-chevron-right"></i>
</a>
<div class="checkboxlist-children">
<div id="<?= $checkboxId ?>_children">
<?php e(renderCheckboxLine($field, $checkedValues, $children, $nameFrom, $readOnly, $index)); ?>
</div>
</div>
<?php endif ?>
</div>
<?php
endforeach;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fatal error if two displayTree relations are on the same page — renderCheckboxLine is a named function.

Defining a named function inside a template means PHP will throw Cannot redeclare renderCheckboxLine() if this partial is rendered more than once in the same request (e.g., two relation fields with displayTree enabled on the same form). Convert this to a closure assigned to a variable.

Proposed fix — use a closure instead
-                function renderCheckboxLine(
+                $renderCheckboxLine = function (
                     $field,
                     array $checkedValues,
                     array $fieldOptions,
                     string $nameFrom,
                     bool $readOnly,
                     int &$index
-                ) {
+                ) use (&$renderCheckboxLine) {
                     foreach ($fieldOptions as $value => $option):
                         $index++;
                         $checkboxId = 'checkbox_'. $field->getId() .'_'. $index;
 
                         if (!is_array($option)) {
                             $option = [$option];
                         }
                         $children = array_get($option, 'children', []);
                         ?>
 
                             <div class="checkboxlist-item">
 
                                 <div class="checkbox custom-checkbox">
                                     <input
                                         type="checkbox"
                                         id="<?= $checkboxId ?>"
                                         name="<?= $field->getName() ?>[]"
                                         value="<?= e($value) ?>"
                                         <?= $readOnly ? 'disabled="disabled"' : '' ?>
                                         <?= in_array($value, $checkedValues) ? 'checked="checked"' : '' ?>>
 
                                     <label for="<?= $checkboxId ?>">
                                         <?= e(trans($option[$nameFrom])) ?>
                                     </label>
                                 </div>
 
                                 <?php if (count($children) > 0): ?>
                                     <a href="javascript:;" class="checkboxlist-item-toggle">
                                         <i class="icon-chevron-right"></i>
                                     </a>
                                     <div class="checkboxlist-children">
                                         <div id="<?= $checkboxId ?>_children">
-                                            <?php e(renderCheckboxLine($field, $checkedValues, $children, $nameFrom, $readOnly, $index)); ?>
+                                            <?php $renderCheckboxLine($field, $checkedValues, $children, $nameFrom, $readOnly, $index); ?>
                                         </div>
                                     </div>
                                 <?php endif ?>
                             </div>
                             <?php
                     endforeach;
-                }
+                };
 
-                renderCheckboxLine($field, $checkedValues, $fieldOptions, $nameFrom, $readOnly, $index);
+                $renderCheckboxLine($field, $checkedValues, $fieldOptions, $nameFrom, $readOnly, $index);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php` around lines
96 - 143, The named function renderCheckboxLine causes a fatal redeclaration
when the partial is included multiple times; replace it with a recursive
closure: define $renderCheckboxLine = function($field, array $checkedValues,
array $fieldOptions, string $nameFrom, bool $readOnly, int &$index) use
(&$renderCheckboxLine) { ... } containing the same body, and update the
recursive call and any invocations to call $renderCheckboxLine(...) (remove the
wrapping e(...) used for renderCheckboxLine since the closure echoes directly)
so the template uses the closure instead of a global function.

</a>
<div class="checkboxlist-children">
<div id="<?= $checkboxId ?>_children">
<?php e(renderCheckboxLine($field, $checkedValues, $children, $nameFrom, $readOnly, $index)); ?>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Unnecessary e() wrapper around renderCheckboxLine call.

renderCheckboxLine outputs HTML directly as a side effect and returns void. Wrapping it in e() is misleading—e(null) just produces an empty string. Remove the e() call.

Proposed fix
-                                            <?php e(renderCheckboxLine($field, $checkedValues, $children, $nameFrom, $readOnly, $index)); ?>
+                                            <?php renderCheckboxLine($field, $checkedValues, $children, $nameFrom, $readOnly, $index); ?>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php` at line 136,
The call to e(renderCheckboxLine(...)) is incorrect because renderCheckboxLine()
echoes HTML and returns void; remove the unnecessary e() wrapper and call
renderCheckboxLine($field, $checkedValues, $children, $nameFrom, $readOnly,
$index) directly in the template so the output is rendered as intended, and if
any escaping is actually required wrap the producing function to return a string
instead of echoing before escaping.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review Issues/PRs that require a review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments