Chris@0
|
1 /**
|
Chris@0
|
2 * @file
|
Chris@0
|
3 * Drupal's states library.
|
Chris@0
|
4 */
|
Chris@0
|
5
|
Chris@0
|
6 (function ($, Drupal) {
|
Chris@0
|
7 /**
|
Chris@0
|
8 * The base States namespace.
|
Chris@0
|
9 *
|
Chris@0
|
10 * Having the local states variable allows us to use the States namespace
|
Chris@0
|
11 * without having to always declare "Drupal.states".
|
Chris@0
|
12 *
|
Chris@0
|
13 * @namespace Drupal.states
|
Chris@0
|
14 */
|
Chris@0
|
15 const states = {
|
Chris@0
|
16
|
Chris@0
|
17 /**
|
Chris@0
|
18 * An array of functions that should be postponed.
|
Chris@0
|
19 */
|
Chris@0
|
20 postponed: [],
|
Chris@0
|
21 };
|
Chris@0
|
22
|
Chris@0
|
23 Drupal.states = states;
|
Chris@0
|
24
|
Chris@0
|
25 /**
|
Chris@0
|
26 * Attaches the states.
|
Chris@0
|
27 *
|
Chris@0
|
28 * @type {Drupal~behavior}
|
Chris@0
|
29 *
|
Chris@0
|
30 * @prop {Drupal~behaviorAttach} attach
|
Chris@0
|
31 * Attaches states behaviors.
|
Chris@0
|
32 */
|
Chris@0
|
33 Drupal.behaviors.states = {
|
Chris@0
|
34 attach(context, settings) {
|
Chris@0
|
35 const $states = $(context).find('[data-drupal-states]');
|
Chris@0
|
36 const il = $states.length;
|
Chris@0
|
37 for (let i = 0; i < il; i++) {
|
Chris@0
|
38 const config = JSON.parse($states[i].getAttribute('data-drupal-states'));
|
Chris@0
|
39 Object.keys(config || {}).forEach((state) => {
|
Chris@0
|
40 new states.Dependent({
|
Chris@0
|
41 element: $($states[i]),
|
Chris@0
|
42 state: states.State.sanitize(state),
|
Chris@0
|
43 constraints: config[state],
|
Chris@0
|
44 });
|
Chris@0
|
45 });
|
Chris@0
|
46 }
|
Chris@0
|
47
|
Chris@0
|
48 // Execute all postponed functions now.
|
Chris@0
|
49 while (states.postponed.length) {
|
Chris@0
|
50 (states.postponed.shift())();
|
Chris@0
|
51 }
|
Chris@0
|
52 },
|
Chris@0
|
53 };
|
Chris@0
|
54
|
Chris@0
|
55 /**
|
Chris@0
|
56 * Object representing an element that depends on other elements.
|
Chris@0
|
57 *
|
Chris@0
|
58 * @constructor Drupal.states.Dependent
|
Chris@0
|
59 *
|
Chris@0
|
60 * @param {object} args
|
Chris@0
|
61 * Object with the following keys (all of which are required)
|
Chris@0
|
62 * @param {jQuery} args.element
|
Chris@0
|
63 * A jQuery object of the dependent element
|
Chris@0
|
64 * @param {Drupal.states.State} args.state
|
Chris@0
|
65 * A State object describing the state that is dependent
|
Chris@0
|
66 * @param {object} args.constraints
|
Chris@0
|
67 * An object with dependency specifications. Lists all elements that this
|
Chris@0
|
68 * element depends on. It can be nested and can contain
|
Chris@0
|
69 * arbitrary AND and OR clauses.
|
Chris@0
|
70 */
|
Chris@0
|
71 states.Dependent = function (args) {
|
Chris@0
|
72 $.extend(this, { values: {}, oldValue: null }, args);
|
Chris@0
|
73
|
Chris@0
|
74 this.dependees = this.getDependees();
|
Chris@0
|
75 Object.keys(this.dependees || {}).forEach((selector) => {
|
Chris@0
|
76 this.initializeDependee(selector, this.dependees[selector]);
|
Chris@0
|
77 });
|
Chris@0
|
78 };
|
Chris@0
|
79
|
Chris@0
|
80 /**
|
Chris@0
|
81 * Comparison functions for comparing the value of an element with the
|
Chris@0
|
82 * specification from the dependency settings. If the object type can't be
|
Chris@0
|
83 * found in this list, the === operator is used by default.
|
Chris@0
|
84 *
|
Chris@0
|
85 * @name Drupal.states.Dependent.comparisons
|
Chris@0
|
86 *
|
Chris@0
|
87 * @prop {function} RegExp
|
Chris@0
|
88 * @prop {function} Function
|
Chris@0
|
89 * @prop {function} Number
|
Chris@0
|
90 */
|
Chris@0
|
91 states.Dependent.comparisons = {
|
Chris@0
|
92 RegExp(reference, value) {
|
Chris@0
|
93 return reference.test(value);
|
Chris@0
|
94 },
|
Chris@0
|
95 Function(reference, value) {
|
Chris@0
|
96 // The "reference" variable is a comparison function.
|
Chris@0
|
97 return reference(value);
|
Chris@0
|
98 },
|
Chris@0
|
99 Number(reference, value) {
|
Chris@0
|
100 // If "reference" is a number and "value" is a string, then cast
|
Chris@0
|
101 // reference as a string before applying the strict comparison in
|
Chris@0
|
102 // compare().
|
Chris@0
|
103 // Otherwise numeric keys in the form's #states array fail to match
|
Chris@0
|
104 // string values returned from jQuery's val().
|
Chris@0
|
105 return (typeof value === 'string') ? compare(reference.toString(), value) : compare(reference, value);
|
Chris@0
|
106 },
|
Chris@0
|
107 };
|
Chris@0
|
108
|
Chris@0
|
109 states.Dependent.prototype = {
|
Chris@0
|
110
|
Chris@0
|
111 /**
|
Chris@0
|
112 * Initializes one of the elements this dependent depends on.
|
Chris@0
|
113 *
|
Chris@0
|
114 * @memberof Drupal.states.Dependent#
|
Chris@0
|
115 *
|
Chris@0
|
116 * @param {string} selector
|
Chris@0
|
117 * The CSS selector describing the dependee.
|
Chris@0
|
118 * @param {object} dependeeStates
|
Chris@0
|
119 * The list of states that have to be monitored for tracking the
|
Chris@0
|
120 * dependee's compliance status.
|
Chris@0
|
121 */
|
Chris@0
|
122 initializeDependee(selector, dependeeStates) {
|
Chris@0
|
123 let state;
|
Chris@0
|
124 const self = this;
|
Chris@0
|
125
|
Chris@0
|
126 function stateEventHandler(e) {
|
Chris@0
|
127 self.update(e.data.selector, e.data.state, e.value);
|
Chris@0
|
128 }
|
Chris@0
|
129
|
Chris@0
|
130 // Cache for the states of this dependee.
|
Chris@0
|
131 this.values[selector] = {};
|
Chris@0
|
132
|
Chris@0
|
133 // eslint-disable-next-line no-restricted-syntax
|
Chris@0
|
134 for (const i in dependeeStates) {
|
Chris@0
|
135 if (dependeeStates.hasOwnProperty(i)) {
|
Chris@0
|
136 state = dependeeStates[i];
|
Chris@0
|
137 // Make sure we're not initializing this selector/state combination
|
Chris@0
|
138 // twice.
|
Chris@0
|
139 if ($.inArray(state, dependeeStates) === -1) {
|
Chris@0
|
140 continue;
|
Chris@0
|
141 }
|
Chris@0
|
142
|
Chris@0
|
143 state = states.State.sanitize(state);
|
Chris@0
|
144
|
Chris@0
|
145 // Initialize the value of this state.
|
Chris@0
|
146 this.values[selector][state.name] = null;
|
Chris@0
|
147
|
Chris@0
|
148 // Monitor state changes of the specified state for this dependee.
|
Chris@0
|
149 $(selector).on(`state:${state}`, { selector, state }, stateEventHandler);
|
Chris@0
|
150
|
Chris@0
|
151 // Make sure the event we just bound ourselves to is actually fired.
|
Chris@0
|
152 new states.Trigger({ selector, state });
|
Chris@0
|
153 }
|
Chris@0
|
154 }
|
Chris@0
|
155 },
|
Chris@0
|
156
|
Chris@0
|
157 /**
|
Chris@0
|
158 * Compares a value with a reference value.
|
Chris@0
|
159 *
|
Chris@0
|
160 * @memberof Drupal.states.Dependent#
|
Chris@0
|
161 *
|
Chris@0
|
162 * @param {object} reference
|
Chris@0
|
163 * The value used for reference.
|
Chris@0
|
164 * @param {string} selector
|
Chris@0
|
165 * CSS selector describing the dependee.
|
Chris@0
|
166 * @param {Drupal.states.State} state
|
Chris@0
|
167 * A State object describing the dependee's updated state.
|
Chris@0
|
168 *
|
Chris@0
|
169 * @return {bool}
|
Chris@0
|
170 * true or false.
|
Chris@0
|
171 */
|
Chris@0
|
172 compare(reference, selector, state) {
|
Chris@0
|
173 const value = this.values[selector][state.name];
|
Chris@0
|
174 if (reference.constructor.name in states.Dependent.comparisons) {
|
Chris@0
|
175 // Use a custom compare function for certain reference value types.
|
Chris@0
|
176 return states.Dependent.comparisons[reference.constructor.name](reference, value);
|
Chris@0
|
177 }
|
Chris@0
|
178
|
Chris@0
|
179 // Do a plain comparison otherwise.
|
Chris@0
|
180 return compare(reference, value);
|
Chris@0
|
181 },
|
Chris@0
|
182
|
Chris@0
|
183 /**
|
Chris@0
|
184 * Update the value of a dependee's state.
|
Chris@0
|
185 *
|
Chris@0
|
186 * @memberof Drupal.states.Dependent#
|
Chris@0
|
187 *
|
Chris@0
|
188 * @param {string} selector
|
Chris@0
|
189 * CSS selector describing the dependee.
|
Chris@0
|
190 * @param {Drupal.states.state} state
|
Chris@0
|
191 * A State object describing the dependee's updated state.
|
Chris@0
|
192 * @param {string} value
|
Chris@0
|
193 * The new value for the dependee's updated state.
|
Chris@0
|
194 */
|
Chris@0
|
195 update(selector, state, value) {
|
Chris@0
|
196 // Only act when the 'new' value is actually new.
|
Chris@0
|
197 if (value !== this.values[selector][state.name]) {
|
Chris@0
|
198 this.values[selector][state.name] = value;
|
Chris@0
|
199 this.reevaluate();
|
Chris@0
|
200 }
|
Chris@0
|
201 },
|
Chris@0
|
202
|
Chris@0
|
203 /**
|
Chris@0
|
204 * Triggers change events in case a state changed.
|
Chris@0
|
205 *
|
Chris@0
|
206 * @memberof Drupal.states.Dependent#
|
Chris@0
|
207 */
|
Chris@0
|
208 reevaluate() {
|
Chris@0
|
209 // Check whether any constraint for this dependent state is satisfied.
|
Chris@0
|
210 let value = this.verifyConstraints(this.constraints);
|
Chris@0
|
211
|
Chris@0
|
212 // Only invoke a state change event when the value actually changed.
|
Chris@0
|
213 if (value !== this.oldValue) {
|
Chris@0
|
214 // Store the new value so that we can compare later whether the value
|
Chris@0
|
215 // actually changed.
|
Chris@0
|
216 this.oldValue = value;
|
Chris@0
|
217
|
Chris@0
|
218 // Normalize the value to match the normalized state name.
|
Chris@0
|
219 value = invert(value, this.state.invert);
|
Chris@0
|
220
|
Chris@0
|
221 // By adding "trigger: true", we ensure that state changes don't go into
|
Chris@0
|
222 // infinite loops.
|
Chris@0
|
223 this.element.trigger({ type: `state:${this.state}`, value, trigger: true });
|
Chris@0
|
224 }
|
Chris@0
|
225 },
|
Chris@0
|
226
|
Chris@0
|
227 /**
|
Chris@0
|
228 * Evaluates child constraints to determine if a constraint is satisfied.
|
Chris@0
|
229 *
|
Chris@0
|
230 * @memberof Drupal.states.Dependent#
|
Chris@0
|
231 *
|
Chris@0
|
232 * @param {object|Array} constraints
|
Chris@0
|
233 * A constraint object or an array of constraints.
|
Chris@0
|
234 * @param {string} selector
|
Chris@0
|
235 * The selector for these constraints. If undefined, there isn't yet a
|
Chris@0
|
236 * selector that these constraints apply to. In that case, the keys of the
|
Chris@0
|
237 * object are interpreted as the selector if encountered.
|
Chris@0
|
238 *
|
Chris@0
|
239 * @return {bool}
|
Chris@0
|
240 * true or false, depending on whether these constraints are satisfied.
|
Chris@0
|
241 */
|
Chris@0
|
242 verifyConstraints(constraints, selector) {
|
Chris@0
|
243 let result;
|
Chris@0
|
244 if ($.isArray(constraints)) {
|
Chris@0
|
245 // This constraint is an array (OR or XOR).
|
Chris@0
|
246 const hasXor = $.inArray('xor', constraints) === -1;
|
Chris@0
|
247 const len = constraints.length;
|
Chris@0
|
248 for (let i = 0; i < len; i++) {
|
Chris@0
|
249 if (constraints[i] !== 'xor') {
|
Chris@0
|
250 const constraint = this.checkConstraints(constraints[i], selector, i);
|
Chris@0
|
251 // Return if this is OR and we have a satisfied constraint or if
|
Chris@0
|
252 // this is XOR and we have a second satisfied constraint.
|
Chris@0
|
253 if (constraint && (hasXor || result)) {
|
Chris@0
|
254 return hasXor;
|
Chris@0
|
255 }
|
Chris@0
|
256 result = result || constraint;
|
Chris@0
|
257 }
|
Chris@0
|
258 }
|
Chris@0
|
259 }
|
Chris@0
|
260 // Make sure we don't try to iterate over things other than objects. This
|
Chris@0
|
261 // shouldn't normally occur, but in case the condition definition is
|
Chris@0
|
262 // bogus, we don't want to end up with an infinite loop.
|
Chris@0
|
263 else if ($.isPlainObject(constraints)) {
|
Chris@0
|
264 // This constraint is an object (AND).
|
Chris@0
|
265 // eslint-disable-next-line no-restricted-syntax
|
Chris@0
|
266 for (const n in constraints) {
|
Chris@0
|
267 if (constraints.hasOwnProperty(n)) {
|
Chris@0
|
268 result = ternary(result, this.checkConstraints(constraints[n], selector, n));
|
Chris@0
|
269 // False and anything else will evaluate to false, so return when
|
Chris@0
|
270 // any false condition is found.
|
Chris@0
|
271 if (result === false) {
|
Chris@0
|
272 return false;
|
Chris@0
|
273 }
|
Chris@0
|
274 }
|
Chris@0
|
275 }
|
Chris@0
|
276 }
|
Chris@0
|
277 return result;
|
Chris@0
|
278 },
|
Chris@0
|
279
|
Chris@0
|
280 /**
|
Chris@0
|
281 * Checks whether the value matches the requirements for this constraint.
|
Chris@0
|
282 *
|
Chris@0
|
283 * @memberof Drupal.states.Dependent#
|
Chris@0
|
284 *
|
Chris@0
|
285 * @param {string|Array|object} value
|
Chris@0
|
286 * Either the value of a state or an array/object of constraints. In the
|
Chris@0
|
287 * latter case, resolving the constraint continues.
|
Chris@0
|
288 * @param {string} [selector]
|
Chris@0
|
289 * The selector for this constraint. If undefined, there isn't yet a
|
Chris@0
|
290 * selector that this constraint applies to. In that case, the state key
|
Chris@0
|
291 * is propagates to a selector and resolving continues.
|
Chris@0
|
292 * @param {Drupal.states.State} [state]
|
Chris@0
|
293 * The state to check for this constraint. If undefined, resolving
|
Chris@0
|
294 * continues. If both selector and state aren't undefined and valid
|
Chris@0
|
295 * non-numeric strings, a lookup for the actual value of that selector's
|
Chris@0
|
296 * state is performed. This parameter is not a State object but a pristine
|
Chris@0
|
297 * state string.
|
Chris@0
|
298 *
|
Chris@0
|
299 * @return {bool}
|
Chris@0
|
300 * true or false, depending on whether this constraint is satisfied.
|
Chris@0
|
301 */
|
Chris@0
|
302 checkConstraints(value, selector, state) {
|
Chris@0
|
303 // Normalize the last parameter. If it's non-numeric, we treat it either
|
Chris@0
|
304 // as a selector (in case there isn't one yet) or as a trigger/state.
|
Chris@0
|
305 if (typeof state !== 'string' || (/[0-9]/).test(state[0])) {
|
Chris@0
|
306 state = null;
|
Chris@0
|
307 }
|
Chris@0
|
308 else if (typeof selector === 'undefined') {
|
Chris@0
|
309 // Propagate the state to the selector when there isn't one yet.
|
Chris@0
|
310 selector = state;
|
Chris@0
|
311 state = null;
|
Chris@0
|
312 }
|
Chris@0
|
313
|
Chris@0
|
314 if (state !== null) {
|
Chris@0
|
315 // Constraints is the actual constraints of an element to check for.
|
Chris@0
|
316 state = states.State.sanitize(state);
|
Chris@0
|
317 return invert(this.compare(value, selector, state), state.invert);
|
Chris@0
|
318 }
|
Chris@0
|
319
|
Chris@0
|
320 // Resolve this constraint as an AND/OR operator.
|
Chris@0
|
321 return this.verifyConstraints(value, selector);
|
Chris@0
|
322 },
|
Chris@0
|
323
|
Chris@0
|
324 /**
|
Chris@0
|
325 * Gathers information about all required triggers.
|
Chris@0
|
326 *
|
Chris@0
|
327 * @memberof Drupal.states.Dependent#
|
Chris@0
|
328 *
|
Chris@0
|
329 * @return {object}
|
Chris@0
|
330 * An object describing the required triggers.
|
Chris@0
|
331 */
|
Chris@0
|
332 getDependees() {
|
Chris@0
|
333 const cache = {};
|
Chris@0
|
334 // Swivel the lookup function so that we can record all available
|
Chris@0
|
335 // selector- state combinations for initialization.
|
Chris@0
|
336 const _compare = this.compare;
|
Chris@0
|
337 this.compare = function (reference, selector, state) {
|
Chris@0
|
338 (cache[selector] || (cache[selector] = [])).push(state.name);
|
Chris@0
|
339 // Return nothing (=== undefined) so that the constraint loops are not
|
Chris@0
|
340 // broken.
|
Chris@0
|
341 };
|
Chris@0
|
342
|
Chris@0
|
343 // This call doesn't actually verify anything but uses the resolving
|
Chris@0
|
344 // mechanism to go through the constraints array, trying to look up each
|
Chris@0
|
345 // value. Since we swivelled the compare function, this comparison returns
|
Chris@0
|
346 // undefined and lookup continues until the very end. Instead of lookup up
|
Chris@0
|
347 // the value, we record that combination of selector and state so that we
|
Chris@0
|
348 // can initialize all triggers.
|
Chris@0
|
349 this.verifyConstraints(this.constraints);
|
Chris@0
|
350 // Restore the original function.
|
Chris@0
|
351 this.compare = _compare;
|
Chris@0
|
352
|
Chris@0
|
353 return cache;
|
Chris@0
|
354 },
|
Chris@0
|
355 };
|
Chris@0
|
356
|
Chris@0
|
357 /**
|
Chris@0
|
358 * @constructor Drupal.states.Trigger
|
Chris@0
|
359 *
|
Chris@0
|
360 * @param {object} args
|
Chris@0
|
361 * Trigger arguments.
|
Chris@0
|
362 */
|
Chris@0
|
363 states.Trigger = function (args) {
|
Chris@0
|
364 $.extend(this, args);
|
Chris@0
|
365
|
Chris@0
|
366 if (this.state in states.Trigger.states) {
|
Chris@0
|
367 this.element = $(this.selector);
|
Chris@0
|
368
|
Chris@0
|
369 // Only call the trigger initializer when it wasn't yet attached to this
|
Chris@0
|
370 // element. Otherwise we'd end up with duplicate events.
|
Chris@0
|
371 if (!this.element.data(`trigger:${this.state}`)) {
|
Chris@0
|
372 this.initialize();
|
Chris@0
|
373 }
|
Chris@0
|
374 }
|
Chris@0
|
375 };
|
Chris@0
|
376
|
Chris@0
|
377 states.Trigger.prototype = {
|
Chris@0
|
378
|
Chris@0
|
379 /**
|
Chris@0
|
380 * @memberof Drupal.states.Trigger#
|
Chris@0
|
381 */
|
Chris@0
|
382 initialize() {
|
Chris@0
|
383 const trigger = states.Trigger.states[this.state];
|
Chris@0
|
384
|
Chris@0
|
385 if (typeof trigger === 'function') {
|
Chris@0
|
386 // We have a custom trigger initialization function.
|
Chris@0
|
387 trigger.call(window, this.element);
|
Chris@0
|
388 }
|
Chris@0
|
389 else {
|
Chris@0
|
390 Object.keys(trigger || {}).forEach((event) => {
|
Chris@0
|
391 this.defaultTrigger(event, trigger[event]);
|
Chris@0
|
392 });
|
Chris@0
|
393 }
|
Chris@0
|
394
|
Chris@0
|
395 // Mark this trigger as initialized for this element.
|
Chris@0
|
396 this.element.data(`trigger:${this.state}`, true);
|
Chris@0
|
397 },
|
Chris@0
|
398
|
Chris@0
|
399 /**
|
Chris@0
|
400 * @memberof Drupal.states.Trigger#
|
Chris@0
|
401 *
|
Chris@0
|
402 * @param {jQuery.Event} event
|
Chris@0
|
403 * The event triggered.
|
Chris@0
|
404 * @param {function} valueFn
|
Chris@0
|
405 * The function to call.
|
Chris@0
|
406 */
|
Chris@0
|
407 defaultTrigger(event, valueFn) {
|
Chris@0
|
408 let oldValue = valueFn.call(this.element);
|
Chris@0
|
409
|
Chris@0
|
410 // Attach the event callback.
|
Chris@0
|
411 this.element.on(event, $.proxy(function (e) {
|
Chris@0
|
412 const value = valueFn.call(this.element, e);
|
Chris@0
|
413 // Only trigger the event if the value has actually changed.
|
Chris@0
|
414 if (oldValue !== value) {
|
Chris@0
|
415 this.element.trigger({ type: `state:${this.state}`, value, oldValue });
|
Chris@0
|
416 oldValue = value;
|
Chris@0
|
417 }
|
Chris@0
|
418 }, this));
|
Chris@0
|
419
|
Chris@0
|
420 states.postponed.push($.proxy(function () {
|
Chris@0
|
421 // Trigger the event once for initialization purposes.
|
Chris@0
|
422 this.element.trigger({ type: `state:${this.state}`, value: oldValue, oldValue: null });
|
Chris@0
|
423 }, this));
|
Chris@0
|
424 },
|
Chris@0
|
425 };
|
Chris@0
|
426
|
Chris@0
|
427 /**
|
Chris@0
|
428 * This list of states contains functions that are used to monitor the state
|
Chris@0
|
429 * of an element. Whenever an element depends on the state of another element,
|
Chris@0
|
430 * one of these trigger functions is added to the dependee so that the
|
Chris@0
|
431 * dependent element can be updated.
|
Chris@0
|
432 *
|
Chris@0
|
433 * @name Drupal.states.Trigger.states
|
Chris@0
|
434 *
|
Chris@0
|
435 * @prop empty
|
Chris@0
|
436 * @prop checked
|
Chris@0
|
437 * @prop value
|
Chris@0
|
438 * @prop collapsed
|
Chris@0
|
439 */
|
Chris@0
|
440 states.Trigger.states = {
|
Chris@0
|
441 // 'empty' describes the state to be monitored.
|
Chris@0
|
442 empty: {
|
Chris@0
|
443 // 'keyup' is the (native DOM) event that we watch for.
|
Chris@0
|
444 keyup() {
|
Chris@0
|
445 // The function associated with that trigger returns the new value for
|
Chris@0
|
446 // the state.
|
Chris@0
|
447 return this.val() === '';
|
Chris@0
|
448 },
|
Chris@0
|
449 },
|
Chris@0
|
450
|
Chris@0
|
451 checked: {
|
Chris@0
|
452 change() {
|
Chris@0
|
453 // prop() and attr() only takes the first element into account. To
|
Chris@0
|
454 // support selectors matching multiple checkboxes, iterate over all and
|
Chris@0
|
455 // return whether any is checked.
|
Chris@0
|
456 let checked = false;
|
Chris@0
|
457 this.each(function () {
|
Chris@0
|
458 // Use prop() here as we want a boolean of the checkbox state.
|
Chris@0
|
459 // @see http://api.jquery.com/prop/
|
Chris@0
|
460 checked = $(this).prop('checked');
|
Chris@0
|
461 // Break the each() loop if this is checked.
|
Chris@0
|
462 return !checked;
|
Chris@0
|
463 });
|
Chris@0
|
464 return checked;
|
Chris@0
|
465 },
|
Chris@0
|
466 },
|
Chris@0
|
467
|
Chris@0
|
468 // For radio buttons, only return the value if the radio button is selected.
|
Chris@0
|
469 value: {
|
Chris@0
|
470 keyup() {
|
Chris@0
|
471 // Radio buttons share the same :input[name="key"] selector.
|
Chris@0
|
472 if (this.length > 1) {
|
Chris@0
|
473 // Initial checked value of radios is undefined, so we return false.
|
Chris@0
|
474 return this.filter(':checked').val() || false;
|
Chris@0
|
475 }
|
Chris@0
|
476 return this.val();
|
Chris@0
|
477 },
|
Chris@0
|
478 change() {
|
Chris@0
|
479 // Radio buttons share the same :input[name="key"] selector.
|
Chris@0
|
480 if (this.length > 1) {
|
Chris@0
|
481 // Initial checked value of radios is undefined, so we return false.
|
Chris@0
|
482 return this.filter(':checked').val() || false;
|
Chris@0
|
483 }
|
Chris@0
|
484 return this.val();
|
Chris@0
|
485 },
|
Chris@0
|
486 },
|
Chris@0
|
487
|
Chris@0
|
488 collapsed: {
|
Chris@0
|
489 collapsed(e) {
|
Chris@0
|
490 return (typeof e !== 'undefined' && 'value' in e) ? e.value : !this.is('[open]');
|
Chris@0
|
491 },
|
Chris@0
|
492 },
|
Chris@0
|
493 };
|
Chris@0
|
494
|
Chris@0
|
495 /**
|
Chris@0
|
496 * A state object is used for describing the state and performing aliasing.
|
Chris@0
|
497 *
|
Chris@0
|
498 * @constructor Drupal.states.State
|
Chris@0
|
499 *
|
Chris@0
|
500 * @param {string} state
|
Chris@0
|
501 * The name of the state.
|
Chris@0
|
502 */
|
Chris@0
|
503 states.State = function (state) {
|
Chris@0
|
504 /**
|
Chris@0
|
505 * Original unresolved name.
|
Chris@0
|
506 */
|
Chris@0
|
507 this.pristine = state;
|
Chris@0
|
508 this.name = state;
|
Chris@0
|
509
|
Chris@0
|
510 // Normalize the state name.
|
Chris@0
|
511 let process = true;
|
Chris@0
|
512 do {
|
Chris@0
|
513 // Iteratively remove exclamation marks and invert the value.
|
Chris@0
|
514 while (this.name.charAt(0) === '!') {
|
Chris@0
|
515 this.name = this.name.substring(1);
|
Chris@0
|
516 this.invert = !this.invert;
|
Chris@0
|
517 }
|
Chris@0
|
518
|
Chris@0
|
519 // Replace the state with its normalized name.
|
Chris@0
|
520 if (this.name in states.State.aliases) {
|
Chris@0
|
521 this.name = states.State.aliases[this.name];
|
Chris@0
|
522 }
|
Chris@0
|
523 else {
|
Chris@0
|
524 process = false;
|
Chris@0
|
525 }
|
Chris@0
|
526 } while (process);
|
Chris@0
|
527 };
|
Chris@0
|
528
|
Chris@0
|
529 /**
|
Chris@0
|
530 * Creates a new State object by sanitizing the passed value.
|
Chris@0
|
531 *
|
Chris@0
|
532 * @name Drupal.states.State.sanitize
|
Chris@0
|
533 *
|
Chris@0
|
534 * @param {string|Drupal.states.State} state
|
Chris@0
|
535 * A state object or the name of a state.
|
Chris@0
|
536 *
|
Chris@0
|
537 * @return {Drupal.states.state}
|
Chris@0
|
538 * A state object.
|
Chris@0
|
539 */
|
Chris@0
|
540 states.State.sanitize = function (state) {
|
Chris@0
|
541 if (state instanceof states.State) {
|
Chris@0
|
542 return state;
|
Chris@0
|
543 }
|
Chris@0
|
544
|
Chris@0
|
545 return new states.State(state);
|
Chris@0
|
546 };
|
Chris@0
|
547
|
Chris@0
|
548 /**
|
Chris@0
|
549 * This list of aliases is used to normalize states and associates negated
|
Chris@0
|
550 * names with their respective inverse state.
|
Chris@0
|
551 *
|
Chris@0
|
552 * @name Drupal.states.State.aliases
|
Chris@0
|
553 */
|
Chris@0
|
554 states.State.aliases = {
|
Chris@0
|
555 enabled: '!disabled',
|
Chris@0
|
556 invisible: '!visible',
|
Chris@0
|
557 invalid: '!valid',
|
Chris@0
|
558 untouched: '!touched',
|
Chris@0
|
559 optional: '!required',
|
Chris@0
|
560 filled: '!empty',
|
Chris@0
|
561 unchecked: '!checked',
|
Chris@0
|
562 irrelevant: '!relevant',
|
Chris@0
|
563 expanded: '!collapsed',
|
Chris@0
|
564 open: '!collapsed',
|
Chris@0
|
565 closed: 'collapsed',
|
Chris@0
|
566 readwrite: '!readonly',
|
Chris@0
|
567 };
|
Chris@0
|
568
|
Chris@0
|
569 states.State.prototype = {
|
Chris@0
|
570
|
Chris@0
|
571 /**
|
Chris@0
|
572 * @memberof Drupal.states.State#
|
Chris@0
|
573 */
|
Chris@0
|
574 invert: false,
|
Chris@0
|
575
|
Chris@0
|
576 /**
|
Chris@0
|
577 * Ensures that just using the state object returns the name.
|
Chris@0
|
578 *
|
Chris@0
|
579 * @memberof Drupal.states.State#
|
Chris@0
|
580 *
|
Chris@0
|
581 * @return {string}
|
Chris@0
|
582 * The name of the state.
|
Chris@0
|
583 */
|
Chris@0
|
584 toString() {
|
Chris@0
|
585 return this.name;
|
Chris@0
|
586 },
|
Chris@0
|
587 };
|
Chris@0
|
588
|
Chris@0
|
589 /**
|
Chris@0
|
590 * Global state change handlers. These are bound to "document" to cover all
|
Chris@0
|
591 * elements whose state changes. Events sent to elements within the page
|
Chris@0
|
592 * bubble up to these handlers. We use this system so that themes and modules
|
Chris@0
|
593 * can override these state change handlers for particular parts of a page.
|
Chris@0
|
594 */
|
Chris@0
|
595
|
Chris@0
|
596 const $document = $(document);
|
Chris@0
|
597 $document.on('state:disabled', (e) => {
|
Chris@0
|
598 // Only act when this change was triggered by a dependency and not by the
|
Chris@0
|
599 // element monitoring itself.
|
Chris@0
|
600 if (e.trigger) {
|
Chris@0
|
601 $(e.target)
|
Chris@0
|
602 .prop('disabled', e.value)
|
Chris@0
|
603 .closest('.js-form-item, .js-form-submit, .js-form-wrapper')
|
Chris@0
|
604 .toggleClass('form-disabled', e.value)
|
Chris@0
|
605 .find('select, input, textarea')
|
Chris@0
|
606 .prop('disabled', e.value);
|
Chris@0
|
607
|
Chris@0
|
608 // Note: WebKit nightlies don't reflect that change correctly.
|
Chris@0
|
609 // See https://bugs.webkit.org/show_bug.cgi?id=23789
|
Chris@0
|
610 }
|
Chris@0
|
611 });
|
Chris@0
|
612
|
Chris@0
|
613 $document.on('state:required', (e) => {
|
Chris@0
|
614 if (e.trigger) {
|
Chris@0
|
615 if (e.value) {
|
Chris@0
|
616 const label = `label${e.target.id ? `[for=${e.target.id}]` : ''}`;
|
Chris@0
|
617 const $label = $(e.target).attr({ required: 'required', 'aria-required': 'aria-required' }).closest('.js-form-item, .js-form-wrapper').find(label);
|
Chris@0
|
618 // Avoids duplicate required markers on initialization.
|
Chris@0
|
619 if (!$label.hasClass('js-form-required').length) {
|
Chris@0
|
620 $label.addClass('js-form-required form-required');
|
Chris@0
|
621 }
|
Chris@0
|
622 }
|
Chris@0
|
623 else {
|
Chris@0
|
624 $(e.target)
|
Chris@0
|
625 .removeAttr('required aria-required')
|
Chris@0
|
626 .closest('.js-form-item, .js-form-wrapper')
|
Chris@0
|
627 .find('label.js-form-required')
|
Chris@0
|
628 .removeClass('js-form-required form-required');
|
Chris@0
|
629 }
|
Chris@0
|
630 }
|
Chris@0
|
631 });
|
Chris@0
|
632
|
Chris@0
|
633 $document.on('state:visible', (e) => {
|
Chris@0
|
634 if (e.trigger) {
|
Chris@0
|
635 $(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggle(e.value);
|
Chris@0
|
636 }
|
Chris@0
|
637 });
|
Chris@0
|
638
|
Chris@0
|
639 $document.on('state:checked', (e) => {
|
Chris@0
|
640 if (e.trigger) {
|
Chris@0
|
641 $(e.target).prop('checked', e.value);
|
Chris@0
|
642 }
|
Chris@0
|
643 });
|
Chris@0
|
644
|
Chris@0
|
645 $document.on('state:collapsed', (e) => {
|
Chris@0
|
646 if (e.trigger) {
|
Chris@0
|
647 if ($(e.target).is('[open]') === e.value) {
|
Chris@0
|
648 $(e.target).find('> summary').trigger('click');
|
Chris@0
|
649 }
|
Chris@0
|
650 }
|
Chris@0
|
651 });
|
Chris@0
|
652
|
Chris@0
|
653 /**
|
Chris@0
|
654 * These are helper functions implementing addition "operators" and don't
|
Chris@0
|
655 * implement any logic that is particular to states.
|
Chris@0
|
656 */
|
Chris@0
|
657
|
Chris@0
|
658 /**
|
Chris@0
|
659 * Bitwise AND with a third undefined state.
|
Chris@0
|
660 *
|
Chris@0
|
661 * @function Drupal.states~ternary
|
Chris@0
|
662 *
|
Chris@0
|
663 * @param {*} a
|
Chris@0
|
664 * Value a.
|
Chris@0
|
665 * @param {*} b
|
Chris@0
|
666 * Value b
|
Chris@0
|
667 *
|
Chris@0
|
668 * @return {bool}
|
Chris@0
|
669 * The result.
|
Chris@0
|
670 */
|
Chris@0
|
671 function ternary(a, b) {
|
Chris@0
|
672 if (typeof a === 'undefined') {
|
Chris@0
|
673 return b;
|
Chris@0
|
674 }
|
Chris@0
|
675 else if (typeof b === 'undefined') {
|
Chris@0
|
676 return a;
|
Chris@0
|
677 }
|
Chris@0
|
678
|
Chris@0
|
679 return a && b;
|
Chris@0
|
680 }
|
Chris@0
|
681
|
Chris@0
|
682 /**
|
Chris@0
|
683 * Inverts a (if it's not undefined) when invertState is true.
|
Chris@0
|
684 *
|
Chris@0
|
685 * @function Drupal.states~invert
|
Chris@0
|
686 *
|
Chris@0
|
687 * @param {*} a
|
Chris@0
|
688 * The value to maybe invert.
|
Chris@0
|
689 * @param {bool} invertState
|
Chris@0
|
690 * Whether to invert state or not.
|
Chris@0
|
691 *
|
Chris@0
|
692 * @return {bool}
|
Chris@0
|
693 * The result.
|
Chris@0
|
694 */
|
Chris@0
|
695 function invert(a, invertState) {
|
Chris@0
|
696 return (invertState && typeof a !== 'undefined') ? !a : a;
|
Chris@0
|
697 }
|
Chris@0
|
698
|
Chris@0
|
699 /**
|
Chris@0
|
700 * Compares two values while ignoring undefined values.
|
Chris@0
|
701 *
|
Chris@0
|
702 * @function Drupal.states~compare
|
Chris@0
|
703 *
|
Chris@0
|
704 * @param {*} a
|
Chris@0
|
705 * Value a.
|
Chris@0
|
706 * @param {*} b
|
Chris@0
|
707 * Value b.
|
Chris@0
|
708 *
|
Chris@0
|
709 * @return {bool}
|
Chris@0
|
710 * The comparison result.
|
Chris@0
|
711 */
|
Chris@0
|
712 function compare(a, b) {
|
Chris@0
|
713 if (a === b) {
|
Chris@0
|
714 return typeof a === 'undefined' ? a : true;
|
Chris@0
|
715 }
|
Chris@0
|
716
|
Chris@0
|
717 return typeof a === 'undefined' || typeof b === 'undefined';
|
Chris@0
|
718 }
|
Chris@0
|
719 }(jQuery, Drupal));
|