Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 42 additions & 29 deletions system/assets/js/admin/specific/toggleControl/toggle.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
( function( $ ) {

$.fn.presideToggleControl = function() {
function resolveTargets( list ) {
var targets = { controls: [], fieldsets: [] }
, items = list ? list.split( "," ) : [];

for ( var i=0; i<items.length; i++ ) {
var name = $.trim( items[ i ] )
, selector = "";

if ( !name.length ) {
continue;
}

switch ( name.charAt( 0 ) ) {
case "#":
case ".":
selector = name;
break;

default:
selector = '[name="' + name + '"]';
}

if ( $( selector ).is( "fieldset" ) ) {
targets.fieldsets.push( selector );
} else {
targets.controls.push( selector );
}
}

return targets;
}

function toggleControl( $control, overridden ) {
var controlAttributes = [ "toggleFields", "toggleDefaultFields" ]
, controlChecked = false;
Expand All @@ -21,30 +53,11 @@
}

for ( var i=0; i<controlAttributes.length; i++ ) {
var controlsName = []
, fieldsets = []
, controlsList = $control.data( controlAttributes[ i ] ) ? $control.data( controlAttributes[ i ] ).split( "," ) : [];

for ( var j=0; j<controlsList.length; j++ ) {
var selector = "";

switch ( controlsList[ j ].charAt(0) ) {
case "#":
case ".":
selector = controlsList[ j ];
break;

default:
selector = '[name="' + controlsList[ j ] + '"]';
}

if ( $( selector ).is( "fieldset" ) ) {
fieldsets.push( selector );
} else {
controlsName.push( selector );
}
var targets = resolveTargets( $control.data( controlAttributes[ i ] ) )
, selectors = targets.controls.concat( targets.fieldsets );

var $child = $( selector );
for ( var j=0; j<selectors.length; j++ ) {
var $child = $( selectors[ j ] );

if ( typeof $child.data( controlAttributes[ i ] ) !== "undefined" ) {
if ( controlChecked ) {
Expand All @@ -61,13 +74,13 @@
} else {
toggleEvent = controlChecked ? "toggledon" : "toggledoff";
}
if ( fieldsets.length ) {
$( fieldsets.join( "," ) ).toggle( controlAttributes[ i ] == "toggleDefaultFields" ? !controlChecked : controlChecked );
$( fieldsets.join( "," ) ).trigger( toggleEvent );
if ( targets.fieldsets.length ) {
$( targets.fieldsets.join( "," ) ).toggle( controlAttributes[ i ] == "toggleDefaultFields" ? !controlChecked : controlChecked );
$( targets.fieldsets.join( "," ) ).trigger( toggleEvent );
}
if ( controlsName.length ) {
$( controlsName.join( "," ) ).closest( ".form-group" ).toggle( controlAttributes[ i ] == "toggleDefaultFields" ? !controlChecked : controlChecked );
$( controlsName.join( "," ) ).trigger( toggleEvent );
if ( targets.controls.length ) {
$( targets.controls.join( "," ) ).closest( ".form-group" ).toggle( controlAttributes[ i ] == "toggleDefaultFields" ? !controlChecked : controlChecked );
$( targets.controls.join( "," ) ).trigger( toggleEvent );
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
( function( $ ) {

function parsePredicate( raw ) {
var predicate = { invert: false, values: [] }
, parts = []
, i = 0;

raw = $.trim( raw || "" );

if ( raw.charAt( 0 ) === "!" ) {
predicate.invert = true;
raw = $.trim( raw.substring( 1 ) );
}

parts = raw.split( "," );
for ( i = 0; i < parts.length; i++ ) {
predicate.values.push( $.trim( parts[ i ] ) );
}

return predicate;
}

function currentValues( $wrapper ) {
var $checkable = $wrapper.find( "input:radio, input:checkbox" )
, $field = null
, val = null;

if ( $checkable.length ) {
return $checkable.filter( ":checked" ).map( function() { return String( $( this ).val() ); } ).get();
}

// Enhanced selects move the name onto a hidden field and never update the original
// select, so the named element is the only reliable source of the current value
$field = $wrapper.find( "select[name], input[name], textarea[name]" ).first();
if ( !$field.length ) {
$field = $wrapper.find( "select, input, textarea" ).first();
}
if ( !$field.length ) {
return [];
}

val = $field.val();
if ( val === null || typeof val === "undefined" ) {
return [];
}

if ( $.isArray( val ) ) {
return $.map( val, String );
}

val = String( val );

return val.indexOf( "," ) === -1 ? [ val ] : $.map( val.split( "," ), $.trim );
}

function predicateMatched( predicate, values ) {
var matched = false
, i = 0;

for ( i = 0; i < values.length && !matched; i++ ) {
matched = $.inArray( values[ i ], predicate.values ) !== -1;
}

return predicate.invert ? !matched : matched;
}

function resolveTargets( list ) {
var targets = { controls: [], fieldsets: [] }
, items = ( list || "" ).split( "," )
, i = 0
, name = ""
, selector = "";

for ( i = 0; i < items.length; i++ ) {
name = $.trim( items[ i ] );
if ( !name.length ) {
continue;
}

switch ( name.charAt( 0 ) ) {
case "#":
case ".":
selector = name;
break;
default:
selector = '[name="' + name + '"]';
}

if ( $( selector ).is( "fieldset" ) ) {
targets.fieldsets.push( selector );
} else {
targets.controls.push( selector );
}
}

return targets;
}

function cascade( $region ) {
$region.each( function() {
var $node = $( this );

if ( $node.is( "[data-toggle-when]" ) ) {
apply( $node );
}
$node.find( "[data-toggle-when]" ).each( function() {
apply( $( this ) );
} );
} );
}

function apply( $wrapper ) {
var predicate = parsePredicate( $wrapper.attr( "data-toggle-when" ) )
, selfVisible = $wrapper.is( ":visible" )
, show = selfVisible && predicateMatched( predicate, currentValues( $wrapper ) )
, targets = resolveTargets( $wrapper.attr( "data-toggle-fields" ) )
, toggleEvent = show ? "toggledon" : "toggledoff"
, $affected = $();

if ( targets.fieldsets.length ) {
var $fieldsets = $( targets.fieldsets.join( "," ) );
$fieldsets.toggle( show ).trigger( toggleEvent );
$affected = $affected.add( $fieldsets );
}

if ( targets.controls.length ) {
var $controls = $( targets.controls.join( "," ) )
, $groups = $controls.closest( ".form-group" );

$groups.toggle( show );
$controls.trigger( toggleEvent );
$affected = $affected.add( $groups );
}

cascade( $affected );
}

$.fn.presideConditionalFieldVisibility = function() {
return this.each( function() {
var $wrapper = $( this );

// Delegated so controls added after init (e.g. an enhanced select's hidden field) are covered
$wrapper.on( "change", "select, input, textarea", function() {
apply( $wrapper );
} );

apply( $wrapper );
} );
};

$( function() {
$( ".form-group[data-toggle-when]" ).presideConditionalFieldVisibility();
} );

} )( jQuery || presideJQuery );
1 change: 1 addition & 0 deletions system/config/Config.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,7 @@ component {
admin = { enabled=true , siteTemplates=[ "*" ], widgets=[] }
, cms = { enabled=true , siteTemplates=[ "*" ], widgets=[] }
, presideForms = { enabled=true , siteTemplates=[ "*" ], widgets=[] }
, conditionalFieldVisibility = { enabled=true , siteTemplates=[ "*" ], widgets=[] , dependsOn=[ "presideForms" ] }
, sitetree = { enabled=true , siteTemplates=[ "*" ], widgets=[] , dependsOn=[ "cms" ] }
, sites = { enabled=true , siteTemplates=[ "*" ], widgets=[] , dependsOn=[ "cms" ] }
, sticker = { enabled=true , siteTemplates=[ "*" ], widgets=[] , dependsOn=[ "admin" ] }
Expand Down
10 changes: 9 additions & 1 deletion system/views/formcontrols/layouts/field.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@
param name="args.nolabel" type="boolean" required="false" default="false";

hasError = Len( Trim( args.error ) );

toggleWhen = Trim( args.toggleWhen ?: "" );
toggleFields = Trim( args.toggleFields ?: "" );
conditionalToggle = Len( toggleFields ) && StructKeyExists( args, "toggleWhen" ) && isFeatureEnabled( "conditionalFieldVisibility" );

if ( conditionalToggle ) {
event.include( "/js/frontend/conditionalFieldVisibility/" );
}
</cfscript>

<cfoutput>
<div class="form-group form-group-grid<cfif hasError> has-error</cfif>">
<div class="form-group form-group-grid<cfif hasError> has-error</cfif>"<cfif conditionalToggle> data-toggle-fields="#EncodeForHtmlAttribute( toggleFields )#" data-toggle-when="#EncodeForHtmlAttribute( toggleWhen )#"</cfif>>
<cfif not args.nolabel>
<label class="control-label no-padding-right" for="#args.for#">
#args.label#
Expand Down
42 changes: 42 additions & 0 deletions tests/e2e/browser/conditionalFieldVisibility/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Conditional field visibility — browser tests

Tests for the `toggleWhen` form field attribute (PRESIDECMS-3321) and its
`conditionalFieldVisibility` plugin.

Unlike the Cypress suite in `tests/e2e/tests/`, these tests need **no running Preside
application and no datasource**. The page loads Preside's own jQuery build and the
plugin itself straight from `system/assets/`, builds markup matching what
`formcontrols/layouts/field.cfm` emits, and asserts the resulting visibility.

A real browser is required rather than a DOM emulator, because the plugin relies on
jQuery's `:visible`, which needs layout.

## Running

Open `index.html` directly in a browser, or serve the repository root and visit
`/tests/e2e/browser/conditionalFieldVisibility/index.html`.

```bash
python3 -m http.server 8900 # from the repository root
```

The summary line reports `ALL PASS — <n> assertions` or the failure count. Results are
also exposed on `window.__results` (`{ pass, fail, tests }`) with `window.__done` set
once the run finishes, for scripted/CI use.

## Coverage

| Acceptance criterion | Covered |
|---|---|
| Reveals/hides on `change` and initial render (select) | yes |
| Radio, checkbox and composite/picker-shaped controls | yes |
| `toggleWhen="a,b"` match-any; `toggleWhen="!a"` invert | yes |
| Targets by field name, `#id`, `.class`, and `fieldset` | yes |
| Wrappers without `toggleWhen` left untouched | yes |
| `toggledon` / `toggledoff` events | yes |
| Nested toggles (a revealed field that is itself a source) | yes |
| No value selected on load starts hidden | yes |

Not covered here — these need a running application: the CFML side (the
`conditionalFieldVisibility` feature flag and the emission of `data-toggle-fields` /
`data-toggle-when` onto the `.form-group` wrapper).
Loading
Loading