Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
project
drupal
Commits
045074d4
Commit
045074d4
authored
Oct 16, 2009
by
Dries
Browse files
- Patch
#557272
by kkaefer, Rob Loach, quicksketch: added FAPI JavaScript States system.
parent
60c58783
Changes
10
Hide whitespace changes
Inline
Side-by-side
includes/common.inc
View file @
045074d4
...
...
@@ -3769,6 +3769,53 @@ function drupal_process_attached($elements, $weight = JS_DEFAULT, $dependency_ch
return
$success
;
}
/**
* Adds JavaScript to the element to allow it to have different active states.
*
* @param $elements
* The structured array that may contain an array item named states. This
* array describes the different JavaScript states that can be applied to the
* element when certain contitions are met. The #states array is first keyed
* by one of the following states:
* - enabled
* - invisible
* - invalid
* - untouched
* - optional
* - filled
* - unchecked
* - irrelevant
* - expanded
* - readwrite
*
* Each of these states is an array containing conditions that must be met in
* order for this state to be active. The key to this conditioning array is
* a jQuery selector for the element that is checked. The value of the
* conditioning array are the states that are checked on the element (empty,
* checked, value, collapsed, etc) and the expected value of that condition.
*
* @code
* $form['email_canceled']['settings'] = array(
* '#type' => 'container',
* '#states' => array(
* // Hide the settings when the cancel notify checkbox is disabled.
* 'invisible' => array(
* 'input[name="email_canceled_toggle"]' => array('checked' => FALSE),
* ),
* ),
* );
* @endcode
*/
function
drupal_process_states
(
&
$elements
)
{
if
(
!
empty
(
$elements
[
'#states'
]))
{
$elements
[
'#attached'
][
'js'
][
'misc/states.js'
]
=
array
(
'weight'
=>
JS_LIBRARY
+
1
);
$elements
[
'#attached'
][
'js'
][]
=
array
(
'type'
=>
'setting'
,
'data'
=>
array
(
'states'
=>
array
(
'#'
.
$elements
[
'#id'
]
=>
$elements
[
'#states'
])),
);
}
}
/**
* Adds multiple JavaScript or CSS files at the same time.
*
...
...
@@ -4646,6 +4693,9 @@ function drupal_render(&$elements) {
}
}
// Add any JavaScript state information associated with the element.
drupal_process_states
(
$elements
);
// Add additional libraries, CSS, JavaScript an other custom
// attached data associated with this element.
drupal_process_attached
(
$elements
);
...
...
@@ -5171,6 +5221,9 @@ function drupal_common_theme() {
'vertical_tabs'
=>
array
(
'arguments'
=>
array
(
'element'
=>
NULL
),
),
'container'
=>
array
(
'arguments'
=>
array
(
'element'
=>
NULL
),
),
);
}
...
...
includes/form.inc
View file @
045074d4
...
...
@@ -2181,6 +2181,39 @@ function form_process_checkboxes($element) {
return
$element
;
}
/**
* Processes a container element.
*
* @param $element
* An associative array containing the properties and children of the
* container.
* @param $form_state
* The $form_state array for the form this element belongs to.
* @return
* The processed element.
*/
function
form_process_container
(
$element
,
&
$form_state
)
{
$element
[
'#id'
]
=
drupal_html_id
(
implode
(
'-'
,
$element
[
'#parents'
])
.
'-wrapper'
);
return
$element
;
}
/**
* Adds a container for grouped items
*
* @param $element
* An associative array containing the properties and children of the
* group.
* Properties used: #children.
* @return
* A themed HTML string representing the form element.
*
* @ingroup themeable
*/
function
theme_container
(
$variables
)
{
$element
=
$variables
[
'element'
];
return
'<div class="form-wrapper" id="'
.
$element
[
'#id'
]
.
'">'
.
$element
[
'#children'
]
.
'</div>'
;
}
/**
* Format a table with radio buttons or checkboxes.
*
...
...
misc/collapse.js
View file @
045074d4
...
...
@@ -9,7 +9,9 @@ Drupal.toggleFieldset = function (fieldset) {
// Action div containers are processed separately because of a IE bug
// that alters the default submit button behavior.
var
content
=
$
(
'
> div:not(.action)
'
,
fieldset
);
$
(
fieldset
).
removeClass
(
'
collapsed
'
);
$
(
fieldset
)
.
removeClass
(
'
collapsed
'
)
.
trigger
({
type
:
'
collapsed
'
,
value
:
false
});
content
.
hide
();
content
.
slideDown
({
duration
:
'
fast
'
,
...
...
@@ -27,6 +29,7 @@ Drupal.toggleFieldset = function (fieldset) {
}
else
{
$
(
'
div.action
'
,
fieldset
).
hide
();
$
(
fieldset
).
trigger
({
type
:
'
collapsed
'
,
value
:
true
});
var
content
=
$
(
'
> div:not(.action)
'
,
fieldset
).
slideUp
(
'
fast
'
,
function
()
{
$
(
this
.
parentNode
).
addClass
(
'
collapsed
'
);
this
.
parentNode
.
animating
=
false
;
...
...
misc/states.js
0 → 100644
View file @
045074d4
// $Id$
(
function
(
$
)
{
/**
* The base States namespace.
*
* Having the local states variable allows us to use the States namespace
* without having to always declare "Drupal.states".
*/
var
states
=
Drupal
.
states
=
{
// An array of functions that should be postponed.
postponed
:
[]
};
/**
* Attaches the states.
*/
Drupal
.
behaviors
.
states
=
{
attach
:
function
(
context
,
settings
)
{
for
(
var
selector
in
settings
.
states
)
{
for
(
var
state
in
settings
.
states
[
selector
])
{
new
states
.
Dependant
({
element
:
$
(
selector
),
state
:
states
.
State
.
sanitize
(
state
),
dependees
:
settings
.
states
[
selector
][
state
]
});
}
}
// Execute all postponed functions now.
while
(
states
.
postponed
.
length
)
{
(
states
.
postponed
.
shift
())();
}
}
};
/**
* Object representing an element that depends on other elements.
*
* @param args
* Object with the following keys (all of which are required):
* - element: A jQuery object of the dependant element
* - state: A State object describing the state that is dependant
* - dependees: An object with dependency specifications. Lists all elements
* that this element depends on.
*/
states
.
Dependant
=
function
(
args
)
{
$
.
extend
(
this
,
{
values
:
{},
oldValue
:
undefined
},
args
);
for
(
var
selector
in
this
.
dependees
)
{
this
.
initializeDependee
(
selector
,
this
.
dependees
[
selector
]);
}
};
/**
* Comparison functions for comparing the value of an element with the
* specification from the dependency settings. If the object type can't be
* found in this list, the === operator is used by default.
*/
states
.
Dependant
.
comparisons
=
{
'
RegExp
'
:
function
(
reference
,
value
)
{
return
reference
.
test
(
value
);
},
'
Function
'
:
function
(
reference
,
value
)
{
// The "reference" variable is a comparison function.
return
reference
(
value
);
}
};
states
.
Dependant
.
prototype
=
{
/**
* Initializes one of the elements this dependant depends on.
*
* @param selector
* The CSS selector describing the dependee.
* @param dependeeStates
* The list of states that have to be monitored for tracking the
* dependee's compliance status.
*/
initializeDependee
:
function
(
selector
,
dependeeStates
)
{
var
self
=
this
;
// Cache for the states of this dependee.
self
.
values
[
selector
]
=
{};
$
.
each
(
dependeeStates
,
function
(
state
,
value
)
{
state
=
states
.
State
.
sanitize
(
state
);
// Initialize the value of this state.
self
.
values
[
selector
][
state
.
pristine
]
=
undefined
;
// Monitor state changes of the specified state for this dependee.
$
(
selector
).
bind
(
'
state:
'
+
state
,
function
(
e
)
{
var
complies
=
self
.
compare
(
value
,
e
.
value
);
self
.
update
(
selector
,
state
,
complies
);
});
// Make sure the event we just bound ourselves to is actually fired.
new
states
.
Trigger
({
selector
:
selector
,
state
:
state
});
});
},
/**
* Compares a value with a reference value.
*
* @param reference
* The value used for reference.
* @param value
* The value to compare with the reference value.
* @return
* true, undefined or false.
*/
compare
:
function
(
reference
,
value
)
{
if
(
reference
.
constructor
.
name
in
states
.
Dependant
.
comparisons
)
{
// Use a custom compare function for certain reference value types.
return
states
.
Dependant
.
comparisons
[
reference
.
constructor
.
name
](
reference
,
value
);
}
else
{
// Do a plain comparison otherwise.
return
compare
(
reference
,
value
);
}
},
/**
* Update the value of a dependee's state.
*
* @param selector
* CSS selector describing the dependee.
* @param state
* A State object describing the dependee's updated state.
* @param value
* The new value for the dependee's updated state.
*/
update
:
function
(
selector
,
state
,
value
)
{
// Only act when the 'new' value is actually new.
if
(
value
!==
this
.
values
[
selector
][
state
.
pristine
])
{
this
.
values
[
selector
][
state
.
pristine
]
=
value
;
this
.
reevaluate
();
}
},
/**
* Triggers change events in case a state changed.
*/
reevaluate
:
function
()
{
var
value
=
undefined
;
// Merge all individual values to find out whether this dependee complies.
for
(
var
selector
in
this
.
values
)
{
for
(
var
state
in
this
.
values
[
selector
])
{
state
=
states
.
State
.
sanitize
(
state
);
var
complies
=
this
.
values
[
selector
][
state
.
pristine
];
value
=
ternary
(
value
,
invert
(
complies
,
state
.
invert
));
}
}
// Only invoke a state change event when the value actually changed.
if
(
value
!==
this
.
oldValue
)
{
// Store the new value so that we can compare later whether the value
// actually changed.
this
.
oldValue
=
value
;
// Normalize the value to match the normalized state name.
value
=
invert
(
value
,
this
.
state
.
invert
);
// By adding "trigger: true", we ensure that state changes don't go into
// infinite loops.
this
.
element
.
trigger
({
type
:
'
state:
'
+
this
.
state
,
value
:
value
,
trigger
:
true
});
}
}
};
states
.
Trigger
=
function
(
args
)
{
$
.
extend
(
this
,
args
);
if
(
this
.
state
in
states
.
Trigger
.
states
)
{
this
.
element
=
$
(
this
.
selector
);
// Only call the trigger initializer when it wasn't yet attached to this
// element. Otherwise we'd end up with duplicate events.
if
(
!
this
.
element
.
data
(
'
trigger:
'
+
this
.
state
))
{
this
.
initialize
();
}
}
};
states
.
Trigger
.
prototype
=
{
initialize
:
function
()
{
var
self
=
this
;
var
trigger
=
states
.
Trigger
.
states
[
this
.
state
];
if
(
typeof
trigger
==
'
function
'
)
{
// We have a custom trigger initialization function.
trigger
.
call
(
window
,
this
.
element
);
}
else
{
$
.
each
(
trigger
,
function
(
event
,
valueFn
)
{
self
.
defaultTrigger
(
event
,
valueFn
);
});
}
// Mark this trigger as initialized for this element.
this
.
element
.
data
(
'
trigger:
'
+
this
.
state
,
true
);
},
defaultTrigger
:
function
(
event
,
valueFn
)
{
var
self
=
this
;
var
oldValue
=
valueFn
.
call
(
this
.
element
);
// Attach the event callback.
this
.
element
.
bind
(
event
,
function
(
e
)
{
var
value
=
valueFn
.
call
(
self
.
element
,
e
);
// Only trigger the event if the value has actually changed.
if
(
oldValue
!==
value
)
{
self
.
element
.
trigger
({
type
:
'
state:
'
+
self
.
state
,
value
:
value
,
oldValue
:
oldValue
});
oldValue
=
value
;
}
});
states
.
postponed
.
push
(
function
()
{
// Trigger the event once for initialization purposes.
self
.
element
.
trigger
({
type
:
'
state:
'
+
self
.
state
,
value
:
oldValue
,
oldValue
:
undefined
});
});
}
};
/**
* This list of states contains functions that are used to monitor the state
* of an element. Whenever an element depends on the state of another element,
* one of these trigger functions is added to the dependee so that the
* dependant element can be updated.
*/
states
.
Trigger
.
states
=
{
// 'empty' describes the state to be monitored
empty
:
{
// 'keyup' is the (native DOM) event that we watch for.
'
keyup
'
:
function
()
{
// The function associated to that trigger returns the new value for the
// state.
return
this
.
val
()
==
''
;
}
},
checked
:
{
'
change
'
:
function
()
{
return
this
.
attr
(
'
checked
'
);
}
},
value
:
{
'
keyup
'
:
function
()
{
return
this
.
val
();
}
},
collapsed
:
{
'
collapsed
'
:
function
(
e
)
{
return
(
e
!==
undefined
&&
'
value
'
in
e
)
?
e
.
value
:
this
.
is
(
'
.collapsed
'
);
}
}
};
/**
* A state object is used for describing the state and performing aliasing.
*/
states
.
State
=
function
(
state
)
{
// We may need the original unresolved name later.
this
.
pristine
=
this
.
name
=
state
;
// Normalize the state name.
while
(
true
)
{
// Iteratively remove exclamation marks and invert the value.
while
(
this
.
name
.
charAt
(
0
)
==
'
!
'
)
{
this
.
name
=
this
.
name
.
substring
(
1
);
this
.
invert
=
!
this
.
invert
;
}
// Replace the state with its normalized name.
if
(
this
.
name
in
states
.
State
.
aliases
)
{
this
.
name
=
states
.
State
.
aliases
[
this
.
name
];
}
else
{
break
;
}
}
};
/**
* Create a new State object by sanitizing the passed value.
*/
states
.
State
.
sanitize
=
function
(
state
)
{
if
(
state
instanceof
states
.
State
)
{
return
state
;
}
else
{
return
new
states
.
State
(
state
);
}
};
/**
* This list of aliases is used to normalize states and associates negated names
* with their respective inverse state.
*/
states
.
State
.
aliases
=
{
'
enabled
'
:
'
!disabled
'
,
'
invisible
'
:
'
!visible
'
,
'
invalid
'
:
'
!valid
'
,
'
untouched
'
:
'
!touched
'
,
'
optional
'
:
'
!required
'
,
'
filled
'
:
'
!empty
'
,
'
unchecked
'
:
'
!checked
'
,
'
irrelevant
'
:
'
!relevant
'
,
'
expanded
'
:
'
!collapsed
'
,
'
readwrite
'
:
'
!readonly
'
};
states
.
State
.
prototype
=
{
invert
:
false
,
/**
* Ensures that just using the state object returns the name.
*/
toString
:
function
()
{
return
this
.
name
;
}
};
/**
* Global state change handlers. These are bound to "document" to cover all
* elements whose state changes. Events sent to elements within the page
* bubble up to these handlers. We use this system so that themes and modules
* can override these state change handlers for particular parts of a page.
*/
{
$
(
document
).
bind
(
'
state:disabled
'
,
function
(
e
)
{
// Only act when this change was triggered by a dependency and not by the
// element monitoring itself.
if
(
e
.
trigger
)
{
$
(
e
.
target
)
.
attr
(
'
disabled
'
,
e
.
value
)
.
filter
(
'
.form-element
'
)
.
closest
(
'
.form-item, .form-wrapper
'
)[
e
.
value
?
'
addClass
'
:
'
removeClass
'
](
'
form-disabled
'
);
// Note: WebKit nightlies don't reflect that change correctly.
// See https://bugs.webkit.org/show_bug.cgi?id=23789
}
});
$
(
document
).
bind
(
'
state:required
'
,
function
(
e
)
{
if
(
e
.
trigger
)
{
$
(
e
.
target
).
closest
(
'
.form-item, .form-wrapper
'
)[
e
.
value
?
'
addClass
'
:
'
removeClass
'
](
'
form-required
'
);
}
});
$
(
document
).
bind
(
'
state:visible
'
,
function
(
e
)
{
if
(
e
.
trigger
)
{
$
(
e
.
target
).
closest
(
'
.form-item, .form-wrapper
'
)[
e
.
value
?
'
show
'
:
'
hide
'
]();
}
});
$
(
document
).
bind
(
'
state:checked
'
,
function
(
e
)
{
if
(
e
.
trigger
)
{
$
(
e
.
target
).
attr
(
'
checked
'
,
e
.
value
);
}
});
$
(
document
).
bind
(
'
state:collapsed
'
,
function
(
e
)
{
if
(
e
.
trigger
)
{
if
(
$
(
e
.
target
).
is
(
'
.collapsed
'
)
!==
e
.
value
)
{
$
(
'
> legend a
'
,
e
.
target
).
click
();
}
}
});
}
/**
* These are helper functions implementing addition "operators" and don't
* implement any logic that is particular to states.
*/
{
// Bitwise AND with a third undefined state.
function
ternary
(
a
,
b
)
{
return
a
===
undefined
?
b
:
(
b
===
undefined
?
a
:
a
&&
b
);
};
// Inverts a (if it's not undefined) when invert is true.
function
invert
(
a
,
invert
)
{
return
(
invert
&&
a
!==
undefined
)
?
!
a
:
a
;
};
// Compares two values while ignoring undefined values.
function
compare
(
a
,
b
)
{
return
(
a
===
b
)
?
(
a
===
undefined
?
a
:
true
)
:
(
a
===
undefined
||
b
===
undefined
);
}
}
})(
jQuery
);
modules/node/node.pages.inc
View file @
045074d4
...
...
@@ -178,6 +178,12 @@ function node_form($form, &$form_state, $node) {
'#type'
=>
'checkbox'
,
'#title'
=>
t
(
'Create new revision'
),
'#default_value'
=>
$node
->
revision
,
'#states'
=>
array
(
// Check the revision log checkbox when the log textarea is filled in.
'checked'
=>
array
(
'textarea[name="log"]'
=>
array
(
'empty'
=>
FALSE
),
),
),
);
$form
[
'revision_information'
][
'log'
]
=
array
(
'#type'
=>
'textarea'
,
...
...
modules/system/system.admin.inc
View file @
045074d4
...
...
@@ -493,13 +493,22 @@ function system_theme_settings($form, &$form_state, $key = '') {
'#tree'
=>
FALSE
,
'#description'
=>
t
(
'Check here if you want the theme to use the logo supplied with it.'
)
);
$form
[
'logo'
][
'logo_path'
]
=
array
(
$form
[
'logo'
][
'settings'
]
=
array
(
'#type'
=>
'container'
,
'#states'
=>
array
(
// Hide the logo settings when using the default logo.
'invisible'
=>
array
(
'input[name="default_logo"]'
=>
array
(
'checked'
=>
TRUE
),
),
),
);
$form
[
'logo'
][
'settings'
][
'logo_path'
]
=
array
(
'#type'
=>
'textfield'
,
'#title'
=>
t
(
'Path to custom logo'
),
'#default_value'
=>
theme_get_setting
(
'logo_path'
,
$key
),
'#description'
=>
t
(
'The path to the file you would like to use as your logo file instead of the default logo.'
)
);
$form
[
'logo'
][
'logo_upload'
]
=
array
(
'#description'
=>
t
(
'The path to the file you would like to use as your logo file instead of the default logo.'
)
,
);
$form
[
'logo'
][
'
settings'
][
'
logo_upload'
]
=
array
(
'#type'
=>
'file'
,
'#title'
=>
t
(
'Upload logo image'
),
'#maxlength'
=>
40
,
...
...
@@ -519,13 +528,22 @@ function system_theme_settings($form, &$form_state, $key = '') {
'#default_value'
=>
theme_get_setting
(
'default_favicon'
,
$key
),
'#description'
=>
t
(
'Check here if you want the theme to use the default shortcut icon.'
)
);
$form
[
'favicon'
][
'favicon_path'
]
=
array
(
$form
[
'favicon'
][
'settings'
]
=
array
(
'#type'
=>
'container'
,
'#states'
=>
array
(
// Hide the favicon settings when using the default favicon.
'invisible'
=>
array
(
'input[name="default_favicon"]'
=>
array
(
'checked'
=>
TRUE
),
),
),
);
$form
[
'favicon'
][
'settings'
][
'favicon_path'
]
=
array
(
'#type'
=>
'textfield'
,
'#title'
=>
t
(
'Path to custom icon'
),
'#default_value'
=>
theme_get_setting
(
'favicon_path'
,
$key
),
'#description'
=>
t
(
'The path to the image file you would like to use as your custom shortcut icon.'
)
);
$form
[
'favicon'
][
'favicon_upload'
]
=
array
(
$form
[
'favicon'
][
'
settings'
][
'
favicon_upload'
]
=
array
(
'#type'
=>
'file'
,
'#title'
=>
t
(
'Upload icon image'
),
'#description'
=>
t
(
"If you don't have direct file access to the server, use this field to upload your shortcut icon."
)
...
...
@@ -1660,12 +1678,16 @@ function system_regional_settings() {
'#default_value'
=>
$configurable_timezones
,
);
$js_hide
=
!
$configurable_timezones
?
' class="js-hide"'
:
''
;
$form
[
'timezone'
][
'configurable_timezones_wrapper'
]
=
array
(
'#prefix'
=>
'<div id="empty-timezone-message-wrapper"'
.
$js_hide
.
'>'
,
'#suffix'
=>
'</div>'
,
'#type'
=>
'container'
,
'#states'
=>
array
(
// Hide the user configured timezone settings when users are forced to use
// the default setting.
'invisible'
=>
array
(
'input[name="configurable_timezones"]'
=>
array
(
'checked'
=>
FALSE
),
),
),
);
$form
[
'timezone'
][
'configurable_timezones_wrapper'
][
'empty_timezone_message'
]
=
array
(
'#type'
=>
'checkbox'
,
'#title'
=>
t
(
'Remind users at login if their time zone is not set.'
),
...
...
modules/system/system.js
View file @
045074d4
...
...
@@ -114,19 +114,6 @@ Drupal.behaviors.dateTime = {
}
};
/**