UNPKG

597 kBJavaScriptView Raw
1/*!
2 infusion - v3.0.0-dev.20190906T123329Z.e5ad6fbc6.FLUID-6359-src
3 Friday, September 6th, 2019, 8:38:44 AM
4 branch: FLUID-6359-src
5 revision: e5ad6fbc6
6*/
7/*!
8 * Fluid Infusion v3.0.0
9 *
10 * Infusion is distributed under the Educational Community License 2.0 and new BSD licenses:
11 * http://wiki.fluidproject.org/display/fluid/Fluid+Licensing
12 *
13 * Copyright The Infusion copyright holders
14 * See the AUTHORS.md file at the top-level directory of this distribution and at
15 * https://github.com/fluid-project/infusion/raw/master/AUTHORS.md
16 */
17/*
18Copyright The Infusion copyright holders
19See the AUTHORS.md file at the top-level directory of this distribution and at
20https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
21
22Licensed under the Educational Community License (ECL), Version 2.0 or the New
23BSD license. You may not use this file except in compliance with one these
24Licenses.
25
26You may obtain a copy of the ECL 2.0 License and BSD License at
27https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
28
29Includes code from Underscore.js 1.4.3
30http://underscorejs.org
31(c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
32Underscore may be freely distributed under the MIT license.
33*/
34
35/* global console */
36
37var fluid_3_0_0 = fluid_3_0_0 || {};
38var fluid = fluid || fluid_3_0_0;
39
40(function ($, fluid) {
41 "use strict";
42
43 fluid.version = "Infusion 3.0.0";
44
45 // Export this for use in environments like node.js, where it is useful for
46 // configuring stack trace behaviour
47 fluid.Error = Error;
48
49 fluid.environment = {
50 fluid: fluid
51 };
52
53 fluid.global = fluid.global || typeof window !== "undefined" ?
54 window : typeof self !== "undefined" ? self : {};
55
56 // A standard utility to schedule the invocation of a function after the current
57 // stack returns. On browsers this defaults to setTimeout(func, 1) but in
58 // other environments can be customised - e.g. to process.nextTick in node.js
59 // In future, this could be optimised in the browser to not dispatch into the event queue
60 fluid.invokeLater = function (func) {
61 return setTimeout(func, 1);
62 };
63
64 // The following flag defeats all logging/tracing activities in the most performance-critical parts of the framework.
65 // This should really be performed by a build-time step which eliminates calls to pushActivity/popActivity and fluid.log.
66 fluid.defeatLogging = true;
67
68 // This flag enables the accumulating of all "activity" records generated by pushActivity into a running trace, rather
69 // than removing them from the stack record permanently when receiving popActivity. This trace will be consumed by
70 // visual debugging tools.
71 fluid.activityTracing = false;
72 fluid.activityTrace = [];
73
74 var activityParser = /(%\w+)/g;
75
76 // Renders a single activity element in a form suitable to be sent to a modern browser's console
77 // unsupported, non-API function
78 fluid.renderOneActivity = function (activity, nowhile) {
79 var togo = nowhile === true ? [] : [" while "];
80 var message = activity.message;
81 var index = activityParser.lastIndex = 0;
82 while (true) {
83 var match = activityParser.exec(message);
84 if (match) {
85 var key = match[1].substring(1);
86 togo.push(message.substring(index, match.index));
87 togo.push(activity.args[key]);
88 index = activityParser.lastIndex;
89 }
90 else {
91 break;
92 }
93 }
94 if (index < message.length) {
95 togo.push(message.substring(index));
96 }
97 return togo;
98 };
99
100 // Renders an activity stack in a form suitable to be sent to a modern browser's console
101 // unsupported, non-API function
102 fluid.renderActivity = function (activityStack, renderer) {
103 renderer = renderer || fluid.renderOneActivity;
104 return fluid.transform(activityStack, renderer);
105 };
106
107 // Definitions for ThreadLocals - lifted here from
108 // FluidIoC.js so that we can issue calls to fluid.describeActivity for debugging purposes
109 // in the core framework
110
111 // unsupported, non-API function
112 fluid.singleThreadLocal = function (initFunc) {
113 var value = initFunc();
114 return function (newValue) {
115 return newValue === undefined ? value : value = newValue;
116 };
117 };
118
119 // Currently we only support single-threaded environments - ensure that this function
120 // is not used on startup so it can be successfully monkey-patched
121 // only remaining uses of threadLocals are for activity reporting and in the renderer utilities
122 // unsupported, non-API function
123 fluid.threadLocal = fluid.singleThreadLocal;
124
125 // unsupported, non-API function
126 fluid.globalThreadLocal = fluid.threadLocal(function () {
127 return {};
128 });
129
130 // Return an array of objects describing the current activity
131 // unsupported, non-API function
132 fluid.getActivityStack = function () {
133 var root = fluid.globalThreadLocal();
134 if (!root.activityStack) {
135 root.activityStack = [];
136 }
137 return root.activityStack;
138 };
139
140 // Return an array of objects describing the current activity
141 // unsupported, non-API function
142 fluid.describeActivity = fluid.getActivityStack;
143
144 // Renders either the current activity or the supplied activity to the console
145 fluid.logActivity = function (activity) {
146 activity = activity || fluid.describeActivity();
147 var rendered = fluid.renderActivity(activity).reverse();
148 if (rendered.length > 0) {
149 fluid.log("Current activity: ");
150 fluid.each(rendered, function (args) {
151 fluid.log.apply(null, args);
152 });
153 }
154 };
155
156 // Execute the supplied function with the specified activity description pushed onto the stack
157 // unsupported, non-API function
158 fluid.pushActivity = function (type, message, args) {
159 var record = {type: type, message: message, args: args, time: new Date().getTime()};
160 if (fluid.activityTracing) {
161 fluid.activityTrace.push(record);
162 }
163 if (fluid.passLogLevel(fluid.logLevel.TRACE)) {
164 fluid.log.apply(null, fluid.renderOneActivity(record, true));
165 }
166 var activityStack = fluid.getActivityStack();
167 activityStack.push(record);
168 };
169
170 // Undo the effect of the most recent pushActivity, or multiple frames if an argument is supplied
171 fluid.popActivity = function (popframes) {
172 popframes = popframes || 1;
173 if (fluid.activityTracing) {
174 fluid.activityTrace.push({pop: popframes});
175 }
176 var activityStack = fluid.getActivityStack();
177 var popped = activityStack.length - popframes;
178 activityStack.length = popped < 0 ? 0 : popped;
179 };
180 // "this-ist" style Error so that we can distinguish framework errors whilst still retaining access to platform Error features
181 // Solution taken from http://stackoverflow.com/questions/8802845/inheriting-from-the-error-object-where-is-the-message-property#answer-17936621
182 fluid.FluidError = function (/*message*/) {
183 var togo = Error.apply(this, arguments);
184 this.message = togo.message;
185 try { // This technique is necessary on IE11 since otherwise the stack entry is not filled in
186 throw togo;
187 } catch (togo) {
188 this.stack = togo.stack;
189 }
190 return this;
191 };
192 fluid.FluidError.prototype = Object.create(Error.prototype);
193
194 // The framework's built-in "log" failure handler - this logs the supplied message as well as any framework activity in progress via fluid.log
195 fluid.logFailure = function (args, activity) {
196 fluid.log.apply(null, [fluid.logLevel.FAIL, "ASSERTION FAILED: "].concat(args));
197 fluid.logActivity(activity);
198 };
199
200 fluid.renderLoggingArg = function (arg) {
201 return arg === undefined ? "undefined" : fluid.isPrimitive(arg) || !fluid.isPlainObject(arg) ? arg : JSON.stringify(arg);
202 };
203
204 // The framework's built-in "fail" failure handler - this throws an exception of type <code>fluid.FluidError</code>
205 fluid.builtinFail = function (args /*, activity*/) {
206 var message = fluid.transform(args, fluid.renderLoggingArg).join("");
207 throw new fluid.FluidError("Assertion failure - check console for more details: " + message);
208 };
209
210 /**
211 * Signals an error to the framework. The default behaviour is to log a structured error message and throw an exception. This strategy may be configured using the legacy
212 * API <code>fluid.pushSoftFailure</code> or else by adding and removing suitably namespaced listeners to the special event <code>fluid.failureEvent</code>
213 *
214 * @param {String} message - The error message to log.
215 *
216 * All arguments after the first are passed on to (and should be suitable to pass on to) the native console.log
217 * function.
218 */
219 fluid.fail = function (/* message, ... */) {
220 var args = fluid.makeArray(arguments);
221 var activity = fluid.makeArray(fluid.describeActivity()); // Take copy since we will destructively modify
222 fluid.popActivity(activity.length); // clear any current activity - TODO: the framework currently has no exception handlers, although it will in time
223 if (fluid.failureEvent) { // notify any framework failure prior to successfully setting up the failure event below
224 fluid.failureEvent.fire(args, activity);
225 } else {
226 fluid.logFailure(args, activity);
227 fluid.builtinFail(args, activity);
228 }
229 };
230
231 // TODO: rescued from kettleCouchDB.js - clean up in time
232 fluid.expect = function (name, target, members) {
233 fluid.transform(fluid.makeArray(members), function (key) {
234 if (typeof target[key] === "undefined") {
235 fluid.fail(name + " missing required parameter " + key);
236 }
237 });
238 };
239
240 // Logging
241
242 /** Returns whether logging is enabled - legacy method
243 * @return {Boolean} `true` if the current logging level exceeds `fluid.logLevel.IMPORTANT`
244 */
245 fluid.isLogging = function () {
246 return logLevelStack[0].priority > fluid.logLevel.IMPORTANT.priority;
247 };
248
249 /** Determines whether the supplied argument is a valid logLevel marker
250 * @param {Any} arg - The value to be tested
251 * @return {Boolean} `true` if the supplied argument is a logLevel marker
252 */
253 fluid.isLogLevel = function (arg) {
254 return fluid.isMarker(arg) && arg.priority !== undefined;
255 };
256
257 /** Check whether the current framework logging level would cause a message logged with the specified level to be
258 * logged. Clients who issue particularly expensive log payload arguments are recommended to guard their logging
259 * statements with this function
260 * @param {LogLevel} testLogLevel - The logLevel value which the current logging level will be tested against.
261 * Accepts one of the members of the <code>fluid.logLevel</code> structure.
262 * @return {Boolean} Returns <code>true</code> if a message supplied at that log priority would be accepted at the current logging level.
263 */
264
265 fluid.passLogLevel = function (testLogLevel) {
266 return testLogLevel.priority <= logLevelStack[0].priority;
267 };
268
269 /** Method to allow user to control the current framework logging level. The supplied level will be pushed onto a stack
270 * of logging levels which may be popped via `fluid.popLogging`.
271 * @param {Boolean|LogLevel} enabled - Either a boolean, for which <code>true</code>
272 * represents <code>fluid.logLevel.INFO</code> and <code>false</code> represents <code>fluid.logLevel.IMPORTANT</code> (the default),
273 * or else any other member of the structure <code>fluid.logLevel</code>
274 * Messages whose priority is strictly less than the current logging level will not be shown by `fluid.log`
275 */
276 fluid.setLogging = function (enabled) {
277 var logLevel;
278 if (typeof enabled === "boolean") {
279 logLevel = fluid.logLevel[enabled ? "INFO" : "IMPORTANT"];
280 } else if (fluid.isLogLevel(enabled)) {
281 logLevel = enabled;
282 } else {
283 fluid.fail("Unrecognised fluid logging level ", enabled);
284 }
285 logLevelStack.unshift(logLevel);
286 fluid.defeatLogging = !fluid.isLogging();
287 };
288
289 fluid.setLogLevel = fluid.setLogging;
290
291 /** Undo the effect of the most recent "setLogging", returning the logging system to its previous state
292 * @return {LogLevel} The logLevel that was just popped
293 */
294 fluid.popLogging = function () {
295 var togo = logLevelStack.length === 1 ? logLevelStack[0] : logLevelStack.shift();
296 fluid.defeatLogging = !fluid.isLogging();
297 return togo;
298 };
299
300 /** Actually do the work of logging <code>args</code> to the environment's console. If the standard "console"
301 * stream is available, the message will be sent there.
302 * @param {Array} args - The complete array of arguments to be logged
303 */
304 fluid.doBrowserLog = function (args) {
305 if (typeof (console) !== "undefined") {
306 if (console.debug) {
307 console.debug.apply(console, args);
308 } else if (typeof (console.log) === "function") {
309 console.log.apply(console, args);
310 }
311 }
312 };
313
314 /* Log a message to a suitable environmental console. If the first argument to fluid.log is
315 * one of the members of the <code>fluid.logLevel</code> structure, this will be taken as the priority
316 * of the logged message - else if will default to <code>fluid.logLevel.INFO</code>. If the logged message
317 * priority does not exceed that set by the most recent call to the <code>fluid.setLogging</code> function,
318 * the message will not appear.
319 */
320 fluid.log = function (/* message /*, ... */) {
321 var directArgs = fluid.makeArray(arguments);
322 var userLogLevel = fluid.logLevel.INFO;
323 if (fluid.isLogLevel(directArgs[0])) {
324 userLogLevel = directArgs.shift();
325 }
326 if (fluid.passLogLevel(userLogLevel)) {
327 fluid.loggingEvent.fire(directArgs);
328 }
329 };
330
331 // Functional programming utilities.
332
333 // Type checking functions
334
335 /** Check whether the argument is a value other than null or undefined
336 * @param {Any} value - The value to be tested
337 * @return {Boolean} `true` if the supplied value is other than null or undefined
338 */
339 fluid.isValue = function (value) {
340 return value !== undefined && value !== null;
341 };
342
343 /** Check whether the argument is a primitive type
344 * @param {Any} value - The value to be tested
345 * @return {Boolean} `true` if the supplied value is a JavaScript (ES5) primitive
346 */
347 fluid.isPrimitive = function (value) {
348 var valueType = typeof (value);
349 return !value || valueType === "string" || valueType === "boolean" || valueType === "number" || valueType === "function";
350 };
351
352 /** Determines whether the supplied object is an jQuery object. The strategy uses optimised inspection of the
353 * constructor prototype since jQuery may not actually be loaded
354 * @param {Any} totest - The value to be tested
355 * @return {Boolean} `true` if the supplied value is a jQuery object
356 */
357 fluid.isJQuery = function (totest) {
358 return Boolean(totest && totest.jquery && totest.constructor && totest.constructor.prototype
359 && totest.constructor.prototype.jquery);
360 };
361
362 /** Determines whether the supplied object is an array. The strategy used is an optimised
363 * approach taken from an earlier version of jQuery - detecting whether the toString() version
364 * of the object agrees with the textual form [object Array], or else whether the object is a
365 * jQuery object (the most common source of "fake arrays").
366 * @param {Any} totest - The value to be tested
367 * @return {Boolean} `true` if the supplied value is an array
368 */
369 // Note: The primary place jQuery->Array conversion is used in the framework is in dynamic components with a jQuery source.
370 fluid.isArrayable = function (totest) {
371 return Boolean(totest) && (Object.prototype.toString.call(totest) === "[object Array]" || fluid.isJQuery(totest));
372 };
373
374 /** Determines whether the supplied object is a plain JSON-forming container - that is, it is either a plain Object
375 * or a plain Array. Note that this differs from jQuery's isPlainObject which does not pass Arrays.
376 * @param {Any} totest - The object to be tested
377 * @param {Boolean} [strict] - (optional) If `true`, plain Arrays will fail the test rather than passing.
378 * @return {Boolean} - `true` if `totest` is a plain object, `false` otherwise.
379 */
380 fluid.isPlainObject = function (totest, strict) {
381 var string = Object.prototype.toString.call(totest);
382 if (string === "[object Array]") {
383 return !strict;
384 } else if (string !== "[object Object]") {
385 return false;
386 } // FLUID-5226: This inventive strategy taken from jQuery detects whether the object's prototype is directly Object.prototype by virtue of having an "isPrototypeOf" direct member
387 return !totest.constructor || !totest.constructor.prototype || Object.prototype.hasOwnProperty.call(totest.constructor.prototype, "isPrototypeOf");
388 };
389
390 /** Returns a string typeCode representing the type of the supplied value at a coarse level.
391 * Returns <code>primitive</code>, <code>array</code> or <code>object</code> depending on whether the supplied object has
392 * one of those types, by use of the <code>fluid.isPrimitive</code>, <code>fluid.isPlainObject</code> and <code>fluid.isArrayable</code> utilities
393 * @param {Any} totest - The value to be tested
394 * @return {String} Either `primitive`, `array` or `object` depending on the type of the supplied value
395 */
396 fluid.typeCode = function (totest) {
397 return fluid.isPrimitive(totest) || !fluid.isPlainObject(totest) ? "primitive" :
398 fluid.isArrayable(totest) ? "array" : "object";
399 };
400
401 fluid.isIoCReference = function (ref) {
402 return typeof(ref) === "string" && ref.charAt(0) === "{" && ref.indexOf("}") > 0;
403 };
404
405 fluid.isDOMNode = function (obj) {
406 // This could be more sound, but messy:
407 // http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
408 // The real problem is browsers like IE6, 7 and 8 which still do not feature a "constructor" property on DOM nodes
409 return obj && typeof (obj.nodeType) === "number";
410 };
411
412 fluid.isComponent = function (obj) {
413 return obj && obj.constructor === fluid.componentConstructor;
414 };
415
416 fluid.isUncopyable = function (totest) {
417 return fluid.isPrimitive(totest) || !fluid.isPlainObject(totest);
418 };
419
420 fluid.isApplicable = function (totest) {
421 return totest.apply && typeof(totest.apply) === "function";
422 };
423
424 /* A basic utility that returns its argument unchanged */
425 fluid.identity = function (arg) {
426 return arg;
427 };
428
429 /** A function which raises a failure if executed */
430 fluid.notImplemented = function () {
431 fluid.fail("This operation is not implemented");
432 };
433
434 /** Returns the first of its arguments if it is not `undefined`, otherwise returns the second.
435 * @param {Any} a - The first argument to be tested for being `undefined`
436 * @param {Any} b - The fallback argument, to be returned if `a` is `undefined`
437 * @return {Any} `a` if it is not `undefined`, else `b`.
438 */
439 fluid.firstDefined = function (a, b) {
440 return a === undefined ? b : a;
441 };
442
443 /* Return an empty container as the same type as the argument (either an array or hash). */
444 fluid.freshContainer = function (tocopy) {
445 return fluid.isArrayable(tocopy) ? [] : {};
446 };
447
448 fluid.copyRecurse = function (tocopy, segs) {
449 if (segs.length > fluid.strategyRecursionBailout) {
450 fluid.fail("Runaway recursion encountered in fluid.copy - reached path depth of " + fluid.strategyRecursionBailout + " via path of " + segs.join(".") +
451 "this object is probably circularly connected. Either adjust your object structure to remove the circularity or increase fluid.strategyRecursionBailout");
452 }
453 if (fluid.isUncopyable(tocopy)) {
454 return tocopy;
455 } else {
456 return fluid.transform(tocopy, function (value, key) {
457 segs.push(key);
458 var togo = fluid.copyRecurse(value, segs);
459 segs.pop();
460 return togo;
461 });
462 }
463 };
464
465 /* Performs a deep copy (clone) of its argument. This will guard against cloning a circular object by terminating if it reaches a path depth
466 * greater than <code>fluid.strategyRecursionBailout</code>
467 */
468
469 fluid.copy = function (tocopy) {
470 return fluid.copyRecurse(tocopy, []);
471 };
472
473 // TODO: Coming soon - reimplementation of $.extend using strategyRecursionBailout
474 fluid.extend = $.extend;
475
476 /* Corrected version of jQuery makeArray that returns an empty array on undefined rather than crashing.
477 * We don't deal with as many pathological cases as jQuery */
478 fluid.makeArray = function (arg) {
479 var togo = [];
480 if (arg !== null && arg !== undefined) {
481 if (fluid.isPrimitive(arg) || fluid.isPlainObject(arg, true) || typeof(arg.length) !== "number") {
482 togo.push(arg);
483 }
484 else {
485 for (var i = 0; i < arg.length; ++i) {
486 togo[i] = arg[i];
487 }
488 }
489 }
490 return togo;
491 };
492
493 /** Pushes an element or elements onto an array, initialising the array as a member of a holding object if it is
494 * not already allocated.
495 * @param {Array|Object} holder - The holding object whose member is to receive the pushed element(s).
496 * @param {String} member - The member of the <code>holder</code> onto which the element(s) are to be pushed
497 * @param {Array|Object} topush - If an array, these elements will be added to the end of the array using Array.push.apply. If an object, it will be pushed to the end of the array using Array.push.
498 */
499 fluid.pushArray = function (holder, member, topush) {
500 var array = holder[member] ? holder[member] : (holder[member] = []);
501 if (fluid.isArrayable(topush)) {
502 array.push.apply(array, topush);
503 } else {
504 array.push(topush);
505 }
506 };
507
508 function transformInternal(source, togo, key, args) {
509 var transit = source[key];
510 for (var j = 0; j < args.length - 1; ++j) {
511 transit = args[j + 1](transit, key);
512 }
513 togo[key] = transit;
514 }
515
516 /** Return an array or hash of objects, transformed by one or more functions. Similar to
517 * jQuery.map, only will accept an arbitrary list of transformation functions and also
518 * works on non-arrays.
519 * @param {Array|Object} source - The initial container of objects to be transformed. If the source is
520 * neither an array nor an object, it will be returned untransformed
521 * @param {...Function} fn1, fn2, etc. - An arbitrary number of optional further arguments,
522 * all of type Function, accepting the signature (object, index), where object is the
523 * structure member to be transformed, and index is its key or index. Each function will be
524 * applied in turn to each structure member, which will be replaced by the return value
525 * from the function.
526 * @return {Array|Object} - The finally transformed list, where each member has been replaced by the
527 * original member acted on by the function or functions.
528 */
529 fluid.transform = function (source) {
530 if (fluid.isPrimitive(source)) {
531 return source;
532 }
533 var togo = fluid.freshContainer(source);
534 if (fluid.isArrayable(source)) {
535 for (var i = 0; i < source.length; ++i) {
536 transformInternal(source, togo, i, arguments);
537 }
538 } else {
539 for (var key in source) {
540 transformInternal(source, togo, key, arguments);
541 }
542 }
543 return togo;
544 };
545
546 /** Better jQuery.each which works on hashes as well as having the arguments the right way round.
547 * @param {Arrayable|Object} source - The container to be iterated over
548 * @param {Function} func - A function accepting (value, key) for each iterated
549 * object.
550 */
551 fluid.each = function (source, func) {
552 if (fluid.isArrayable(source)) {
553 for (var i = 0; i < source.length; ++i) {
554 func(source[i], i);
555 }
556 } else {
557 for (var key in source) {
558 func(source[key], key);
559 }
560 }
561 };
562
563 fluid.make_find = function (find_if) {
564 var target = find_if ? false : undefined;
565 return function (source, func, deffolt) {
566 var disp;
567 if (fluid.isArrayable(source)) {
568 for (var i = 0; i < source.length; ++i) {
569 disp = func(source[i], i);
570 if (disp !== target) {
571 return find_if ? source[i] : disp;
572 }
573 }
574 } else {
575 for (var key in source) {
576 disp = func(source[key], key);
577 if (disp !== target) {
578 return find_if ? source[key] : disp;
579 }
580 }
581 }
582 return deffolt;
583 };
584 };
585
586 /** Scan through an array or hash of objects, terminating on the first member which
587 * matches a predicate function.
588 * @param {Arrayable|Object} source - The array or hash of objects to be searched.
589 * @param {Function} func - A predicate function, acting on a member. A predicate which
590 * returns any value which is not <code>undefined</code> will terminate
591 * the search. The function accepts (object, index).
592 * @param {Object} deflt - A value to be returned in the case no predicate function matches
593 * a structure member. The default will be the natural value of <code>undefined</code>
594 * @return The first return value from the predicate function which is not <code>undefined</code>
595 */
596 fluid.find = fluid.make_find(false);
597 /* The same signature as fluid.find, only the return value is the actual element for which the
598 * predicate returns a value different from <code>false</code>
599 */
600 fluid.find_if = fluid.make_find(true);
601
602 /** Scan through an array of objects, "accumulating" a value over them
603 * (may be a straightforward "sum" or some other chained computation). "accumulate" is the name derived
604 * from the C++ STL, other names for this algorithm are "reduce" or "fold".
605 * @param {Array} list - The list of objects to be accumulated over.
606 * @param {Function} fn - An "accumulation function" accepting the signature (object, total, index) where
607 * object is the list member, total is the "running total" object (which is the return value from the previous function),
608 * and index is the index number.
609 * @param {Object} arg - The initial value for the "running total" object.
610 * @return {Object} the final running total object as returned from the final invocation of the function on the last list member.
611 */
612 fluid.accumulate = function (list, fn, arg) {
613 for (var i = 0; i < list.length; ++i) {
614 arg = fn(list[i], arg, i);
615 }
616 return arg;
617 };
618
619 /** Returns the sum of its two arguments. A useful utility to combine with fluid.accumulate to compute totals
620 * @param {Number|Boolean} a - The first operand to be added
621 * @param {Number|Boolean} b - The second operand to be added
622 * @return {Number} The sum of the two operands
623 **/
624 fluid.add = function (a, b) {
625 return a + b;
626 };
627
628 /** Scan through an array or hash of objects, removing those which match a predicate. Similar to
629 * jQuery.grep, only acts on the list in-place by removal, rather than by creating
630 * a new list by inclusion.
631 * @param {Array|Object} source - The array or hash of objects to be scanned over. Note that in the case this is an array,
632 * the iteration will proceed from the end of the array towards the front.
633 * @param {Function} fn - A predicate function determining whether an element should be
634 * removed. This accepts the standard signature (object, index) and returns a "truthy"
635 * result in order to determine that the supplied object should be removed from the structure.
636 * @param {Array|Object} [target] - (optional) A target object of the same type as <code>source</code>, which will
637 * receive any objects removed from it.
638 * @return {Array|Object} - <code>target</code>, containing the removed elements, if it was supplied, or else <code>source</code>
639 * modified by the operation of removing the matched elements.
640 */
641 fluid.remove_if = function (source, fn, target) {
642 if (fluid.isArrayable(source)) {
643 for (var i = source.length - 1; i >= 0; --i) {
644 if (fn(source[i], i)) {
645 if (target) {
646 target.unshift(source[i]);
647 }
648 source.splice(i, 1);
649 }
650 }
651 } else {
652 for (var key in source) {
653 if (fn(source[key], key)) {
654 if (target) {
655 target[key] = source[key];
656 }
657 delete source[key];
658 }
659 }
660 }
661 return target || source;
662 };
663
664 /** Fills an array of given size with copies of a value or result of a function invocation
665 * @param {Number} n - The size of the array to be filled
666 * @param {Object|Function} generator - Either a value to be replicated or function to be called
667 * @param {Boolean} applyFunc - If true, treat the generator value as a function to be invoked with
668 * argument equal to the index position
669 */
670
671 fluid.generate = function (n, generator, applyFunc) {
672 var togo = [];
673 for (var i = 0; i < n; ++i) {
674 togo[i] = applyFunc ? generator(i) : generator;
675 }
676 return togo;
677 };
678
679 /** Returns an array of size count, filled with increasing integers, starting at 0 or at the index specified by first.
680 * @param {Number} count - Size of the filled array to be returned
681 * @param {Number} [first] - (optional, defaults to 0) First element to appear in the array
682 */
683
684 fluid.iota = function (count, first) {
685 first = first || 0;
686 var togo = [];
687 for (var i = 0; i < count; ++i) {
688 togo[togo.length] = first++;
689 }
690 return togo;
691 };
692
693 /** Extracts a particular member from each top-level member of a container, returning a new container of the same type
694 * @param {Array|Object} holder - The container to be filtered
695 * @param {String|String[]} name - An EL path to be fetched from each top-level member
696 * @return {Object} - The desired member component.
697 */
698 fluid.getMembers = function (holder, name) {
699 return fluid.transform(holder, function (member) {
700 return fluid.get(member, name);
701 });
702 };
703
704 /** Accepts an object to be filtered, and an array of keys. Either all keys not present in
705 * the array are removed, or only keys present in the array are returned.
706 * @param {Array|Object} toFilter - The object to be filtered - this will be NOT modified by the operation (current implementation
707 * passes through $.extend shallow algorithm)
708 * @param {String[]} keys - The array of keys to operate with
709 * @param {Boolean} exclude - If <code>true</code>, the keys listed are removed rather than included
710 * @return {Object} the filtered object (the same object that was supplied as <code>toFilter</code>
711 */
712 fluid.filterKeys = function (toFilter, keys, exclude) {
713 return fluid.remove_if($.extend({}, toFilter), function (value, key) {
714 return exclude ^ (keys.indexOf(key) === -1);
715 });
716 };
717
718 /* A convenience wrapper for <code>fluid.filterKeys</code> with the parameter <code>exclude</code> set to <code>true</code>
719 * Returns the supplied object with listed keys removed */
720 fluid.censorKeys = function (toCensor, keys) {
721 return fluid.filterKeys(toCensor, keys, true);
722 };
723
724 /* Return the keys in the supplied object as an array. Note that this will return keys found in the prototype chain as well as "own properties", unlike Object.keys() */
725 fluid.keys = function (obj) {
726 var togo = [];
727 for (var key in obj) {
728 togo.push(key);
729 }
730 return togo;
731 };
732
733 /* Return the values in the supplied object as an array */
734 fluid.values = function (obj) {
735 var togo = [];
736 for (var key in obj) {
737 togo.push(obj[key]);
738 }
739 return togo;
740 };
741
742 /*
743 * Searches through the supplied object, and returns <code>true</code> if the supplied value
744 * can be found
745 */
746 fluid.contains = function (obj, value) {
747 return obj ? (fluid.isArrayable(obj) ? obj.indexOf(value) !== -1 : fluid.find(obj, function (thisValue) {
748 if (value === thisValue) {
749 return true;
750 }
751 })) : undefined;
752 };
753
754 /**
755 * Searches through the supplied object for the first value which matches the one supplied.
756 * @param {Object} obj - the Object to be searched through
757 * @param {Object} value - the value to be found. This will be compared against the object's
758 * member using === equality.
759 * @return {String} The first key whose value matches the one supplied
760 */
761 fluid.keyForValue = function (obj, value) {
762 return fluid.find(obj, function (thisValue, key) {
763 if (value === thisValue) {
764 return key;
765 }
766 });
767 };
768
769 /** Converts an array into an object whose keys are the elements of the array, each with the value "true"
770 * @param {String[]} array - The array to be converted to a hash
771 * @return hash {Object} An object with value <code>true</code> for each key taken from a member of <code>array</code>
772 */
773
774 fluid.arrayToHash = function (array) {
775 var togo = {};
776 fluid.each(array, function (el) {
777 togo[el] = true;
778 });
779 return togo;
780 };
781
782 /** Applies a stable sorting algorithm to the supplied array and comparator (note that Array.sort in JavaScript is not specified
783 * to be stable). The algorithm used will be an insertion sort, which whilst quadratic in time, will perform well
784 * on small array sizes.
785 * @param {Array} array - The array to be sorted. This input array will be modified in place.
786 * @param {Function} func - A comparator returning >0, 0, or <0 on pairs of elements representing their sort order (same contract as Array.sort comparator)
787 */
788
789 fluid.stableSort = function (array, func) {
790 for (var i = 0; i < array.length; i++) {
791 var j, k = array[i];
792 for (j = i; j > 0 && func(k, array[j - 1]) < 0; j--) {
793 array[j] = array[j - 1];
794 }
795 array[j] = k;
796 }
797 };
798
799 /* Converts a hash into an object by hoisting out the object's keys into an array element via the supplied String "key", and then transforming via an optional further function, which receives the signature
800 * (newElement, oldElement, key) where newElement is the freshly cloned element, oldElement is the original hash's element, and key is the key of the element.
801 * If the function is not supplied, the old element is simply deep-cloned onto the new element (same effect as transform fluid.transforms.deindexIntoArrayByKey).
802 * The supplied hash will not be modified, unless the supplied function explicitly does so by modifying its 2nd argument.
803 */
804 fluid.hashToArray = function (hash, keyName, func) {
805 var togo = [];
806 fluid.each(hash, function (el, key) {
807 var newEl = {};
808 newEl[keyName] = key;
809 if (func) {
810 newEl = func(newEl, el, key) || newEl;
811 } else {
812 $.extend(true, newEl, el);
813 }
814 togo.push(newEl);
815 });
816 return togo;
817 };
818
819 /* Converts an array consisting of a mixture of arrays and non-arrays into the concatenation of any inner arrays
820 * with the non-array elements
821 */
822 fluid.flatten = function (array) {
823 var togo = [];
824 fluid.each(array, function (element) {
825 if (fluid.isArrayable(element)) {
826 togo = togo.concat(element);
827 } else {
828 togo.push(element);
829 }
830 });
831 return togo;
832 };
833
834 /**
835 * Clears an object or array of its contents. For objects, each property is deleted.
836 *
837 * @param {Object|Array} target - the target to be cleared
838 */
839 fluid.clear = function (target) {
840 if (fluid.isArrayable(target)) {
841 target.length = 0;
842 } else {
843 for (var i in target) {
844 delete target[i];
845 }
846 }
847 };
848
849 /**
850 * @param {Boolean} ascending <code>true</code> if a comparator is to be returned which
851 * sorts strings in descending order of length.
852 * @return {Function} - A comparison function.
853 */
854 fluid.compareStringLength = function (ascending) {
855 return ascending ? function (a, b) {
856 return a.length - b.length;
857 } : function (a, b) {
858 return b.length - a.length;
859 };
860 };
861
862 /**
863 * Returns the converted integer if the input string can be converted to an integer. Otherwise, return NaN.
864 * @param {String} string - A string to be returned in integer form.
865 * @return {Number|NaN} - The numeric value if the string can be converted, otherwise, returns NaN.
866 */
867 fluid.parseInteger = function (string) {
868 return isFinite(string) && ((string % 1) === 0) ? Number(string) : NaN;
869 };
870
871 /**
872 * Derived from Sindre Sorhus's round-to node module ( https://github.com/sindresorhus/round-to ).
873 * License: MIT
874 *
875 * Rounds the supplied number to at most the number of decimal places indicated by the scale, omitting any trailing 0s.
876 * There are three possible rounding methods described below: "round", "ceil", "floor"
877 * Round: Numbers are rounded away from 0 (i.e 0.5 -> 1, -0.5 -> -1).
878 * Ceil: Numbers are rounded up
879 * Floor: Numbers are rounded down
880 * If the scale is invalid (i.e falsey, not a number, negative value), it is treated as 0.
881 * If the scale is a floating point number, it is rounded to an integer.
882 *
883 * @param {Number} num - the number to be rounded
884 * @param {Number} scale - the maximum number of decimal places to round to.
885 * @param {String} [method] - (optional) Request a rounding method to use ("round", "ceil", "floor").
886 * If nothing or an invalid method is provided, it will default to "round".
887 * @return {Number} The num value rounded to the specified number of decimal places.
888 */
889 fluid.roundToDecimal = function (num, scale, method) {
890 // treat invalid scales as 0
891 scale = scale && scale >= 0 ? Math.round(scale) : 0;
892
893 if (method === "ceil" || method === "floor") {
894 // The following is derived from https://github.com/sindresorhus/round-to/blob/v2.0.0/index.js#L20
895 return Number(Math[method](num + "e" + scale) + "e-" + scale);
896 } else {
897 // The following is derived from https://github.com/sindresorhus/round-to/blob/v2.0.0/index.js#L17
898 var sign = num >= 0 ? 1 : -1; // manually calculating the sign because Math.sign is not supported in IE
899 return Number(sign * (Math.round(Math.abs(num) + "e" + scale) + "e-" + scale));
900 }
901 };
902
903 /**
904 * Copied from Underscore.js 1.4.3 - see licence at head of this file
905 *
906 * Will execute the passed in function after the specified amount of time since it was last executed.
907 * @param {Function} func - the function to execute
908 * @param {Number} wait - the number of milliseconds to wait before executing the function
909 * @param {Boolean} immediate - Whether to trigger the function at the start (true) or end (false) of
910 * the wait interval.
911 * @return {Function} - A function that can be called as though it were the original function.
912 */
913 fluid.debounce = function (func, wait, immediate) {
914 var timeout, result;
915 return function () {
916 var context = this, args = arguments;
917 var later = function () {
918 timeout = null;
919 if (!immediate) {
920 result = func.apply(context, args);
921 }
922 };
923 var callNow = immediate && !timeout;
924 clearTimeout(timeout);
925 timeout = setTimeout(later, wait);
926 if (callNow) {
927 result = func.apply(context, args);
928 }
929 return result;
930 };
931 };
932
933 /** Calls Object.freeze at each level of containment of the supplied object
934 * @param {Any} tofreeze - The material to freeze.
935 * @return {Any} - The supplied argument, recursively frozen.
936 */
937 fluid.freezeRecursive = function (tofreeze) {
938 if (fluid.isPlainObject(tofreeze)) {
939 fluid.each(tofreeze, function (value) {
940 fluid.freezeRecursive(value);
941 });
942 return Object.freeze(tofreeze);
943 } else {
944 return tofreeze;
945 }
946 };
947
948 /* A set of special "marker values" used in signalling in function arguments and return values,
949 * to partially compensate for JavaScript's lack of distinguished types. These should never appear
950 * in JSON structures or other kinds of static configuration. An API specifically documents if it
951 * accepts or returns any of these values, and if so, what its semantic is - most are of private
952 * use internal to the framework */
953
954 fluid.marker = function () {};
955
956 fluid.makeMarker = function (value, extra) {
957 var togo = Object.create(fluid.marker.prototype);
958 togo.value = value;
959 $.extend(togo, extra);
960 return Object.freeze(togo);
961 };
962
963 /* A special "marker object" representing that a distinguished
964 * (probably context-dependent) value should be substituted.
965 */
966 fluid.VALUE = fluid.makeMarker("VALUE");
967
968 /* A special "marker object" representing that no value is present (where
969 * signalling using the value "undefined" is not possible - e.g. the return value from a "strategy") */
970 fluid.NO_VALUE = fluid.makeMarker("NO_VALUE");
971
972 /* A marker indicating that a value requires to be expanded after component construction begins */
973 fluid.EXPAND = fluid.makeMarker("EXPAND");
974
975 /* Determine whether an object is any marker, or a particular marker - omit the
976 * 2nd argument to detect any marker
977 */
978 fluid.isMarker = function (totest, type) {
979 if (!(totest instanceof fluid.marker)) {
980 return false;
981 }
982 if (!type) {
983 return true;
984 }
985 return totest.value === type.value;
986 };
987
988 fluid.logLevelsSpec = {
989 "FATAL": 0,
990 "FAIL": 5,
991 "WARN": 10,
992 "IMPORTANT": 12, // The default logging "off" level - corresponds to the old "false"
993 "INFO": 15, // The default logging "on" level - corresponds to the old "true"
994 "TRACE": 20
995 };
996
997 /* A structure holding all supported log levels as supplied as a possible first argument to fluid.log
998 * Members with a higher value of the "priority" field represent lower priority logging levels */
999 // Moved down here since it uses fluid.transform and fluid.makeMarker on startup
1000 fluid.logLevel = fluid.transform(fluid.logLevelsSpec, function (value, key) {
1001 return fluid.makeMarker(key, {priority: value});
1002 });
1003 var logLevelStack = [fluid.logLevel.IMPORTANT]; // The stack of active logging levels, with the current level at index 0
1004
1005
1006 // Model functions
1007 fluid.model = {}; // cannot call registerNamespace yet since it depends on fluid.model
1008
1009 /* Copy a source "model" onto a target */
1010 fluid.model.copyModel = function (target, source) {
1011 fluid.clear(target);
1012 $.extend(true, target, source);
1013 };
1014
1015 /** Parse an EL expression separated by periods (.) into its component segments.
1016 * @param {String} EL - The EL expression to be split
1017 * @return {String[]} the component path expressions.
1018 * TODO: This needs to be upgraded to handle (the same) escaping rules (as RSF), so that
1019 * path segments containing periods and backslashes etc. can be processed, and be harmonised
1020 * with the more complex implementations in fluid.pathUtil(data binding).
1021 */
1022 fluid.model.parseEL = function (EL) {
1023 return EL === "" ? [] : String(EL).split(".");
1024 };
1025
1026 /* Compose an EL expression from two separate EL expressions. The returned
1027 * expression will be the one that will navigate the first expression, and then
1028 * the second, from the value reached by the first. Either prefix or suffix may be
1029 * the empty string */
1030 fluid.model.composePath = function (prefix, suffix) {
1031 return prefix === "" ? suffix : (suffix === "" ? prefix : prefix + "." + suffix);
1032 };
1033
1034 /* Compose any number of path segments, none of which may be empty */
1035 fluid.model.composeSegments = function () {
1036 return fluid.makeArray(arguments).join(".");
1037 };
1038
1039 /* Returns the index of the last occurrence of the period character . in the supplied string */
1040 fluid.lastDotIndex = function (path) {
1041 return path.lastIndexOf(".");
1042 };
1043
1044 /* Returns all of an EL path minus its final segment - if the path consists of just one segment, returns "" -
1045 * WARNING - this method does not follow escaping rules */
1046 fluid.model.getToTailPath = function (path) {
1047 var lastdot = fluid.lastDotIndex(path);
1048 return lastdot === -1 ? "" : path.substring(0, lastdot);
1049 };
1050
1051 /* Returns the very last path component of an EL path
1052 * WARNING - this method does not follow escaping rules */
1053 fluid.model.getTailPath = function (path) {
1054 var lastdot = fluid.lastDotIndex(path);
1055 return path.substring(lastdot + 1);
1056 };
1057
1058 /* Helpful alias for old-style API */
1059 fluid.path = fluid.model.composeSegments;
1060 fluid.composePath = fluid.model.composePath;
1061
1062
1063 // unsupported, NON-API function
1064 fluid.requireDataBinding = function () {
1065 fluid.fail("Please include DataBinding.js in order to operate complex model accessor configuration");
1066 };
1067
1068 fluid.model.setWithStrategy = fluid.model.getWithStrategy = fluid.requireDataBinding;
1069
1070 // unsupported, NON-API function
1071 fluid.model.resolvePathSegment = function (root, segment, create, origEnv) {
1072 // TODO: This branch incurs a huge cost that we incur across the whole framework, just to support the DOM binder
1073 // usage. We need to either do something "schematic" or move to proxies
1074 if (!origEnv && root.resolvePathSegment) {
1075 var togo = root.resolvePathSegment(segment);
1076 if (togo !== undefined) { // To resolve FLUID-6132
1077 return togo;
1078 }
1079 }
1080 if (create && root[segment] === undefined) {
1081 // This optimisation in this heavily used function has a fair effect
1082 return root[segment] = {};
1083 }
1084 return root[segment];
1085 };
1086
1087 // unsupported, NON-API function
1088 fluid.model.parseToSegments = function (EL, parseEL, copy) {
1089 return typeof(EL) === "number" || typeof(EL) === "string" ? parseEL(EL) : (copy ? fluid.makeArray(EL) : EL);
1090 };
1091
1092 // unsupported, NON-API function
1093 fluid.model.pathToSegments = function (EL, config) {
1094 var parser = config && config.parser ? config.parser.parse : fluid.model.parseEL;
1095 return fluid.model.parseToSegments(EL, parser);
1096 };
1097
1098 // Overall strategy skeleton for all implementations of fluid.get/set
1099 fluid.model.accessImpl = function (root, EL, newValue, config, initSegs, returnSegs, traverser) {
1100 var segs = fluid.model.pathToSegments(EL, config);
1101 var initPos = 0;
1102 if (initSegs) {
1103 initPos = initSegs.length;
1104 segs = initSegs.concat(segs);
1105 }
1106 var uncess = newValue === fluid.NO_VALUE ? 0 : 1;
1107 root = traverser(root, segs, initPos, config, uncess);
1108 if (newValue === fluid.NO_VALUE || newValue === fluid.VALUE) { // get or custom
1109 return returnSegs ? {root: root, segs: segs} : root;
1110 }
1111 else { // set
1112 root[segs[segs.length - 1]] = newValue;
1113 }
1114 };
1115
1116 // unsupported, NON-API function
1117 fluid.model.accessSimple = function (root, EL, newValue, environment, initSegs, returnSegs) {
1118 return fluid.model.accessImpl(root, EL, newValue, environment, initSegs, returnSegs, fluid.model.traverseSimple);
1119 };
1120
1121 // unsupported, NON-API function
1122 fluid.model.traverseSimple = function (root, segs, initPos, environment, uncess) {
1123 var origEnv = environment;
1124 var limit = segs.length - uncess;
1125 for (var i = 0; i < limit; ++i) {
1126 if (!root) {
1127 return undefined;
1128 }
1129 var segment = segs[i];
1130 if (environment && environment[segment]) {
1131 root = environment[segment];
1132 } else {
1133 root = fluid.model.resolvePathSegment(root, segment, uncess === 1, origEnv);
1134 }
1135 environment = null;
1136 }
1137 return root;
1138 };
1139
1140 fluid.model.setSimple = function (root, EL, newValue, environment, initSegs) {
1141 fluid.model.accessSimple(root, EL, newValue, environment, initSegs, false);
1142 };
1143
1144 /* Optimised version of fluid.get for uncustomised configurations */
1145
1146 fluid.model.getSimple = function (root, EL, environment, initSegs) {
1147 if (EL === null || EL === undefined || EL.length === 0) {
1148 return root;
1149 }
1150 return fluid.model.accessSimple(root, EL, fluid.NO_VALUE, environment, initSegs, false);
1151 };
1152
1153 /* Even more optimised version which assumes segs are parsed and no configuration */
1154 fluid.getImmediate = function (root, segs, i) {
1155 var limit = (i === undefined ? segs.length : i + 1);
1156 for (var j = 0; j < limit; ++j) {
1157 root = root ? root[segs[j]] : undefined;
1158 }
1159 return root;
1160 };
1161
1162 // unsupported, NON-API function
1163 // Returns undefined to signal complex configuration which needs to be farmed out to DataBinding.js
1164 // any other return represents an environment value AND a simple configuration we can handle here
1165 fluid.decodeAccessorArg = function (arg3) {
1166 return (!arg3 || arg3 === fluid.model.defaultGetConfig || arg3 === fluid.model.defaultSetConfig) ?
1167 null : (arg3.type === "environment" ? arg3.value : undefined);
1168 };
1169
1170 fluid.set = function (root, EL, newValue, config, initSegs) {
1171 var env = fluid.decodeAccessorArg(config);
1172 if (env === undefined) {
1173 fluid.model.setWithStrategy(root, EL, newValue, config, initSegs);
1174 } else {
1175 fluid.model.setSimple(root, EL, newValue, env, initSegs);
1176 }
1177 };
1178
1179 /** Evaluates an EL expression by fetching a dot-separated list of members
1180 * recursively from a provided root.
1181 * @param {Object} root - The root data structure in which the EL expression is to be evaluated
1182 * @param {String|Array} EL - The EL expression to be evaluated, or an array of path segments
1183 * @param {Object} [config] - An optional configuration or environment structure which can customise the fetch operation
1184 * @return {Any} The fetched data value.
1185 */
1186
1187 fluid.get = function (root, EL, config, initSegs) {
1188 var env = fluid.decodeAccessorArg(config);
1189 return env === undefined ?
1190 fluid.model.getWithStrategy(root, EL, config, initSegs)
1191 : fluid.model.accessImpl(root, EL, fluid.NO_VALUE, env, null, false, fluid.model.traverseSimple);
1192 };
1193
1194 fluid.getGlobalValue = function (path, env) {
1195 if (path) {
1196 env = env || fluid.environment;
1197 return fluid.get(fluid.global, path, {type: "environment", value: env});
1198 }
1199 };
1200
1201 /**
1202 * Allows for the binding to a "this-ist" function
1203 * @param {Object} obj - "this-ist" object to bind to
1204 * @param {Object} fnName - The name of the function to call.
1205 * @param {Object} args - Arguments to call the function with.
1206 * @return {Any} - The return value (if any) of the underlying function.
1207 */
1208 fluid.bind = function (obj, fnName, args) {
1209 return obj[fnName].apply(obj, fluid.makeArray(args));
1210 };
1211
1212 /**
1213 * Allows for the calling of a function from an EL expression "functionPath", with the arguments "args", scoped to an framework version "environment".
1214 * @param {Object} functionPath - An EL expression
1215 * @param {Object} args - An array of arguments to be applied to the function, specified in functionPath
1216 * @param {Object} [environment] - (optional) The object to scope the functionPath to (typically the framework root for version control)
1217 * @return {Any} - The return value from the invoked function.
1218 */
1219 fluid.invokeGlobalFunction = function (functionPath, args, environment) {
1220 var func = fluid.getGlobalValue(functionPath, environment);
1221 if (!func) {
1222 fluid.fail("Error invoking global function: " + functionPath + " could not be located");
1223 } else {
1224 return func.apply(null, fluid.isArrayable(args) ? args : fluid.makeArray(args));
1225 }
1226 };
1227
1228 /* Registers a new global function at a given path */
1229
1230 fluid.registerGlobalFunction = function (functionPath, func, env) {
1231 env = env || fluid.environment;
1232 fluid.set(fluid.global, functionPath, func, {type: "environment", value: env});
1233 };
1234
1235 fluid.setGlobalValue = fluid.registerGlobalFunction;
1236
1237 /* Ensures that an entry in the global namespace exists. If it does not, a new entry is created as {} and returned. If an existing
1238 * value is found, it is returned instead */
1239 fluid.registerNamespace = function (naimspace, env) {
1240 env = env || fluid.environment;
1241 var existing = fluid.getGlobalValue(naimspace, env);
1242 if (!existing) {
1243 existing = {};
1244 fluid.setGlobalValue(naimspace, existing, env);
1245 }
1246 return existing;
1247 };
1248
1249 // stubs for two functions in FluidDebugging.js
1250 fluid.dumpEl = fluid.identity;
1251 fluid.renderTimestamp = fluid.identity;
1252
1253 /*** The Fluid instance id ***/
1254
1255 // unsupported, NON-API function
1256 fluid.generateUniquePrefix = function () {
1257 return (Math.floor(Math.random() * 1e12)).toString(36) + "-";
1258 };
1259
1260 var fluid_prefix = fluid.generateUniquePrefix();
1261
1262 fluid.fluidInstance = fluid_prefix;
1263
1264 var fluid_guid = 1;
1265
1266 /* Allocate a string value that will be unique within this Infusion instance (frame or process), and
1267 * globally unique with high probability (50% chance of collision after a million trials) */
1268
1269 fluid.allocateGuid = function () {
1270 return fluid_prefix + (fluid_guid++);
1271 };
1272
1273 /*** The Fluid Event system. ***/
1274
1275 fluid.registerNamespace("fluid.event");
1276
1277 // Fluid priority system for encoding relative positions of, e.g. listeners, transforms, options, in lists
1278
1279 fluid.extremePriority = 4e9; // around 2^32 - allows headroom of 21 fractional bits for sub-priorities
1280 fluid.priorityTypes = {
1281 first: -1,
1282 last: 1,
1283 before: 0,
1284 after: 0
1285 };
1286 // TODO: This should be properly done with defaults blocks and a much more performant fluid.indexDefaults
1287 fluid.extremalPriorities = {
1288 // a built-in definition to allow test infrastructure "last" listeners to sort after all impl listeners, and authoring/debugging listeners to sort after those
1289 // these are "priority intensities", and will be flipped for "first" listeners
1290 none: 0,
1291 testing: 10,
1292 authoring: 20
1293 };
1294
1295 // unsupported, NON-API function
1296 // TODO: Note - no "fixedOnly = true" sites remain in the framework
1297 fluid.parsePriorityConstraint = function (constraint, fixedOnly, site) {
1298 var segs = constraint.split(":");
1299 var type = segs[0];
1300 var lookup = fluid.priorityTypes[type];
1301 if (lookup === undefined) {
1302 fluid.fail("Invalid constraint type in priority field " + constraint + ": the only supported values are " + fluid.keys(fluid.priorityTypes).join(", ") + " or numeric");
1303 }
1304 if (fixedOnly && lookup === 0) {
1305 fluid.fail("Constraint type in priority field " + constraint + " is not supported in a " + site + " record - you must use either a numeric value or first, last");
1306 }
1307 return {
1308 type: segs[0],
1309 target: segs[1]
1310 };
1311 };
1312
1313 // unsupported, NON-API function
1314 fluid.parsePriority = function (priority, count, fixedOnly, site) {
1315 priority = priority || 0;
1316 var togo = {
1317 count: count || 0,
1318 fixed: null,
1319 constraint: null,
1320 site: site
1321 };
1322 if (typeof(priority) === "number") {
1323 togo.fixed = -priority;
1324 } else {
1325 togo.constraint = fluid.parsePriorityConstraint(priority, fixedOnly, site);
1326 }
1327 var multiplier = togo.constraint ? fluid.priorityTypes[togo.constraint.type] : 0;
1328 if (multiplier !== 0) {
1329 var target = togo.constraint.target || "none";
1330 var extremal = fluid.extremalPriorities[target];
1331 if (extremal === undefined) {
1332 fluid.fail("Unrecognised extremal priority target " + target + ": the currently supported values are " + fluid.keys(fluid.extremalPriorities).join(", ") + ": register your value in fluid.extremalPriorities");
1333 }
1334 togo.fixed = multiplier * (fluid.extremePriority + extremal);
1335 }
1336 if (togo.fixed !== null) {
1337 togo.fixed += togo.count / 1024; // use some fractional bits to encode count bias
1338 }
1339
1340 return togo;
1341 };
1342
1343 fluid.renderPriority = function (parsed) {
1344 return parsed.constraint ? (parsed.constraint.target ? parsed.constraint.type + ":" + parsed.constraint.target : parsed.constraint.type ) : Math.floor(parsed.fixed);
1345 };
1346
1347 // unsupported, NON-API function
1348 fluid.compareByPriority = function (recA, recB) {
1349 if (recA.priority.fixed !== null && recB.priority.fixed !== null) {
1350 return recA.priority.fixed - recB.priority.fixed;
1351 } else { // sort constraint records to the end
1352 // relies on JavaScript boolean coercion rules (ECMA 9.3 toNumber)
1353 return (recA.priority.fixed === null) - (recB.priority.fixed === null);
1354 }
1355 };
1356
1357 fluid.honourConstraint = function (array, firstConstraint, c) {
1358 var constraint = array[c].priority.constraint;
1359 var matchIndex = fluid.find(array, function (element, index) {
1360 return element.namespace === constraint.target ? index : undefined;
1361 }, -1);
1362 if (matchIndex === -1) { // TODO: We should report an error during firing if this condition persists until then
1363 return true;
1364 } else if (matchIndex >= firstConstraint) {
1365 return false;
1366 } else {
1367 var offset = constraint.type === "after" ? 1 : 0;
1368 var target = matchIndex + offset;
1369 var temp = array[c];
1370 for (var shift = c; shift >= target; --shift) {
1371 array[shift] = array[shift - 1];
1372 }
1373 array[target] = temp;
1374 return true;
1375 }
1376 };
1377
1378 // unsupported, NON-API function
1379 // Priorities accepted from users have higher numbers representing high priority (sort first) -
1380 fluid.sortByPriority = function (array) {
1381 fluid.stableSort(array, fluid.compareByPriority);
1382
1383 var firstConstraint = fluid.find(array, function (element, index) {
1384 return element.priority.constraint && fluid.priorityTypes[element.priority.constraint.type] === 0 ? index : undefined;
1385 }, array.length);
1386
1387 while (true) {
1388 if (firstConstraint === array.length) {
1389 return array;
1390 }
1391 var oldFirstConstraint = firstConstraint;
1392 for (var c = firstConstraint; c < array.length; ++c) {
1393 var applied = fluid.honourConstraint(array, firstConstraint, c);
1394 if (applied) {
1395 ++firstConstraint;
1396 }
1397 }
1398 if (firstConstraint === oldFirstConstraint) {
1399 var holders = array.slice(firstConstraint);
1400 fluid.fail("Could not find targets for any constraints in " + holders[0].priority.site + " ", holders, ": none of the targets (" + fluid.getMembers(holders, "priority.constraint.target").join(", ") +
1401 ") matched any namespaces of the elements in (", array.slice(0, firstConstraint), ") - this is caused by either an invalid or circular reference");
1402 }
1403 }
1404 };
1405
1406 /** Parse a hash containing prioritised records (for example, as found in a ContextAwareness record) and return a sorted array of these records in priority order.
1407 * @param {Object} records - A hash of key names to prioritised records. Each record may contain an member `namespace` - if it does not, the namespace will be taken from the
1408 * record's key. It may also contain a `String` member `priority` encoding a priority with respect to these namespaces as document at http://docs.fluidproject.org/infusion/development/Priorities.html .
1409 * @param {String} name - A human-readable name describing the supplied records, which will be incorporated into the message of any error encountered when resolving the priorities
1410 * @return {Array} An array of the same elements supplied to `records`, sorted into priority order. The supplied argument `records` will not be modified.
1411 */
1412 fluid.parsePriorityRecords = function (records, name) {
1413 var array = fluid.hashToArray(records, "namespace", function (newElement, oldElement) {
1414 $.extend(newElement, oldElement);
1415 newElement.priority = fluid.parsePriority(oldElement.priority, 0, false, name);
1416 });
1417 fluid.sortByPriority(array);
1418 return array;
1419 };
1420
1421 fluid.event.identifyListener = function (listener, soft) {
1422 if (typeof(listener) !== "string" && !listener.$$fluid_guid && !soft) {
1423 listener.$$fluid_guid = fluid.allocateGuid();
1424 }
1425 return listener.$$fluid_guid;
1426 };
1427
1428 // unsupported, NON-API function
1429 fluid.event.impersonateListener = function (origListener, newListener) {
1430 fluid.event.identifyListener(origListener);
1431 newListener.$$fluid_guid = origListener.$$fluid_guid;
1432 };
1433
1434
1435 // unsupported, NON-API function
1436 fluid.event.sortListeners = function (listeners) {
1437 var togo = [];
1438 fluid.each(listeners, function (oneNamespace) {
1439 var headHard; // notify only the first listener with hard namespace - or else all if all are soft
1440 for (var i = 0; i < oneNamespace.length; ++i) {
1441 var thisListener = oneNamespace[i];
1442 if (!thisListener.softNamespace && !headHard) {
1443 headHard = thisListener;
1444 }
1445 }
1446 if (headHard) {
1447 togo.push(headHard);
1448 } else {
1449 togo = togo.concat(oneNamespace);
1450 }
1451 });
1452 return fluid.sortByPriority(togo);
1453 };
1454
1455 // unsupported, NON-API function
1456 fluid.event.resolveListener = function (listener) {
1457 var listenerName = listener.globalName || (typeof(listener) === "string" ? listener : null);
1458 if (listenerName) {
1459 var listenerFunc = fluid.getGlobalValue(listenerName);
1460 if (!listenerFunc) {
1461 fluid.fail("Unable to look up name " + listenerName + " as a global function");
1462 } else {
1463 listener = listenerFunc;
1464 }
1465 }
1466 return listener;
1467 };
1468
1469 /* Generate a name for a component for debugging purposes */
1470 fluid.nameComponent = function (that) {
1471 return that ? "component with typename " + that.typeName + " and id " + that.id : "[unknown component]";
1472 };
1473
1474 fluid.event.nameEvent = function (that, eventName) {
1475 return eventName + " of " + fluid.nameComponent(that);
1476 };
1477
1478 /** Construct an "event firer" object which can be used to register and deregister
1479 * listeners, to which "events" can be fired. These events consist of an arbitrary
1480 * function signature. General documentation on the Fluid events system is at
1481 * http://docs.fluidproject.org/infusion/development/InfusionEventSystem.html .
1482 * @param {Object} options - A structure to configure this event firer. Supported fields:
1483 * {String} name - a readable name for this firer to be used in diagnostics and debugging
1484 * {Boolean} preventable - If <code>true</code> the return value of each handler will
1485 * be checked for <code>false</code> in which case further listeners will be shortcircuited, and this
1486 * will be the return value of fire()
1487 * @return {Object} - The newly-created event firer.
1488 */
1489 fluid.makeEventFirer = function (options) {
1490 options = options || {};
1491 var name = options.name || "<anonymous>";
1492 var that;
1493
1494 var lazyInit = function () { // Lazy init function to economise on object references for events which are never listened to
1495 // The authoritative list of all listeners, a hash indexed by namespace, looking up to a stack (array) of
1496 // listener records in "burial order"
1497 that.listeners = {};
1498 // An index of all listeners by "id" - we should consider removing this since it is only used during removal
1499 // and because that.listeners is a hash of stacks we can't really accelerate removal by much
1500 that.byId = {};
1501 // The "live" list of listeners which will be notified in order on any firing. Recomputed on any use of
1502 // addListener/removeListener
1503 that.sortedListeners = [];
1504 // arguments after 3rd are not part of public API
1505 // listener as Object is used only by ChangeApplier to tunnel path, segs, etc as part of its "spec"
1506 /** Adds a listener to this event.
1507 * @param {Function|String} listener - The listener function to be added, or a global name resolving to a function. The signature of the function is arbitrary and matches that sent to event.fire()
1508 * @param {String} namespace - (Optional) A namespace for this listener. At most one listener with a particular namespace can be active on an event at one time. Removing successively added listeners with a particular
1509 * namespace will expose previously added ones in a stack idiom
1510 * @param {String|Number} priority - A priority for the listener relative to others, perhaps expressed with a constraint relative to the namespace of another - see
1511 * http://docs.fluidproject.org/infusion/development/Priorities.html
1512 * @param {String} softNamespace - An unsupported internal option that is not part of the public API.
1513 * @param {String} listenerId - An unsupported internal option that is not part of the public API.
1514 */
1515 that.addListener = function (listener, namespace, priority, softNamespace, listenerId) {
1516 var record;
1517 if (that.destroyed) {
1518 fluid.fail("Cannot add listener to destroyed event firer " + that.name);
1519 }
1520 if (!listener) {
1521 return;
1522 }
1523 if (fluid.isPlainObject(listener, true) && !fluid.isApplicable(listener)) {
1524 record = listener;
1525 listener = record.listener;
1526 namespace = record.namespace;
1527 priority = record.priority;
1528 softNamespace = record.softNamespace;
1529 listenerId = record.listenerId;
1530 }
1531 if (typeof(listener) === "string") {
1532 listener = {globalName: listener};
1533 }
1534 var id = listenerId || fluid.event.identifyListener(listener);
1535 namespace = namespace || id;
1536 record = $.extend(record || {}, {
1537 namespace: namespace,
1538 listener: listener,
1539 softNamespace: softNamespace,
1540 listenerId: listenerId,
1541 priority: fluid.parsePriority(priority, that.sortedListeners.length, false, "listeners")
1542 });
1543 that.byId[id] = record;
1544
1545 var thisListeners = (that.listeners[namespace] = fluid.makeArray(that.listeners[namespace]));
1546 thisListeners[softNamespace ? "push" : "unshift"] (record);
1547
1548 that.sortedListeners = fluid.event.sortListeners(that.listeners);
1549 };
1550 that.addListener.apply(null, arguments);
1551 };
1552 that = {
1553 eventId: fluid.allocateGuid(),
1554 name: name,
1555 ownerId: options.ownerId,
1556 typeName: "fluid.event.firer",
1557 destroy: function () {
1558 that.destroyed = true;
1559 },
1560 addListener: function () {
1561 lazyInit.apply(null, arguments);
1562 },
1563 /** Removes a listener previously registered with this event.
1564 * @param {Function|String} toremove - Either the listener function, the namespace of a listener (in which case a previous listener with that namespace may be uncovered) or an id sent to the undocumented
1565 * `listenerId` argument of `addListener
1566 */
1567 // Can be supplied either listener, namespace, or id (which may match either listener function's guid or original listenerId argument)
1568 removeListener: function (listener) {
1569 if (!that.listeners) { return; }
1570 var namespace, id, record;
1571 if (typeof (listener) === "string") {
1572 namespace = listener;
1573 record = that.listeners[namespace];
1574 if (!record) { // it was an id and not a namespace - take the namespace from its record later
1575 id = namespace;
1576 namespace = null;
1577 }
1578 }
1579 else if (typeof(listener) === "function") {
1580 id = fluid.event.identifyListener(listener, true);
1581 if (!id) {
1582 fluid.fail("Cannot remove unregistered listener function ", listener, " from event " + that.name);
1583 }
1584 }
1585 var rec = that.byId[id];
1586 var softNamespace = rec && rec.softNamespace;
1587 namespace = namespace || (rec && rec.namespace) || id;
1588 delete that.byId[id];
1589 record = that.listeners[namespace];
1590 if (record) {
1591 if (softNamespace) {
1592 fluid.remove_if(record, function (thisLis) {
1593 return thisLis.listener.$$fluid_guid === id || thisLis.listenerId === id;
1594 });
1595 } else {
1596 record.shift();
1597 }
1598 if (record.length === 0) {
1599 delete that.listeners[namespace];
1600 }
1601 }
1602 that.sortedListeners = fluid.event.sortListeners(that.listeners);
1603 },
1604 /* Fires this event to all listeners which are active. They will be notified in order of priority. The signature of this method is free. */
1605 fire: function () {
1606 var listeners = that.sortedListeners;
1607 if (!listeners || that.destroyed) { return; }
1608 for (var i = 0; i < listeners.length; ++i) {
1609 var lisrec = listeners[i];
1610 if (typeof(lisrec.listener) !== "function") {
1611 lisrec.listener = fluid.event.resolveListener(lisrec.listener);
1612 }
1613 var listener = lisrec.listener;
1614 var ret = listener.apply(null, arguments);
1615 var value;
1616 if (options.preventable && ret === false || that.destroyed) {
1617 value = false;
1618 }
1619 if (value !== undefined) {
1620 return value;
1621 }
1622 }
1623 }
1624 };
1625 return that;
1626 };
1627
1628 // unsupported, NON-API function
1629 // Fires to an event which may not be instantiated (in which case no-op) - primary modern usage is to resolve FLUID-5904
1630 fluid.fireEvent = function (component, eventName, args) {
1631 var firer = component.events[eventName];
1632 if (firer) {
1633 firer.fire.apply(null, fluid.makeArray(args));
1634 }
1635 };
1636
1637 // unsupported, NON-API function
1638 fluid.event.addListenerToFirer = function (firer, value, namespace, wrapper) {
1639 wrapper = wrapper || fluid.identity;
1640 if (fluid.isArrayable(value)) {
1641 for (var i = 0; i < value.length; ++i) {
1642 fluid.event.addListenerToFirer(firer, value[i], namespace, wrapper);
1643 }
1644 } else if (typeof (value) === "function" || typeof (value) === "string") {
1645 wrapper(firer).addListener(value, namespace);
1646 } else if (value && typeof (value) === "object") {
1647 wrapper(firer).addListener(value.listener, namespace || value.namespace, value.priority, value.softNamespace, value.listenerId);
1648 }
1649 };
1650
1651 // unsupported, NON-API function - non-IOC passthrough
1652 fluid.event.resolveListenerRecord = function (records) {
1653 return { records: records };
1654 };
1655
1656 fluid.expandImmediate = function (material) {
1657 fluid.fail("fluid.expandImmediate could not be loaded - please include FluidIoC.js in order to operate IoC-driven event with descriptor " + material);
1658 };
1659
1660 // unsupported, NON-API function
1661 fluid.mergeListeners = function (that, events, listeners) {
1662 fluid.each(listeners, function (value, key) {
1663 var firer, namespace;
1664 if (fluid.isIoCReference(key)) {
1665 firer = fluid.expandImmediate(key, that);
1666 if (!firer) {
1667 fluid.fail("Error in listener record: key " + key + " could not be looked up to an event firer - did you miss out \"events.\" when referring to an event firer?");
1668 }
1669 } else {
1670 var keydot = key.indexOf(".");
1671
1672 if (keydot !== -1) {
1673 namespace = key.substring(keydot + 1);
1674 key = key.substring(0, keydot);
1675 }
1676 if (!events[key]) {
1677 fluid.fail("Listener registered for event " + key + " which is not defined for this component");
1678 }
1679 firer = events[key];
1680 }
1681 var record = fluid.event.resolveListenerRecord(value, that, key, namespace, true);
1682 fluid.event.addListenerToFirer(firer, record.records, namespace, record.adderWrapper);
1683 });
1684 };
1685
1686 // unsupported, NON-API function
1687 fluid.eventFromRecord = function (eventSpec, eventKey, that) {
1688 var isIoCEvent = eventSpec && (typeof (eventSpec) !== "string" || fluid.isIoCReference(eventSpec));
1689 var event;
1690 if (isIoCEvent) {
1691 if (!fluid.event.resolveEvent) {
1692 fluid.fail("fluid.event.resolveEvent could not be loaded - please include FluidIoC.js in order to operate IoC-driven event with descriptor ",
1693 eventSpec);
1694 } else {
1695 event = fluid.event.resolveEvent(that, eventKey, eventSpec);
1696 }
1697 } else {
1698 event = fluid.makeEventFirer({
1699 name: fluid.event.nameEvent(that, eventKey),
1700 preventable: eventSpec === "preventable",
1701 ownerId: that.id
1702 });
1703 }
1704 return event;
1705 };
1706
1707 // unsupported, NON-API function - this is patched from FluidIoC.js
1708 fluid.instantiateFirers = function (that, options) {
1709 fluid.each(options.events, function (eventSpec, eventKey) {
1710 that.events[eventKey] = fluid.eventFromRecord(eventSpec, eventKey, that);
1711 });
1712 };
1713
1714 // unsupported, NON-API function
1715 fluid.mergeListenerPolicy = function (target, source, key) {
1716 if (typeof (key) !== "string") {
1717 fluid.fail("Error in listeners declaration - the keys in this structure must resolve to event names - got " + key + " from ", source);
1718 }
1719 // cf. triage in mergeListeners
1720 var hasNamespace = !fluid.isIoCReference(key) && key.indexOf(".") !== -1;
1721 return hasNamespace ? (source || target) : fluid.arrayConcatPolicy(target, source);
1722 };
1723
1724 // unsupported, NON-API function
1725 fluid.makeMergeListenersPolicy = function (merger, modelRelay) {
1726 return function (target, source) {
1727 target = target || {};
1728 if (modelRelay && (fluid.isArrayable(source) || typeof(source.target) === "string")) { // This form allowed for modelRelay
1729 target[""] = merger(target[""], source, "");
1730 } else {
1731 fluid.each(source, function (listeners, key) {
1732 target[key] = merger(target[key], listeners, key);
1733 });
1734 }
1735 return target;
1736 };
1737 };
1738
1739 fluid.validateListenersImplemented = function (that) {
1740 var errors = [];
1741 fluid.each(that.events, function (event, name) {
1742 fluid.each(event.sortedListeners, function (lisrec) {
1743 if (lisrec.listener === fluid.notImplemented || lisrec.listener.globalName === "fluid.notImplemented") {
1744 errors.push({name: name, namespace: lisrec.namespace, componentSource: fluid.model.getSimple(that.options.listeners, [name + "." + lisrec.namespace, 0, "componentSource"])});
1745 }
1746 });
1747 });
1748 return errors;
1749 };
1750
1751 /* Removes duplicated and empty elements from an already sorted array. */
1752 fluid.unique = function (array) {
1753 return fluid.remove_if(array, function (element, i) {
1754 return !element || i > 0 && element === array[i - 1];
1755 });
1756 };
1757
1758 fluid.arrayConcatPolicy = function (target, source) {
1759 return fluid.makeArray(target).concat(fluid.makeArray(source));
1760 };
1761
1762 /*** FLUID LOGGING SYSTEM ***/
1763
1764 // This event represents the process of resolving the action of a request to fluid.log. Each listener shares
1765 // access to an array, shallow-copied from the original arguments list to fluid.log, which is assumed writeable
1766 // and which they may splice, transform, etc. before it is dispatched to the listener with namespace "log" which
1767 // actually performs the logging action
1768 fluid.loggingEvent = fluid.makeEventFirer({name: "logging event"});
1769
1770 fluid.addTimestampArg = function (args) {
1771 var arg0 = fluid.renderTimestamp(new Date()) + ": ";
1772 args.unshift(arg0);
1773 };
1774
1775 fluid.loggingEvent.addListener(fluid.doBrowserLog, "log");
1776 // Not intended to be overridden - just a positional placeholder so that the priority of
1777 // actions filtering the log arguments before dispatching may be referred to it
1778 fluid.loggingEvent.addListener(fluid.identity, "filterArgs", "before:log");
1779 fluid.loggingEvent.addListener(fluid.addTimestampArg, "addTimestampArg", "after:filterArgs");
1780
1781 /*** FLUID ERROR SYSTEM ***/
1782
1783 fluid.failureEvent = fluid.makeEventFirer({name: "failure event"});
1784
1785 fluid.failureEvent.addListener(fluid.builtinFail, "fail");
1786 fluid.failureEvent.addListener(fluid.logFailure, "log", "before:fail");
1787
1788 /**
1789 * Configure the behaviour of fluid.fail by pushing or popping a disposition record onto a stack.
1790 * @param {Number|Function} condition - Supply either a function, which will be called with two arguments, args (the complete arguments to
1791 * fluid.fail) and activity, an array of strings describing the current framework invocation state.
1792 * Or, the argument may be the number <code>-1</code> indicating that the previously supplied disposition should
1793 * be popped off the stack
1794 */
1795 fluid.pushSoftFailure = function (condition) {
1796 if (typeof (condition) === "function") {
1797 fluid.failureEvent.addListener(condition, "fail");
1798 } else if (condition === -1) {
1799 fluid.failureEvent.removeListener("fail");
1800 } else if (typeof(condition) === "boolean") {
1801 fluid.fail("pushSoftFailure with boolean value is no longer supported");
1802 }
1803 };
1804
1805 /*** DEFAULTS AND OPTIONS MERGING SYSTEM ***/
1806
1807 // A function to tag the types of all Fluid components
1808 fluid.componentConstructor = function () {};
1809
1810 /** Create a "type tag" component with no state but simply a type name and id. The most
1811 * minimal form of Fluid component */
1812 // No longer a publically supported function - we don't abolish this because it is too annoying to prevent
1813 // circularity during the bootup of the IoC system if we try to construct full components before it is complete
1814 // unsupported, non-API function
1815 fluid.typeTag = function (name) {
1816 var that = Object.create(fluid.componentConstructor.prototype);
1817 that.typeName = name;
1818 that.id = fluid.allocateGuid();
1819 return that;
1820 };
1821
1822 var gradeTick = 1; // tick counter for managing grade cache invalidation
1823 var gradeTickStore = {};
1824
1825 fluid.defaultsStore = {};
1826
1827 // unsupported, NON-API function
1828 // Recursively builds up "gradeStructure" in first argument. 2nd arg receives gradeNames to be resolved, with stronger grades at right (defaults order)
1829 // builds up gradeStructure.gradeChain pushed from strongest to weakest (reverse defaults order)
1830 fluid.resolveGradesImpl = function (gs, gradeNames) {
1831 gradeNames = fluid.makeArray(gradeNames);
1832 for (var i = gradeNames.length - 1; i >= 0; --i) { // from stronger to weaker
1833 var gradeName = gradeNames[i];
1834 if (gradeName && !gs.gradeHash[gradeName]) {
1835 var isDynamic = fluid.isIoCReference(gradeName);
1836 var options = (isDynamic ? null : fluid.rawDefaults(gradeName)) || {};
1837 var thisTick = gradeTickStore[gradeName] || (gradeTick - 1); // a nonexistent grade is recorded as just previous to current
1838 gs.lastTick = Math.max(gs.lastTick, thisTick);
1839 gs.gradeHash[gradeName] = true;
1840 gs.gradeChain.push(gradeName);
1841 var oGradeNames = fluid.makeArray(options.gradeNames);
1842 for (var j = oGradeNames.length - 1; j >= 0; --j) { // from stronger to weaker grades
1843 // TODO: in future, perhaps restore mergedDefaultsCache function of storing resolved gradeNames for bare grades
1844 fluid.resolveGradesImpl(gs, oGradeNames[j]);
1845 }
1846 }
1847 }
1848 return gs;
1849 };
1850
1851 // unsupported, NON-API function
1852 fluid.resolveGradeStructure = function (defaultName, gradeNames) {
1853 var gradeStruct = {
1854 lastTick: 0,
1855 gradeChain: [],
1856 gradeHash: {}
1857 };
1858 // stronger grades appear to the right in defaults - dynamic grades are stronger still - FLUID-5085
1859 // we supply these to resolveGradesImpl with strong grades at the right
1860 fluid.resolveGradesImpl(gradeStruct, [defaultName].concat(fluid.makeArray(gradeNames)));
1861 gradeStruct.gradeChain.reverse(); // reverse into defaults order
1862 return gradeStruct;
1863 };
1864
1865 fluid.hasGrade = function (options, gradeName) {
1866 return !options || !options.gradeNames ? false : fluid.contains(options.gradeNames, gradeName);
1867 };
1868
1869 // unsupported, NON-API function
1870 fluid.resolveGrade = function (defaults, defaultName, gradeNames) {
1871 var gradeStruct = fluid.resolveGradeStructure(defaultName, gradeNames);
1872 // TODO: Fault in the merging algorithm does not actually treat arguments as immutable - failure in FLUID-5082 tests
1873 // due to listeners mergePolicy
1874 var mergeArgs = fluid.transform(gradeStruct.gradeChain, fluid.rawDefaults, fluid.copy);
1875 fluid.remove_if(mergeArgs, function (options) {
1876 return !options;
1877 });
1878 var mergePolicy = {};
1879 for (var i = 0; i < mergeArgs.length; ++i) {
1880 if (mergeArgs[i] && mergeArgs[i].mergePolicy) {
1881 mergePolicy = $.extend(true, mergePolicy, mergeArgs[i].mergePolicy);
1882 }
1883 }
1884 mergeArgs = [mergePolicy, {}].concat(mergeArgs);
1885 var mergedDefaults = fluid.merge.apply(null, mergeArgs);
1886 mergedDefaults.gradeNames = gradeStruct.gradeChain; // replace these since mergePolicy version is inadequate
1887 fluid.freezeRecursive(mergedDefaults);
1888 return {defaults: mergedDefaults, lastTick: gradeStruct.lastTick};
1889 };
1890
1891 fluid.mergedDefaultsCache = {};
1892
1893 // unsupported, NON-API function
1894 fluid.gradeNamesToKey = function (defaultName, gradeNames) {
1895 return defaultName + "|" + gradeNames.join("|");
1896 };
1897
1898 // unsupported, NON-API function
1899 // The main entry point to acquire the fully merged defaults for a combination of defaults plus mixin grades - from FluidIoC.js as well as recursively within itself
1900 fluid.getMergedDefaults = function (defaultName, gradeNames) {
1901 gradeNames = fluid.makeArray(gradeNames);
1902 var key = fluid.gradeNamesToKey(defaultName, gradeNames);
1903 var mergedDefaults = fluid.mergedDefaultsCache[key];
1904 if (mergedDefaults) {
1905 var lastTick = 0; // check if cache should be invalidated through real latest tick being later than the one stored
1906 var searchGrades = mergedDefaults.defaults.gradeNames || [];
1907 for (var i = 0; i < searchGrades.length; ++i) {
1908 lastTick = Math.max(lastTick, gradeTickStore[searchGrades[i]] || 0);
1909 }
1910 if (lastTick > mergedDefaults.lastTick) {
1911 if (fluid.passLogLevel(fluid.logLevel.TRACE)) {
1912 fluid.log(fluid.logLevel.TRACE, "Clearing cache for component " + defaultName + " with gradeNames ", searchGrades);
1913 }
1914 mergedDefaults = null;
1915 }
1916 }
1917 if (!mergedDefaults) {
1918 var defaults = fluid.rawDefaults(defaultName);
1919 if (!defaults) {
1920 return defaults;
1921 }
1922 mergedDefaults = fluid.mergedDefaultsCache[key] = fluid.resolveGrade(defaults, defaultName, gradeNames);
1923 }
1924 return mergedDefaults.defaults;
1925 };
1926
1927 // unsupported, NON-API function
1928 /** Upgrades an element of an IoC record which designates a function to prepare for a {func, args} representation.
1929 * @param {Any} rec - If the record is of a primitive type,
1930 * @param {String} key - The key in the returned record to hold the function, this will default to `funcName` if `rec` is a `string` *not*
1931 * holding an IoC reference, or `func` otherwise
1932 * @return {Object} The original `rec` if it was not of primitive type, else a record holding { key : rec } if it was of primitive type.
1933 */
1934 fluid.upgradePrimitiveFunc = function (rec, key) {
1935 if (rec && fluid.isPrimitive(rec)) {
1936 var togo = {};
1937 togo[key || (typeof(rec) === "string" && rec.charAt(0) !== "{" ? "funcName" : "func")] = rec;
1938 togo.args = fluid.NO_VALUE;
1939 return togo;
1940 } else {
1941 return rec;
1942 }
1943 };
1944
1945 // unsupported, NON-API function
1946 // Modify supplied options record to include "componentSource" annotation required by FLUID-5082
1947 // TODO: This function really needs to act recursively in order to catch listeners registered for subcomponents - fix with FLUID-5614
1948 fluid.annotateListeners = function (componentName, options) {
1949 options.listeners = fluid.transform(options.listeners, function (record) {
1950 var togo = fluid.makeArray(record);
1951 return fluid.transform(togo, function (onerec) {
1952 onerec = fluid.upgradePrimitiveFunc(onerec, "listener");
1953 onerec.componentSource = componentName;
1954 return onerec;
1955 });
1956 });
1957 options.invokers = fluid.transform(options.invokers, function (record) {
1958 record = fluid.upgradePrimitiveFunc(record);
1959 if (record) {
1960 record.componentSource = componentName;
1961 }
1962 return record;
1963 });
1964 };
1965
1966 // unsupported, NON-API function
1967 fluid.rawDefaults = function (componentName) {
1968 var entry = fluid.defaultsStore[componentName];
1969 return entry && entry.options;
1970 };
1971
1972 // unsupported, NON-API function
1973 fluid.registerRawDefaults = function (componentName, options) {
1974 fluid.pushActivity("registerRawDefaults", "registering defaults for grade %componentName with options %options",
1975 {componentName: componentName, options: options});
1976 var optionsCopy = fluid.expandCompact ? fluid.expandCompact(options) : fluid.copy(options);
1977 fluid.annotateListeners(componentName, optionsCopy);
1978 var callerInfo = fluid.getCallerInfo && fluid.getCallerInfo(6);
1979 fluid.defaultsStore[componentName] = {
1980 options: optionsCopy,
1981 callerInfo: callerInfo
1982 };
1983 gradeTickStore[componentName] = gradeTick++;
1984 fluid.popActivity();
1985 };
1986
1987 // unsupported, NON-API function
1988 fluid.doIndexDefaults = function (defaultName, defaults, index, indexSpec) {
1989 var requiredGrades = fluid.makeArray(indexSpec.gradeNames);
1990 for (var i = 0; i < requiredGrades.length; ++i) {
1991 if (!fluid.hasGrade(defaults, requiredGrades[i])) { return; }
1992 }
1993 var indexFunc = typeof(indexSpec.indexFunc) === "function" ? indexSpec.indexFunc : fluid.getGlobalValue(indexSpec.indexFunc);
1994 var keys = indexFunc(defaults) || [];
1995 for (var j = 0; j < keys.length; ++j) {
1996 fluid.pushArray(index, keys[j], defaultName);
1997 }
1998 };
1999
2000 /** Evaluates an index specification over all the defaults records registered into the system.
2001 * @param {String} indexName - The name of this index record (currently ignored)
2002 * @param {Object} indexSpec - Specification of the index to be performed - fields:
2003 * gradeNames: {String|String[]} List of grades that must be matched by this indexer
2004 * indexFunc: {String|Function} An index function which accepts a defaults record and returns an array of keys
2005 * @return A structure indexing keys to arrays of matched gradenames
2006 */
2007 // The expectation is that this function is extremely rarely used with respect to registration of defaults
2008 // in the system, so currently we do not make any attempts to cache the results. The field "indexName" is
2009 // supplied in case a future implementation chooses to implement caching
2010 fluid.indexDefaults = function (indexName, indexSpec) {
2011 var index = {};
2012 for (var defaultName in fluid.defaultsStore) {
2013 var defaults = fluid.getMergedDefaults(defaultName);
2014 fluid.doIndexDefaults(defaultName, defaults, index, indexSpec);
2015 }
2016 return index;
2017 };
2018
2019 /**
2020 * Retrieves and stores a grade's configuration centrally.
2021 * @param {String} componentName - The name of the grade whose options are to be read or written
2022 * @param {Object} [options] - An (optional) object containing the options to be set
2023 * @return {Object|undefined} - If `options` is omitted, returns the defaults for `componentName`. Otherwise,
2024 * creates an instance of the named component with the supplied options.
2025 */
2026 fluid.defaults = function (componentName, options) {
2027 if (options === undefined) {
2028 return fluid.getMergedDefaults(componentName);
2029 }
2030 else {
2031 if (options && options.options) {
2032 fluid.fail("Probable error in options structure for " + componentName +
2033 " with option named \"options\" - perhaps you meant to write these options at top level in fluid.defaults? - ", options);
2034 }
2035 fluid.registerRawDefaults(componentName, options);
2036 var gradedDefaults = fluid.getMergedDefaults(componentName);
2037 if (!fluid.hasGrade(gradedDefaults, "fluid.function")) {
2038 fluid.makeComponentCreator(componentName);
2039 }
2040 }
2041 };
2042
2043 fluid.makeComponentCreator = function (componentName) {
2044 var creator = function () {
2045 var defaults = fluid.getMergedDefaults(componentName);
2046 if (!defaults.gradeNames || defaults.gradeNames.length === 0) {
2047 fluid.fail("Cannot make component creator for type " + componentName + " which does not have any gradeNames defined");
2048 } else if (!defaults.initFunction) {
2049 var blankGrades = [];
2050 for (var i = 0; i < defaults.gradeNames.length; ++i) {
2051 var gradeName = defaults.gradeNames[i];
2052 var rawDefaults = fluid.rawDefaults(gradeName);
2053 if (!rawDefaults) {
2054 blankGrades.push(gradeName);
2055 }
2056 }
2057 if (blankGrades.length === 0) {
2058 fluid.fail("Cannot make component creator for type " + componentName + " which does not have an initFunction defined");
2059 } else {
2060 fluid.fail("The grade hierarchy of component with type " + componentName + " is incomplete - it inherits from the following grade(s): " +
2061 blankGrades.join(", ") + " for which the grade definitions are corrupt or missing. Please check the files which might include these " +
2062 "grades and ensure they are readable and have been loaded by this instance of Infusion");
2063 }
2064 } else {
2065 return fluid.initComponent(componentName, arguments);
2066 }
2067 };
2068 var existing = fluid.getGlobalValue(componentName);
2069 if (existing) {
2070 $.extend(creator, existing);
2071 }
2072 fluid.setGlobalValue(componentName, creator);
2073 };
2074
2075 fluid.emptyPolicy = fluid.freezeRecursive({});
2076 // unsupported, NON-API function
2077 fluid.derefMergePolicy = function (policy) {
2078 return (policy ? policy["*"] : fluid.emptyPolicy) || fluid.emptyPolicy;
2079 };
2080
2081 // unsupported, NON-API function
2082 fluid.compileMergePolicy = function (mergePolicy) {
2083 var builtins = {}, defaultValues = {};
2084 var togo = {builtins: builtins, defaultValues: defaultValues};
2085
2086 if (!mergePolicy) {
2087 return togo;
2088 }
2089 fluid.each(mergePolicy, function (value, key) {
2090 var parsed = {}, builtin = true;
2091 if (typeof(value) === "function") {
2092 parsed.func = value;
2093 }
2094 else if (typeof(value) === "object") {
2095 parsed = value;
2096 }
2097 else if (!fluid.isDefaultValueMergePolicy(value)) {
2098 var split = value.split(/\s*,\s*/);
2099 for (var i = 0; i < split.length; ++i) {
2100 parsed[split[i]] = true;
2101 }
2102 }
2103 else {
2104 // Convert to ginger self-reference - NB, this can only be parsed by IoC
2105 fluid.set(defaultValues, key, "{that}.options." + value);
2106 togo.hasDefaults = true;
2107 builtin = false;
2108 }
2109 if (builtin) {
2110 fluid.set(builtins, fluid.composePath(key, "*"), parsed);
2111 }
2112 });
2113 return togo;
2114 };
2115
2116 // TODO: deprecate this method of detecting default value merge policies before 1.6 in favour of
2117 // explicit typed records a la ModelTransformations
2118 // unsupported, NON-API function
2119 fluid.isDefaultValueMergePolicy = function (policy) {
2120 return typeof(policy) === "string" &&
2121 (policy.indexOf(",") === -1 && !/replace|nomerge|noexpand/.test(policy));
2122 };
2123
2124 // unsupported, NON-API function
2125 fluid.mergeOneImpl = function (thisTarget, thisSource, j, sources, newPolicy, i, segs) {
2126 var togo = thisTarget;
2127
2128 var primitiveTarget = fluid.isPrimitive(thisTarget);
2129
2130 if (thisSource !== undefined) {
2131 if (!newPolicy.func && thisSource !== null && fluid.isPlainObject(thisSource) && !newPolicy.nomerge) {
2132 if (primitiveTarget) {
2133 togo = thisTarget = fluid.freshContainer(thisSource);
2134 }
2135 // recursion is now external? We can't do it from here since sources are not all known
2136 // options.recurse(thisTarget, i + 1, segs, sources, newPolicyHolder, options);
2137 } else {
2138 sources[j] = undefined;
2139 if (newPolicy.func) {
2140 togo = newPolicy.func.call(null, thisTarget, thisSource, segs[i - 1], segs, i); // NB - change in this mostly unused argument
2141 } else {
2142 togo = thisSource;
2143 }
2144 }
2145 }
2146 return togo;
2147 };
2148 // NB - same quadratic worry about these as in FluidIoC in the case the RHS trundler is live -
2149 // since at each regeneration step driving the RHS we are discarding the "cursor arguments" these
2150 // would have to be regenerated at each step - although in practice this can only happen once for
2151 // each object for all time, since after first resolution it will be concrete.
2152 function regenerateCursor(source, segs, limit, sourceStrategy) {
2153 for (var i = 0; i < limit; ++i) {
2154 source = sourceStrategy(source, segs[i], i, fluid.makeArray(segs)); // copy for FLUID-5243
2155 }
2156 return source;
2157 }
2158
2159 function regenerateSources(sources, segs, limit, sourceStrategies) {
2160 var togo = [];
2161 for (var i = 0; i < sources.length; ++i) {
2162 var thisSource = regenerateCursor(sources[i], segs, limit, sourceStrategies[i]);
2163 if (thisSource !== undefined) {
2164 togo.push(thisSource);
2165 }
2166 }
2167 return togo;
2168 }
2169
2170 // unsupported, NON-API function
2171 fluid.fetchMergeChildren = function (target, i, segs, sources, mergePolicy, options) {
2172 var thisPolicy = fluid.derefMergePolicy(mergePolicy);
2173 for (var j = sources.length - 1; j >= 0; --j) { // this direction now irrelevant - control is in the strategy
2174 var source = sources[j];
2175 // NB - this detection relies on strategy return being complete objects - which they are
2176 // although we need to set up the roots separately. We need to START the process of evaluating each
2177 // object root (sources) COMPLETELY, before we even begin! Even if the effect of this is to cause a
2178 // dispatch into ourselves almost immediately. We can do this because we can take control over our
2179 // TARGET objects and construct them early. Even if there is a self-dispatch, it will be fine since it is
2180 // DIRECTED and so will not trouble our "slow" detection of properties. After all self-dispatches end, control
2181 // will THEN return to "evaluation of arguments" (expander blocks) and only then FINALLY to this "slow"
2182 // traversal of concrete properties to do the final merge.
2183 if (source !== undefined) {
2184 fluid.each(source, function (newSource, name) {
2185 var childPolicy = fluid.concreteTrundler(mergePolicy, name);
2186 // 2nd arm of condition is an Outrageous bodge to fix FLUID-4930 further. See fluid.tests.retrunking in FluidIoCTests.js
2187 // We make extra use of the old "evaluateFully" flag and ensure to flood any trunk objects again during final "initter" phase of merging.
2188 // The problem is that a custom mergePolicy may have replaced the system generated trunk with a differently structured object which we must not
2189 // corrupt. This work should properly be done with a set of dedicated provenance/progress records in a separate structure
2190 if (!(name in target) || (options.evaluateFully && childPolicy === undefined && !fluid.isPrimitive(target[name]))) { // only request each new target key once -- all sources will be queried per strategy
2191 segs[i] = name;
2192 options.strategy(target, name, i + 1, segs, sources, mergePolicy);
2193 }
2194 });
2195 if (thisPolicy.replace) { // this branch primarily deals with a policy of replace at the root
2196 break;
2197 }
2198 }
2199 }
2200 return target;
2201 };
2202
2203 // A special marker object which will be placed at a current evaluation point in the tree in order
2204 // to protect against circular evaluation
2205 fluid.inEvaluationMarker = Object.freeze({"__CURRENTLY_IN_EVALUATION__": true});
2206
2207 // A path depth above which the core "process strategies" will bail out, assuming that the
2208 // structure has become circularly linked. Helpful in environments such as Firebug which will
2209 // kill the browser process if they happen to be open when a stack overflow occurs. Also provides
2210 // a more helpful diagnostic.
2211 fluid.strategyRecursionBailout = 50;
2212
2213 // unsupported, NON-API function
2214 fluid.makeMergeStrategy = function (options) {
2215 var strategy = function (target, name, i, segs, sources, policy) {
2216 if (i > fluid.strategyRecursionBailout) {
2217 fluid.fail("Overflow/circularity in options merging, current path is ", segs, " at depth " , i, " - please protect components from merging using the \"nomerge\" merge policy");
2218 }
2219 if (fluid.isPrimitive(target)) { // For "use strict"
2220 return undefined; // Review this after FLUID-4925 since the only trigger is in slow component lookahead
2221 }
2222 if (fluid.isTracing) {
2223 fluid.tracing.pathCount.push(fluid.path(segs.slice(0, i)));
2224 }
2225
2226 var oldTarget;
2227 if (name in target) { // bail out if our work has already been done
2228 oldTarget = target[name];
2229 if (!options.evaluateFully) { // see notes on this hack in "initter" - early attempt to deal with FLUID-4930
2230 return oldTarget;
2231 }
2232 }
2233 else {
2234 if (target !== fluid.inEvaluationMarker) { // TODO: blatant "coding to the test" - this enables the simplest "re-trunking" in
2235 // FluidIoCTests to function. In practice, we need to throw away this implementation entirely in favour of the
2236 // "iterative deepening" model coming with FLUID-4925
2237 target[name] = fluid.inEvaluationMarker;
2238 }
2239 }
2240 if (sources === undefined) { // recover our state in case this is an external entry point
2241 segs = fluid.makeArray(segs); // avoid trashing caller's segs
2242 sources = regenerateSources(options.sources, segs, i - 1, options.sourceStrategies);
2243 policy = regenerateCursor(options.mergePolicy, segs, i - 1, fluid.concreteTrundler);
2244 }
2245 var newPolicyHolder = fluid.concreteTrundler(policy, name);
2246 var newPolicy = fluid.derefMergePolicy(newPolicyHolder);
2247
2248 var start, limit, mul;
2249 if (newPolicy.replace) {
2250 start = 1 - sources.length; limit = 0; mul = -1;
2251 }
2252 else {
2253 start = 0; limit = sources.length - 1; mul = +1;
2254 }
2255 var newSources = [];
2256 var thisTarget;
2257
2258 for (var j = start; j <= limit; ++j) { // TODO: try to economise on this array and on gaps
2259 var k = mul * j;
2260 var thisSource = options.sourceStrategies[k](sources[k], name, i, segs); // Run the RH algorithm in "driving" mode
2261 if (thisSource !== undefined) {
2262 if (!fluid.isPrimitive(thisSource)) {
2263 newSources[k] = thisSource;
2264 }
2265 if (oldTarget === undefined) {
2266 if (mul === -1) { // if we are going backwards, it is "replace"
2267 thisTarget = target[name] = thisSource;
2268 break;
2269 }
2270 else {
2271 // write this in early, since early expansions may generate a trunk object which is written in to by later ones
2272 thisTarget = fluid.mergeOneImpl(thisTarget, thisSource, j, newSources, newPolicy, i, segs, options);
2273 if (target !== fluid.inEvaluationMarker) {
2274 target[name] = thisTarget;
2275 }
2276 }
2277 }
2278 }
2279 }
2280 if (oldTarget !== undefined) {
2281 thisTarget = oldTarget;
2282 }
2283 if (newSources.length > 0) {
2284 if (fluid.isPlainObject(thisTarget)) {
2285 fluid.fetchMergeChildren(thisTarget, i, segs, newSources, newPolicyHolder, options);
2286 }
2287 }
2288 if (oldTarget === undefined && newSources.length === 0) {
2289 delete target[name]; // remove the evaluation marker - nothing to evaluate
2290 }
2291 return thisTarget;
2292 };
2293 options.strategy = strategy;
2294 return strategy;
2295 };
2296
2297 // A simple stand-in for "fluid.get" where the material is covered by a single strategy
2298 fluid.driveStrategy = function (root, pathSegs, strategy) {
2299 pathSegs = fluid.makeArray(pathSegs);
2300 for (var i = 0; i < pathSegs.length; ++i) {
2301 if (!root) {
2302 return undefined;
2303 }
2304 root = strategy(root, pathSegs[i], i + 1, pathSegs);
2305 }
2306 return root;
2307 };
2308
2309 // A very simple "new inner trundler" that just performs concrete property access
2310 // Note that every "strategy" is also a "trundler" of this type, considering just the first two arguments
2311 fluid.concreteTrundler = function (source, seg) {
2312 return !source ? undefined : source[seg];
2313 };
2314
2315 /** Merge a collection of options structures onto a target, following an optional policy.
2316 * This method is now used only for the purpose of merging "dead" option documents in order to
2317 * cache graded component defaults. Component option merging is now performed by the
2318 * fluid.makeMergeOptions pathway which sets up a deferred merging process. This function
2319 * will not be removed in the Fluid 2.0 release but it is recommended that users not call it
2320 * directly.
2321 * The behaviour of this function is explained more fully on
2322 * the page http://wiki.fluidproject.org/display/fluid/Options+Merging+for+Fluid+Components .
2323 * @param {Object|String} policy - A "policy object" specifiying the type of merge to be performed.
2324 * If policy is of type {String} it should take on the value "replace" representing
2325 * a static policy. If it is an
2326 * Object, it should contain a mapping of EL paths onto these String values, representing a
2327 * fine-grained policy. If it is an Object, the values may also themselves be EL paths
2328 * representing that a default value is to be taken from that path.
2329 * @param {...Object} options1, options2, .... - an arbitrary list of options structure which are to
2330 * be merged together. These will not be modified.
2331 */
2332
2333 fluid.merge = function (policy /*, ... sources */) {
2334 var sources = Array.prototype.slice.call(arguments, 1);
2335 var compiled = fluid.compileMergePolicy(policy).builtins;
2336 var options = fluid.makeMergeOptions(compiled, sources, {});
2337 options.initter();
2338 return options.target;
2339 };
2340
2341 // unsupported, NON-API function
2342 fluid.simpleGingerBlock = function (source, recordType) {
2343 var block = {
2344 target: source,
2345 simple: true,
2346 strategy: fluid.concreteTrundler,
2347 initter: fluid.identity,
2348 recordType: recordType,
2349 priority: fluid.mergeRecordTypes[recordType]
2350 };
2351 return block;
2352 };
2353
2354 // unsupported, NON-API function
2355 fluid.makeMergeOptions = function (policy, sources, userOptions) {
2356 // note - we close over the supplied policy as a shared object reference - it will be updated during discovery
2357 var options = {
2358 mergePolicy: policy,
2359 sources: sources
2360 };
2361 options = $.extend(options, userOptions);
2362 options.target = options.target || fluid.freshContainer(options.sources[0]);
2363 options.sourceStrategies = options.sourceStrategies || fluid.generate(options.sources.length, fluid.concreteTrundler);
2364 options.initter = function () {
2365 // This hack is necessary to ensure that the FINAL evaluation doesn't balk when discovering a trunk path which was already
2366 // visited during self-driving via the expander. This bi-modality is sort of rubbish, but we currently don't have "room"
2367 // in the strategy API to express when full evaluation is required - and the "flooding API" is not standardised. See FLUID-4930
2368 options.evaluateFully = true;
2369 fluid.fetchMergeChildren(options.target, 0, [], options.sources, options.mergePolicy, options);
2370 };
2371 fluid.makeMergeStrategy(options);
2372 return options;
2373 };
2374
2375 // unsupported, NON-API function
2376 fluid.transformOptions = function (options, transRec) {
2377 fluid.expect("Options transformation record", transRec, ["transformer", "config"]);
2378 var transFunc = fluid.getGlobalValue(transRec.transformer);
2379 return transFunc.call(null, options, transRec.config);
2380 };
2381
2382 // unsupported, NON-API function
2383 fluid.findMergeBlocks = function (mergeBlocks, recordType) {
2384 return fluid.remove_if(fluid.makeArray(mergeBlocks), function (block) { return block.recordType !== recordType; });
2385 };
2386
2387 // unsupported, NON-API function
2388 fluid.transformOptionsBlocks = function (mergeBlocks, transformOptions, recordTypes) {
2389 fluid.each(recordTypes, function (recordType) {
2390 var blocks = fluid.findMergeBlocks(mergeBlocks, recordType);
2391 fluid.each(blocks, function (block) {
2392 var source = block.source ? "source" : "target"; // TODO: Problem here with irregular presentation of options which consist of a reference in their entirety
2393 block[block.simple || source === "target" ? "target" : "source"] = fluid.transformOptions(block[source], transformOptions);
2394 });
2395 });
2396 };
2397
2398 // unsupported, NON-API function
2399 fluid.dedupeDistributionNamespaces = function (mergeBlocks) { // to implement FLUID-5824
2400 var byNamespace = {};
2401 fluid.remove_if(mergeBlocks, function (mergeBlock) {
2402 var ns = mergeBlock.namespace;
2403 if (ns) {
2404 if (byNamespace[ns] && byNamespace[ns] !== mergeBlock.contextThat.id) { // source check for FLUID-5835
2405 return true;
2406 } else {
2407 byNamespace[ns] = mergeBlock.contextThat.id;
2408 }
2409 }
2410 });
2411 };
2412
2413 // unsupported, NON-API function
2414 fluid.deliverOptionsStrategy = fluid.identity;
2415 fluid.computeComponentAccessor = fluid.identity;
2416 fluid.computeDynamicComponents = fluid.identity;
2417
2418 // The types of merge record the system supports, with the weakest records first
2419 fluid.mergeRecordTypes = {
2420 defaults: 1000,
2421 defaultValueMerge: 900,
2422 subcomponentRecord: 800,
2423 user: 700,
2424 distribution: 100 // and above
2425 };
2426
2427 // Utility used in the framework (primarily with distribution assembly), unconnected with new ChangeApplier
2428 // unsupported, NON-API function
2429 fluid.model.applyChangeRequest = function (model, request) {
2430 var segs = request.segs;
2431 if (segs.length === 0) {
2432 if (request.type === "ADD") {
2433 $.extend(true, model, request.value);
2434 } else {
2435 fluid.clear(model);
2436 }
2437 } else if (request.type === "ADD") {
2438 fluid.model.setSimple(model, request.segs, request.value);
2439 } else {
2440 for (var i = 0; i < segs.length - 1; ++i) {
2441 model = model[segs[i]];
2442 if (!model) {
2443 return;
2444 }
2445 }
2446 var last = segs[segs.length - 1];
2447 delete model[last];
2448 }
2449 };
2450
2451 /** Delete the value in the supplied object held at the specified path
2452 * @param {Object} target - The object holding the value to be deleted (possibly empty)
2453 * @param {String[]} segs - the path of the value to be deleted
2454 */
2455 // unsupported, NON-API function
2456 fluid.destroyValue = function (target, segs) {
2457 if (target) {
2458 fluid.model.applyChangeRequest(target, {type: "DELETE", segs: segs});
2459 }
2460 };
2461
2462 /**
2463 * Merges the component's declared defaults, as obtained from fluid.defaults(),
2464 * with the user's specified overrides.
2465 *
2466 * @param {Object} that - the instance to attach the options to
2467 * @param {String} componentName - the unique "name" of the component, which will be used
2468 * to fetch the default options from store. By recommendation, this should be the global
2469 * name of the component's creator function.
2470 * @param {Object} userOptions - the user-specified configuration options for this component
2471 */
2472 // unsupported, NON-API function
2473 fluid.mergeComponentOptions = function (that, componentName, userOptions, localOptions) {
2474 var rawDefaults = fluid.rawDefaults(componentName);
2475 var defaults = fluid.getMergedDefaults(componentName, rawDefaults && rawDefaults.gradeNames ? null : localOptions.gradeNames);
2476 var sharedMergePolicy = {};
2477
2478 var mergeBlocks = [];
2479
2480 if (fluid.expandComponentOptions) {
2481 mergeBlocks = mergeBlocks.concat(fluid.expandComponentOptions(sharedMergePolicy, defaults, userOptions, that));
2482 }
2483 else {
2484 mergeBlocks = mergeBlocks.concat([fluid.simpleGingerBlock(defaults, "defaults"),
2485 fluid.simpleGingerBlock(userOptions, "user")]);
2486 }
2487 var options = {}; // ultimate target
2488 var sourceStrategies = [], sources = [];
2489 var baseMergeOptions = {
2490 target: options,
2491 sourceStrategies: sourceStrategies
2492 };
2493 // Called both from here and from IoC whenever there is a change of block content or arguments which
2494 // requires them to be resorted and rebound
2495 var updateBlocks = function () {
2496 fluid.each(mergeBlocks, function (block) {
2497 if (fluid.isPrimitive(block.priority)) {
2498 block.priority = fluid.parsePriority(block.priority, 0, false, "options distribution");
2499 }
2500 });
2501 fluid.sortByPriority(mergeBlocks);
2502 fluid.dedupeDistributionNamespaces(mergeBlocks);
2503 sourceStrategies.length = 0;
2504 sources.length = 0;
2505 fluid.each(mergeBlocks, function (block) {
2506 sourceStrategies.push(block.strategy);
2507 sources.push(block.target);
2508 });
2509 };
2510 updateBlocks();
2511 var mergeOptions = fluid.makeMergeOptions(sharedMergePolicy, sources, baseMergeOptions);
2512 mergeOptions.mergeBlocks = mergeBlocks;
2513 mergeOptions.updateBlocks = updateBlocks;
2514 mergeOptions.destroyValue = function (segs) { // This method is a temporary hack to assist FLUID-5091
2515 for (var i = 0; i < mergeBlocks.length; ++i) {
2516 if (!mergeBlocks[i].immutableTarget) {
2517 fluid.destroyValue(mergeBlocks[i].target, segs);
2518 }
2519 }
2520 fluid.destroyValue(baseMergeOptions.target, segs);
2521 };
2522
2523 var compiledPolicy;
2524 var mergePolicy;
2525 function computeMergePolicy() {
2526 // Decode the now available mergePolicy
2527 mergePolicy = fluid.driveStrategy(options, "mergePolicy", mergeOptions.strategy);
2528 mergePolicy = $.extend({}, fluid.rootMergePolicy, mergePolicy);
2529 compiledPolicy = fluid.compileMergePolicy(mergePolicy);
2530 // TODO: expandComponentOptions has already put some builtins here - performance implications of the now huge
2531 // default mergePolicy material need to be investigated as well as this deep merge
2532 $.extend(true, sharedMergePolicy, compiledPolicy.builtins); // ensure it gets broadcast to all sharers
2533 }
2534 computeMergePolicy();
2535 mergeOptions.computeMergePolicy = computeMergePolicy;
2536
2537 if (compiledPolicy.hasDefaults) {
2538 if (fluid.generateExpandBlock) {
2539 mergeBlocks.push(fluid.generateExpandBlock({
2540 options: compiledPolicy.defaultValues,
2541 recordType: "defaultValueMerge",
2542 priority: fluid.mergeRecordTypes.defaultValueMerge
2543 }, that, {}));
2544 updateBlocks();
2545 }
2546 else {
2547 fluid.fail("Cannot operate mergePolicy ", mergePolicy, " for component ", that, " without including FluidIoC.js");
2548 }
2549 }
2550 that.options = options;
2551 fluid.driveStrategy(options, "gradeNames", mergeOptions.strategy);
2552
2553 fluid.deliverOptionsStrategy(that, options, mergeOptions); // do this early to broadcast and receive "distributeOptions"
2554
2555 fluid.computeComponentAccessor(that, userOptions && userOptions.localRecord);
2556
2557 var transformOptions = fluid.driveStrategy(options, "transformOptions", mergeOptions.strategy);
2558 if (transformOptions) {
2559 fluid.transformOptionsBlocks(mergeBlocks, transformOptions, ["user", "subcomponentRecord"]);
2560 updateBlocks(); // because the possibly simple blocks may have changed target
2561 }
2562
2563 if (!baseMergeOptions.target.mergePolicy) {
2564 computeMergePolicy();
2565 }
2566
2567 return mergeOptions;
2568 };
2569
2570 // The Fluid Component System proper
2571
2572 // The base system grade definitions
2573
2574 fluid.defaults("fluid.function", {});
2575
2576 /** Invoke a global function by name and named arguments. A courtesy to allow declaratively encoded function calls
2577 * to use named arguments rather than bare arrays.
2578 * @param {String} name - A global name which can be resolved to a Function. The defaults for this name must
2579 * resolve onto a grade including "fluid.function". The defaults record should also contain an entry
2580 * <code>argumentMap</code>, a hash of argument names onto indexes.
2581 * @param {Object} spec - A named hash holding the argument values to be sent to the function. These will be looked
2582 * up in the <code>argumentMap</code> and resolved into a flat list of arguments.
2583 * @return {Any} The return value from the function
2584 */
2585 fluid.invokeGradedFunction = function (name, spec) {
2586 var defaults = fluid.defaults(name);
2587 if (!defaults || !defaults.argumentMap || !fluid.hasGrade(defaults, "fluid.function")) {
2588 fluid.fail("Cannot look up name " + name +
2589 " to a function with registered argumentMap - got defaults ", defaults);
2590 }
2591 var args = [];
2592 fluid.each(defaults.argumentMap, function (value, key) {
2593 args[value] = spec[key];
2594 });
2595 return fluid.invokeGlobalFunction(name, args);
2596 };
2597
2598 fluid.noNamespaceDistributionPrefix = "no-namespace-distribution-";
2599
2600 fluid.mergeOneDistribution = function (target, source, key) {
2601 var namespace = source.namespace || key || fluid.noNamespaceDistributionPrefix + fluid.allocateGuid();
2602 source.namespace = namespace;
2603 target[namespace] = $.extend(true, {}, target[namespace], source);
2604 };
2605
2606 fluid.distributeOptionsPolicy = function (target, source) {
2607 target = target || {};
2608 if (fluid.isArrayable(source)) {
2609 for (var i = 0; i < source.length; ++i) {
2610 fluid.mergeOneDistribution(target, source[i]);
2611 }
2612 } else if (typeof(source.target) === "string") {
2613 fluid.mergeOneDistribution(target, source);
2614 } else {
2615 fluid.each(source, function (oneSource, key) {
2616 fluid.mergeOneDistribution(target, oneSource, key);
2617 });
2618 }
2619 return target;
2620 };
2621
2622 fluid.mergingArray = function () {};
2623 fluid.mergingArray.prototype = [];
2624
2625 // Defer all evaluation of all nested members to resolve FLUID-5668
2626 fluid.membersMergePolicy = function (target, source) {
2627 target = target || {};
2628 fluid.each(source, function (oneSource, key) {
2629 if (!target[key]) {
2630 target[key] = new fluid.mergingArray();
2631 }
2632 if (oneSource instanceof fluid.mergingArray) {
2633 target[key].push.apply(target[key], oneSource);
2634 } else if (oneSource !== undefined) {
2635 target[key].push(oneSource);
2636 }
2637 });
2638 return target;
2639 };
2640
2641 fluid.invokerStrategies = fluid.arrayToHash(["func", "funcName", "listener", "this", "method", "changePath", "value"]);
2642
2643 // Resolve FLUID-5741, FLUID-5184 by ensuring that we avoid mixing incompatible invoker strategies
2644 fluid.invokersMergePolicy = function (target, source) {
2645 target = target || {};
2646 fluid.each(source, function (oneInvoker, name) {
2647 if (!oneInvoker) {
2648 target[name] = oneInvoker;
2649 return;
2650 } else {
2651 oneInvoker = fluid.upgradePrimitiveFunc(oneInvoker);
2652 }
2653 var oneT = target[name];
2654 if (!oneT) {
2655 oneT = target[name] = {};
2656 }
2657 for (var key in fluid.invokerStrategies) {
2658 if (key in oneInvoker) {
2659 for (var key2 in fluid.invokerStrategies) {
2660 oneT[key2] = undefined; // can't delete since stupid driveStrategy bug from recordStrategy reinstates them
2661 }
2662 }
2663 }
2664 $.extend(oneT, oneInvoker);
2665 });
2666 return target;
2667 };
2668
2669 fluid.rootMergePolicy = {
2670 gradeNames: fluid.arrayConcatPolicy,
2671 distributeOptions: fluid.distributeOptionsPolicy,
2672 members: {
2673 noexpand: true,
2674 func: fluid.membersMergePolicy
2675 },
2676 invokers: {
2677 noexpand: true,
2678 func: fluid.invokersMergePolicy
2679 },
2680 transformOptions: "replace",
2681 listeners: fluid.makeMergeListenersPolicy(fluid.mergeListenerPolicy)
2682 };
2683
2684 fluid.defaults("fluid.component", {
2685 initFunction: "fluid.initLittleComponent",
2686 mergePolicy: fluid.rootMergePolicy,
2687 argumentMap: {
2688 options: 0
2689 },
2690 events: { // Three standard lifecycle points common to all components
2691 onCreate: null,
2692 onDestroy: null,
2693 afterDestroy: null
2694 }
2695 });
2696
2697 fluid.defaults("fluid.emptySubcomponent", {
2698 gradeNames: ["fluid.component"]
2699 });
2700
2701 /* Compute a "nickname" given a fully qualified typename, by returning the last path
2702 * segment.
2703 */
2704 fluid.computeNickName = function (typeName) {
2705 var segs = fluid.model.parseEL(typeName);
2706 return segs[segs.length - 1];
2707 };
2708
2709 /** A specially recognised grade tag which directs the IoC framework to instantiate this component first amongst
2710 * its set of siblings, since it is likely to bear a context-forming type name. This will be removed from the framework
2711 * once we have implemented FLUID-4925 "wave of explosions" */
2712 fluid.defaults("fluid.typeFount", {
2713 gradeNames: ["fluid.component"]
2714 });
2715
2716 /**
2717 * Creates a new "little component": a that-ist object with options merged into it by the framework.
2718 * This method is a convenience for creating small objects that have options but don't require full
2719 * View-like features such as the DOM Binder or events
2720 *
2721 * @param {Object} name - The name of the little component to create
2722 * @param {Object} options - User-supplied options to merge with the defaults
2723 */
2724 // NOTE: the 3rd argument localOptions is NOT to be advertised as part of the stable API, it is present
2725 // just to allow backward compatibility whilst grade specifications are not mandatory - similarly for 4th arg "receiver"
2726 // NOTE historical name to avoid confusion with fluid.initComponent below - this will all be refactored with FLUID-4925
2727 fluid.initLittleComponent = function (name, userOptions, localOptions, receiver) {
2728 var that = fluid.typeTag(name);
2729 that.lifecycleStatus = "constructing";
2730 localOptions = localOptions || {gradeNames: "fluid.component"};
2731
2732 that.destroy = fluid.makeRootDestroy(that); // overwritten by FluidIoC for constructed subcomponents
2733 var mergeOptions = fluid.mergeComponentOptions(that, name, userOptions, localOptions);
2734 mergeOptions.exceptions = {members: {model: true, modelRelay: true}}; // don't evaluate these in "early flooding" - they must be fetched explicitly
2735 var options = that.options;
2736 that.events = {};
2737 // deliver to a non-IoC side early receiver of the component (currently only initView)
2738 (receiver || fluid.identity)(that, options, mergeOptions.strategy);
2739 fluid.computeDynamicComponents(that, mergeOptions);
2740
2741 // TODO: ****THIS**** is the point we must deliver and suspend!! Construct the "component skeleton" first, and then continue
2742 // for as long as we can continue to find components.
2743 for (var i = 0; i < mergeOptions.mergeBlocks.length; ++i) {
2744 mergeOptions.mergeBlocks[i].initter();
2745 }
2746 mergeOptions.initter();
2747 delete options.mergePolicy;
2748
2749 fluid.instantiateFirers(that, options);
2750 fluid.mergeListeners(that, that.events, options.listeners);
2751
2752 return that;
2753 };
2754
2755 fluid.diagnoseFailedView = fluid.identity;
2756
2757 // unsupported, NON-API function
2758 fluid.makeRootDestroy = function (that) {
2759 return function () {
2760 fluid.doDestroy(that);
2761 fluid.fireEvent(that, "afterDestroy", [that, "", null]);
2762 };
2763 };
2764
2765 /* Returns <code>true</code> if the supplied reference holds a component which has been destroyed */
2766 fluid.isDestroyed = function (that) {
2767 return that.lifecycleStatus === "destroyed";
2768 };
2769
2770 // unsupported, NON-API function
2771 fluid.doDestroy = function (that, name, parent) {
2772 fluid.fireEvent(that, "onDestroy", [that, name || "", parent]);
2773 that.lifecycleStatus = "destroyed";
2774 for (var key in that.events) {
2775 if (key !== "afterDestroy" && typeof(that.events[key].destroy) === "function") {
2776 that.events[key].destroy();
2777 }
2778 }
2779 if (that.applier) { // TODO: Break this out into the grade's destroyer
2780 that.applier.destroy();
2781 }
2782 };
2783
2784 // unsupported, NON-API function
2785 fluid.initComponent = function (componentName, initArgs) {
2786 var options = fluid.defaults(componentName);
2787 if (!options.gradeNames) {
2788 fluid.fail("Cannot initialise component " + componentName + " which has no gradeName registered");
2789 }
2790 var args = [componentName].concat(fluid.makeArray(initArgs));
2791 var that;
2792 fluid.pushActivity("initComponent", "constructing component of type %componentName with arguments %initArgs",
2793 {componentName: componentName, initArgs: initArgs});
2794 that = fluid.invokeGlobalFunction(options.initFunction, args);
2795 fluid.diagnoseFailedView(componentName, that, options, args);
2796 if (fluid.initDependents) {
2797 fluid.initDependents(that);
2798 }
2799 var errors = fluid.validateListenersImplemented(that);
2800 if (errors.length > 0) {
2801 fluid.fail(fluid.transform(errors, function (error) {
2802 return ["Error constructing component ", that, " - the listener for event " + error.name + " with namespace " + error.namespace + (
2803 (error.componentSource ? " which was defined in grade " + error.componentSource : "") + " needs to be overridden with a concrete implementation")];
2804 })).join("\n");
2805 }
2806 if (that.lifecycleStatus === "constructing") {
2807 that.lifecycleStatus = "constructed";
2808 }
2809 that.events.onCreate.fire(that);
2810 fluid.popActivity();
2811 return that;
2812 };
2813
2814 // unsupported, NON-API function
2815 fluid.initSubcomponentImpl = function (that, entry, args) {
2816 var togo;
2817 if (typeof (entry) !== "function") {
2818 var entryType = typeof (entry) === "string" ? entry : entry.type;
2819 togo = entryType === "fluid.emptySubcomponent" ?
2820 null : fluid.invokeGlobalFunction(entryType, args);
2821 } else {
2822 togo = entry.apply(null, args);
2823 }
2824 return togo;
2825 };
2826
2827 // ******* SELECTOR ENGINE *********
2828
2829 // selector regexps copied from jQuery - recent versions correct the range to start C0
2830 // The initial portion of the main character selector: "just add water" to add on extra
2831 // accepted characters, as well as the "\\\\." -> "\." portion necessary for matching
2832 // period characters escaped in selectors
2833 var charStart = "(?:[\\w\\u00c0-\\uFFFF*_-";
2834
2835 fluid.simpleCSSMatcher = {
2836 regexp: new RegExp("([#.]?)(" + charStart + "]|\\\\.)+)", "g"),
2837 charToTag: {
2838 "": "tag",
2839 "#": "id",
2840 ".": "clazz"
2841 }
2842 };
2843
2844 fluid.IoCSSMatcher = {
2845 regexp: new RegExp("([&#]?)(" + charStart + "]|\\.|\\/)+)", "g"),
2846 charToTag: {
2847 "": "context",
2848 "&": "context",
2849 "#": "id"
2850 }
2851 };
2852
2853 var childSeg = new RegExp("\\s*(>)?\\s*", "g");
2854// var whiteSpace = new RegExp("^\\w*$");
2855
2856 // Parses a selector expression into a data structure holding a list of predicates
2857 // 2nd argument is a "strategy" structure, e.g. fluid.simpleCSSMatcher or fluid.IoCSSMatcher
2858 // unsupported, non-API function
2859 fluid.parseSelector = function (selstring, strategy) {
2860 var togo = [];
2861 selstring = selstring.trim();
2862 //ws-(ss*)[ws/>]
2863 var regexp = strategy.regexp;
2864 regexp.lastIndex = 0;
2865 var lastIndex = 0;
2866 while (true) {
2867 var atNode = []; // a list of predicates at a particular node
2868 var first = true;
2869 while (true) {
2870 var segMatch = regexp.exec(selstring);
2871 if (!segMatch) {
2872 break;
2873 }
2874 if (segMatch.index !== lastIndex) {
2875 if (first) {
2876 fluid.fail("Error in selector string - cannot match child selector expression starting at " + selstring.substring(lastIndex));
2877 }
2878 else {
2879 break;
2880 }
2881 }
2882 var thisNode = {};
2883 var text = segMatch[2];
2884 var targetTag = strategy.charToTag[segMatch[1]];
2885 if (targetTag) {
2886 thisNode[targetTag] = text;
2887 }
2888 atNode[atNode.length] = thisNode;
2889 lastIndex = regexp.lastIndex;
2890 first = false;
2891 }
2892 childSeg.lastIndex = lastIndex;
2893 var fullAtNode = {predList: atNode};
2894 var childMatch = childSeg.exec(selstring);
2895 if (!childMatch || childMatch.index !== lastIndex) {
2896 fluid.fail("Error in selector string - can not match child selector expression at " + selstring.substring(lastIndex));
2897 }
2898 if (childMatch[1] === ">") {
2899 fullAtNode.child = true;
2900 }
2901 togo[togo.length] = fullAtNode;
2902 // >= test here to compensate for IE bug http://blog.stevenlevithan.com/archives/exec-bugs
2903 if (childSeg.lastIndex >= selstring.length) {
2904 break;
2905 }
2906 lastIndex = childSeg.lastIndex;
2907 regexp.lastIndex = childSeg.lastIndex;
2908 }
2909 return togo;
2910 };
2911
2912 // Message resolution and templating
2913
2914 /**
2915 *
2916 * Take an original object and represent it using top-level sub-elements whose keys are EL Paths. For example,
2917 * `originalObject` might look like:
2918 *
2919 * ```
2920 * {
2921 * deep: {
2922 * path: {
2923 * value: "foo",
2924 * emptyObject: {},
2925 * array: [ "peas", "porridge", "hot"]
2926 * }
2927 * }
2928 * }
2929 * ```
2930 *
2931 * Calling `fluid.flattenObjectKeys` on this would result in a new object that looks like:
2932 *
2933 * ```
2934 * {
2935 * "deep": "[object Object]",
2936 * "deep.path": "[object Object]",
2937 * "deep.path.value": "foo",
2938 * "deep.path.array": "peas,porridge,hot",
2939 * "deep.path.array.0": "peas",
2940 * "deep.path.array.1": "porridge",
2941 * "deep.path.array.2": "hot"
2942 * }
2943 * ```
2944 *
2945 * This function preserves the previous functionality of displaying an entire object using its `toString` function,
2946 * which is why many of the paths above resolve to "[object Object]".
2947 *
2948 * This function is an unsupported non-API function that is used in by `fluid.stringTemplate` (see below).
2949 *
2950 * @param {Object} originalObject - An object.
2951 * @return {Object} A representation of the original object that only contains top-level sub-elements whose keys are EL Paths.
2952 *
2953 */
2954 // unsupported, non-API function
2955 fluid.flattenObjectPaths = function (originalObject) {
2956 var flattenedObject = {};
2957 fluid.each(originalObject, function (value, key) {
2958 if (value !== null && typeof value === "object") {
2959 var flattenedSubObject = fluid.flattenObjectPaths(value);
2960 fluid.each(flattenedSubObject, function (subValue, subKey) {
2961 flattenedObject[key + "." + subKey] = subValue;
2962 });
2963 if (typeof fluid.get(value, "toString") === "function") {
2964 flattenedObject[key] = value.toString();
2965 }
2966 }
2967 else {
2968 flattenedObject[key] = value;
2969 }
2970 });
2971 return flattenedObject;
2972 };
2973
2974 /**
2975 *
2976 * Simple string template system. Takes a template string containing tokens in the form of "%value" or
2977 * "%deep.path.to.value". Returns a new string with the tokens replaced by the specified values. Keys and values
2978 * can be of any data type that can be coerced into a string.
2979 *
2980 * @param {String} template - A string (can be HTML) that contains tokens embedded into it.
2981 * @param {Object} values - A collection of token keys and values.
2982 * @return {String} A string whose tokens have been replaced with values.
2983 *
2984 */
2985 fluid.stringTemplate = function (template, values) {
2986 var flattenedValues = fluid.flattenObjectPaths(values);
2987 var keys = fluid.keys(flattenedValues);
2988 keys = keys.sort(fluid.compareStringLength());
2989 for (var i = 0; i < keys.length; ++i) {
2990 var key = keys[i];
2991 var templatePlaceholder = "%" + key;
2992 var replacementValue = flattenedValues[key];
2993
2994 var indexOfPlaceHolder = -1;
2995 while ((indexOfPlaceHolder = template.indexOf(templatePlaceholder)) !== -1) {
2996 template = template.slice(0, indexOfPlaceHolder) + replacementValue + template.slice(indexOfPlaceHolder + templatePlaceholder.length);
2997 }
2998 }
2999 return template;
3000 };
3001
3002})(jQuery, fluid_3_0_0);
3003;
3004/*!
3005 Copyright 2011 unscriptable.com / John Hann
3006 Copyright The Infusion copyright holders
3007 See the AUTHORS.md file at the top-level directory of this distribution and at
3008 https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
3009
3010 License MIT
3011*/
3012
3013var fluid_3_0_0 = fluid_3_0_0 || {};
3014
3015(function ($, fluid) {
3016 "use strict";
3017
3018// Light fluidification of minimal promises library. See original gist at
3019// https://gist.github.com/unscriptable/814052 for limitations and commentary
3020
3021// This implementation provides what could be described as "flat promises" with
3022// no support for structured programming idioms involving promise composition.
3023// It provides what a proponent of mainstream promises would describe as
3024// a "glorified callback aggregator"
3025
3026 fluid.promise = function () {
3027 var that = {
3028 onResolve: [],
3029 onReject: []
3030 // disposition
3031 // value
3032 };
3033 that.then = function (onResolve, onReject) {
3034 if (onResolve) {
3035 if (that.disposition === "resolve") {
3036 onResolve(that.value);
3037 } else {
3038 that.onResolve.push(onResolve);
3039 }
3040 }
3041 if (onReject) {
3042 if (that.disposition === "reject") {
3043 onReject(that.value);
3044 } else {
3045 that.onReject.push(onReject);
3046 }
3047 }
3048 return that;
3049 };
3050 that.resolve = function (value) {
3051 if (that.disposition) {
3052 fluid.fail("Error: resolving promise ", that,
3053 " which has already received \"" + that.disposition + "\"");
3054 } else {
3055 that.complete("resolve", that.onResolve, value);
3056 }
3057 return that;
3058 };
3059 that.reject = function (reason) {
3060 if (that.disposition) {
3061 fluid.fail("Error: rejecting promise ", that,
3062 "which has already received \"" + that.disposition + "\"");
3063 } else {
3064 that.complete("reject", that.onReject, reason);
3065 }
3066 return that;
3067 };
3068 // PRIVATE, NON-API METHOD
3069 that.complete = function (which, queue, arg) {
3070 that.disposition = which;
3071 that.value = arg;
3072 for (var i = 0; i < queue.length; ++i) {
3073 queue[i](arg);
3074 }
3075 };
3076 return that;
3077 };
3078
3079 /* Any object with a member <code>then</code> of type <code>function</code> passes this test.
3080 * This includes essentially every known variety, including jQuery promises.
3081 */
3082 fluid.isPromise = function (totest) {
3083 return totest && typeof(totest.then) === "function";
3084 };
3085
3086 /** Coerces any value to a promise
3087 * @param {Any} promiseOrValue - The value to be coerced
3088 * @return {Promise} - If the supplied value is already a promise, it is returned unchanged. Otherwise a fresh promise is created with the value as resolution and returned
3089 */
3090 fluid.toPromise = function (promiseOrValue) {
3091 if (fluid.isPromise(promiseOrValue)) {
3092 return promiseOrValue;
3093 } else {
3094 var togo = fluid.promise();
3095 togo.resolve(promiseOrValue);
3096 return togo;
3097 }
3098 };
3099
3100 /* Chains the resolution methods of one promise (target) so that they follow those of another (source).
3101 * That is, whenever source resolves, target will resolve, or when source rejects, target will reject, with the
3102 * same payloads in each case.
3103 */
3104 fluid.promise.follow = function (source, target) {
3105 source.then(target.resolve, target.reject);
3106 };
3107
3108 /** Returns a promise whose resolved value is mapped from the source promise or value by the supplied function.
3109 * @param {Object|Promise} source - An object or promise whose value is to be mapped
3110 * @param {Function} func - A function which will map the resolved promise value
3111 * @return {Promise} - A promise for the resolved mapped value.
3112 */
3113 fluid.promise.map = function (source, func) {
3114 var promise = fluid.toPromise(source);
3115 var togo = fluid.promise();
3116 promise.then(function (value) {
3117 var mapped = func(value);
3118 if (fluid.isPromise(mapped)) {
3119 fluid.promise.follow(mapped, togo);
3120 } else {
3121 togo.resolve(mapped);
3122 }
3123 }, function (error) {
3124 togo.reject(error);
3125 });
3126 return togo;
3127 };
3128
3129 /* General skeleton for all sequential promise algorithms, e.g. transform, reduce, sequence, etc.
3130 * These accept a variable "strategy" pair to customise the interchange of values and final return
3131 */
3132
3133 fluid.promise.makeSequencer = function (sources, options, strategy) {
3134 if (!fluid.isArrayable(sources)) {
3135 fluid.fail("fluid.promise sequence algorithms must be supplied an array as source");
3136 }
3137 return {
3138 sources: sources,
3139 resolvedSources: [], // the values of "sources" only with functions invoked (an array of promises or values)
3140 index: 0,
3141 strategy: strategy,
3142 options: options, // available to be supplied to each listener
3143 returns: [],
3144 promise: fluid.promise() // the final return value
3145 };
3146 };
3147
3148 fluid.promise.progressSequence = function (that, retValue) {
3149 that.returns.push(retValue);
3150 that.index++;
3151 // No we dun't have no tail recursion elimination
3152 fluid.promise.resumeSequence(that);
3153 };
3154
3155 fluid.promise.processSequenceReject = function (that, error) { // Allow earlier promises in the sequence to wrap the rejection supplied by later ones (FLUID-5584)
3156 for (var i = that.index - 1; i >= 0; --i) {
3157 var resolved = that.resolvedSources[i];
3158 var accumulator = fluid.isPromise(resolved) && typeof(resolved.accumulateRejectionReason) === "function" ? resolved.accumulateRejectionReason : fluid.identity;
3159 error = accumulator(error);
3160 }
3161 that.promise.reject(error);
3162 };
3163
3164 fluid.promise.resumeSequence = function (that) {
3165 if (that.index === that.sources.length) {
3166 that.promise.resolve(that.strategy.resolveResult(that));
3167 } else {
3168 var value = that.strategy.invokeNext(that);
3169 that.resolvedSources[that.index] = value;
3170 if (fluid.isPromise(value)) {
3171 value.then(function (retValue) {
3172 fluid.promise.progressSequence(that, retValue);
3173 }, function (error) {
3174 fluid.promise.processSequenceReject(that, error);
3175 });
3176 } else {
3177 fluid.promise.progressSequence(that, value);
3178 }
3179 }
3180 };
3181
3182 // SEQUENCE ALGORITHM APPLYING PROMISES
3183
3184 fluid.promise.makeSequenceStrategy = function () {
3185 return {
3186 invokeNext: function (that) {
3187 var source = that.sources[that.index];
3188 return typeof(source) === "function" ? source(that.options) : source;
3189 },
3190 resolveResult: function (that) {
3191 return that.returns;
3192 }
3193 };
3194 };
3195
3196 // accepts an array of values, promises or functions returning promises - in the case of functions returning promises,
3197 // will assure that at most one of these is "in flight" at a time - that is, the succeeding function will not be invoked
3198 // until the promise at the preceding position has resolved
3199 fluid.promise.sequence = function (sources, options) {
3200 var sequencer = fluid.promise.makeSequencer(sources, options, fluid.promise.makeSequenceStrategy());
3201 fluid.promise.resumeSequence(sequencer);
3202 return sequencer.promise;
3203 };
3204
3205 // TRANSFORM ALGORITHM APPLYING PROMISES
3206
3207 fluid.promise.makeTransformerStrategy = function () {
3208 return {
3209 invokeNext: function (that) {
3210 var lisrec = that.sources[that.index];
3211 lisrec.listener = fluid.event.resolveListener(lisrec.listener);
3212 var value = lisrec.listener.apply(null, [that.returns[that.index], that.options]);
3213 return value;
3214 },
3215 resolveResult: function (that) {
3216 return that.returns[that.index];
3217 }
3218 };
3219 };
3220
3221 // Construct a "mini-object" managing the process of a sequence of transforms,
3222 // each of which may be synchronous or return a promise
3223 fluid.promise.makeTransformer = function (listeners, payload, options) {
3224 listeners.unshift({listener:
3225 function () {
3226 return payload;
3227 }
3228 });
3229 var sequencer = fluid.promise.makeSequencer(listeners, options, fluid.promise.makeTransformerStrategy());
3230 sequencer.returns.push(null); // first dummy return from initial entry
3231 fluid.promise.resumeSequence(sequencer);
3232 return sequencer;
3233 };
3234
3235 fluid.promise.filterNamespaces = function (listeners, namespaces) {
3236 if (!namespaces) {
3237 return listeners;
3238 }
3239 return fluid.remove_if(fluid.makeArray(listeners), function (element) {
3240 return element.namespace && !element.softNamespace && !fluid.contains(namespaces, element.namespace);
3241 });
3242 };
3243
3244 /** Top-level API to operate a Fluid event which manages a sequence of
3245 * chained transforms. Rather than being a standard listener accepting the
3246 * same payload, each listener to the event accepts the payload returned by the
3247 * previous listener, and returns either a transformed payload or else a promise
3248 * yielding such a payload.
3249 * @param {fluid.eventFirer} event - A Fluid event to which the listeners are to be interpreted as
3250 * elements cooperating in a chained transform. Each listener will receive arguments <code>(payload, options)</code> where <code>payload</code>
3251 * is the (successful, resolved) return value of the previous listener, and <code>options</code> is the final argument to this function
3252 * @param {Object|Promise} payload - The initial payload input to the transform chain
3253 * @param {Object} options - A free object containing options governing the transform. Fields interpreted at this top level are:
3254 * reverse {Boolean}: <code>true</code> if the listeners are to be called in reverse order of priority (typically the case for an inverse transform)
3255 * filterTransforms {Array}: An array of listener namespaces. If this field is set, only the transform elements whose listener namespaces listed in this array will be applied.
3256 * @return {fluid.promise} A promise which will yield either the final transformed value, or the response of the first transform which fails.
3257 */
3258
3259 fluid.promise.fireTransformEvent = function (event, payload, options) {
3260 options = options || {};
3261 var listeners = options.reverse ? fluid.makeArray(event.sortedListeners).reverse() :
3262 fluid.makeArray(event.sortedListeners);
3263 listeners = fluid.promise.filterNamespaces(listeners, options.filterNamespaces);
3264 var transformer = fluid.promise.makeTransformer(listeners, payload, options);
3265 return transformer.promise;
3266 };
3267
3268
3269})(jQuery, fluid_3_0_0);
3270;
3271/*
3272Copyright The Infusion copyright holders
3273See the AUTHORS.md file at the top-level directory of this distribution and at
3274https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
3275
3276Licensed under the Educational Community License (ECL), Version 2.0 or the New
3277BSD license. You may not use this file except in compliance with one these
3278Licenses.
3279
3280You may obtain a copy of the ECL 2.0 License and BSD License at
3281https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
3282*/
3283
3284var fluid_3_0_0 = fluid_3_0_0 || {};
3285
3286(function ($, fluid) {
3287 "use strict";
3288
3289 /** NOTE: Much of this work originated from https://github.com/fluid-project/kettle/blob/master/lib/dataSource-core.js **/
3290
3291 /** Some common content encodings - suitable to appear as the "encoding" subcomponent of a dataSource **/
3292
3293 fluid.defaults("fluid.dataSource.encoding.JSON", {
3294 gradeNames: "fluid.component",
3295 invokers: {
3296 parse: "fluid.dataSource.parseJSON",
3297 render: "fluid.dataSource.stringifyJSON"
3298 },
3299 contentType: "application/json"
3300 });
3301
3302 fluid.defaults("fluid.dataSource.encoding.none", {
3303 gradeNames: "fluid.component",
3304 invokers: {
3305 parse: "fluid.identity",
3306 render: "fluid.identity"
3307 },
3308 contentType: "text/plain"
3309 });
3310
3311 fluid.dataSource.parseJSON = function (string) {
3312 var togo = fluid.promise();
3313 if (!string) {
3314 togo.resolve(undefined);
3315 } else {
3316 try {
3317 togo.resolve(JSON.parse(string));
3318 } catch (err) {
3319 togo.reject({
3320 message: err
3321 });
3322 }
3323 }
3324 return togo;
3325 };
3326
3327 fluid.dataSource.stringifyJSON = function (obj) {
3328 return obj === undefined ? "" : JSON.stringify(obj, null, 4);
3329 };
3330
3331 /**
3332 * The head of the hierarchy of dataSource components. These abstract
3333 * over the process of read and write access to data, following a simple CRUD-type semantic, indexed by
3334 * a coordinate model (directModel) and which may be asynchronous.
3335 * Top-level methods are:
3336 * get(directModel[, callback|options] - to get the data from data resource
3337 * set(directModel, model[, callback|options] - to set the data
3338 *
3339 *
3340 * directModel: An object expressing an "index" into some set of
3341 * state which can be read or written.
3342 *
3343 * model: The payload sent to the storage.
3344 *
3345 * options: An object expressing implementation specific details
3346 * regarding the handling of a request. Note: this does not
3347 * include details for identifying the resource. Those should be
3348 * placed in the directModel.
3349 */
3350 fluid.defaults("fluid.dataSource", {
3351 gradeNames: ["fluid.component"],
3352 events: {
3353 // The "onRead" event is operated in a custom workflow by fluid.fireTransformEvent to
3354 // process dataSource payloads during the get process. Each listener
3355 // receives the data returned by the last.
3356 onRead: null,
3357 onError: null
3358 },
3359 components: {
3360 encoding: {
3361 type: "fluid.dataSource.encoding.JSON"
3362 }
3363 },
3364 listeners: {
3365 // handler for "onRead.impl" must be implemented by a concrete subgrade
3366 // Note: The intial payload (first argument) will be undefined
3367 "onRead.impl": {
3368 func: "fluid.notImplemented",
3369 priority: "first"
3370 },
3371 "onRead.encoding": {
3372 func: "{encoding}.parse",
3373 priority: "after:impl"
3374 }
3375 },
3376 invokers: {
3377 get: {
3378 funcName: "fluid.dataSource.get",
3379 args: ["{that}", "{arguments}.0", "{arguments}.1"] // directModel, options/callback
3380 }
3381 }
3382 });
3383
3384
3385 /**
3386 * Base grade for adding write configuration to a dataSource.
3387 *
3388 * Grade linkage should be used to apply the concrete writable grade to the datasource configuration.
3389 * For example fluid.makeGradeLinkage("kettle.dataSource.CouchDB.linkage", ["fluid.dataSource.writable", "kettle.dataSource.CouchDB"], "kettle.dataSource.CouchDB.writable");
3390 */
3391 fluid.defaults("fluid.dataSource.writable", {
3392 gradeNames: ["fluid.component"],
3393 events: {
3394 // events "onWrite" and "onWriteResponse" are operated in a custom workflow by fluid.fireTransformEvent to
3395 // process dataSource payloads during the set process. Each listener
3396 // receives the data returned by the last.
3397 onWrite: null,
3398 onWriteResponse: null
3399 },
3400 listeners: {
3401 "onWrite.encoding": {
3402 func: "{encoding}.render"
3403 },
3404 // handler for "onWrite.impl" must be implemented by a concrete subgrade
3405 "onWrite.impl": {
3406 func: "fluid.notImplemented",
3407 priority: "after:encoding"
3408 },
3409 "onWriteResponse.encoding": {
3410 func: "{encoding}.parse"
3411 }
3412 },
3413 invokers: {
3414 set: {
3415 funcName: "fluid.dataSource.set",
3416 args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // directModel, model, options/callback
3417 }
3418 }
3419 });
3420
3421 // Registers the default promise handlers for a dataSource operation -
3422 // i) If the user has supplied a function in place of method `options`, register this function as a success handler
3423 // ii) if the user has supplied an onError handler in method `options`, this is registered - otherwise
3424 // we register the firer of the dataSource's own onError method.
3425
3426 fluid.dataSource.registerStandardPromiseHandlers = function (that, promise, options) {
3427 promise.then(typeof(options) === "function" ? options : null,
3428 options.onError ? options.onError : that.events.onError.fire);
3429 };
3430
3431 fluid.dataSource.defaultiseOptions = function (componentOptions, options, directModel, isSet) {
3432 options = options || {};
3433 options.directModel = directModel;
3434 options.operation = isSet ? "set" : "get";
3435 options.notFoundIsEmpty = options.notFoundIsEmpty || componentOptions.notFoundIsEmpty;
3436 return options;
3437 };
3438
3439 /** Operate the core "transforming promise workflow" of a dataSource's `get` method. The initial listener provides the initial payload;
3440 * which then proceeds through the transform chain to arrive at the final payload.
3441 * @param that {Component} The dataSource itself
3442 * @param directModel {Object} The direct model expressing the "coordinates" of the model to be fetched
3443 * @param options {Object} A structure of options configuring the action of this get request - many of these will be specific to the particular concrete DataSource
3444 * @return {Promise} A promise for the final resolved payload
3445 */
3446
3447 fluid.dataSource.get = function (that, directModel, options) {
3448 options = fluid.dataSource.defaultiseOptions(that.options, options, directModel);
3449 var promise = fluid.promise.fireTransformEvent(that.events.onRead, undefined, options);
3450 fluid.dataSource.registerStandardPromiseHandlers(that, promise, options);
3451 return promise;
3452 };
3453
3454 /** Operate the core "transforming promise workflow" of a dataSource's `set` method.
3455 * Any return from this is then pushed forwards through a range of the transforms (typically, e.g. just decoding it as JSON)
3456 * on its way back to the user via the onWriteResponse event.
3457 * @param that {Component} The dataSource itself
3458 * @param directModel {Object} The direct model expressing the "coordinates" of the model to be written
3459 * @param model {Object} The payload to be written to the dataSource
3460 * @param options {Object} A structure of options configuring the action of this set request - many of these will be specific to the particular concrete DataSource
3461 * @return {Promise} A promise for the final resolved payload (not all DataSources will provide any for a `set` method)
3462 */
3463
3464 fluid.dataSource.set = function (that, directModel, model, options) {
3465 options = fluid.dataSource.defaultiseOptions(that.options, options, directModel, true); // shared and writeable between all participants
3466 var transformPromise = fluid.promise.fireTransformEvent(that.events.onWrite, model, options);
3467 var togo = fluid.promise();
3468 transformPromise.then(function (setResponse) {
3469 var options2 = fluid.dataSource.defaultiseOptions(that.options, fluid.copy(options), directModel);
3470 var retransformed = fluid.promise.fireTransformEvent(that.events.onWriteResponse, setResponse, options2);
3471 fluid.promise.follow(retransformed, togo);
3472 }, function (error) {
3473 togo.reject(error);
3474 });
3475 fluid.dataSource.registerStandardPromiseHandlers(that, togo, options);
3476 return togo;
3477 };
3478
3479})(jQuery, fluid_3_0_0);
3480;
3481/*
3482Copyright 2005-2013 jQuery Foundation, Inc. and other contributors
3483Copyright The Infusion copyright holders
3484See the AUTHORS.md file at the top-level directory of this distribution and at
3485https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
3486
3487Licensed under the Educational Community License (ECL), Version 2.0 or the New
3488BSD license. You may not use this file except in compliance with one these
3489Licenses.
3490
3491You may obtain a copy of the ECL 2.0 License and BSD License at
3492https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
3493*/
3494
3495/** This file contains functions which depend on the presence of a DOM document
3496 * but which do not depend on the contents of Fluid.js **/
3497
3498var fluid_3_0_0 = fluid_3_0_0 || {};
3499
3500(function ($, fluid) {
3501 "use strict";
3502
3503 // polyfill for $.browser which was removed in jQuery 1.9 and later
3504 // Taken from jquery-migrate-1.2.1.js,
3505 // jQuery Migrate - v1.2.1 - 2013-05-08
3506 // https://github.com/jquery/jquery-migrate
3507 // Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors; Licensed MIT
3508
3509 fluid.uaMatch = function (ua) {
3510 ua = ua.toLowerCase();
3511
3512 var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
3513 /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
3514 /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
3515 /(msie) ([\w.]+)/.exec( ua ) ||
3516 ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || [];
3517
3518 return {
3519 browser: match[ 1 ] || "",
3520 version: match[ 2 ] || "0"
3521 };
3522 };
3523
3524 var matched, browser;
3525
3526 // Don't clobber any existing jQuery.browser in case it's different
3527 if (!$.browser) {
3528 if (!!navigator.userAgent.match(/Trident\/7\./)) {
3529 browser = { // From http://stackoverflow.com/questions/18684099/jquery-fail-to-detect-ie-11
3530 msie: true,
3531 version: 11
3532 };
3533 } else {
3534 matched = fluid.uaMatch(navigator.userAgent);
3535 browser = {};
3536
3537 if (matched.browser) {
3538 browser[matched.browser] = true;
3539 browser.version = matched.version;
3540 }
3541 // Chrome is Webkit, but Webkit is also Safari.
3542 if (browser.chrome) {
3543 browser.webkit = true;
3544 } else if (browser.webkit) {
3545 browser.safari = true;
3546 }
3547 }
3548 $.browser = browser;
3549 }
3550
3551 // Private constants.
3552 var NAMESPACE_KEY = "fluid-scoped-data";
3553
3554 /*
3555 * Gets stored state from the jQuery instance's data map.
3556 * This function is unsupported: It is not really intended for use by implementors.
3557 */
3558 fluid.getScopedData = function (target, key) {
3559 var data = $(target).data(NAMESPACE_KEY);
3560 return data ? data[key] : undefined;
3561 };
3562
3563 /*
3564 * Stores state in the jQuery instance's data map. Unlike jQuery's version,
3565 * accepts multiple-element jQueries.
3566 * This function is unsupported: It is not really intended for use by implementors.
3567 */
3568 fluid.setScopedData = function (target, key, value) {
3569 $(target).each(function () {
3570 var data = $.data(this, NAMESPACE_KEY) || {};
3571 data[key] = value;
3572
3573 $.data(this, NAMESPACE_KEY, data);
3574 });
3575 };
3576
3577 /** Global focus manager - makes use of "focusin" event supported in jquery 1.4.2 or later.
3578 */
3579
3580 var lastFocusedElement = null;
3581
3582 $(document).on("focusin", function (event) {
3583 lastFocusedElement = event.target;
3584 });
3585
3586 fluid.getLastFocusedElement = function () {
3587 return lastFocusedElement;
3588 };
3589
3590
3591 var ENABLEMENT_KEY = "enablement";
3592
3593 /** Queries or sets the enabled status of a control. An activatable node
3594 * may be "disabled" in which case its keyboard bindings will be inoperable
3595 * (but still stored) until it is reenabled again.
3596 * This function is unsupported: It is not really intended for use by implementors.
3597 */
3598
3599 fluid.enabled = function (target, state) {
3600 target = $(target);
3601 if (state === undefined) {
3602 return fluid.getScopedData(target, ENABLEMENT_KEY) !== false;
3603 }
3604 else {
3605 $("*", target).add(target).each(function () {
3606 if (fluid.getScopedData(this, ENABLEMENT_KEY) !== undefined) {
3607 fluid.setScopedData(this, ENABLEMENT_KEY, state);
3608 }
3609 else if (/select|textarea|input/i.test(this.nodeName)) {
3610 $(this).prop("disabled", !state);
3611 }
3612 });
3613 fluid.setScopedData(target, ENABLEMENT_KEY, state);
3614 }
3615 };
3616
3617 fluid.initEnablement = function (target) {
3618 fluid.setScopedData(target, ENABLEMENT_KEY, true);
3619 };
3620
3621 // This utility is required through the use of newer versions of jQuery which will obscure the original
3622 // event responsible for interaction with a target. This is currently use in Tooltip.js and FluidView.js
3623 // "dead man's blur" but would be of general utility
3624
3625 fluid.resolveEventTarget = function (event) {
3626 while (event.originalEvent && event.originalEvent.target) {
3627 event = event.originalEvent;
3628 }
3629 return event.target;
3630 };
3631
3632 // These function (fluid.focus() and fluid.blur()) serve several functions. They should be used by
3633 // all implementation both in test cases and component implementation which require to trigger a focus
3634 // event. Firstly, they restore the old behaviour in jQuery versions prior to 1.10 in which a focus
3635 // trigger synchronously relays to a focus handler. In newer jQueries this defers to the real browser
3636 // relay with numerous platform and timing-dependent effects.
3637 // Secondly, they are necessary since simulation of focus events by jQuery under IE
3638 // is not sufficiently good to intercept the "focusin" binding. Any code which triggers
3639 // focus or blur synthetically throughout the framework and client code must use this function,
3640 // especially if correct cross-platform interaction is required with the "deadMansBlur" function.
3641
3642 function applyOp(node, func) {
3643 node = $(node);
3644 node.trigger("fluid-" + func);
3645 node.triggerHandler(func);
3646 node[func]();
3647 return node;
3648 }
3649
3650 $.each(["focus", "blur"], function (i, name) {
3651 fluid[name] = function (elem) {
3652 return applyOp(elem, name);
3653 };
3654 });
3655
3656 /* Sets the value to the DOM element and triggers the change event on the element.
3657 * Note: when using jQuery val() function to change the node value, the change event would
3658 * not be fired automatically, it requires to be initiated by the user.
3659 *
3660 * @param {A jQueryable DOM element} node - A selector, a DOM node, or a jQuery instance
3661 * @param {String|Number|Array} value - A string of text, a number, or an array of strings
3662 * corresponding to the value of each matched element to set in the node
3663 */
3664 fluid.changeElementValue = function (node, value) {
3665 node = $(node);
3666 node.val(value).change();
3667 };
3668
3669})(jQuery, fluid_3_0_0);
3670;
3671/*
3672Copyright The Infusion copyright holders
3673See the AUTHORS.md file at the top-level directory of this distribution and at
3674https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
3675
3676Licensed under the Educational Community License (ECL), Version 2.0 or the New
3677BSD license. You may not use this file except in compliance with one these
3678Licenses.
3679
3680You may obtain a copy of the ECL 2.0 License and BSD License at
3681https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
3682*/
3683
3684var fluid_3_0_0 = fluid_3_0_0 || {};
3685
3686(function ($, fluid) {
3687 "use strict";
3688
3689 fluid.dom = fluid.dom || {};
3690
3691 // Node walker function for iterateDom.
3692 var getNextNode = function (iterator) {
3693 if (iterator.node.firstChild) {
3694 iterator.node = iterator.node.firstChild;
3695 iterator.depth += 1;
3696 return iterator;
3697 }
3698 while (iterator.node) {
3699 if (iterator.node.nextSibling) {
3700 iterator.node = iterator.node.nextSibling;
3701 return iterator;
3702 }
3703 iterator.node = iterator.node.parentNode;
3704 iterator.depth -= 1;
3705 }
3706 return iterator;
3707 };
3708
3709 /**
3710 * Walks the DOM, applying the specified acceptor function to each element.
3711 * There is a special case for the acceptor, allowing for quick deletion of elements and their children.
3712 * Return "delete" from your acceptor function if you want to delete the element in question.
3713 * Return "stop" to terminate iteration.
3714
3715 * Implementation note - this utility exists mainly for performance reasons. It was last tested
3716 * carefully some time ago (around jQuery 1.2) but at that time was around 3-4x faster at raw DOM
3717 * filtration tasks than the jQuery equivalents, which was an important source of performance loss in the
3718 * Reorderer component. General clients of the framework should use this method with caution if at all, and
3719 * the performance issues should be reassessed when we have time.
3720 *
3721 * @param {Element} node - The node to start walking from.
3722 * @param {Function} acceptor - The function to invoke with each DOM element.
3723 * @param {Boolean} allNodes - Use <code>true</code> to call acceptor on all nodes, rather than just element nodes
3724 * (type 1).
3725 * @return {Object|undefined} - Returns `undefined` if the run completed successfully. If a node stopped the run,
3726 * that node is returned.
3727 */
3728 fluid.dom.iterateDom = function (node, acceptor, allNodes) {
3729 var currentNode = {node: node, depth: 0};
3730 var prevNode = node;
3731 var condition;
3732 while (currentNode.node !== null && currentNode.depth >= 0 && currentNode.depth < fluid.dom.iterateDom.DOM_BAIL_DEPTH) {
3733 condition = null;
3734 if (currentNode.node.nodeType === 1 || allNodes) {
3735 condition = acceptor(currentNode.node, currentNode.depth);
3736 }
3737 if (condition) {
3738 if (condition === "delete") {
3739 currentNode.node.parentNode.removeChild(currentNode.node);
3740 currentNode.node = prevNode;
3741 }
3742 else if (condition === "stop") {
3743 return currentNode.node;
3744 }
3745 }
3746 prevNode = currentNode.node;
3747 currentNode = getNextNode(currentNode);
3748 }
3749 };
3750
3751 // Work around IE circular DOM issue. This is the default max DOM depth on IE.
3752 // http://msdn2.microsoft.com/en-us/library/ms761392(VS.85).aspx
3753 fluid.dom.iterateDom.DOM_BAIL_DEPTH = 256;
3754
3755 /**
3756 * Checks if the specified container is actually the parent of containee.
3757 *
3758 * @param {Element} container - the potential parent
3759 * @param {Element} containee - the child in question
3760 * @return {Boolean} - `true` if `container` contains `containee`, `false` otherwise.
3761 */
3762 fluid.dom.isContainer = function (container, containee) {
3763 for (; containee; containee = containee.parentNode) {
3764 if (container === containee) {
3765 return true;
3766 }
3767 }
3768 return false;
3769 };
3770
3771 /* Return the element text from the supplied DOM node as a single String.
3772 * Implementation note - this is a special-purpose utility used in the framework in just one
3773 * position in the Reorderer. It only performs a "shallow" traversal of the text and was intended
3774 * as a quick and dirty means of extracting element labels where the user had not explicitly provided one.
3775 * It should not be used by general users of the framework and its presence here needs to be
3776 * reassessed.
3777 */
3778 fluid.dom.getElementText = function (element) {
3779 var nodes = element.childNodes;
3780 var text = "";
3781 for (var i = 0; i < nodes.length; ++i) {
3782 var child = nodes[i];
3783 if (child.nodeType === 3) {
3784 text = text + child.nodeValue;
3785 }
3786 }
3787 return text;
3788 };
3789
3790})(jQuery, fluid_3_0_0);
3791;
3792/*
3793Copyright The Infusion copyright holders
3794See the AUTHORS.md file at the top-level directory of this distribution and at
3795https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
3796
3797Licensed under the Educational Community License (ECL), Version 2.0 or the New
3798BSD license. You may not use this file except in compliance with one these
3799Licenses.
3800
3801You may obtain a copy of the ECL 2.0 License and BSD License at
3802https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
3803*/
3804
3805fluid_3_0_0 = fluid_3_0_0 || {};
3806
3807(function ($, fluid) {
3808 "use strict";
3809
3810 var unUnicode = /(\\u[\dabcdef]{4}|\\x[\dabcdef]{2})/g;
3811
3812 fluid.unescapeProperties = function (string) {
3813 string = string.replace(unUnicode, function (match) {
3814 var code = match.substring(2);
3815 var parsed = parseInt(code, 16);
3816 return String.fromCharCode(parsed);
3817 });
3818 var pos = 0;
3819 while (true) {
3820 var backpos = string.indexOf("\\", pos);
3821 if (backpos === -1) {
3822 break;
3823 }
3824 if (backpos === string.length - 1) {
3825 return [string.substring(0, string.length - 1), true];
3826 }
3827 var replace = string.charAt(backpos + 1);
3828 if (replace === "n") { replace = "\n"; }
3829 if (replace === "r") { replace = "\r"; }
3830 if (replace === "t") { replace = "\t"; }
3831 string = string.substring(0, backpos) + replace + string.substring(backpos + 2);
3832 pos = backpos + 1;
3833 }
3834 return [string, false];
3835 };
3836
3837 var breakPos = /[^\\][\s:=]/;
3838
3839 fluid.parseJavaProperties = function (text) {
3840 // File format described at http://java.sun.com/javase/6/docs/api/java/util/Properties.html#load(java.io.Reader)
3841 var togo = {};
3842 text = text.replace(/\r\n/g, "\n");
3843 text = text.replace(/\r/g, "\n");
3844 var lines = text.split("\n");
3845 var contin, key, valueComp, valueRaw, valueEsc;
3846 for (var i = 0; i < lines.length; ++i) {
3847 var line = $.trim(lines[i]);
3848 if (!line || line.charAt(0) === "#" || line.charAt(0) === "!") {
3849 continue;
3850 }
3851 if (!contin) {
3852 valueComp = "";
3853 var breakpos = line.search(breakPos);
3854 if (breakpos === -1) {
3855 key = line;
3856 valueRaw = "";
3857 }
3858 else {
3859 key = $.trim(line.substring(0, breakpos + 1)); // +1 since first char is escape exclusion
3860 valueRaw = $.trim(line.substring(breakpos + 2));
3861 if (valueRaw.charAt(0) === ":" || valueRaw.charAt(0) === "=") {
3862 valueRaw = $.trim(valueRaw.substring(1));
3863 }
3864 }
3865
3866 key = fluid.unescapeProperties(key)[0];
3867 valueEsc = fluid.unescapeProperties(valueRaw);
3868 }
3869 else {
3870 valueEsc = fluid.unescapeProperties(line);
3871 }
3872
3873 contin = valueEsc[1];
3874 if (!valueEsc[1]) { // this line was not a continuation line - store the value
3875 togo[key] = valueComp + valueEsc[0];
3876 }
3877 else {
3878 valueComp += valueEsc[0];
3879 }
3880 }
3881 return togo;
3882 };
3883
3884 /**
3885 *
3886 * Expand a message string with respect to a set of arguments, following a basic subset of the Java MessageFormat
3887 * rules.
3888 * http://java.sun.com/j2se/1.4.2/docs/api/java/text/MessageFormat.html
3889 *
3890 * The message string is expected to contain replacement specifications such as {0}, {1}, {2}, etc.
3891 *
3892 * @param {String} messageString - The message key to be expanded
3893 * @param {String|String[]} args - A single string or array of strings to be substituted into the message.
3894 * @return {String} - The expanded message string.
3895 */
3896 fluid.formatMessage = function (messageString, args) {
3897 if (!args) {
3898 return messageString;
3899 }
3900 if (typeof(args) === "string") {
3901 args = [args];
3902 }
3903 for (var i = 0; i < args.length; ++i) {
3904 messageString = messageString.replace("{" + i + "}", args[i]);
3905 }
3906 return messageString;
3907 };
3908
3909})(jQuery, fluid_3_0_0);
3910;
3911/*
3912Copyright The Infusion copyright holders
3913See the AUTHORS.md file at the top-level directory of this distribution and at
3914https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
3915
3916Licensed under the Educational Community License (ECL), Version 2.0 or the New
3917BSD license. You may not use this file except in compliance with one these
3918Licenses.
3919
3920You may obtain a copy of the ECL 2.0 License and BSD License at
3921https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
3922*/
3923
3924var fluid_3_0_0 = fluid_3_0_0 || {};
3925
3926(function ($, fluid) {
3927 "use strict";
3928
3929 /** Render a timestamp from a Date object into a helpful fixed format for debug logs to millisecond accuracy
3930 * @param {Date} date - The date to be rendered
3931 * @return {String} - A string format consisting of hours:minutes:seconds.millis for the datestamp padded to fixed with
3932 */
3933
3934 fluid.renderTimestamp = function (date) {
3935 var zeropad = function (num, width) {
3936 if (!width) { width = 2; }
3937 var numstr = (num === undefined ? "" : num.toString());
3938 return "00000".substring(5 - width + numstr.length) + numstr;
3939 };
3940 return zeropad(date.getHours()) + ":" + zeropad(date.getMinutes()) + ":" + zeropad(date.getSeconds()) + "." + zeropad(date.getMilliseconds(), 3);
3941 };
3942
3943 fluid.isTracing = false;
3944
3945 fluid.registerNamespace("fluid.tracing");
3946
3947 fluid.tracing.pathCount = [];
3948
3949 fluid.tracing.summarisePathCount = function (pathCount) {
3950 pathCount = pathCount || fluid.tracing.pathCount;
3951 var togo = {};
3952 for (var i = 0; i < pathCount.length; ++i) {
3953 var path = pathCount[i];
3954 if (!togo[path]) {
3955 togo[path] = 1;
3956 }
3957 else {
3958 ++togo[path];
3959 }
3960 }
3961 var toReallyGo = [];
3962 fluid.each(togo, function (el, path) {
3963 toReallyGo.push({path: path, count: el});
3964 });
3965 toReallyGo.sort(function (a, b) {return b.count - a.count;});
3966 return toReallyGo;
3967 };
3968
3969 fluid.tracing.condensePathCount = function (prefixes, pathCount) {
3970 prefixes = fluid.makeArray(prefixes);
3971 var prefixCount = {};
3972 fluid.each(prefixes, function (prefix) {
3973 prefixCount[prefix] = 0;
3974 });
3975 var togo = [];
3976 fluid.each(pathCount, function (el) {
3977 var path = el.path;
3978 if (!fluid.find(prefixes, function (prefix) {
3979 if (path.indexOf(prefix) === 0) {
3980 prefixCount[prefix] += el.count;
3981 return true;
3982 }
3983 })) {
3984 togo.push(el);
3985 }
3986 });
3987 fluid.each(prefixCount, function (count, path) {
3988 togo.unshift({path: path, count: count});
3989 });
3990 return togo;
3991 };
3992
3993 // Exception stripping code taken from https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js
3994 // BSD licence, see header
3995
3996 fluid.detectStackStyle = function (e) {
3997 var style = "other";
3998 var stackStyle = {
3999 offset: 0
4000 };
4001 if (e.arguments) {
4002 style = "chrome";
4003 } else if (typeof window !== "undefined" && window.opera && e.stacktrace) {
4004 style = "opera10";
4005 } else if (e.stack) {
4006 style = "firefox";
4007 // Detect FireFox 4-style stacks which are 1 level less deep
4008 stackStyle.offset = e.stack.indexOf("Trace exception") === -1 ? 1 : 0;
4009 } else if (typeof window !== "undefined" && window.opera && !("stacktrace" in e)) { //Opera 9-
4010 style = "opera";
4011 }
4012 stackStyle.style = style;
4013 return stackStyle;
4014 };
4015
4016 fluid.obtainException = function () {
4017 try {
4018 throw new Error("Trace exception");
4019 }
4020 catch (e) {
4021 return e;
4022 }
4023 };
4024
4025 var stackStyle = fluid.detectStackStyle(fluid.obtainException());
4026
4027 fluid.registerNamespace("fluid.exceptionDecoders");
4028
4029 fluid.decodeStack = function () {
4030 if (stackStyle.style !== "firefox") {
4031 return null;
4032 }
4033 var e = fluid.obtainException();
4034 return fluid.exceptionDecoders[stackStyle.style](e);
4035 };
4036
4037 fluid.exceptionDecoders.firefox = function (e) {
4038 var delimiter = "at ";
4039 var lines = e.stack.replace(/(?:\n@:0)?\s+$/m, "").replace(/^\(/gm, "{anonymous}(").split("\n");
4040 return fluid.transform(lines, function (line) {
4041 line = line.replace(/\)/g, "");
4042 var atind = line.indexOf(delimiter);
4043 return atind === -1 ? [line] : [line.substring(atind + delimiter.length), line.substring(0, atind)];
4044 });
4045 };
4046
4047 // Main entry point for callers.
4048 fluid.getCallerInfo = function (atDepth) {
4049 atDepth = (atDepth || 3) - stackStyle.offset;
4050 var stack = fluid.decodeStack();
4051 var element = stack && stack[atDepth][0];
4052 if (element) {
4053 var lastslash = element.lastIndexOf("/");
4054 if (lastslash === -1) {
4055 lastslash = 0;
4056 }
4057 var nextColon = element.indexOf(":", lastslash);
4058 return {
4059 path: element.substring(0, lastslash),
4060 filename: element.substring(lastslash + 1, nextColon),
4061 index: element.substring(nextColon + 1)
4062 };
4063 } else {
4064 return null;
4065 }
4066 };
4067
4068 /** Generates a string for padding purposes by replicating a character a given number of times
4069 * @param {Character} c - A character to be used for padding
4070 * @param {Integer} count - The number of times to repeat the character
4071 * @return A string of length <code>count</code> consisting of repetitions of the supplied character
4072 */
4073 // UNOPTIMISED
4074 fluid.generatePadding = function (c, count) {
4075 var togo = "";
4076 for (var i = 0; i < count; ++i) {
4077 togo += c;
4078 }
4079 return togo;
4080 };
4081
4082 // Marker so that we can render a custom string for properties which are not direct and concrete
4083 fluid.SYNTHETIC_PROPERTY = Object.freeze({});
4084
4085 // utility to avoid triggering custom getter code which could throw an exception - e.g. express 3.x's request object
4086 fluid.getSafeProperty = function (obj, key) {
4087 var desc = Object.getOwnPropertyDescriptor(obj, key); // supported on all of our environments - is broken on IE8
4088 return desc && !desc.get ? obj[key] : fluid.SYNTHETIC_PROPERTY;
4089 };
4090
4091 function printImpl(obj, small, options) {
4092 function out(str) {
4093 options.output += str;
4094 }
4095 var big = small + options.indentChars, isFunction = typeof(obj) === "function";
4096 if (options.maxRenderChars !== undefined && options.output.length > options.maxRenderChars) {
4097 return true;
4098 }
4099 if (obj === null) {
4100 out("null");
4101 } else if (obj === undefined) {
4102 out("undefined"); // NB - object invalid for JSON interchange
4103 } else if (obj === fluid.SYNTHETIC_PROPERTY) {
4104 out("[Synthetic property]");
4105 } else if (fluid.isPrimitive(obj) && !isFunction) {
4106 out(JSON.stringify(obj));
4107 }
4108 else {
4109 if (options.stack.indexOf(obj) !== -1) {
4110 out("(CIRCULAR)"); // NB - object invalid for JSON interchange
4111 return;
4112 }
4113 options.stack.push(obj);
4114 var i;
4115 if (fluid.isArrayable(obj)) {
4116 if (obj.length === 0) {
4117 out("[]");
4118 } else {
4119 out("[\n" + big);
4120 for (i = 0; i < obj.length; ++i) {
4121 if (printImpl(obj[i], big, options)) {
4122 return true;
4123 }
4124 if (i !== obj.length - 1) {
4125 out(",\n" + big);
4126 }
4127 }
4128 out("\n" + small + "]");
4129 }
4130 }
4131 else {
4132 out("{" + (isFunction ? " Function" : "") + "\n" + big); // NB - Function object invalid for JSON interchange
4133 var keys = fluid.keys(obj);
4134 for (i = 0; i < keys.length; ++i) {
4135 var key = keys[i];
4136 var value = fluid.getSafeProperty(obj, key);
4137 out(JSON.stringify(key) + ": ");
4138 if (printImpl(value, big, options)) {
4139 return true;
4140 }
4141 if (i !== keys.length - 1) {
4142 out(",\n" + big);
4143 }
4144 }
4145 out("\n" + small + "}");
4146 }
4147 options.stack.pop();
4148 }
4149 return;
4150 }
4151
4152 /** Render a complex JSON object into a nicely indented format suitable for human readability.
4153 * @param {Object} obj - The object to be rendered
4154 * @param {Object} options - An options structure governing the rendering process. This supports the following options:
4155 * <code>indent</code> {Integer} the number of space characters to be used to indent each level of containment (default value: 4)
4156 * <code>maxRenderChars</code> {Integer} rendering the object will cease once this number of characters has been generated
4157 * @return {String} - The generated output.
4158 */
4159 fluid.prettyPrintJSON = function (obj, options) {
4160 options = $.extend({indent: 4, stack: [], output: ""}, options);
4161 options.indentChars = fluid.generatePadding(" ", options.indent);
4162 printImpl(obj, "", options);
4163 return options.output;
4164 };
4165
4166 /**
4167 * Dumps a DOM element into a readily recognisable form for debugging - produces a
4168 * "semi-selector" summarising its tag name, class and id, whichever are set.
4169 *
4170 * @param {jQueryable} element - The element to be dumped
4171 * @return {String} - A string representing the element.
4172 */
4173 fluid.dumpEl = function (element) {
4174 var togo;
4175
4176 if (!element) {
4177 return "null";
4178 }
4179 if (element.nodeType === 3 || element.nodeType === 8) {
4180 return "[data: " + element.data + "]";
4181 }
4182 if (element.nodeType === 9) {
4183 return "[document: location " + element.location + "]";
4184 }
4185 if (!element.nodeType && fluid.isArrayable(element)) {
4186 togo = "[";
4187 for (var i = 0; i < element.length; ++i) {
4188 togo += fluid.dumpEl(element[i]);
4189 if (i < element.length - 1) {
4190 togo += ", ";
4191 }
4192 }
4193 return togo + "]";
4194 }
4195 element = $(element);
4196 togo = element.get(0).tagName;
4197 if (element.id) {
4198 togo += "#" + element.id;
4199 }
4200 if (element.attr("class")) {
4201 togo += "." + element.attr("class");
4202 }
4203 return togo;
4204 };
4205
4206})(jQuery, fluid_3_0_0);
4207;
4208/*
4209Copyright The Infusion copyright holders
4210See the AUTHORS.md file at the top-level directory of this distribution and at
4211https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
4212
4213Licensed under the Educational Community License (ECL), Version 2.0 or the New
4214BSD license. You may not use this file except in compliance with one these
4215Licenses.
4216
4217You may obtain a copy of the ECL 2.0 License and BSD License at
4218https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
4219*/
4220
4221var fluid_3_0_0 = fluid_3_0_0 || {};
4222
4223(function ($, fluid) {
4224 "use strict";
4225
4226 /** NOTE: The contents of this file are by default NOT PART OF THE PUBLIC FLUID API unless explicitly annotated before the function **/
4227
4228 /* The Fluid "IoC System proper" - resolution of references and
4229 * completely automated instantiation of declaratively defined
4230 * component trees */
4231
4232 // Currently still uses manual traversal - once we ban manually instantiated components,
4233 // it will use the instantiator's records instead.
4234 fluid.visitComponentChildren = function (that, visitor, options, segs) {
4235 segs = segs || [];
4236 for (var name in that) {
4237 var component = that[name];
4238 // This entire algorithm is primitive and expensive and will be removed once we can abolish manual init components
4239 if (!fluid.isComponent(component) || (options.visited && options.visited[component.id])) {
4240 continue;
4241 }
4242 segs.push(name);
4243 if (options.visited) { // recall that this is here because we may run into a component that has been cross-injected which might otherwise cause cyclicity
4244 options.visited[component.id] = true;
4245 }
4246 if (visitor(component, name, segs, segs.length - 1)) {
4247 return true;
4248 }
4249 if (!options.flat) {
4250 fluid.visitComponentChildren(component, visitor, options, segs);
4251 }
4252 segs.pop();
4253 }
4254 };
4255
4256 fluid.getContextHash = function (instantiator, that) {
4257 var shadow = instantiator.idToShadow[that.id];
4258 return shadow && shadow.contextHash;
4259 };
4260
4261 fluid.componentHasGrade = function (that, gradeName) {
4262 var contextHash = fluid.getContextHash(fluid.globalInstantiator, that);
4263 return !!(contextHash && contextHash[gradeName]);
4264 };
4265
4266 // A variant of fluid.visitComponentChildren that supplies the signature expected for fluid.matchIoCSelector
4267 // this is: thatStack, contextHashes, memberNames, i - note, the supplied arrays are NOT writeable and shared through the iteration
4268 fluid.visitComponentsForMatching = function (that, options, visitor) {
4269 var instantiator = fluid.getInstantiator(that);
4270 options = $.extend({
4271 visited: {},
4272 instantiator: instantiator
4273 }, options);
4274 var thatStack = [that];
4275 var contextHashes = [fluid.getContextHash(instantiator, that)];
4276 var visitorWrapper = function (component, name, segs) {
4277 thatStack.length = 1;
4278 contextHashes.length = 1;
4279 for (var i = 0; i < segs.length; ++i) {
4280 var child = thatStack[i][segs[i]];
4281 thatStack[i + 1] = child;
4282 contextHashes[i + 1] = fluid.getContextHash(instantiator, child) || {};
4283 }
4284 return visitor(component, thatStack, contextHashes, segs, segs.length);
4285 };
4286 fluid.visitComponentChildren(that, visitorWrapper, options, []);
4287 };
4288
4289 fluid.getMemberNames = function (instantiator, thatStack) {
4290 if (thatStack.length === 0) { // Odd edge case for FLUID-6126 from fluid.computeDistributionPriority
4291 return [];
4292 } else {
4293 var path = instantiator.idToPath(thatStack[thatStack.length - 1].id);
4294 var segs = instantiator.parseEL(path);
4295 // TODO: we should now have no longer shortness in the stack
4296 segs.unshift.apply(segs, fluid.generate(thatStack.length - segs.length, ""));
4297 return segs;
4298 }
4299 };
4300
4301 // thatStack contains an increasing list of MORE SPECIFIC thats.
4302 // this visits all components starting from the current location (end of stack)
4303 // in visibility order UP the tree.
4304 fluid.visitComponentsForVisibility = function (instantiator, thatStack, visitor, options) {
4305 options = options || {
4306 visited: {},
4307 flat: true,
4308 instantiator: instantiator
4309 };
4310 var memberNames = fluid.getMemberNames(instantiator, thatStack);
4311 for (var i = thatStack.length - 1; i >= 0; --i) {
4312 var that = thatStack[i];
4313
4314 // explicitly visit the direct parent first
4315 options.visited[that.id] = true;
4316 if (visitor(that, memberNames[i], memberNames, i)) {
4317 return;
4318 }
4319
4320 if (fluid.visitComponentChildren(that, visitor, options, memberNames)) {
4321 return;
4322 }
4323 memberNames.pop();
4324 }
4325 };
4326
4327 fluid.mountStrategy = function (prefix, root, toMount) {
4328 var offset = prefix.length;
4329 return function (target, name, i, segs) {
4330 if (i <= prefix.length) { // Avoid OOB to not trigger deoptimisation!
4331 return;
4332 }
4333 for (var j = 0; j < prefix.length; ++j) {
4334 if (segs[j] !== prefix[j]) {
4335 return;
4336 }
4337 }
4338 return toMount(target, name, i - prefix.length, segs.slice(offset));
4339 };
4340 };
4341
4342 fluid.invokerFromRecord = function (invokerec, name, that) {
4343 fluid.pushActivity("makeInvoker", "beginning instantiation of invoker with name %name and record %record as child of %that",
4344 {name: name, record: invokerec, that: that});
4345 var invoker = invokerec ? fluid.makeInvoker(that, invokerec, name) : undefined;
4346 fluid.popActivity();
4347 return invoker;
4348 };
4349
4350 fluid.memberFromRecord = function (memberrecs, name, that) {
4351 var togo;
4352 for (var i = 0; i < memberrecs.length; ++i) { // memberrecs is the special "fluid.mergingArray" type which is not Arrayable
4353 var expanded = fluid.expandImmediate(memberrecs[i], that);
4354 if (!fluid.isPlainObject(togo)) { // poor man's "merge" algorithm to hack FLUID-5668 for now
4355 togo = expanded;
4356 } else {
4357 togo = $.extend(true, togo, expanded);
4358 }
4359 }
4360 return togo;
4361 };
4362
4363 fluid.recordStrategy = function (that, options, optionsStrategy, recordPath, recordMaker, prefix, exceptions) {
4364 prefix = prefix || [];
4365 return {
4366 strategy: function (target, name, i) {
4367 if (i !== 1) {
4368 return;
4369 }
4370 var record = fluid.driveStrategy(options, [recordPath, name], optionsStrategy);
4371 if (record === undefined) {
4372 return;
4373 }
4374 fluid.set(target, [name], fluid.inEvaluationMarker);
4375 var member = recordMaker(record, name, that);
4376 fluid.set(target, [name], member);
4377 return member;
4378 },
4379 initter: function () {
4380 var records = fluid.driveStrategy(options, recordPath, optionsStrategy) || {};
4381 for (var name in records) {
4382 if (!exceptions || !exceptions[name]) {
4383 fluid.getForComponent(that, prefix.concat([name]));
4384 }
4385 }
4386 }
4387 };
4388 };
4389
4390 // patch Fluid.js version for timing
4391 fluid.instantiateFirers = function (that) {
4392 var shadow = fluid.shadowForComponent(that);
4393 var initter = fluid.get(shadow, ["eventStrategyBlock", "initter"]) || fluid.identity;
4394 initter();
4395 };
4396
4397 fluid.makeDistributionRecord = function (contextThat, sourceRecord, sourcePath, targetSegs, exclusions, sourceType) {
4398 sourceType = sourceType || "distribution";
4399 fluid.pushActivity("makeDistributionRecord", "Making distribution record from source record %sourceRecord path %sourcePath to target path %targetSegs", {sourceRecord: sourceRecord, sourcePath: sourcePath, targetSegs: targetSegs});
4400
4401 var source = fluid.copy(fluid.get(sourceRecord, sourcePath));
4402 fluid.each(exclusions, function (exclusion) {
4403 fluid.model.applyChangeRequest(source, {segs: exclusion, type: "DELETE"});
4404 });
4405
4406 var record = {options: {}};
4407 fluid.model.applyChangeRequest(record, {segs: targetSegs, type: "ADD", value: source});
4408 fluid.checkComponentRecord(record);
4409 fluid.popActivity();
4410 return $.extend(record, {contextThat: contextThat, recordType: sourceType});
4411 };
4412
4413 // Part of the early "distributeOptions" workflow. Given the description of the blocks to be distributed, assembles "canned" records
4414 // suitable to be either registered into the shadow record for later or directly pushed to an existing component, as well as honouring
4415 // any "removeSource" annotations by removing these options from the source block.
4416 fluid.filterBlocks = function (contextThat, sourceBlocks, sourceSegs, targetSegs, exclusions, removeSource) {
4417 var togo = [];
4418 fluid.each(sourceBlocks, function (block) {
4419 var source = fluid.get(block.source, sourceSegs);
4420 if (source !== undefined) {
4421 togo.push(fluid.makeDistributionRecord(contextThat, block.source, sourceSegs, targetSegs, exclusions, block.recordType));
4422 var rescued = $.extend({}, source);
4423 if (removeSource) {
4424 fluid.model.applyChangeRequest(block.source, {segs: sourceSegs, type: "DELETE"});
4425 }
4426 fluid.each(exclusions, function (exclusion) {
4427 var orig = fluid.get(rescued, exclusion);
4428 fluid.set(block.source, sourceSegs.concat(exclusion), orig);
4429 });
4430 }
4431 });
4432 return togo;
4433 };
4434
4435 // Use this peculiar signature since the actual component and shadow itself may not exist yet. Perhaps clean up with FLUID-4925
4436 fluid.noteCollectedDistribution = function (parentShadow, memberName, distribution) {
4437 fluid.model.setSimple(parentShadow, ["collectedDistributions", memberName, distribution.id], true);
4438 };
4439
4440 fluid.isCollectedDistribution = function (parentShadow, memberName, distribution) {
4441 return fluid.model.getSimple(parentShadow, ["collectedDistributions", memberName, distribution.id]);
4442 };
4443
4444 fluid.clearCollectedDistributions = function (parentShadow, memberName) {
4445 fluid.model.applyChangeRequest(parentShadow, {segs: ["collectedDistributions", memberName], type: "DELETE"});
4446 };
4447
4448 fluid.collectDistributions = function (distributedBlocks, parentShadow, distribution, thatStack, contextHashes, memberNames, i) {
4449 var lastMember = memberNames[memberNames.length - 1];
4450 if (!fluid.isCollectedDistribution(parentShadow, lastMember, distribution) &&
4451 fluid.matchIoCSelector(distribution.selector, thatStack, contextHashes, memberNames, i)) {
4452 distributedBlocks.push.apply(distributedBlocks, distribution.blocks);
4453 fluid.noteCollectedDistribution(parentShadow, lastMember, distribution);
4454 }
4455 };
4456
4457 // Slightly silly function to clean up the "appliedDistributions" records. In general we need to be much more aggressive both
4458 // about clearing instantiation garbage (e.g. onCreate and most of the shadow)
4459 // as well as caching frequently-used records such as the "thatStack" which
4460 // would mean this function could be written in a sensible way
4461 fluid.registerCollectedClearer = function (shadow, parentShadow, memberName) {
4462 if (!shadow.collectedClearer && parentShadow) {
4463 shadow.collectedClearer = function () {
4464 fluid.clearCollectedDistributions(parentShadow, memberName);
4465 };
4466 }
4467 };
4468
4469 fluid.receiveDistributions = function (parentThat, gradeNames, memberName, that) {
4470 var instantiator = fluid.getInstantiator(parentThat || that);
4471 var thatStack = instantiator.getThatStack(parentThat || that); // most specific is at end
4472 thatStack.unshift(fluid.rootComponent);
4473 var memberNames = fluid.getMemberNames(instantiator, thatStack);
4474 var shadows = fluid.transform(thatStack, function (thisThat) {
4475 return instantiator.idToShadow[thisThat.id];
4476 });
4477 var parentShadow = shadows[shadows.length - (parentThat ? 1 : 2)];
4478 var contextHashes = fluid.getMembers(shadows, "contextHash");
4479 if (parentThat) { // if called before construction of component from assembleCreatorArguments - NB this path will be abolished/amalgamated
4480 memberNames.push(memberName);
4481 contextHashes.push(fluid.gradeNamesToHash(gradeNames));
4482 thatStack.push(that);
4483 } else {
4484 fluid.registerCollectedClearer(shadows[shadows.length - 1], parentShadow, memberNames[memberNames.length - 1]);
4485 }
4486 var distributedBlocks = [];
4487 for (var i = 0; i < thatStack.length - 1; ++i) {
4488 fluid.each(shadows[i].distributions, function (distribution) { // eslint-disable-line no-loop-func
4489 fluid.collectDistributions(distributedBlocks, parentShadow, distribution, thatStack, contextHashes, memberNames, i);
4490 });
4491 }
4492 return distributedBlocks;
4493 };
4494
4495 fluid.computeTreeDistance = function (path1, path2) {
4496 var i = 0;
4497 while (i < path1.length && i < path2.length && path1[i] === path2[i]) {
4498 ++i;
4499 }
4500 return path1.length + path2.length - 2*i; // eslint-disable-line space-infix-ops
4501 };
4502
4503 // Called from applyDistributions (immediate application route) as well as mergeRecordsToList (pre-instantiation route) AS WELL AS assembleCreatorArguments (pre-pre-instantiation route)
4504 fluid.computeDistributionPriority = function (targetThat, distributedBlock) {
4505 if (!distributedBlock.priority) {
4506 var instantiator = fluid.getInstantiator(targetThat);
4507 var targetStack = instantiator.getThatStack(targetThat);
4508 var targetPath = fluid.getMemberNames(instantiator, targetStack);
4509 var sourceStack = instantiator.getThatStack(distributedBlock.contextThat);
4510 var sourcePath = fluid.getMemberNames(instantiator, sourceStack);
4511 var distance = fluid.computeTreeDistance(targetPath, sourcePath);
4512 distributedBlock.priority = fluid.mergeRecordTypes.distribution - distance;
4513 }
4514 return distributedBlock;
4515 };
4516
4517 // convert "preBlocks" as produced from fluid.filterBlocks into "real blocks" suitable to be used by the expansion machinery.
4518 fluid.applyDistributions = function (that, preBlocks, targetShadow) {
4519 var distributedBlocks = fluid.transform(preBlocks, function (preBlock) {
4520 return fluid.generateExpandBlock(preBlock, that, targetShadow.mergePolicy);
4521 }, function (distributedBlock) {
4522 return fluid.computeDistributionPriority(that, distributedBlock);
4523 });
4524 var mergeOptions = targetShadow.mergeOptions;
4525 mergeOptions.mergeBlocks.push.apply(mergeOptions.mergeBlocks, distributedBlocks);
4526 mergeOptions.updateBlocks();
4527 return distributedBlocks;
4528 };
4529
4530 // TODO: This implementation is obviously poor and has numerous flaws - in particular it does no backtracking as well as matching backwards through the selector
4531 /** Match a parsed IoC selector against a selection of data structures representing a component's tree context.
4532 * @param {ParsedSelector} selector - A parsed selector structure as returned from `fluid.parseSelector`.
4533 * @param {Component[]} thatStack - An array of components ascending up the tree from the component being matched,
4534 * which will be held in the last position.
4535 * @param {Object[]} contextHashes - An array of context hashes as cached in the component's shadows - a hash to
4536 * `true`/"memberName" depending on the reason the context matches
4537 * @param {String[]} [memberNames] - An array of member names of components in their parents. This is only used in the distributeOptions route.
4538 * @param {Number} i - One plus the index of the IoCSS head component within `thatStack` - all components before this
4539 * index will be ignored for matching. Will have value `1` in the queryIoCSelector route.
4540 * @return {Boolean} `true` if the selector matches the leaf component at the end of `thatStack`
4541 */
4542 fluid.matchIoCSelector = function (selector, thatStack, contextHashes, memberNames, i) {
4543 var thatpos = thatStack.length - 1;
4544 var selpos = selector.length - 1;
4545 while (true) {
4546 var isChild = selector[selpos].child;
4547 var mustMatchHere = thatpos === thatStack.length - 1 || isChild;
4548
4549 var that = thatStack[thatpos];
4550 var selel = selector[selpos];
4551 var match = true;
4552 for (var j = 0; j < selel.predList.length; ++j) {
4553 var pred = selel.predList[j];
4554 var context = pred.context;
4555 if (context && context !== "*" && !(contextHashes[thatpos][context] || memberNames[thatpos] === context)) {
4556 match = false;
4557 break;
4558 }
4559 if (pred.id && that.id !== pred.id) {
4560 match = false;
4561 break;
4562 }
4563 }
4564 if (selpos === 0 && thatpos > i && mustMatchHere && isChild) {
4565 match = false; // child selector must exhaust stack completely - FLUID-5029
4566 }
4567 if (match) {
4568 if (selpos === 0) {
4569 return true;
4570 }
4571 --thatpos;
4572 --selpos;
4573 }
4574 else {
4575 if (mustMatchHere) {
4576 return false;
4577 }
4578 else {
4579 --thatpos;
4580 }
4581 }
4582 if (thatpos < i) {
4583 return false;
4584 }
4585 }
4586 };
4587
4588 /** Query for all components matching a selector in a particular tree
4589 * @param {Component} root - The root component at which to start the search
4590 * @param {String} selector - An IoCSS selector, in form of a string. Note that since selectors supplied to this function implicitly
4591 * match downwards, they need not contain the "head context" followed by whitespace required in the distributeOptions form. E.g.
4592 * simply <code>"fluid.viewComponent"</code> will match all viewComponents below the root.
4593 * @param {Boolean} flat - [Optional] <code>true</code> if the search should just be performed at top level of the component tree
4594 * Note that with <code>flat=true</code> this search will scan every component in the tree and may well be very slow.
4595 * @return {Component[]} The list of all components matching the selector
4596 */
4597 // supported, PUBLIC API function
4598 fluid.queryIoCSelector = function (root, selector, flat) {
4599 var parsed = fluid.parseSelector(selector, fluid.IoCSSMatcher);
4600 var togo = [];
4601
4602 fluid.visitComponentsForMatching(root, {flat: flat}, function (that, thatStack, contextHashes) {
4603 if (fluid.matchIoCSelector(parsed, thatStack, contextHashes, [], 1)) {
4604 togo.push(that);
4605 }
4606 });
4607 return togo;
4608 };
4609
4610 fluid.isIoCSSSelector = function (context) {
4611 return context.indexOf(" ") !== -1; // simple-minded check for an IoCSS reference
4612 };
4613
4614 fluid.pushDistributions = function (targetHead, selector, target, blocks) {
4615 var targetShadow = fluid.shadowForComponent(targetHead);
4616 var id = fluid.allocateGuid();
4617 var distribution = {
4618 id: id, // This id is used in clearDistributions
4619 target: target, // Here for improved debuggability - info is duplicated in "selector"
4620 selector: selector,
4621 blocks: blocks
4622 };
4623 Object.freeze(distribution);
4624 Object.freeze(distribution.blocks);
4625 fluid.pushArray(targetShadow, "distributions", distribution);
4626 return id;
4627 };
4628
4629 fluid.clearDistribution = function (targetHeadId, id) {
4630 var targetHeadShadow = fluid.globalInstantiator.idToShadow[targetHeadId];
4631 // By FLUID-6193, the head component may already have been destroyed, in which case the distributions are gone,
4632 // and we have leaked only its id. In theory we may want to re-establish the distribution if the head is
4633 // re-created, but that is a far wider issue.
4634 if (targetHeadShadow) {
4635 fluid.remove_if(targetHeadShadow.distributions, function (distribution) {
4636 return distribution.id === id;
4637 });
4638 }
4639 };
4640
4641 fluid.clearDistributions = function (shadow) {
4642 fluid.each(shadow.outDistributions, function (outDist) {
4643 fluid.clearDistribution(outDist.targetHeadId, outDist.distributionId);
4644 });
4645 };
4646
4647 // Modifies a parsed selector to extract and remove its head context which will be matched upwards
4648 fluid.extractSelectorHead = function (parsedSelector) {
4649 var predList = parsedSelector[0].predList;
4650 var context = predList[0].context;
4651 predList.length = 0;
4652 return context;
4653 };
4654
4655 fluid.parseExpectedOptionsPath = function (path, role) {
4656 var segs = fluid.model.parseEL(path);
4657 if (segs[0] !== "options") {
4658 fluid.fail("Error in options distribution path ", path, " - only " + role + " paths beginning with \"options\" are supported");
4659 }
4660 return segs.slice(1);
4661 };
4662
4663 fluid.replicateProperty = function (source, property, targets) {
4664 if (source[property] !== undefined) {
4665 fluid.each(targets, function (target) {
4666 target[property] = source[property];
4667 });
4668 }
4669 };
4670
4671 fluid.undistributableOptions = ["gradeNames", "distributeOptions", "argumentMap", "initFunction", "mergePolicy", "progressiveCheckerOptions"]; // automatically added to "exclusions" of every distribution
4672
4673 fluid.distributeOptions = function (that, optionsStrategy) {
4674 var thatShadow = fluid.shadowForComponent(that);
4675 var records = fluid.driveStrategy(that.options, "distributeOptions", optionsStrategy);
4676 fluid.each(records, function distributeOptionsOne(record) {
4677 fluid.pushActivity("distributeOptions", "parsing distributeOptions block %record %that ", {that: that, record: record});
4678 if (typeof(record.target) !== "string") {
4679 fluid.fail("Error in options distribution record ", record, " a member named \"target\" must be supplied holding an IoC reference");
4680 }
4681 if (typeof(record.source) === "string" ^ record.record === undefined) {
4682 fluid.fail("Error in options distribution record ", record, ": must supply either a member \"source\" holding an IoC reference or a member \"record\" holding a literal record");
4683 }
4684 var targetRef = fluid.parseContextReference(record.target);
4685 var targetHead, selector, context;
4686 if (fluid.isIoCSSSelector(targetRef.context)) {
4687 selector = fluid.parseSelector(targetRef.context, fluid.IoCSSMatcher);
4688 var headContext = fluid.extractSelectorHead(selector);
4689 if (headContext === "/") {
4690 targetHead = fluid.rootComponent;
4691 } else {
4692 context = headContext;
4693 }
4694 }
4695 else {
4696 context = targetRef.context;
4697 }
4698 targetHead = targetHead || fluid.resolveContext(context, that);
4699 if (!targetHead) {
4700 fluid.fail("Error in options distribution record ", record, " - could not resolve context {" + context + "} to a head component");
4701 }
4702 var targetSegs = fluid.model.parseEL(targetRef.path);
4703 var preBlocks;
4704 if (record.record !== undefined) {
4705 preBlocks = [(fluid.makeDistributionRecord(that, record.record, [], targetSegs, []))];
4706 }
4707 else {
4708 var source = fluid.parseContextReference(record.source);
4709 if (source.context !== "that") {
4710 fluid.fail("Error in options distribution record ", record, " only a context of {that} is supported");
4711 }
4712 var sourceSegs = fluid.parseExpectedOptionsPath(source.path, "source");
4713 var fullExclusions = fluid.makeArray(record.exclusions).concat(sourceSegs.length === 0 ? fluid.undistributableOptions : []);
4714
4715 var exclusions = fluid.transform(fullExclusions, function (exclusion) {
4716 return fluid.model.parseEL(exclusion);
4717 });
4718
4719 preBlocks = fluid.filterBlocks(that, thatShadow.mergeOptions.mergeBlocks, sourceSegs, targetSegs, exclusions, record.removeSource);
4720 thatShadow.mergeOptions.updateBlocks(); // perhaps unnecessary
4721 }
4722 fluid.replicateProperty(record, "priority", preBlocks);
4723 fluid.replicateProperty(record, "namespace", preBlocks);
4724 // TODO: inline material has to be expanded in its original context!
4725
4726 if (selector) {
4727 var distributionId = fluid.pushDistributions(targetHead, selector, record.target, preBlocks);
4728 thatShadow.outDistributions = thatShadow.outDistributions || [];
4729 thatShadow.outDistributions.push({
4730 targetHeadId: targetHead.id,
4731 distributionId: distributionId
4732 });
4733 }
4734 else { // The component exists now, we must rebalance it
4735 var targetShadow = fluid.shadowForComponent(targetHead);
4736 fluid.applyDistributions(that, preBlocks, targetShadow);
4737 }
4738 fluid.popActivity();
4739 });
4740 };
4741
4742 fluid.gradeNamesToHash = function (gradeNames) {
4743 var contextHash = {};
4744 fluid.each(gradeNames, function (gradeName) {
4745 contextHash[gradeName] = true;
4746 contextHash[fluid.computeNickName(gradeName)] = true;
4747 });
4748 return contextHash;
4749 };
4750
4751 fluid.cacheShadowGrades = function (that, shadow) {
4752 var contextHash = fluid.gradeNamesToHash(that.options.gradeNames);
4753 if (!contextHash[shadow.memberName]) {
4754 contextHash[shadow.memberName] = "memberName"; // This is filtered out again in recordComponent - TODO: Ensure that ALL resolution uses the scope chain eventually
4755 }
4756 shadow.contextHash = contextHash;
4757 fluid.each(contextHash, function (troo, context) {
4758 shadow.ownScope[context] = that;
4759 if (shadow.parentShadow && shadow.parentShadow.that.type !== "fluid.rootComponent") {
4760 shadow.parentShadow.childrenScope[context] = that;
4761 }
4762 });
4763 };
4764
4765 // First sequence point where the mergeOptions strategy is delivered from Fluid.js - here we take care
4766 // of both receiving and transmitting options distributions
4767 fluid.deliverOptionsStrategy = function (that, target, mergeOptions) {
4768 var shadow = fluid.shadowForComponent(that, shadow);
4769 fluid.cacheShadowGrades(that, shadow);
4770 shadow.mergeOptions = mergeOptions;
4771 };
4772
4773 /** Dynamic grade closure algorithm - the following 4 functions share access to a small record structure "rec" which is
4774 * constructed at the start of fluid.computeDynamicGrades
4775 */
4776
4777 fluid.collectDistributedGrades = function (rec) {
4778 // Receive distributions first since these may cause arrival of more contextAwareness blocks.
4779 var distributedBlocks = fluid.receiveDistributions(null, null, null, rec.that);
4780 if (distributedBlocks.length > 0) {
4781 var readyBlocks = fluid.applyDistributions(rec.that, distributedBlocks, rec.shadow);
4782 var gradeNamesList = fluid.transform(fluid.getMembers(readyBlocks, ["source", "gradeNames"]), fluid.makeArray);
4783 fluid.accumulateDynamicGrades(rec, fluid.flatten(gradeNamesList));
4784 }
4785 };
4786
4787 // Apply a batch of freshly acquired plain dynamic grades to the target component and recompute its options
4788 fluid.applyDynamicGrades = function (rec) {
4789 rec.oldGradeNames = fluid.makeArray(rec.gradeNames);
4790 // Note that this crude algorithm doesn't allow us to determine which grades are "new" and which not // TODO: can no longer interpret comment
4791 var newDefaults = fluid.copy(fluid.getMergedDefaults(rec.that.typeName, rec.gradeNames));
4792 rec.gradeNames.length = 0; // acquire derivatives of dynamic grades (FLUID-5054)
4793 rec.gradeNames.push.apply(rec.gradeNames, newDefaults.gradeNames);
4794
4795 fluid.each(rec.gradeNames, function (gradeName) {
4796 if (!fluid.isIoCReference(gradeName)) {
4797 rec.seenGrades[gradeName] = true;
4798 }
4799 });
4800
4801 var shadow = rec.shadow;
4802 fluid.cacheShadowGrades(rec.that, shadow);
4803 // This cheap strategy patches FLUID-5091 for now - some more sophisticated activity will take place
4804 // at this site when we have a full fix for FLUID-5028
4805 shadow.mergeOptions.destroyValue(["mergePolicy"]);
4806 shadow.mergeOptions.destroyValue(["components"]);
4807 shadow.mergeOptions.destroyValue(["invokers"]);
4808
4809 rec.defaultsBlock.source = newDefaults;
4810 shadow.mergeOptions.updateBlocks();
4811 shadow.mergeOptions.computeMergePolicy(); // TODO: we should really only do this if its content changed - this implies moving all options evaluation over to some (cheap) variety of the ChangeApplier
4812
4813 fluid.accumulateDynamicGrades(rec, newDefaults.gradeNames);
4814 };
4815
4816 // Filter some newly discovered grades into their plain and dynamic queues
4817 fluid.accumulateDynamicGrades = function (rec, newGradeNames) {
4818 fluid.each(newGradeNames, function (gradeName) {
4819 if (!rec.seenGrades[gradeName]) {
4820 if (fluid.isIoCReference(gradeName)) {
4821 rec.rawDynamic.push(gradeName);
4822 rec.seenGrades[gradeName] = true;
4823 } else if (!fluid.contains(rec.oldGradeNames, gradeName)) {
4824 rec.plainDynamic.push(gradeName);
4825 }
4826 }
4827 });
4828 };
4829
4830 fluid.computeDynamicGrades = function (that, shadow, strategy) {
4831 delete that.options.gradeNames; // Recompute gradeNames for FLUID-5012 and others
4832 var gradeNames = fluid.driveStrategy(that.options, "gradeNames", strategy); // Just acquire the reference and force eval of mergeBlocks "target", contents are wrong
4833 gradeNames.length = 0;
4834 // TODO: In complex distribution cases, a component might end up with multiple default blocks
4835 var defaultsBlock = fluid.findMergeBlocks(shadow.mergeOptions.mergeBlocks, "defaults")[0];
4836
4837 var rec = {
4838 that: that,
4839 shadow: shadow,
4840 defaultsBlock: defaultsBlock,
4841 gradeNames: gradeNames, // remember that this array is globally shared
4842 seenGrades: {},
4843 plainDynamic: [],
4844 rawDynamic: []
4845 };
4846 fluid.each(shadow.mergeOptions.mergeBlocks, function (block) { // acquire parents of earlier blocks before applying later ones
4847 gradeNames.push.apply(gradeNames, fluid.makeArray(block.target && block.target.gradeNames));
4848 fluid.applyDynamicGrades(rec);
4849 });
4850 fluid.collectDistributedGrades(rec);
4851 while (true) {
4852 while (rec.plainDynamic.length > 0) {
4853 gradeNames.push.apply(gradeNames, rec.plainDynamic);
4854 rec.plainDynamic.length = 0;
4855 fluid.applyDynamicGrades(rec);
4856 fluid.collectDistributedGrades(rec);
4857 }
4858 if (rec.rawDynamic.length > 0) {
4859 var expanded = fluid.expandImmediate(rec.rawDynamic.shift(), that, shadow.localDynamic);
4860 if (typeof(expanded) === "function") {
4861 expanded = expanded();
4862 }
4863 if (expanded) {
4864 rec.plainDynamic = rec.plainDynamic.concat(expanded);
4865 }
4866 } else {
4867 break;
4868 }
4869 }
4870
4871 if (shadow.collectedClearer) {
4872 shadow.collectedClearer();
4873 delete shadow.collectedClearer;
4874 }
4875 };
4876
4877 fluid.computeDynamicComponentKey = function (recordKey, sourceKey) {
4878 return recordKey + (sourceKey === 0 ? "" : "-" + sourceKey); // TODO: configurable name strategies
4879 };
4880 // Hacked resolution of FLUID-6371 - we can't add a listener because this version of the framework doesn't
4881 // support multiple records as subcomponents, and there may have been a total options injection
4882 fluid.hasDynamicComponentCount = function (shadow, key) {
4883 var hypos = key.indexOf("-");
4884 if (hypos !== -1) {
4885 var recordKey = key.substring(0, hypos);
4886 return shadow.dynamicComponentCount !== undefined && shadow.dynamicComponentCount[recordKey] !== undefined;
4887 }
4888 };
4889
4890 fluid.clearDynamicParentRecord = function (shadow, key) {
4891 if (fluid.hasDynamicComponentCount(shadow, key)) {
4892 var holder = fluid.get(shadow.that, ["options", "components"]);
4893 if (holder) {
4894 delete holder[key];
4895 }
4896 }
4897 };
4898
4899 fluid.registerDynamicRecord = function (that, recordKey, sourceKey, record, toCensor) {
4900 var key = fluid.computeDynamicComponentKey(recordKey, sourceKey);
4901 var recordCopy = fluid.copy(record);
4902 delete recordCopy[toCensor];
4903 fluid.set(that.options, ["components", key], recordCopy);
4904 return key;
4905 };
4906
4907 fluid.computeDynamicComponents = function (that, mergeOptions) {
4908 var shadow = fluid.shadowForComponent(that);
4909 var localSub = shadow.subcomponentLocal = {};
4910 var records = fluid.driveStrategy(that.options, "dynamicComponents", mergeOptions.strategy);
4911 fluid.each(records, function (record, recordKey) {
4912 if (!record.sources && !record.createOnEvent) {
4913 fluid.fail("Cannot process dynamicComponents record ", record, " without a \"sources\" or \"createOnEvent\" entry");
4914 }
4915 if (record.sources) {
4916 var sources = fluid.expandOptions(record.sources, that);
4917 fluid.each(sources, function (source, sourceKey) {
4918 var key = fluid.registerDynamicRecord(that, recordKey, sourceKey, record, "sources");
4919 localSub[key] = {"source": source, "sourcePath": sourceKey};
4920 });
4921 }
4922 else if (record.createOnEvent) {
4923 var event = fluid.event.expandOneEvent(that, record.createOnEvent);
4924 fluid.set(shadow, ["dynamicComponentCount", recordKey], 0);
4925 var listener = function () {
4926 var key = fluid.registerDynamicRecord(that, recordKey, shadow.dynamicComponentCount[recordKey]++, record, "createOnEvent");
4927 var localRecord = {"arguments": fluid.makeArray(arguments)};
4928 fluid.initDependent(that, key, localRecord);
4929 };
4930 event.addListener(listener);
4931 fluid.recordListener(event, listener, shadow);
4932 }
4933 });
4934 };
4935
4936 // Second sequence point for mergeOptions from Fluid.js - here we construct all further
4937 // strategies required on the IoC side and mount them into the shadow's getConfig for universal use
4938 fluid.computeComponentAccessor = function (that, localRecord) {
4939 var instantiator = fluid.globalInstantiator;
4940 var shadow = fluid.shadowForComponent(that);
4941 shadow.localDynamic = localRecord; // for signalling to dynamic grades from dynamic components
4942 var options = that.options;
4943 var strategy = shadow.mergeOptions.strategy;
4944 var optionsStrategy = fluid.mountStrategy(["options"], options, strategy);
4945 shadow.invokerStrategy = fluid.recordStrategy(that, options, strategy, "invokers", fluid.invokerFromRecord);
4946 shadow.eventStrategyBlock = fluid.recordStrategy(that, options, strategy, "events", fluid.eventFromRecord, ["events"]);
4947 var eventStrategy = fluid.mountStrategy(["events"], that, shadow.eventStrategyBlock.strategy, ["events"]);
4948 shadow.memberStrategy = fluid.recordStrategy(that, options, strategy, "members", fluid.memberFromRecord, null, {model: true, modelRelay: true});
4949 // NB - ginger strategy handles concrete, rationalise
4950 shadow.getConfig = {strategies: [fluid.model.funcResolverStrategy, fluid.makeGingerStrategy(that),
4951 optionsStrategy, shadow.invokerStrategy.strategy, shadow.memberStrategy.strategy, eventStrategy]};
4952
4953 fluid.computeDynamicGrades(that, shadow, strategy, shadow.mergeOptions.mergeBlocks);
4954 fluid.distributeOptions(that, strategy);
4955 if (shadow.contextHash["fluid.resolveRoot"]) {
4956 var memberName;
4957 if (shadow.contextHash["fluid.resolveRootSingle"]) {
4958 var singleRootType = fluid.getForComponent(that, ["options", "singleRootType"]);
4959 if (!singleRootType) {
4960 fluid.fail("Cannot register object with grades " + Object.keys(shadow.contextHash).join(", ") + " as fluid.resolveRootSingle since it has not defined option singleRootType");
4961 }
4962 memberName = fluid.typeNameToMemberName(singleRootType);
4963 } else {
4964 memberName = fluid.computeGlobalMemberName(that);
4965 }
4966 var parent = fluid.resolveRootComponent;
4967 if (parent[memberName]) {
4968 instantiator.clearComponent(parent, memberName);
4969 }
4970 instantiator.recordKnownComponent(parent, that, memberName, false);
4971 }
4972
4973 return shadow.getConfig;
4974 };
4975
4976 // About the SHADOW:
4977 // Allocated at: instantiator's "recordComponent"
4978 // Contents:
4979 // path {String} Principal allocated path (point of construction) in tree
4980 // that {Component} The component itself
4981 // contextHash {String to Boolean} Map of context names which this component matches
4982 // mergePolicy, mergeOptions: Machinery for last phase of options merging
4983 // invokerStrategy, eventStrategyBlock, memberStrategy, getConfig: Junk required to operate the accessor
4984 // listeners: Listeners registered during this component's construction, to be cleared during clearListeners
4985 // distributions, collectedClearer: Managing options distributions
4986 // outDistributions: A list of distributions registered from this component, signalling from distributeOptions to clearDistributions
4987 // subcomponentLocal: Signalling local record from computeDynamicComponents to assembleCreatorArguments
4988 // dynamicLocal: Local signalling for dynamic grades
4989 // ownScope: A hash of names to components which are in scope from this component - populated in cacheShadowGrades
4990 // childrenScope: A hash of names to components which are in scope because they are children of this component (BELOW own ownScope in resolution order)
4991
4992 fluid.shadowForComponent = function (component) {
4993 var instantiator = fluid.getInstantiator(component);
4994 return instantiator && component ? instantiator.idToShadow[component.id] : null;
4995 };
4996
4997 // Access the member at a particular path in a component, forcing it to be constructed gingerly if necessary
4998 // supported, PUBLIC API function
4999 fluid.getForComponent = function (component, path) {
5000 var shadow = fluid.shadowForComponent(component);
5001 var getConfig = shadow ? shadow.getConfig : undefined;
5002 return fluid.get(component, path, getConfig);
5003 };
5004
5005 // An EL segment resolver strategy that will attempt to trigger creation of
5006 // components that it discovers along the EL path, if they have been defined but not yet
5007 // constructed.
5008 fluid.makeGingerStrategy = function (that) {
5009 var instantiator = fluid.getInstantiator(that);
5010 return function (component, thisSeg, index, segs) {
5011 var atval = component[thisSeg];
5012 if (atval === fluid.inEvaluationMarker && index === segs.length) {
5013 fluid.fail("Error in component configuration - a circular reference was found during evaluation of path segment \"" + thisSeg +
5014 "\": for more details, see the activity records following this message in the console, or issue fluid.setLogging(fluid.logLevel.TRACE) when running your application");
5015 }
5016 if (index > 1) {
5017 return atval;
5018 }
5019 if (atval === undefined && component.hasOwnProperty(thisSeg)) { // avoid recomputing properties that have been explicitly evaluated to undefined
5020 return fluid.NO_VALUE;
5021 }
5022 if (atval === undefined) { // pick up components in instantiation here - we can cut this branch by attaching early
5023 var parentPath = instantiator.idToShadow[component.id].path;
5024 var childPath = instantiator.composePath(parentPath, thisSeg);
5025 atval = instantiator.pathToComponent[childPath];
5026 }
5027 if (atval === undefined) {
5028 // TODO: This check is very expensive - once gingerness is stable, we ought to be able to
5029 // eagerly compute and cache the value of options.components - check is also incorrect and will miss injections
5030 var subRecord = fluid.getForComponent(component, ["options", "components", thisSeg]);
5031 if (subRecord) {
5032 if (subRecord.createOnEvent) {
5033 fluid.fail("Error resolving path segment \"" + thisSeg + "\" of path " + segs.join(".") + " since component with record ", subRecord,
5034 " has annotation \"createOnEvent\" - this very likely represents an implementation error. Either alter the reference so it does not " +
5035 " match this component, or alter your workflow to ensure that the component is instantiated by the time this reference resolves");
5036 }
5037 fluid.initDependent(component, thisSeg);
5038 atval = component[thisSeg];
5039 }
5040 }
5041 return atval;
5042 };
5043 };
5044
5045 // Listed in dependence order
5046 fluid.frameworkGrades = ["fluid.component", "fluid.modelComponent", "fluid.viewComponent", "fluid.rendererComponent"];
5047
5048 fluid.filterBuiltinGrades = function (gradeNames) {
5049 return fluid.remove_if(fluid.makeArray(gradeNames), function (gradeName) {
5050 return fluid.frameworkGrades.indexOf(gradeName) !== -1;
5051 });
5052 };
5053
5054 fluid.dumpGradeNames = function (that) {
5055 return that.options && that.options.gradeNames ?
5056 " gradeNames: " + JSON.stringify(fluid.filterBuiltinGrades(that.options.gradeNames)) : "";
5057 };
5058
5059 fluid.dumpThat = function (that) {
5060 return "{ typeName: \"" + that.typeName + "\"" + fluid.dumpGradeNames(that) + " id: " + that.id + "}";
5061 };
5062
5063 fluid.dumpThatStack = function (thatStack, instantiator) {
5064 var togo = fluid.transform(thatStack, function (that) {
5065 var path = instantiator.idToPath(that.id);
5066 return fluid.dumpThat(that) + (path ? (" - path: " + path) : "");
5067 });
5068 return togo.join("\n");
5069 };
5070
5071 fluid.dumpComponentPath = function (that) {
5072 var path = fluid.pathForComponent(that);
5073 return path ? fluid.pathUtil.composeSegments(path) : "** no path registered for component **";
5074 };
5075
5076 fluid.resolveContext = function (context, that, fast) {
5077 if (context === "that") {
5078 return that;
5079 }
5080 // TODO: Check performance impact of this type check introduced for FLUID-5903 in a very sensitive corner
5081 if (typeof(context) === "object") {
5082 var innerContext = fluid.resolveContext(context.context, that, fast);
5083 if (!fluid.isComponent(innerContext)) {
5084 fluid.triggerMismatchedPathError(context.context, that);
5085 }
5086 var rawValue = fluid.getForComponent(innerContext, context.path);
5087 // TODO: Terrible, slow dispatch for this route
5088 var expanded = fluid.expandOptions(rawValue, that);
5089 if (!fluid.isComponent(expanded)) {
5090 fluid.fail("Unable to resolve recursive context expression " + fluid.renderContextReference(context) + ": the directly resolved value of " + rawValue +
5091 " did not resolve to a component in the scope of component ", that, ": got ", expanded);
5092 }
5093 return expanded;
5094 } else {
5095 var foundComponent;
5096 var instantiator = fluid.globalInstantiator; // fluid.getInstantiator(that); // this hash lookup takes over 1us!
5097 if (fast) {
5098 var shadow = instantiator.idToShadow[that.id];
5099 return shadow.ownScope[context];
5100 } else {
5101 var thatStack = instantiator.getFullStack(that);
5102 fluid.visitComponentsForVisibility(instantiator, thatStack, function (component, name) {
5103 var shadow = fluid.shadowForComponent(component);
5104 // TODO: Some components, e.g. the static environment and typeTags do not have a shadow, which slows us down here
5105 if (context === name || shadow && shadow.contextHash && shadow.contextHash[context] || context === component.typeName) {
5106 foundComponent = component;
5107 return true; // YOUR VISIT IS AT AN END!!
5108 }
5109 if (fluid.getForComponent(component, ["options", "components", context]) && !component[context]) {
5110 // This is an expensive guess since we make it for every component up the stack - must apply the WAVE OF EXPLOSIONS (FLUID-4925) to discover all components first
5111 // This line attempts a hopeful construction of components that could be guessed by nickname through finding them unconstructed
5112 // in options. In the near future we should eagerly BEGIN the process of constructing components, discovering their
5113 // types and then attaching them to the tree VERY EARLY so that we get consistent results from different strategies.
5114 foundComponent = fluid.getForComponent(component, context);
5115 return true;
5116 }
5117 });
5118 return foundComponent;
5119 }
5120 }
5121 };
5122
5123 fluid.triggerMismatchedPathError = function (parsed, parentThat) {
5124 var ref = fluid.renderContextReference(parsed);
5125 fluid.fail("Failed to resolve reference " + ref + " - could not match context with name " +
5126 parsed.context + " from component " + fluid.dumpThat(parentThat) + " at path " + fluid.dumpComponentPath(parentThat) + " component: " , parentThat);
5127 };
5128
5129 fluid.makeStackFetcher = function (parentThat, localRecord, fast) {
5130 var fetcher = function (parsed) {
5131 if (parentThat && parentThat.lifecycleStatus === "destroyed") {
5132 fluid.fail("Cannot resolve reference " + fluid.renderContextReference(parsed) + " from component " + fluid.dumpThat(parentThat) + " which has been destroyed");
5133 }
5134 var context = parsed.context;
5135 if (localRecord && context in localRecord) {
5136 return fluid.get(localRecord[context], parsed.path);
5137 }
5138 var foundComponent = fluid.resolveContext(context, parentThat, fast);
5139 if (!foundComponent && parsed.path !== "") {
5140 fluid.triggerMismatchedPathError(parsed, parentThat);
5141 }
5142 return fluid.getForComponent(foundComponent, parsed.path);
5143 };
5144 return fetcher;
5145 };
5146
5147 fluid.makeStackResolverOptions = function (parentThat, localRecord, fast) {
5148 return $.extend(fluid.copy(fluid.rawDefaults("fluid.makeExpandOptions")), {
5149 localRecord: localRecord || {},
5150 fetcher: fluid.makeStackFetcher(parentThat, localRecord, fast),
5151 contextThat: parentThat,
5152 exceptions: {members: {model: true, modelRelay: true}}
5153 });
5154 };
5155
5156 fluid.clearListeners = function (shadow) {
5157 // TODO: bug here - "afterDestroy" listeners will be unregistered already unless they come from this component
5158 fluid.each(shadow.listeners, function (rec) {
5159 rec.event.removeListener(rec.listenerId || rec.listener);
5160 });
5161 delete shadow.listeners;
5162 };
5163
5164 fluid.recordListener = function (event, listener, shadow, listenerId) {
5165 if (event.ownerId !== shadow.that.id) { // don't bother recording listeners registered from this component itself
5166 fluid.pushArray(shadow, "listeners", {event: event, listener: listener, listenerId: listenerId});
5167 }
5168 };
5169
5170 fluid.constructScopeObjects = function (instantiator, parent, child, childShadow) {
5171 var parentShadow = parent ? instantiator.idToShadow[parent.id] : null;
5172 childShadow.childrenScope = parentShadow ? Object.create(parentShadow.ownScope) : {};
5173 childShadow.ownScope = Object.create(childShadow.childrenScope);
5174 childShadow.parentShadow = parentShadow;
5175 };
5176
5177 fluid.clearChildrenScope = function (instantiator, parentShadow, child, childShadow) {
5178 fluid.each(childShadow.contextHash, function (troo, context) {
5179 if (parentShadow.childrenScope[context] === child) {
5180 delete parentShadow.childrenScope[context]; // TODO: ambiguous resolution
5181 }
5182 });
5183 };
5184
5185 // unsupported, non-API function - however, this structure is of considerable interest to those debugging
5186 // into IoC issues. The structures idToShadow and pathToComponent contain a complete map of the component tree
5187 // forming the surrounding scope
5188 fluid.instantiator = function () {
5189 var that = fluid.typeTag("instantiator");
5190 $.extend(that, {
5191 lifecycleStatus: "constructed",
5192 pathToComponent: {},
5193 idToShadow: {},
5194 modelTransactions: {init: {}}, // a map of transaction id to map of component id to records of components enlisted in a current model initialisation transaction
5195 composePath: fluid.model.composePath, // For speed, we declare that no component's name may contain a period
5196 composeSegments: fluid.model.composeSegments,
5197 parseEL: fluid.model.parseEL,
5198 events: {
5199 onComponentAttach: fluid.makeEventFirer({name: "instantiator's onComponentAttach event"}),
5200 onComponentClear: fluid.makeEventFirer({name: "instantiator's onComponentClear event"})
5201 }
5202 });
5203 // TODO: this API can shortly be removed
5204 that.idToPath = function (id) {
5205 var shadow = that.idToShadow[id];
5206 return shadow ? shadow.path : "";
5207 };
5208 // Note - the returned stack is assumed writeable and does not include the root
5209 that.getThatStack = function (component) {
5210 var shadow = that.idToShadow[component.id];
5211 if (shadow) {
5212 var path = shadow.path;
5213 var parsed = that.parseEL(path);
5214 var root = that.pathToComponent[""], togo = [];
5215 for (var i = 0; i < parsed.length; ++i) {
5216 root = root[parsed[i]];
5217 togo.push(root);
5218 }
5219 return togo;
5220 }
5221 else { return [];}
5222 };
5223 that.getFullStack = function (component) {
5224 var thatStack = component ? that.getThatStack(component) : [];
5225 thatStack.unshift(fluid.resolveRootComponent);
5226 return thatStack;
5227 };
5228 function recordComponent(parent, component, path, name, created) {
5229 var shadow;
5230 if (created) {
5231 shadow = that.idToShadow[component.id] = {};
5232 shadow.that = component;
5233 shadow.path = path;
5234 shadow.memberName = name;
5235 fluid.constructScopeObjects(that, parent, component, shadow);
5236 } else {
5237 shadow = that.idToShadow[component.id];
5238 shadow.injectedPaths = shadow.injectedPaths || {}; // a hash since we will modify whilst iterating
5239 shadow.injectedPaths[path] = true;
5240 var parentShadow = that.idToShadow[parent.id]; // structural parent shadow - e.g. resolveRootComponent
5241 var keys = fluid.keys(shadow.contextHash);
5242 fluid.remove_if(keys, function (key) {
5243 return shadow.contextHash && shadow.contextHash[key] === "memberName";
5244 });
5245 keys.push(name); // add local name - FLUID-5696 and FLUID-5820
5246 fluid.each(keys, function (context) {
5247 if (!parentShadow.childrenScope[context]) {
5248 parentShadow.childrenScope[context] = component;
5249 }
5250 });
5251 }
5252 if (that.pathToComponent[path]) {
5253 fluid.fail("Error during instantiation - path " + path + " which has just created component " + fluid.dumpThat(component) +
5254 " has already been used for component " + fluid.dumpThat(that.pathToComponent[path]) + " - this is a circular instantiation or other oversight." +
5255 " Please clear the component using instantiator.clearComponent() before reusing the path.");
5256 }
5257 that.pathToComponent[path] = component;
5258 }
5259 that.recordRoot = function (component) {
5260 recordComponent(null, component, "", "", true);
5261 };
5262 that.recordKnownComponent = function (parent, component, name, created) {
5263 parent[name] = component;
5264 if (fluid.isComponent(component) || component.type === "instantiator") {
5265 var parentPath = that.idToShadow[parent.id].path;
5266 var path = that.composePath(parentPath, name);
5267 recordComponent(parent, component, path, name, created);
5268 that.events.onComponentAttach.fire(component, path, that, created);
5269 } else {
5270 fluid.fail("Cannot record non-component with value ", component, " at path \"" + name + "\" of parent ", parent);
5271 }
5272 };
5273 that.clearConcreteComponent = function (destroyRec) {
5274 // Clear injected instance of this component from all other paths - historically we didn't bother
5275 // to do this since injecting into a shorter scope is an error - but now we have resolveRoot area
5276 fluid.each(destroyRec.childShadow.injectedPaths, function (troo, injectedPath) {
5277 var parentPath = fluid.model.getToTailPath(injectedPath);
5278 var otherParent = that.pathToComponent[parentPath];
5279 that.clearComponent(otherParent, fluid.model.getTailPath(injectedPath), destroyRec.child);
5280 });
5281 fluid.clearDistributions(destroyRec.childShadow);
5282 fluid.clearListeners(destroyRec.childShadow);
5283 fluid.clearDynamicParentRecord(destroyRec.shadow, destroyRec.name);
5284 fluid.fireEvent(destroyRec.child, "afterDestroy", [destroyRec.child, destroyRec.name, destroyRec.component]);
5285 delete that.idToShadow[destroyRec.child.id];
5286 };
5287 that.clearComponent = function (component, name, child, options, nested, path) {
5288 // options are visitor options for recursive driving
5289 var shadow = that.idToShadow[component.id];
5290 // use flat recursion since we want to use our own recursion rather than rely on "visited" records
5291 options = options || {flat: true, instantiator: that, destroyRecs: []};
5292 child = child || component[name];
5293 path = path || shadow.path;
5294 if (path === undefined) {
5295 fluid.fail("Cannot clear component " + name + " from component ", component,
5296 " which was not created by this instantiator");
5297 }
5298
5299 var childPath = that.composePath(path, name);
5300 var childShadow = that.idToShadow[child.id];
5301 if (!childShadow) { // Explicit FLUID-5812 check - this can be eliminated once we move visitComponentChildren to instantiator's records
5302 return;
5303 }
5304 var created = childShadow.path === childPath;
5305 that.events.onComponentClear.fire(child, childPath, component, created);
5306
5307 // only recurse on components which were created in place - if the id record disagrees with the
5308 // recurse path, it must have been injected
5309 if (created) {
5310 fluid.visitComponentChildren(child, function (gchild, gchildname, segs, i) {
5311 var parentPath = that.composeSegments.apply(null, segs.slice(0, i));
5312 that.clearComponent(child, gchildname, null, options, true, parentPath);
5313 }, options, that.parseEL(childPath));
5314 fluid.doDestroy(child, name, component); // call "onDestroy", null out events and invokers, setting lifecycleStatus to "destroyed"
5315 options.destroyRecs.push({child: child, childShadow: childShadow, name: name, component: component, shadow: shadow});
5316 } else {
5317 fluid.remove_if(childShadow.injectedPaths, function (troo, path) {
5318 return path === childPath;
5319 });
5320 }
5321 fluid.clearChildrenScope(that, shadow, child, childShadow);
5322 // Note that "pathToComponent" will not be available during afterDestroy. This is so that we can synchronously recreate the component
5323 // in an afterDestroy listener (FLUID-5931). We don't clear up the shadow itself until after afterDestroy.
5324 delete that.pathToComponent[childPath];
5325 if (!nested) {
5326 delete component[name]; // there may be no entry - if creation is not concluded
5327 // Do actual destruction for the whole tree here, including "afterDestroy" and deleting shadows
5328 fluid.each(options.destroyRecs, that.clearConcreteComponent);
5329 }
5330 };
5331 return that;
5332 };
5333
5334 // The global instantiator, holding all components instantiated in this context (instance of Infusion)
5335 fluid.globalInstantiator = fluid.instantiator();
5336
5337 // Look up the globally registered instantiator for a particular component - we now only really support a
5338 // single, global instantiator, but this method is left as a notation point in case this ever reverts
5339 // Returns null if argument is a noncomponent or has no shadow
5340 fluid.getInstantiator = function (component) {
5341 var instantiator = fluid.globalInstantiator;
5342 return component && instantiator.idToShadow[component.id] ? instantiator : null;
5343 };
5344
5345 // The grade supplied to components which will be resolvable from all parts of the component tree
5346 fluid.defaults("fluid.resolveRoot");
5347 // In addition to being resolvable at the root, "resolveRootSingle" component will have just a single instance available. Fresh
5348 // instances will displace older ones.
5349 fluid.defaults("fluid.resolveRootSingle", {
5350 gradeNames: "fluid.resolveRoot"
5351 });
5352
5353 fluid.constructRootComponents = function (instantiator) {
5354 // Instantiate the primordial components at the root of each context tree
5355 fluid.rootComponent = instantiator.rootComponent = fluid.typeTag("fluid.rootComponent");
5356 instantiator.recordRoot(fluid.rootComponent);
5357
5358 // The component which for convenience holds injected instances of all components with fluid.resolveRoot grade
5359 fluid.resolveRootComponent = instantiator.resolveRootComponent = fluid.typeTag("fluid.resolveRootComponent");
5360 instantiator.recordKnownComponent(fluid.rootComponent, fluid.resolveRootComponent, "resolveRootComponent", true);
5361
5362 // obliterate resolveRoot's scope objects and replace by the real root scope - which is unused by its own children
5363 var rootShadow = instantiator.idToShadow[fluid.rootComponent.id];
5364 rootShadow.contextHash = {}; // Fix for FLUID-6128
5365 var resolveRootShadow = instantiator.idToShadow[fluid.resolveRootComponent.id];
5366 resolveRootShadow.ownScope = rootShadow.ownScope;
5367 resolveRootShadow.childrenScope = rootShadow.childrenScope;
5368
5369 instantiator.recordKnownComponent(fluid.resolveRootComponent, instantiator, "instantiator", true); // needs to have a shadow so it can be injected
5370 resolveRootShadow.childrenScope.instantiator = instantiator; // needs to be mounted since it never passes through cacheShadowGrades
5371 };
5372
5373 fluid.constructRootComponents(fluid.globalInstantiator); // currently a singleton - in future, alternative instantiators might come back
5374
5375 /** Expand a set of component options either immediately, or with deferred effect.
5376 * The current policy is to expand immediately function arguments within fluid.assembleCreatorArguments which are not the main options of a
5377 * component. The component's own options take <code>{defer: true}</code> as part of
5378 * <code>outerExpandOptions</code> which produces an "expandOptions" structure holding the "strategy" and "initter" pattern
5379 * common to ginger participants.
5380 * Probably not to be advertised as part of a public API, but is considerably more stable than most of the rest
5381 * of the IoC API structure especially with respect to the first arguments.
5382 */
5383
5384// TODO: Can we move outerExpandOptions to 2nd place? only user of 3 and 4 is fluid.makeExpandBlock
5385// TODO: Actually we want localRecord in 2nd place since outerExpandOptions is now almost disused
5386 fluid.expandOptions = function (args, that, mergePolicy, localRecord, outerExpandOptions) {
5387 if (!args) {
5388 return args;
5389 }
5390 fluid.pushActivity("expandOptions", "expanding options %args for component %that ", {that: that, args: args});
5391 var expandOptions = fluid.makeStackResolverOptions(that, localRecord);
5392 expandOptions.mergePolicy = mergePolicy;
5393 expandOptions.defer = outerExpandOptions && outerExpandOptions.defer;
5394 var expanded = expandOptions.defer ?
5395 fluid.makeExpandOptions(args, expandOptions) : fluid.expand(args, expandOptions);
5396 fluid.popActivity();
5397 return expanded;
5398 };
5399
5400 fluid.localRecordExpected = fluid.arrayToHash(["type", "options", "container", "createOnEvent", "priority", "recordType"]); // last element unavoidably polluting
5401
5402 fluid.checkComponentRecord = function (localRecord) {
5403 fluid.each(localRecord, function (value, key) {
5404 if (!fluid.localRecordExpected[key]) {
5405 fluid.fail("Probable error in subcomponent record ", localRecord, " - key \"" + key +
5406 "\" found, where the only legal options are " +
5407 fluid.keys(fluid.localRecordExpected).join(", "));
5408 }
5409 });
5410 };
5411
5412 fluid.mergeRecordsToList = function (that, mergeRecords) {
5413 var list = [];
5414 fluid.each(mergeRecords, function (value, key) {
5415 value.recordType = key;
5416 if (key === "distributions") {
5417 list.push.apply(list, fluid.transform(value, function (distributedBlock) {
5418 return fluid.computeDistributionPriority(that, distributedBlock);
5419 }));
5420 }
5421 else {
5422 if (!value.options) { return; }
5423 value.priority = fluid.mergeRecordTypes[key];
5424 if (value.priority === undefined) {
5425 fluid.fail("Merge record with unrecognised type " + key + ": ", value);
5426 }
5427 list.push(value);
5428 }
5429 });
5430 return list;
5431 };
5432
5433 // TODO: overall efficiency could huge be improved by resorting to the hated PROTOTYPALISM as an optimisation
5434 // for this mergePolicy which occurs in every component. Although it is a deep structure, the root keys are all we need
5435 var addPolicyBuiltins = function (policy) {
5436 fluid.each(["gradeNames", "mergePolicy", "argumentMap", "components", "dynamicComponents", "events", "listeners", "modelListeners", "modelRelay", "distributeOptions", "transformOptions"], function (key) {
5437 fluid.set(policy, [key, "*", "noexpand"], true);
5438 });
5439 return policy;
5440 };
5441
5442 // used from Fluid.js
5443 fluid.generateExpandBlock = function (record, that, mergePolicy, localRecord) {
5444 var expanded = fluid.expandOptions(record.options, record.contextThat || that, mergePolicy, localRecord, {defer: true});
5445 expanded.priority = record.priority;
5446 expanded.namespace = record.namespace;
5447 expanded.recordType = record.recordType;
5448 return expanded;
5449 };
5450
5451 var expandComponentOptionsImpl = function (mergePolicy, defaults, initRecord, that) {
5452 var defaultCopy = fluid.copy(defaults);
5453 addPolicyBuiltins(mergePolicy);
5454 var shadow = fluid.shadowForComponent(that);
5455 shadow.mergePolicy = mergePolicy;
5456 var mergeRecords = {
5457 defaults: {options: defaultCopy}
5458 };
5459
5460 $.extend(mergeRecords, initRecord.mergeRecords);
5461 // Do this here for gradeless components that were corrected by "localOptions"
5462 if (mergeRecords.subcomponentRecord) {
5463 fluid.checkComponentRecord(mergeRecords.subcomponentRecord);
5464 }
5465
5466 var expandList = fluid.mergeRecordsToList(that, mergeRecords);
5467
5468 var togo = fluid.transform(expandList, function (value) {
5469 return fluid.generateExpandBlock(value, that, mergePolicy, initRecord.localRecord);
5470 });
5471 return togo;
5472 };
5473
5474 fluid.fabricateDestroyMethod = function (that, name, instantiator, child) {
5475 return function () {
5476 instantiator.clearComponent(that, name, child);
5477 };
5478 };
5479
5480 // Computes a name for a component appearing at the global root which is globally unique, from its nickName and id
5481 fluid.computeGlobalMemberName = function (that) {
5482 var nickName = fluid.computeNickName(that.typeName);
5483 return nickName + "-" + that.id;
5484 };
5485
5486 // Maps a type name to the member name to be used for it at a particular path level where it is intended to be unique
5487 // Note that "." is still not supported within a member name
5488 // supported, PUBLIC API function
5489 fluid.typeNameToMemberName = function (typeName) {
5490 return typeName.replace(/\./g, "_");
5491 };
5492
5493 // This is the initial entry point from the non-IoC side reporting the first presence of a new component - called from fluid.mergeComponentOptions
5494 fluid.expandComponentOptions = function (mergePolicy, defaults, userOptions, that) {
5495 var initRecord = userOptions; // might have been tunnelled through "userOptions" from "assembleCreatorArguments"
5496 var instantiator = userOptions && userOptions.marker === fluid.EXPAND ? userOptions.instantiator : null;
5497 fluid.pushActivity("expandComponentOptions", "expanding component options %options with record %record for component %that",
5498 {options: instantiator ? userOptions.mergeRecords.user : userOptions, record: initRecord, that: that});
5499 if (!instantiator) { // it is a top-level component which needs to be attached to the global root
5500 instantiator = fluid.globalInstantiator;
5501 initRecord = { // upgrade "userOptions" to the same format produced by fluid.assembleCreatorArguments via the subcomponent route
5502 mergeRecords: {user: {options: fluid.expandCompact(userOptions, true)}},
5503 memberName: fluid.computeGlobalMemberName(that),
5504 instantiator: instantiator,
5505 parentThat: fluid.rootComponent
5506 };
5507 }
5508 that.destroy = fluid.fabricateDestroyMethod(initRecord.parentThat, initRecord.memberName, instantiator, that);
5509
5510 instantiator.recordKnownComponent(initRecord.parentThat, that, initRecord.memberName, true);
5511 var togo = expandComponentOptionsImpl(mergePolicy, defaults, initRecord, that);
5512
5513 fluid.popActivity();
5514 return togo;
5515 };
5516
5517 /** Given a typeName, determine the final concrete
5518 * "invocation specification" consisting of a concrete global function name
5519 * and argument list which is suitable to be executed directly by fluid.invokeGlobalFunction.
5520 */
5521 // options is just a disposition record containing memberName, componentRecord
5522 fluid.assembleCreatorArguments = function (parentThat, typeName, options) {
5523 var upDefaults = fluid.defaults(typeName); // we're not responsive to dynamic changes in argMap, but we don't believe in these anyway
5524 if (!upDefaults || !upDefaults.argumentMap) {
5525 fluid.fail("Error in assembleCreatorArguments: cannot look up component type name " + typeName + " to a component creator grade with an argumentMap");
5526 }
5527
5528 var fakeThat = {}; // fake "that" for receiveDistributions since we try to match selectors before creation for FLUID-5013
5529 var distributions = parentThat ? fluid.receiveDistributions(parentThat, upDefaults.gradeNames, options.memberName, fakeThat) : [];
5530 fluid.each(distributions, function (distribution) { // TODO: The duplicated route for this is in fluid.mergeComponentOptions
5531 fluid.computeDistributionPriority(parentThat, distribution);
5532 if (fluid.isPrimitive(distribution.priority)) { // TODO: These should be immutable and parsed just once on registration - but we can't because of crazy target-dependent distance system
5533 distribution.priority = fluid.parsePriority(distribution.priority, 0, false, "options distribution");
5534 }
5535 });
5536 fluid.sortByPriority(distributions);
5537
5538 var localDynamic = options.localDynamic;
5539 var localRecord = $.extend({}, fluid.censorKeys(options.componentRecord, ["type"]), localDynamic);
5540
5541 var argMap = upDefaults.argumentMap;
5542 var findKeys = Object.keys(argMap).concat(["type"]);
5543
5544 fluid.each(findKeys, function (name) {
5545 for (var i = 0; i < distributions.length; ++i) { // Apply non-options material from distributions (FLUID-5013)
5546 if (distributions[i][name] !== undefined) {
5547 localRecord[name] = distributions[i][name];
5548 }
5549 }
5550 });
5551 typeName = localRecord.type || typeName;
5552
5553 delete localRecord.type;
5554 delete localRecord.options;
5555
5556 var mergeRecords = {distributions: distributions};
5557
5558 if (options.componentRecord !== undefined) {
5559 // Deliberately put too many things here so they can be checked in expandComponentOptions (FLUID-4285)
5560 mergeRecords.subcomponentRecord = $.extend({}, options.componentRecord);
5561 }
5562 var args = [];
5563 fluid.each(argMap, function (index, name) {
5564 var arg;
5565 if (name === "options") {
5566 arg = {marker: fluid.EXPAND,
5567 localRecord: localDynamic,
5568 mergeRecords: mergeRecords,
5569 instantiator: fluid.getInstantiator(parentThat),
5570 parentThat: parentThat,
5571 memberName: options.memberName};
5572 } else {
5573 var value = localRecord[name];
5574 arg = fluid.expandImmediate(value, parentThat, localRecord);
5575 }
5576 args[index] = arg;
5577 });
5578
5579 var togo = {
5580 args: args,
5581 funcName: typeName
5582 };
5583 return togo;
5584 };
5585
5586 /** Instantiate the subcomponent with the supplied name of the supplied top-level component. Although this method
5587 * is published as part of the Fluid API, it should not be called by general users and may not remain stable. It is
5588 * currently the only mechanism provided for instantiating components whose definitions are dynamic, and will be
5589 * replaced in time by dedicated declarative framework described by FLUID-5022.
5590 * @param {Component} that - The parent component for which the subcomponent is to be instantiated
5591 * @param {String} name - The name of the component - the index of the options block which configures it as part of the
5592 * <code>components</code> section of its parent's options
5593 * @param {Object} [localRecord] - A local scope record keyed by context names which should specially be in scope for this
5594 * construction, e.g. `arguments`. Primarily for internal framework use.
5595 * @return {Component} The constructed subcomponent
5596 */
5597 fluid.initDependent = function (that, name, localRecord) {
5598 if (that[name]) { return; } // TODO: move this into strategy
5599 var component = that.options.components[name];
5600 var instance;
5601 var instantiator = fluid.globalInstantiator;
5602 var shadow = instantiator.idToShadow[that.id];
5603 var localDynamic = localRecord || shadow.subcomponentLocal && shadow.subcomponentLocal[name];
5604 fluid.pushActivity("initDependent", "instantiating dependent component at path \"%path\" with record %record as child of %parent",
5605 {path: shadow.path + "." + name, record: component, parent: that});
5606
5607 if (typeof(component) === "string" || component.expander) {
5608 that[name] = fluid.inEvaluationMarker;
5609 instance = fluid.expandImmediate(component, that);
5610 if (instance) {
5611 instantiator.recordKnownComponent(that, instance, name, false);
5612 } else {
5613 delete that[name];
5614 }
5615 }
5616 else if (component.type) {
5617 var type = fluid.expandImmediate(component.type, that, localDynamic);
5618 if (!type) {
5619 fluid.fail("Error in subcomponent record: ", component.type, " could not be resolved to a type for component ", name,
5620 " of parent ", that);
5621 }
5622 var invokeSpec = fluid.assembleCreatorArguments(that, type, {componentRecord: component, memberName: name, localDynamic: localDynamic});
5623 instance = fluid.initSubcomponentImpl(that, {type: invokeSpec.funcName}, invokeSpec.args);
5624 }
5625 else {
5626 fluid.fail("Unrecognised material in place of subcomponent " + name + " - no \"type\" field found");
5627 }
5628 fluid.popActivity();
5629 return instance;
5630 };
5631
5632 fluid.bindDeferredComponent = function (that, componentName, component) {
5633 var events = fluid.makeArray(component.createOnEvent);
5634 fluid.each(events, function (eventName) {
5635 var event = fluid.isIoCReference(eventName) ? fluid.expandOptions(eventName, that) : that.events[eventName];
5636 if (!event || !event.addListener) {
5637 fluid.fail("Error instantiating createOnEvent component with name " + componentName + " of parent ", that, " since event specification " +
5638 eventName + " could not be expanded to an event - got ", event);
5639 }
5640 event.addListener(function () {
5641 fluid.pushActivity("initDeferred", "instantiating deferred component %componentName of parent %that due to event %eventName",
5642 {componentName: componentName, that: that, eventName: eventName});
5643 if (that[componentName]) {
5644 fluid.globalInstantiator.clearComponent(that, componentName);
5645 }
5646 var localRecord = {"arguments": fluid.makeArray(arguments)};
5647 fluid.initDependent(that, componentName, localRecord);
5648 fluid.popActivity();
5649 }, null, component.priority);
5650 });
5651 };
5652
5653 fluid.priorityForComponent = function (component) {
5654 return component.priority ? component.priority :
5655 (component.type === "fluid.typeFount" || fluid.hasGrade(fluid.defaults(component.type), "fluid.typeFount")) ?
5656 "first" : undefined;
5657 };
5658
5659 fluid.initDependents = function (that) {
5660 fluid.pushActivity("initDependents", "instantiating dependent components for component %that", {that: that});
5661 var shadow = fluid.shadowForComponent(that);
5662 shadow.memberStrategy.initter();
5663 shadow.invokerStrategy.initter();
5664
5665 fluid.getForComponent(that, "modelRelay");
5666 fluid.getForComponent(that, "model"); // trigger this as late as possible - but must be before components so that child component has model on its onCreate
5667 if (fluid.isDestroyed(that)) {
5668 return; // Further fix for FLUID-5869 - if we managed to destroy ourselves through some bizarre model self-reaction, bail out here
5669 }
5670
5671 var options = that.options;
5672 var components = options.components || {};
5673 var componentSort = [];
5674
5675 fluid.each(components, function (component, name) {
5676 if (!component.createOnEvent) {
5677 var priority = fluid.priorityForComponent(component);
5678 componentSort.push({namespace: name, priority: fluid.parsePriority(priority)});
5679 }
5680 else {
5681 fluid.bindDeferredComponent(that, name, component);
5682 }
5683 });
5684 fluid.sortByPriority(componentSort);
5685 fluid.each(componentSort, function (entry) {
5686 fluid.initDependent(that, entry.namespace);
5687 });
5688 if (shadow.subcomponentLocal) {
5689 fluid.clear(shadow.subcomponentLocal); // still need repo for event-driven dynamic components - abolish these in time
5690 }
5691 that.lifecycleStatus = "constructed";
5692 fluid.assessTreeConstruction(that, shadow);
5693
5694 fluid.popActivity();
5695 };
5696
5697 fluid.assessTreeConstruction = function (that, shadow) {
5698 var instantiator = fluid.globalInstantiator;
5699 var thatStack = instantiator.getThatStack(that);
5700 var unstableUp = fluid.find_if(thatStack, function (that) {
5701 return that.lifecycleStatus === "constructing";
5702 });
5703 if (unstableUp) {
5704 that.lifecycleStatus = "constructed";
5705 } else {
5706 fluid.markSubtree(instantiator, that, shadow.path, "treeConstructed");
5707 }
5708 };
5709
5710 fluid.markSubtree = function (instantiator, that, path, state) {
5711 that.lifecycleStatus = state;
5712 fluid.visitComponentChildren(that, function (child, name) {
5713 var childPath = instantiator.composePath(path, name);
5714 var childShadow = instantiator.idToShadow[child.id];
5715 var created = childShadow && childShadow.path === childPath;
5716 if (created) {
5717 fluid.markSubtree(instantiator, child, childPath, state);
5718 }
5719 }, {flat: true});
5720 };
5721
5722
5723 /* == BEGIN NEXUS METHODS == */
5724
5725 /**
5726 * Given a component reference, returns the path of that component within its component tree.
5727 *
5728 * @param {Component} component - A reference to a component.
5729 * @param {Instantiator} [instantiator] - (optional) An instantiator to use for the lookup.
5730 * @return {String[]} An array of {String} path segments of the component within its tree, or `null` if the reference does not hold a live component.
5731 */
5732 fluid.pathForComponent = function (component, instantiator) {
5733 instantiator = instantiator || fluid.getInstantiator(component) || fluid.globalInstantiator;
5734 var shadow = instantiator.idToShadow[component.id];
5735 if (!shadow) {
5736 return null;
5737 }
5738 return instantiator.parseEL(shadow.path);
5739 };
5740
5741 /** Construct a component with the supplied options at the specified path in the component tree. The parent path of the location must already be a component.
5742 * @param {String|String[]} path - Path where the new component is to be constructed, represented as a string or array of string segments
5743 * @param {Object} options - Top-level options supplied to the component - must at the very least include a field <code>type</code> holding the component's type
5744 * @param {Instantiator} [instantiator] - [optional] The instantiator holding the component to be created - if blank, the global instantiator will be used
5745 * @return {Object} The constructed component.
5746 */
5747 fluid.construct = function (path, options, instantiator) {
5748 var record = fluid.destroy(path, instantiator);
5749 // TODO: We must construct a more principled scheme for designating child components than this - especially once options become immutable
5750 fluid.set(record.parent, ["options", "components", record.memberName], {
5751 type: options.type,
5752 options: options
5753 });
5754 return fluid.initDependent(record.parent, record.memberName);
5755 };
5756
5757 /** Destroys a component held at the specified path. The parent path must represent a component, although the component itself may be nonexistent
5758 * @param {String|String[]} path - Path where the new component is to be destroyed, represented as a string or array of string segments
5759 * @param {Instantiator} [instantiator] - [optional] The instantiator holding the component to be destroyed - if blank, the global instantiator will be used.
5760 * @return {Object} - An object containing a reference to the parent of the destroyed element, and the member name of the destroyed component.
5761 */
5762 fluid.destroy = function (path, instantiator) {
5763 instantiator = instantiator || fluid.globalInstantiator;
5764 var segs = fluid.model.parseToSegments(path, instantiator.parseEL, true);
5765 if (segs.length === 0) {
5766 fluid.fail("Cannot destroy the root component");
5767 }
5768 var memberName = segs.pop(), parentPath = instantiator.composeSegments.apply(null, segs);
5769 var parent = instantiator.pathToComponent[parentPath];
5770 if (!parent) {
5771 fluid.fail("Cannot modify component with nonexistent parent at path ", path);
5772 }
5773 if (parent[memberName]) {
5774 parent[memberName].destroy();
5775 }
5776 return {
5777 parent: parent,
5778 memberName: memberName
5779 };
5780 };
5781
5782 /** Construct an instance of a component as a child of the specified parent, with a well-known, unique name derived from its typeName
5783 * @param {String|String[]} parentPath - Parent of path where the new component is to be constructed, represented as a {String} or array of {String} segments
5784 * @param {String|Object} options - Options encoding the component to be constructed. If this is of type String, it is assumed to represent the component's typeName with no options
5785 * @param {Instantiator} [instantiator] - [optional] The instantiator holding the component to be created - if blank, the global instantiator will be used
5786 */
5787 fluid.constructSingle = function (parentPath, options, instantiator) {
5788 instantiator = instantiator || fluid.globalInstantiator;
5789 parentPath = parentPath || "";
5790 var segs = fluid.model.parseToSegments(parentPath, instantiator.parseEL, true);
5791 if (typeof(options) === "string") {
5792 options = {type: options};
5793 }
5794 var type = options.type;
5795 if (!type) {
5796 fluid.fail("Cannot construct singleton object without a type entry");
5797 }
5798 options = $.extend({}, options);
5799 var gradeNames = options.gradeNames = fluid.makeArray(options.gradeNames);
5800 gradeNames.unshift(type); // principal type may be noninstantiable
5801 options.type = "fluid.component";
5802 var root = segs.length === 0;
5803 if (root) {
5804 gradeNames.push("fluid.resolveRoot");
5805 }
5806 var memberName = fluid.typeNameToMemberName(options.singleRootType || type);
5807 segs.push(memberName);
5808 fluid.construct(segs, options, instantiator);
5809 };
5810
5811 /** Destroy an instance created by `fluid.constructSingle`
5812 * @param {String|String[]} parentPath - Parent of path where the new component is to be constructed, represented as a {String} or array of {String} segments
5813 * @param {String} typeName - The type name used to construct the component (either `type` or `singleRootType` of the `options` argument to `fluid.constructSingle`
5814 * @param {Instantiator} [instantiator] - [optional] The instantiator holding the component to be created - if blank, the global instantiator will be used
5815 */
5816 fluid.destroySingle = function (parentPath, typeName, instantiator) {
5817 instantiator = instantiator || fluid.globalInstantiator;
5818 var segs = fluid.model.parseToSegments(parentPath, instantiator.parseEL, true);
5819 var memberName = fluid.typeNameToMemberName(typeName);
5820 segs.push(memberName);
5821 fluid.destroy(segs, instantiator);
5822 };
5823
5824 /** Registers and constructs a "linkage distribution" which will ensure that wherever a set of "input grades" co-occur, they will
5825 * always result in a supplied "output grades" in the component where they co-occur.
5826 * @param {String} linkageName - The name of the grade which will broadcast the resulting linkage. If required, this linkage can be destroyed by supplying this name to `fluid.destroySingle`.
5827 * @param {String[]} inputNames - An array of grade names which will be tested globally for co-occurrence
5828 * @param {String|String[]} outputNames - A single grade name or array of grade names which will be output into the co-occuring component
5829 */
5830 fluid.makeGradeLinkage = function (linkageName, inputNames, outputNames) {
5831 fluid.defaults(linkageName, {
5832 gradeNames: "fluid.component",
5833 distributeOptions: {
5834 record: outputNames,
5835 target: "{/ " + inputNames.join("&") + "}.options.gradeNames"
5836 }
5837 });
5838 fluid.constructSingle([], linkageName);
5839 };
5840
5841 /** Retrieves a component by global path.
5842 * @param {String|String[]} path - The global path of the component to look up, expressed as a string or as an array of segments.
5843 * @return {Object} - The component at the specified path, or undefined if none is found.
5844 */
5845 fluid.componentForPath = function (path) {
5846 return fluid.globalInstantiator.pathToComponent[fluid.isArrayable(path) ? path.join(".") : path];
5847 };
5848
5849 /** END NEXUS METHODS **/
5850
5851 /** BEGIN IOC DEBUGGING METHODS **/
5852 fluid["debugger"] = function () {
5853 debugger; // eslint-disable-line no-debugger
5854 };
5855
5856 fluid.defaults("fluid.debuggingProbe", {
5857 gradeNames: ["fluid.component"]
5858 });
5859
5860 // probe looks like:
5861 // target: {preview other}.listeners.eventName
5862 // priority: first/last
5863 // func: console.log/fluid.log/fluid.debugger
5864 fluid.probeToDistribution = function (probe) {
5865 var instantiator = fluid.globalInstantiator;
5866 var parsed = fluid.parseContextReference(probe.target);
5867 var segs = fluid.model.parseToSegments(parsed.path, instantiator.parseEL, true);
5868 if (segs[0] !== "options") {
5869 segs.unshift("options"); // compensate for this insanity until we have the great options flattening
5870 }
5871 var parsedPriority = fluid.parsePriority(probe.priority);
5872 if (parsedPriority.constraint && !parsedPriority.constraint.target) {
5873 parsedPriority.constraint.target = "authoring";
5874 }
5875 return {
5876 target: "{/ " + parsed.context + "}." + instantiator.composeSegments.apply(null, segs),
5877 record: {
5878 func: probe.func,
5879 funcName: probe.funcName,
5880 args: probe.args,
5881 priority: fluid.renderPriority(parsedPriority)
5882 }
5883 };
5884 };
5885
5886 fluid.registerProbes = function (probes) {
5887 var probeDistribution = fluid.transform(probes, fluid.probeToDistribution);
5888 var memberName = "fluid_debuggingProbe_" + fluid.allocateGuid();
5889 fluid.construct([memberName], {
5890 type: "fluid.debuggingProbe",
5891 distributeOptions: probeDistribution
5892 });
5893 return memberName;
5894 };
5895
5896 fluid.deregisterProbes = function (probeName) {
5897 fluid.destroy([probeName]);
5898 };
5899
5900 /** END IOC DEBUGGING METHODS **/
5901
5902 fluid.thisistToApplicable = function (record, recthis, that) {
5903 return {
5904 apply: function (noThis, args) {
5905 // Resolve this material late, to deal with cases where the target has only just been brought into existence
5906 // (e.g. a jQuery target for rendered material) - TODO: Possibly implement cached versions of these as we might do for invokers
5907 var resolvedThis = fluid.expandOptions(recthis, that);
5908 if (typeof(resolvedThis) === "string") {
5909 resolvedThis = fluid.getGlobalValue(resolvedThis);
5910 }
5911 if (!resolvedThis) {
5912 fluid.fail("Could not resolve reference " + recthis + " to a value");
5913 }
5914 var resolvedFunc = resolvedThis[record.method];
5915 if (typeof(resolvedFunc) !== "function") {
5916 fluid.fail("Object ", resolvedThis, " at reference " + recthis + " has no member named " + record.method + " which is a function ");
5917 }
5918 if (fluid.passLogLevel(fluid.logLevel.TRACE)) {
5919 fluid.log(fluid.logLevel.TRACE, "Applying arguments ", args, " to method " + record.method + " of instance ", resolvedThis);
5920 }
5921 return resolvedFunc.apply(resolvedThis, args);
5922 }
5923 };
5924 };
5925
5926 fluid.changeToApplicable = function (record, that) {
5927 return {
5928 apply: function (noThis, args, localRecord, mergeRecord) {
5929 var parsed = fluid.parseValidModelReference(that, "changePath listener record", record.changePath);
5930 var value = fluid.expandOptions(record.value, that, {}, fluid.extend(localRecord, {"arguments": args}));
5931 var sources = mergeRecord && mergeRecord.source && mergeRecord.source.length ? fluid.makeArray(record.source).concat(mergeRecord.source) : record.source;
5932 parsed.applier.change(parsed.modelSegs, value, record.type, sources); // FLUID-5586 now resolved
5933 }
5934 };
5935 };
5936
5937 // Convert "exotic records" into an applicable form ("this/method" for FLUID-4878 or "changePath" for FLUID-3674)
5938 fluid.recordToApplicable = function (record, that, standard) {
5939 if (record.changePath !== undefined) { // Allow falsy paths for FLUID-5586
5940 return fluid.changeToApplicable(record, that, standard);
5941 }
5942 var recthis = record["this"];
5943 if (record.method ^ recthis) {
5944 fluid.fail("Record ", that, " must contain both entries \"method\" and \"this\" if it contains either");
5945 }
5946 return record.method ? fluid.thisistToApplicable(record, recthis, that) : null;
5947 };
5948
5949 fluid.getGlobalValueNonComponent = function (funcName, context) { // TODO: Guard this in listeners as well
5950 var defaults = fluid.defaults(funcName);
5951 if (defaults && fluid.hasGrade(defaults, "fluid.component")) {
5952 fluid.fail("Error in function specification - cannot invoke function " + funcName + " in the context of " + context + ": component creator functions can only be used as subcomponents");
5953 }
5954 return fluid.getGlobalValue(funcName);
5955 };
5956
5957 fluid.makeInvoker = function (that, invokerec, name) {
5958 invokerec = fluid.upgradePrimitiveFunc(invokerec); // shorthand case for direct function invokers (FLUID-4926)
5959 if (invokerec.args !== undefined && invokerec.args !== fluid.NO_VALUE && !fluid.isArrayable(invokerec.args)) {
5960 invokerec.args = fluid.makeArray(invokerec.args);
5961 }
5962 var func = fluid.recordToApplicable(invokerec, that);
5963 var invokePre = fluid.preExpand(invokerec.args);
5964 var localRecord = {};
5965 var expandOptions = fluid.makeStackResolverOptions(that, localRecord, true);
5966 func = func || (invokerec.funcName ? fluid.getGlobalValueNonComponent(invokerec.funcName, "an invoker") : fluid.expandImmediate(invokerec.func, that));
5967 if (!func || !func.apply) {
5968 fluid.fail("Error in invoker record: could not resolve members func, funcName or method to a function implementation - got " + func + " from ", invokerec);
5969 } else if (func === fluid.notImplemented) {
5970 fluid.fail("Error constructing component ", that, " - the invoker named " + name + " which was defined in grade " + invokerec.componentSource + " needs to be overridden with a concrete implementation");
5971 }
5972 return function invokeInvoker() {
5973 if (fluid.defeatLogging === false) {
5974 fluid.pushActivity("invokeInvoker", "invoking invoker with name %name and record %record from path %path holding component %that",
5975 {name: name, record: invokerec, path: fluid.dumpComponentPath(that), that: that});
5976 }
5977 var togo, finalArgs;
5978 if (that.lifecycleStatus === "destroyed") {
5979 fluid.log(fluid.logLevel.WARN, "Ignoring call to invoker " + name + " of component ", that, " which has been destroyed");
5980 } else {
5981 localRecord.arguments = arguments;
5982 if (invokerec.args === undefined || invokerec.args === fluid.NO_VALUE) {
5983 finalArgs = arguments;
5984 } else {
5985 fluid.expandImmediateImpl(invokePre, expandOptions);
5986 finalArgs = invokePre.source;
5987 }
5988 togo = func.apply(null, finalArgs);
5989 }
5990 if (fluid.defeatLogging === false) {
5991 fluid.popActivity();
5992 }
5993 return togo;
5994 };
5995 };
5996
5997 // weird higher-order function so that we can staightforwardly dispatch original args back onto listener
5998 fluid.event.makeTrackedListenerAdder = function (source) {
5999 var shadow = fluid.shadowForComponent(source);
6000 return function (event) {
6001 return {addListener: function (listener, namespace, priority, softNamespace, listenerId) {
6002 fluid.recordListener(event, listener, shadow, listenerId);
6003 event.addListener.apply(null, arguments);
6004 }};
6005 };
6006 };
6007
6008 fluid.event.listenerEngine = function (eventSpec, callback, adder) {
6009 var argstruc = {};
6010 function checkFire() {
6011 var notall = fluid.find(eventSpec, function (value, key) {
6012 if (argstruc[key] === undefined) {
6013 return true;
6014 }
6015 });
6016 if (!notall) {
6017 var oldstruc = argstruc;
6018 argstruc = {}; // guard against the case the callback perversely fires one of its prerequisites (FLUID-5112)
6019 callback(oldstruc);
6020 }
6021 }
6022 fluid.each(eventSpec, function (event, eventName) {
6023 adder(event).addListener(function () {
6024 argstruc[eventName] = fluid.makeArray(arguments);
6025 checkFire();
6026 });
6027 });
6028 };
6029
6030 fluid.event.dispatchListener = function (that, listener, eventName, eventSpec, wrappedArgs) {
6031 if (eventSpec.args !== undefined && eventSpec.args !== fluid.NO_VALUE && !fluid.isArrayable(eventSpec.args)) {
6032 eventSpec.args = fluid.makeArray(eventSpec.args);
6033 }
6034 listener = fluid.event.resolveListener(listener); // In theory this optimisation is too aggressive if global name is not defined yet
6035 var dispatchPre = fluid.preExpand(eventSpec.args);
6036 var localRecord = {};
6037 var expandOptions = fluid.makeStackResolverOptions(that, localRecord, true);
6038 var togo = function () {
6039 if (fluid.defeatLogging === false) {
6040 fluid.pushActivity("dispatchListener", "firing to listener to event named %eventName of component %that",
6041 {eventName: eventName, that: that});
6042 }
6043
6044 var args = wrappedArgs ? arguments[0] : arguments, finalArgs;
6045 localRecord.arguments = args;
6046 if (eventSpec.args !== undefined && eventSpec.args !== fluid.NO_VALUE) {
6047 // In theory something more exotic happens here, and in makeInvoker - where "source" is an array we want to
6048 // keep its base reference stable since Function.apply will fork it sufficiently, but we really need to
6049 // clone each structured argument. Implies that expandImmediateImpl needs to be split in two, and operate
6050 // reference by "segs" rather than by "holder"
6051 fluid.expandImmediateImpl(dispatchPre, expandOptions);
6052 finalArgs = dispatchPre.source;
6053 } else {
6054 finalArgs = args;
6055 }
6056 var togo = listener.apply(null, finalArgs);
6057 if (fluid.defeatLogging === false) {
6058 fluid.popActivity();
6059 }
6060 return togo;
6061 };
6062 fluid.event.impersonateListener(listener, togo); // still necessary for FLUID-5254 even though framework's listeners now get explicit guids
6063 return togo;
6064 };
6065
6066 fluid.event.resolveSoftNamespace = function (key) {
6067 if (typeof(key) !== "string") {
6068 return null;
6069 } else {
6070 var lastpos = Math.max(key.lastIndexOf("."), key.lastIndexOf("}"));
6071 return key.substring(lastpos + 1);
6072 }
6073 };
6074
6075 fluid.event.resolveListenerRecord = function (lisrec, that, eventName, namespace, standard) {
6076 var badRec = function (record, extra) {
6077 fluid.fail("Error in listener record - could not resolve reference ", record, " to a listener or firer. " +
6078 "Did you miss out \"events.\" when referring to an event firer?" + extra);
6079 };
6080 fluid.pushActivity("resolveListenerRecord", "resolving listener record for event named %eventName for component %that",
6081 {eventName: eventName, that: that});
6082 var records = fluid.makeArray(lisrec);
6083 var transRecs = fluid.transform(records, function (record) {
6084 // TODO: FLUID-5242 fix - we copy here since distributeOptions does not copy options blocks that it distributes and we can hence corrupt them.
6085 // need to clarify policy on options sharing - for slightly better efficiency, copy should happen during distribution and not here
6086 // Note that fluid.mergeModelListeners expects to write to these too
6087 var expanded = fluid.isPrimitive(record) || record.expander ? {listener: record} : fluid.copy(record);
6088 var methodist = fluid.recordToApplicable(record, that, standard);
6089 if (methodist) {
6090 expanded.listener = methodist;
6091 }
6092 else {
6093 expanded.listener = expanded.listener || expanded.func || expanded.funcName;
6094 }
6095 if (!expanded.listener) {
6096 badRec(record, " Listener record must contain a member named \"listener\", \"func\", \"funcName\" or \"method\"");
6097 }
6098 var softNamespace = record.method ?
6099 fluid.event.resolveSoftNamespace(record["this"]) + "." + record.method :
6100 fluid.event.resolveSoftNamespace(expanded.listener);
6101 if (!expanded.namespace && !namespace && softNamespace) {
6102 expanded.softNamespace = true;
6103 expanded.namespace = (record.componentSource ? record.componentSource : that.typeName) + "." + softNamespace;
6104 }
6105 var listener = expanded.listener = fluid.expandOptions(expanded.listener, that);
6106 if (!listener) {
6107 badRec(record, "");
6108 }
6109 var firer = false;
6110 if (listener.typeName === "fluid.event.firer") {
6111 listener = listener.fire;
6112 firer = true;
6113 }
6114 expanded.listener = (standard && (expanded.args && listener !== "fluid.notImplemented" || firer)) ? fluid.event.dispatchListener(that, listener, eventName, expanded) : listener;
6115 expanded.listenerId = fluid.allocateGuid();
6116 return expanded;
6117 });
6118 var togo = {
6119 records: transRecs,
6120 adderWrapper: standard ? fluid.event.makeTrackedListenerAdder(that) : null
6121 };
6122 fluid.popActivity();
6123 return togo;
6124 };
6125
6126 fluid.event.expandOneEvent = function (that, event) {
6127 var origin;
6128 if (typeof(event) === "string" && event.charAt(0) !== "{") {
6129 // Shorthand for resolving onto our own events, but with GINGER WORLD!
6130 origin = fluid.getForComponent(that, ["events", event]);
6131 }
6132 else {
6133 origin = fluid.expandOptions(event, that);
6134 }
6135 if (!origin || origin.typeName !== "fluid.event.firer") {
6136 fluid.fail("Error in event specification - could not resolve base event reference ", event, " to an event firer: got ", origin);
6137 }
6138 return origin;
6139 };
6140
6141 fluid.event.expandEvents = function (that, event) {
6142 return typeof(event) === "string" ?
6143 fluid.event.expandOneEvent(that, event) :
6144 fluid.transform(event, function (oneEvent) {
6145 return fluid.event.expandOneEvent(that, oneEvent);
6146 });
6147 };
6148
6149 fluid.event.resolveEvent = function (that, eventName, eventSpec) {
6150 fluid.pushActivity("resolveEvent", "resolving event with name %eventName attached to component %that",
6151 {eventName: eventName, that: that});
6152 var adder = fluid.event.makeTrackedListenerAdder(that);
6153 if (typeof(eventSpec) === "string") {
6154 eventSpec = {event: eventSpec};
6155 }
6156 var event = eventSpec.typeName === "fluid.event.firer" ? eventSpec : eventSpec.event || eventSpec.events;
6157 if (!event) {
6158 fluid.fail("Event specification for event with name " + eventName + " does not include a base event specification: ", eventSpec);
6159 }
6160
6161 var origin = event.typeName === "fluid.event.firer" ? event : fluid.event.expandEvents(that, event);
6162
6163 var isMultiple = origin.typeName !== "fluid.event.firer";
6164 var isComposite = eventSpec.args || isMultiple;
6165 // If "event" is not composite, we want to share the listener list and FIRE method with the original
6166 // If "event" is composite, we need to create a new firer. "composite" includes case where any boiling
6167 // occurred - this was implemented wrongly in 1.4.
6168 var firer;
6169 if (isComposite) {
6170 firer = fluid.makeEventFirer({name: " [composite] " + fluid.event.nameEvent(that, eventName)});
6171 var dispatcher = fluid.event.dispatchListener(that, firer.fire, eventName, eventSpec, isMultiple);
6172 if (isMultiple) {
6173 fluid.event.listenerEngine(origin, dispatcher, adder);
6174 }
6175 else {
6176 adder(origin).addListener(dispatcher);
6177 }
6178 }
6179 else {
6180 firer = {typeName: "fluid.event.firer"};
6181 firer.fire = function () {
6182 var outerArgs = fluid.makeArray(arguments);
6183 fluid.pushActivity("fireSynthetic", "firing synthetic event %eventName ", {eventName: eventName});
6184 var togo = origin.fire.apply(null, outerArgs);
6185 fluid.popActivity();
6186 return togo;
6187 };
6188 firer.addListener = function (listener, namespace, priority, softNamespace, listenerId) {
6189 var dispatcher = fluid.event.dispatchListener(that, listener, eventName, eventSpec);
6190 adder(origin).addListener(dispatcher, namespace, priority, softNamespace, listenerId);
6191 };
6192 firer.removeListener = function (listener) {
6193 origin.removeListener(listener);
6194 };
6195 // To allow introspection on listeners in cases such as fluid.test.findListenerId
6196 firer.originEvent = origin;
6197 }
6198 fluid.popActivity();
6199 return firer;
6200 };
6201
6202 /** BEGIN unofficial IoC material **/
6203 // The following three functions are unsupported ane only used in the renderer expander.
6204 // The material they produce is no longer recognised for component resolution.
6205
6206 fluid.withEnvironment = function (envAdd, func, root) {
6207 var key;
6208 root = root || fluid.globalThreadLocal();
6209 try {
6210 for (key in envAdd) {
6211 root[key] = envAdd[key];
6212 }
6213 $.extend(root, envAdd);
6214 return func();
6215 } finally {
6216 for (key in envAdd) {
6217 delete root[key]; // TODO: users may want a recursive "scoping" model
6218 }
6219 }
6220 };
6221
6222 fluid.fetchContextReference = function (parsed, directModel, env, elResolver, externalFetcher) {
6223 // The "elResolver" is a hack to make certain common idioms in protoTrees work correctly, where a contextualised EL
6224 // path actually resolves onto a further EL reference rather than directly onto a value target
6225 if (elResolver) {
6226 parsed = elResolver(parsed, env);
6227 }
6228 var base = parsed.context ? env[parsed.context] : directModel;
6229 if (!base) {
6230 var resolveExternal = externalFetcher && externalFetcher(parsed);
6231 return resolveExternal || base;
6232 }
6233 return parsed.noDereference ? parsed.path : fluid.get(base, parsed.path);
6234 };
6235
6236 fluid.makeEnvironmentFetcher = function (directModel, elResolver, envGetter, externalFetcher) {
6237 envGetter = envGetter || fluid.globalThreadLocal;
6238 return function (parsed) {
6239 var env = envGetter();
6240 return fluid.fetchContextReference(parsed, directModel, env, elResolver, externalFetcher);
6241 };
6242 };
6243
6244 /** END of unofficial IoC material **/
6245
6246 /* Compact expansion machinery - for short form invoker and expander references such as @expand:func(arg) and func(arg) */
6247
6248 fluid.coerceToPrimitive = function (string) {
6249 return string === "false" ? false : (string === "true" ? true :
6250 (isFinite(string) ? Number(string) : string));
6251 };
6252
6253 fluid.compactStringToRec = function (string, type) {
6254 var openPos = string.indexOf("(");
6255 var closePos = string.indexOf(")");
6256 if (openPos === -1 ^ closePos === -1 || openPos > closePos) {
6257 fluid.fail("Badly-formed compact " + type + " record without matching parentheses: " + string);
6258 }
6259 if (openPos !== -1 && closePos !== -1) {
6260 var trail = string.substring(closePos + 1);
6261 if ($.trim(trail) !== "") {
6262 fluid.fail("Badly-formed compact " + type + " record " + string + " - unexpected material following close parenthesis: " + trail);
6263 }
6264 var prefix = string.substring(0, openPos);
6265 var body = $.trim(string.substring(openPos + 1, closePos));
6266 var args = body === "" ? [] : fluid.transform(body.split(","), $.trim, fluid.coerceToPrimitive);
6267 var togo = fluid.upgradePrimitiveFunc(prefix, null);
6268 togo.args = args;
6269 return togo;
6270 }
6271 else if (type === "expander") {
6272 fluid.fail("Badly-formed compact expander record without parentheses: " + string);
6273 }
6274 return string;
6275 };
6276
6277 fluid.expandPrefix = "@expand:";
6278
6279 fluid.expandCompactString = function (string, active) {
6280 var rec = string;
6281 if (string.indexOf(fluid.expandPrefix) === 0) {
6282 var rem = string.substring(fluid.expandPrefix.length);
6283 rec = {
6284 expander: fluid.compactStringToRec(rem, "expander")
6285 };
6286 }
6287 else if (active) {
6288 rec = fluid.compactStringToRec(string, active);
6289 }
6290 return rec;
6291 };
6292
6293 var singularPenRecord = {
6294 listeners: "listener",
6295 modelListeners: "modelListener"
6296 };
6297
6298 var singularRecord = $.extend({
6299 invokers: "invoker"
6300 }, singularPenRecord);
6301
6302 fluid.expandCompactRec = function (segs, target, source) {
6303 fluid.guardCircularExpansion(segs, segs.length);
6304 var pen = segs.length > 0 ? segs[segs.length - 1] : "";
6305 var active = singularRecord[pen];
6306 if (!active && segs.length > 1) {
6307 active = singularPenRecord[segs[segs.length - 2]]; // support array of listeners and modelListeners
6308 }
6309 fluid.each(source, function (value, key) {
6310 if (fluid.isPlainObject(value)) {
6311 target[key] = fluid.freshContainer(value);
6312 segs.push(key);
6313 fluid.expandCompactRec(segs, target[key], value);
6314 segs.pop();
6315 return;
6316 }
6317 else if (typeof(value) === "string") {
6318 value = fluid.expandCompactString(value, active);
6319 }
6320 target[key] = value;
6321 });
6322 };
6323
6324 fluid.expandCompact = function (options) {
6325 var togo = {};
6326 fluid.expandCompactRec([], togo, options);
6327 return togo;
6328 };
6329
6330 /** End compact record expansion machinery **/
6331
6332 fluid.extractEL = function (string, options) {
6333 if (options.ELstyle === "ALL") {
6334 return string;
6335 }
6336 else if (options.ELstyle.length === 1) {
6337 if (string.charAt(0) === options.ELstyle) {
6338 return string.substring(1);
6339 }
6340 }
6341 else if (options.ELstyle === "${}") {
6342 var i1 = string.indexOf("${");
6343 var i2 = string.lastIndexOf("}");
6344 if (i1 === 0 && i2 !== -1) {
6345 return string.substring(2, i2);
6346 }
6347 }
6348 };
6349
6350 fluid.extractELWithContext = function (string, options) {
6351 var EL = fluid.extractEL(string, options);
6352 if (fluid.isIoCReference(EL)) {
6353 return fluid.parseContextReference(EL);
6354 }
6355 return EL ? {path: EL} : EL;
6356 };
6357
6358 /** Parse the string form of a contextualised IoC reference into an object.
6359 * @param {String} reference - The reference to be parsed. The character at position `index` is assumed to be `{`
6360 * @param {String} [index] - [optional] The index into the string to start parsing at, if omitted, defaults to 0
6361 * @param {Character} [delimiter] - [optional] A character which will delimit the end of the context expression. If omitted, the expression continues to the end of the string.
6362 * @return {ParsedContext} A structure holding the parsed structure, with members
6363 * context {String|ParsedContext} The context portion of the reference. This will be a `string` for a flat reference, or a further `ParsedContext` for a recursive reference
6364 * path {String} The string portion of the reference
6365 * endpos {Integer} The position in the string where parsing stopped [this member is not supported and will be removed in a future release]
6366 */
6367 fluid.parseContextReference = function (reference, index, delimiter) {
6368 index = index || 0;
6369 var isNested = reference.charAt(index + 1) === "{", endcpos, context, nested;
6370 if (isNested) {
6371 nested = fluid.parseContextReference(reference, index + 1, "}");
6372 endcpos = nested.endpos;
6373 } else {
6374 endcpos = reference.indexOf("}", index + 1);
6375 }
6376 if (endcpos === -1) {
6377 fluid.fail("Cannot parse context reference \"" + reference + "\": Malformed context reference without }");
6378 }
6379 if (isNested) {
6380 context = nested;
6381 } else {
6382 context = reference.substring(index + 1, endcpos);
6383 }
6384 var endpos = delimiter ? reference.indexOf(delimiter, endcpos + 1) : reference.length;
6385 var path = reference.substring(endcpos + 1, endpos);
6386 if (path.charAt(0) === ".") {
6387 path = path.substring(1);
6388 }
6389 return {context: context, path: path, endpos: endpos};
6390 };
6391
6392 fluid.renderContextReference = function (parsed) {
6393 var context = parsed.context;
6394 return "{" + (typeof(context) === "string" ? context : fluid.renderContextReference(context)) + "}" + (parsed.path ? "." + parsed.path : "");
6395 };
6396
6397 // TODO: Once we eliminate expandSource (in favour of fluid.expander.fetch), all of this tree of functions can be hived off to RendererUtilities
6398 fluid.resolveContextValue = function (string, options) {
6399 function fetch(parsed) {
6400 fluid.pushActivity("resolveContextValue", "resolving context value %parsed", {parsed: parsed});
6401 var togo = options.fetcher(parsed);
6402 fluid.pushActivity("resolvedContextValue", "resolved value %parsed to value %value", {parsed: parsed, value: togo});
6403 fluid.popActivity(2);
6404 return togo;
6405 }
6406 var parsed;
6407 if (options.bareContextRefs && fluid.isIoCReference(string)) {
6408 parsed = fluid.parseContextReference(string);
6409 return fetch(parsed);
6410 }
6411 else if (options.ELstyle && options.ELstyle !== "${}") {
6412 parsed = fluid.extractELWithContext(string, options);
6413 if (parsed) {
6414 return fetch(parsed);
6415 }
6416 }
6417 while (typeof(string) === "string") {
6418 var i1 = string.indexOf("${");
6419 var i2 = string.indexOf("}", i1 + 2);
6420 if (i1 !== -1 && i2 !== -1) {
6421 if (string.charAt(i1 + 2) === "{") {
6422 parsed = fluid.parseContextReference(string, i1 + 2, "}");
6423 i2 = parsed.endpos;
6424 }
6425 else {
6426 parsed = {path: string.substring(i1 + 2, i2)};
6427 }
6428 var subs = fetch(parsed);
6429 var all = (i1 === 0 && i2 === string.length - 1);
6430 // TODO: test case for all undefined substitution
6431 if (subs === undefined || subs === null) {
6432 return subs;
6433 }
6434 string = all ? subs : string.substring(0, i1) + subs + string.substring(i2 + 1);
6435 }
6436 else {
6437 break;
6438 }
6439 }
6440 return string;
6441 };
6442
6443 // This function appears somewhat reusable, but not entirely - it probably needs to be packaged
6444 // along with the particular "strategy". Very similar to the old "filter"... the "outer driver" needs
6445 // to execute it to get the first recursion going at top level. This was one of the most odd results
6446 // of the reorganisation, since the "old work" seemed much more naturally expressed in terms of values
6447 // and what happened to them. The "new work" is expressed in terms of paths and how to move amongst them.
6448 fluid.fetchExpandChildren = function (target, i, segs, source, mergePolicy, options) {
6449 if (source.expander) { // possible expander at top level
6450 var expanded = fluid.expandExpander(target, source, options);
6451 if (fluid.isPrimitive(expanded) || !fluid.isPlainObject(expanded) || (fluid.isArrayable(expanded) ^ fluid.isArrayable(target))) {
6452 return expanded;
6453 }
6454 else { // make an attempt to preserve the root reference if possible
6455 $.extend(true, target, expanded);
6456 }
6457 }
6458 // NOTE! This expects that RHS is concrete! For material input to "expansion" this happens to be the case, but is not
6459 // true for other algorithms. Inconsistently, this algorithm uses "sourceStrategy" below. In fact, this "fetchChildren"
6460 // operation looks like it is a fundamental primitive of the system. We do call "deliverer" early which enables correct
6461 // reference to parent nodes up the tree - however, anyone processing a tree IN THE CHAIN requires that it is produced
6462 // concretely at the point STRATEGY returns. Which in fact it is...............
6463 fluid.each(source, function (newSource, key) {
6464 if (newSource === undefined) {
6465 target[key] = undefined; // avoid ever dispatching to ourselves with undefined source
6466 }
6467 else if (key !== "expander") {
6468 segs[i] = key;
6469 if (fluid.getImmediate(options.exceptions, segs, i) !== true) {
6470 options.strategy(target, key, i + 1, segs, source, mergePolicy);
6471 }
6472 }
6473 });
6474 return target;
6475 };
6476
6477 // TODO: This method is unnecessary and will quadratic inefficiency if RHS block is not concrete.
6478 // The driver should detect "homogeneous uni-strategy trundling" and agree to preserve the extra
6479 // "cursor arguments" which should be advertised somehow (at least their number)
6480 function regenerateCursor(source, segs, limit, sourceStrategy) {
6481 for (var i = 0; i < limit; ++i) {
6482 // copy segs to avoid aliasing with FLUID-5243
6483 source = sourceStrategy(source, segs[i], i, fluid.makeArray(segs));
6484 }
6485 return source;
6486 }
6487
6488 fluid.isUnexpandable = function (source) { // slightly more efficient compound of fluid.isCopyable and fluid.isComponent - review performance
6489 return fluid.isPrimitive(source) || !fluid.isPlainObject(source);
6490 };
6491
6492 fluid.expandSource = function (options, target, i, segs, deliverer, source, policy, recurse) {
6493 var expanded, isTrunk;
6494 var thisPolicy = fluid.derefMergePolicy(policy);
6495 if (typeof (source) === "string" && !thisPolicy.noexpand) {
6496 if (!options.defaultEL || source.charAt(0) === "{") { // hard-code this for performance
6497 fluid.pushActivity("expandContextValue", "expanding context value %source held at path %path", {source: source, path: fluid.path.apply(null, segs.slice(0, i))});
6498 expanded = fluid.resolveContextValue(source, options);
6499 fluid.popActivity(1);
6500 } else {
6501 expanded = source;
6502 }
6503 }
6504 else if (thisPolicy.noexpand || fluid.isUnexpandable(source)) {
6505 expanded = source;
6506 }
6507 else if (source.expander) {
6508 expanded = fluid.expandExpander(deliverer, source, options);
6509 }
6510 else {
6511 expanded = fluid.freshContainer(source);
6512 isTrunk = true;
6513 }
6514 if (expanded !== fluid.NO_VALUE) {
6515 deliverer(expanded);
6516 }
6517 if (isTrunk) {
6518 recurse(expanded, source, i, segs, policy);
6519 }
6520 return expanded;
6521 };
6522
6523 fluid.guardCircularExpansion = function (segs, i) {
6524 if (i > fluid.strategyRecursionBailout) {
6525 fluid.fail("Overflow/circularity in options expansion, current path is ", segs, " at depth " , i, " - please ensure options are not circularly connected, or protect from expansion using the \"noexpand\" policy or expander");
6526 }
6527 };
6528
6529 fluid.makeExpandStrategy = function (options) {
6530 var recurse = function (target, source, i, segs, policy) {
6531 return fluid.fetchExpandChildren(target, i || 0, segs || [], source, policy, options);
6532 };
6533 var strategy = function (target, name, i, segs, source, policy) {
6534 fluid.guardCircularExpansion(segs, i);
6535 if (!target) {
6536 return;
6537 }
6538 if (target.hasOwnProperty(name)) { // bail out if our work has already been done
6539 return target[name];
6540 }
6541 if (source === undefined) { // recover our state in case this is an external entry point
6542 source = regenerateCursor(options.source, segs, i - 1, options.sourceStrategy);
6543 policy = regenerateCursor(options.mergePolicy, segs, i - 1, fluid.concreteTrundler);
6544 }
6545 var thisSource = options.sourceStrategy(source, name, i, segs);
6546 var thisPolicy = fluid.concreteTrundler(policy, name);
6547 function deliverer(value) {
6548 target[name] = value;
6549 }
6550 return fluid.expandSource(options, target, i, segs, deliverer, thisSource, thisPolicy, recurse);
6551 };
6552 options.recurse = recurse;
6553 options.strategy = strategy;
6554 return strategy;
6555 };
6556
6557 fluid.defaults("fluid.makeExpandOptions", {
6558 ELstyle: "${}",
6559 bareContextRefs: true,
6560 target: fluid.inCreationMarker
6561 });
6562
6563 fluid.makeExpandOptions = function (source, options) {
6564 options = $.extend({}, fluid.rawDefaults("fluid.makeExpandOptions"), options);
6565 options.defaultEL = options.ELStyle === "${}" && options.bareContextRefs; // optimisation to help expander
6566 options.expandSource = function (source) {
6567 return fluid.expandSource(options, null, 0, [], fluid.identity, source, options.mergePolicy, false);
6568 };
6569 if (!fluid.isUnexpandable(source)) {
6570 options.source = source;
6571 options.target = fluid.freshContainer(source);
6572 options.sourceStrategy = options.sourceStrategy || fluid.concreteTrundler;
6573 fluid.makeExpandStrategy(options);
6574 options.initter = function () {
6575 options.target = fluid.fetchExpandChildren(options.target, 0, [], options.source, options.mergePolicy, options);
6576 };
6577 }
6578 else { // these init immediately since we must deliver a valid root target
6579 options.strategy = fluid.concreteTrundler;
6580 options.initter = fluid.identity;
6581 if (typeof(source) === "string") {
6582 // Copy is necessary to resolve FLUID-6213 since targets are regularly scrawled over with "undefined" by dim expansion pathway
6583 // However, we can't screw up object identity for uncloneable things like events resolved via local expansion
6584 options.target = (options.defer ? fluid.copy : fluid.identity)(options.expandSource(source));
6585 }
6586 else {
6587 options.target = source;
6588 }
6589 options.immutableTarget = true;
6590 }
6591 return options;
6592 };
6593
6594 // supported, PUBLIC API function
6595 fluid.expand = function (source, options) {
6596 var expandOptions = fluid.makeExpandOptions(source, options);
6597 expandOptions.initter();
6598 return expandOptions.target;
6599 };
6600
6601 fluid.preExpandRecurse = function (root, source, holder, member, rootSegs) { // on entry, holder[member] = source
6602 fluid.guardCircularExpansion(rootSegs, rootSegs.length);
6603 function pushExpander(expander) {
6604 root.expanders.push({expander: expander, holder: holder, member: member});
6605 delete holder[member];
6606 }
6607 if (fluid.isIoCReference(source)) {
6608 var parsed = fluid.parseContextReference(source);
6609 var segs = fluid.model.parseEL(parsed.path);
6610 pushExpander({
6611 typeFunc: fluid.expander.fetch,
6612 context: parsed.context,
6613 segs: segs
6614 });
6615 } else if (fluid.isPlainObject(source)) {
6616 if (source.expander) {
6617 source.expander.typeFunc = fluid.getGlobalValue(source.expander.type || "fluid.invokeFunc");
6618 pushExpander(source.expander);
6619 } else {
6620 fluid.each(source, function (value, key) {
6621 rootSegs.push(key);
6622 fluid.preExpandRecurse(root, value, source, key, rootSegs);
6623 rootSegs.pop();
6624 });
6625 }
6626 }
6627 };
6628
6629 fluid.preExpand = function (source) {
6630 var root = {
6631 expanders: [],
6632 source: fluid.isUnexpandable(source) ? source : fluid.copy(source)
6633 };
6634 fluid.preExpandRecurse(root, root.source, root, "source", []);
6635 return root;
6636 };
6637
6638 // Main pathway for freestanding material that is not part of a component's options
6639 fluid.expandImmediate = function (source, that, localRecord) {
6640 var options = fluid.makeStackResolverOptions(that, localRecord, true); // TODO: ELstyle and target are now ignored
6641 var root = fluid.preExpand(source);
6642 fluid.expandImmediateImpl(root, options);
6643 return root.source;
6644 };
6645
6646 // High performance expander for situations such as invokers, listeners, where raw materials can be cached - consumes "root" structure produced by preExpand
6647 fluid.expandImmediateImpl = function (root, options) {
6648 var expanders = root.expanders;
6649 for (var i = 0; i < expanders.length; ++i) {
6650 var expander = expanders[i];
6651 expander.holder[expander.member] = expander.expander.typeFunc(null, expander, options);
6652 }
6653 };
6654
6655 fluid.expandExpander = function (deliverer, source, options) {
6656 var expander = fluid.getGlobalValue(source.expander.type || "fluid.invokeFunc");
6657 if (!expander) {
6658 fluid.fail("Unknown expander with type " + source.expander.type);
6659 }
6660 return expander(deliverer, source, options);
6661 };
6662
6663 fluid.registerNamespace("fluid.expander");
6664
6665 // "deliverer" is null in the new (fast) pathway, this is a relic of the old "source expander" signature. It appears we can already globally remove this
6666 fluid.expander.fetch = function (deliverer, source, options) {
6667 var localRecord = options.localRecord, context = source.expander.context, segs = source.expander.segs;
6668 // TODO: Either type-check on context as string or else create fetchSlow
6669 var inLocal = localRecord[context] !== undefined;
6670 var contextStatus = options.contextThat.lifecycleStatus;
6671 // somewhat hack to anticipate "fits" for FLUID-4925 - we assume that if THIS component is in construction, its reference target might be too
6672 // if context is destroyed, we are most likely in an afterDestroy listener and so path records have been destroyed
6673 var fast = contextStatus === "treeConstructed" || contextStatus === "destroyed";
6674 var component = inLocal ? localRecord[context] : fluid.resolveContext(context, options.contextThat, fast);
6675 if (component) {
6676 var root = component;
6677 if (inLocal || component.lifecycleStatus !== "constructing") {
6678 for (var i = 0; i < segs.length; ++i) { // fast resolution of paths when no ginger process active
6679 root = root ? root[segs[i]] : undefined;
6680 }
6681 } else {
6682 root = fluid.getForComponent(component, segs);
6683 }
6684 if (root === undefined && !inLocal) { // last-ditch attempt to get exotic EL value from component
6685 root = fluid.getForComponent(component, segs);
6686 }
6687 return root;
6688 } else if (segs.length > 0) {
6689 fluid.triggerMismatchedPathError(source.expander, options.contextThat);
6690 }
6691 };
6692
6693 /* "light" expanders, starting with the default expander invokeFunc,
6694 which makes an arbitrary function call (after expanding arguments) and are then replaced in
6695 the configuration with the call results. These will probably be abolished and replaced with
6696 equivalent model transformation machinery */
6697
6698 // This one is now positioned as the "universal expander" - default if no type supplied
6699 fluid.invokeFunc = function (deliverer, source, options) {
6700 var expander = source.expander;
6701 var args = fluid.makeArray(expander.args);
6702 expander.args = args; // head off case where args is an EL reference which resolves to an array
6703 if (options.recurse) { // only available in the path from fluid.expandOptions - this will be abolished in the end
6704 args = options.recurse([], args);
6705 } else {
6706 expander = fluid.expandImmediate(expander, options.contextThat, options.localRecord);
6707 args = expander.args;
6708 }
6709 var funcEntry = expander.func || expander.funcName;
6710 var func = (options.expandSource ? options.expandSource(funcEntry) : funcEntry) || fluid.recordToApplicable(expander, options.contextThat);
6711 if (typeof(func) === "string") {
6712 func = fluid.getGlobalValue(func);
6713 }
6714 if (!func) {
6715 fluid.fail("Error in expander record ", expander, ": " + funcEntry + " could not be resolved to a function for component ", options.contextThat);
6716 }
6717 return func.apply(null, args);
6718 };
6719
6720 // The "noexpand" expander which simply unwraps one level of expansion and ceases.
6721 fluid.noexpand = function (deliverer, source) {
6722 return source.expander.value ? source.expander.value : source.expander.tree;
6723 };
6724
6725})(jQuery, fluid_3_0_0);
6726;
6727/*
6728Copyright The Infusion copyright holders
6729See the AUTHORS.md file at the top-level directory of this distribution and at
6730https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
6731
6732Licensed under the Educational Community License (ECL), Version 2.0 or the New
6733BSD license. You may not use this file except in compliance with one these
6734Licenses.
6735
6736You may obtain a copy of the ECL 2.0 License and BSD License at
6737https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
6738*/
6739
6740var fluid_3_0_0 = fluid_3_0_0 || {};
6741
6742(function ($, fluid) {
6743 "use strict";
6744
6745 /** NOTE: The contents of this file are by default NOT PART OF THE PUBLIC FLUID API unless explicitly annotated before the function **/
6746
6747 /** MODEL ACCESSOR ENGINE **/
6748
6749 /** Standard strategies for resolving path segments **/
6750
6751 fluid.model.makeEnvironmentStrategy = function (environment) {
6752 return function (root, segment, index) {
6753 return index === 0 && environment[segment] ?
6754 environment[segment] : undefined;
6755 };
6756 };
6757
6758 fluid.model.defaultCreatorStrategy = function (root, segment) {
6759 if (root[segment] === undefined) {
6760 root[segment] = {};
6761 return root[segment];
6762 }
6763 };
6764
6765 fluid.model.defaultFetchStrategy = function (root, segment) {
6766 return root[segment];
6767 };
6768
6769 fluid.model.funcResolverStrategy = function (root, segment) {
6770 if (root.resolvePathSegment) {
6771 return root.resolvePathSegment(segment);
6772 }
6773 };
6774
6775 fluid.model.traverseWithStrategy = function (root, segs, initPos, config, uncess) {
6776 var strategies = config.strategies;
6777 var limit = segs.length - uncess;
6778 for (var i = initPos; i < limit; ++i) {
6779 if (!root) {
6780 return root;
6781 }
6782 var accepted;
6783 for (var j = 0; j < strategies.length; ++j) {
6784 accepted = strategies[j](root, segs[i], i + 1, segs);
6785 if (accepted !== undefined) {
6786 break; // May now short-circuit with stateless strategies
6787 }
6788 }
6789 if (accepted === fluid.NO_VALUE) {
6790 accepted = undefined;
6791 }
6792 root = accepted;
6793 }
6794 return root;
6795 };
6796
6797 /* Returns both the value and the path of the value held at the supplied EL path */
6798 fluid.model.getValueAndSegments = function (root, EL, config, initSegs) {
6799 return fluid.model.accessWithStrategy(root, EL, fluid.NO_VALUE, config, initSegs, true);
6800 };
6801
6802 // Very lightweight remnant of trundler, only used in resolvers
6803 fluid.model.makeTrundler = function (config) {
6804 return function (valueSeg, EL) {
6805 return fluid.model.getValueAndSegments(valueSeg.root, EL, config, valueSeg.segs);
6806 };
6807 };
6808
6809 fluid.model.getWithStrategy = function (root, EL, config, initSegs) {
6810 return fluid.model.accessWithStrategy(root, EL, fluid.NO_VALUE, config, initSegs);
6811 };
6812
6813 fluid.model.setWithStrategy = function (root, EL, newValue, config, initSegs) {
6814 fluid.model.accessWithStrategy(root, EL, newValue, config, initSegs);
6815 };
6816
6817 fluid.model.accessWithStrategy = function (root, EL, newValue, config, initSegs, returnSegs) {
6818 // This function is written in this unfortunate style largely for efficiency reasons. In many cases
6819 // it should be capable of running with 0 allocations (EL is preparsed, initSegs is empty)
6820 if (!fluid.isPrimitive(EL) && !fluid.isArrayable(EL)) {
6821 var key = EL.type || "default";
6822 var resolver = config.resolvers[key];
6823 if (!resolver) {
6824 fluid.fail("Unable to find resolver of type " + key);
6825 }
6826 var trundler = fluid.model.makeTrundler(config); // very lightweight trundler for resolvers
6827 var valueSeg = {root: root, segs: initSegs};
6828 valueSeg = resolver(valueSeg, EL, trundler);
6829 if (EL.path && valueSeg) { // every resolver supports this piece of output resolution
6830 valueSeg = trundler(valueSeg, EL.path);
6831 }
6832 return returnSegs ? valueSeg : (valueSeg ? valueSeg.root : undefined);
6833 }
6834 else {
6835 return fluid.model.accessImpl(root, EL, newValue, config, initSegs, returnSegs, fluid.model.traverseWithStrategy);
6836 }
6837 };
6838
6839 // Implementation notes: The EL path manipulation utilities here are equivalents of the simpler ones
6840 // that are provided in Fluid.js and elsewhere - they apply escaping rules to parse characters .
6841 // as \. and \ as \\ - allowing us to process member names containing periods. These versions are mostly
6842 // in use within model machinery, whereas the cheaper versions based on String.split(".") are mostly used
6843 // within the IoC machinery.
6844 // Performance testing in early 2015 suggests that modern browsers now allow these to execute slightly faster
6845 // than the equivalent machinery written using complex regexps - therefore they will continue to be maintained
6846 // here. However, there is still a significant performance gap with respect to the performance of String.split(".")
6847 // especially on Chrome, so we will continue to insist that component member names do not contain a "." character
6848 // for the time being.
6849 // See http://jsperf.com/parsing-escaped-el for some experiments
6850
6851 fluid.registerNamespace("fluid.pathUtil");
6852
6853 fluid.pathUtil.getPathSegmentImpl = function (accept, path, i) {
6854 var segment = null;
6855 if (accept) {
6856 segment = "";
6857 }
6858 var escaped = false;
6859 var limit = path.length;
6860 for (; i < limit; ++i) {
6861 var c = path.charAt(i);
6862 if (!escaped) {
6863 if (c === ".") {
6864 break;
6865 }
6866 else if (c === "\\") {
6867 escaped = true;
6868 }
6869 else if (segment !== null) {
6870 segment += c;
6871 }
6872 }
6873 else {
6874 escaped = false;
6875 if (segment !== null) {
6876 segment += c;
6877 }
6878 }
6879 }
6880 if (segment !== null) {
6881 accept[0] = segment;
6882 }
6883 return i;
6884 };
6885
6886 var globalAccept = []; // TODO: reentrancy risk here. This holder is here to allow parseEL to make two returns without an allocation.
6887
6888 /* A version of fluid.model.parseEL that apples escaping rules - this allows path segments
6889 * to contain period characters . - characters "\" and "}" will also be escaped. WARNING -
6890 * this current implementation is EXTREMELY slow compared to fluid.model.parseEL and should
6891 * not be used in performance-sensitive applications */
6892 // supported, PUBLIC API function
6893 fluid.pathUtil.parseEL = function (path) {
6894 var togo = [];
6895 var index = 0;
6896 var limit = path.length;
6897 while (index < limit) {
6898 var firstdot = fluid.pathUtil.getPathSegmentImpl(globalAccept, path, index);
6899 togo.push(globalAccept[0]);
6900 index = firstdot + 1;
6901 }
6902 return togo;
6903 };
6904
6905 // supported, PUBLIC API function
6906 fluid.pathUtil.composeSegment = function (prefix, toappend) {
6907 toappend = toappend.toString();
6908 for (var i = 0; i < toappend.length; ++i) {
6909 var c = toappend.charAt(i);
6910 if (c === "." || c === "\\" || c === "}") {
6911 prefix += "\\";
6912 }
6913 prefix += c;
6914 }
6915 return prefix;
6916 };
6917
6918 /* Escapes a single path segment by replacing any character ".", "\" or "}" with itself prepended by \ */
6919 // supported, PUBLIC API function
6920 fluid.pathUtil.escapeSegment = function (segment) {
6921 return fluid.pathUtil.composeSegment("", segment);
6922 };
6923
6924 /*
6925 * Compose a prefix and suffix EL path, where the prefix is already escaped.
6926 * Prefix may be empty, but not null. The suffix will become escaped.
6927 */
6928 // supported, PUBLIC API function
6929 fluid.pathUtil.composePath = function (prefix, suffix) {
6930 if (prefix.length !== 0) {
6931 prefix += ".";
6932 }
6933 return fluid.pathUtil.composeSegment(prefix, suffix);
6934 };
6935
6936 /*
6937 * Compose a set of path segments supplied as arguments into an escaped EL expression. Escaped version
6938 * of fluid.model.composeSegments
6939 */
6940
6941 // supported, PUBLIC API function
6942 fluid.pathUtil.composeSegments = function () {
6943 var path = "";
6944 for (var i = 0; i < arguments.length; ++i) {
6945 path = fluid.pathUtil.composePath(path, arguments[i]);
6946 }
6947 return path;
6948 };
6949
6950 /* Helpful utility for use in resolvers - matches a path which has already been parsed into segments */
6951 fluid.pathUtil.matchSegments = function (toMatch, segs, start, end) {
6952 if (end - start !== toMatch.length) {
6953 return false;
6954 }
6955 for (var i = start; i < end; ++i) {
6956 if (segs[i] !== toMatch[i - start]) {
6957 return false;
6958 }
6959 }
6960 return true;
6961 };
6962
6963 fluid.model.unescapedParser = {
6964 parse: fluid.model.parseEL,
6965 compose: fluid.model.composeSegments
6966 };
6967
6968 // supported, PUBLIC API record
6969 fluid.model.defaultGetConfig = {
6970 parser: fluid.model.unescapedParser,
6971 strategies: [fluid.model.funcResolverStrategy, fluid.model.defaultFetchStrategy]
6972 };
6973
6974 // supported, PUBLIC API record
6975 fluid.model.defaultSetConfig = {
6976 parser: fluid.model.unescapedParser,
6977 strategies: [fluid.model.funcResolverStrategy, fluid.model.defaultFetchStrategy, fluid.model.defaultCreatorStrategy]
6978 };
6979
6980 fluid.model.escapedParser = {
6981 parse: fluid.pathUtil.parseEL,
6982 compose: fluid.pathUtil.composeSegments
6983 };
6984
6985 // supported, PUBLIC API record
6986 fluid.model.escapedGetConfig = {
6987 parser: fluid.model.escapedParser,
6988 strategies: [fluid.model.defaultFetchStrategy]
6989 };
6990
6991 // supported, PUBLIC API record
6992 fluid.model.escapedSetConfig = {
6993 parser: fluid.model.escapedParser,
6994 strategies: [fluid.model.defaultFetchStrategy, fluid.model.defaultCreatorStrategy]
6995 };
6996
6997 /** CONNECTED COMPONENTS AND TOPOLOGICAL SORTING **/
6998
6999 // Following "tarjan" at https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
7000
7001 /** Compute the strongly connected components of a graph, specified as a list of vertices and an accessor function.
7002 * Returns an array of arrays of strongly connected vertices, with each component in topologically sorted order.
7003 * @param {Vertex[]} vertices - An array of vertices of the graph to be processed. Each vertex object will be polluted
7004 * with three extra fields: `tarjanIndex`, `lowIndex` and `onStack`.
7005 * @param {Function} accessor - A function that returns the accessor vertex or vertices.
7006 * @return {Array.<Vertex[]>} - An array of arrays of vertices.
7007 */
7008 fluid.stronglyConnected = function (vertices, accessor) {
7009 var that = {
7010 stack: [],
7011 accessor: accessor,
7012 components: [],
7013 index: 0
7014 };
7015 vertices.forEach(function (vertex) {
7016 if (vertex.tarjanIndex === undefined) {
7017 fluid.stronglyConnectedOne(vertex, that);
7018 }
7019 });
7020 return that.components;
7021 };
7022
7023 // Perform one round of the Tarjan search algorithm using the state structure generated in fluid.stronglyConnected
7024 fluid.stronglyConnectedOne = function (vertex, that) {
7025 vertex.tarjanIndex = that.index;
7026 vertex.lowIndex = that.index;
7027 ++that.index;
7028 that.stack.push(vertex);
7029 vertex.onStack = true;
7030 var outEdges = that.accessor(vertex);
7031 outEdges.forEach(function (outVertex) {
7032 if (outVertex.tarjanIndex === undefined) {
7033 // Successor has not yet been visited; recurse on it
7034 fluid.stronglyConnectedOne(outVertex, that);
7035 vertex.lowIndex = Math.min(vertex.lowIndex, outVertex.lowIndex);
7036 } else if (outVertex.onStack) {
7037 // Successor is on the stack and hence in the current component
7038 vertex.lowIndex = Math.min(vertex.lowIndex, outVertex.tarjanIndex);
7039 }
7040 });
7041 // If vertex is a root node, pop the stack back as far as it and generate a component
7042 if (vertex.lowIndex === vertex.tarjanIndex) {
7043 var component = [], outVertex;
7044 do {
7045 outVertex = that.stack.pop();
7046 outVertex.onStack = false;
7047 component.push(outVertex);
7048 } while (outVertex !== vertex);
7049 that.components.push(component);
7050 }
7051 };
7052
7053 /** MODEL COMPONENT HIERARCHY AND RELAY SYSTEM **/
7054
7055 fluid.initRelayModel = function (that) {
7056 fluid.deenlistModelComponent(that);
7057 return that.model;
7058 };
7059
7060 // TODO: This utility compensates for our lack of control over "wave of explosions" initialisation - we may
7061 // catch a model when it is apparently "completely initialised" and that's the best we can do, since we have
7062 // missed its own initial transaction
7063
7064 fluid.isModelComplete = function (that) {
7065 return "model" in that && that.model !== fluid.inEvaluationMarker;
7066 };
7067
7068 // Enlist this model component as part of the "initial transaction" wave - note that "special transaction" init
7069 // is indexed by component, not by applier, and has special record type (complete + initModel), not transaction
7070 fluid.enlistModelComponent = function (that) {
7071 var instantiator = fluid.getInstantiator(that);
7072 var enlist = instantiator.modelTransactions.init[that.id];
7073 if (!enlist) {
7074 enlist = {
7075 that: that,
7076 applier: fluid.getForComponent(that, "applier"), // required for FLUID-5504 even though currently unused
7077 complete: fluid.isModelComplete(that)
7078 };
7079 instantiator.modelTransactions.init[that.id] = enlist;
7080 }
7081 return enlist;
7082 };
7083
7084 fluid.clearTransactions = function () {
7085 var instantiator = fluid.globalInstantiator;
7086 fluid.clear(instantiator.modelTransactions);
7087 instantiator.modelTransactions.init = {};
7088 };
7089
7090 fluid.failureEvent.addListener(fluid.clearTransactions, "clearTransactions", "before:fail");
7091
7092 // Utility to coordinate with our crude "oscillation prevention system" which limits each link to 2 updates (presumably
7093 // in opposite directions). In the case of the initial transaction, we need to reset the count given that genuine
7094 // changes are arising in the system with each new enlisted model. TODO: if we ever get users operating their own
7095 // transactions, think of a way to incorporate this into that workflow
7096 fluid.clearLinkCounts = function (transRec, relaysAlso) {
7097 // TODO: Separate this record out into different types of records (relays are already in their own area)
7098 fluid.each(transRec, function (value, key) {
7099 if (typeof(value) === "number") {
7100 transRec[key] = 0;
7101 } else if (relaysAlso && value.options && typeof(value.relayCount) === "number") {
7102 value.relayCount = 0;
7103 }
7104 });
7105 };
7106
7107 /** Compute relay dependency out arcs for a group of initialising components.
7108 * @param {Object} transacs - Hash of component id to local ChangeApplier transaction.
7109 * @param {Object} mrec - Hash of component id to enlisted component record.
7110 * @return {Object} - Hash of component id to list of enlisted component record.
7111 */
7112 fluid.computeInitialOutArcs = function (transacs, mrec) {
7113 return fluid.transform(mrec, function (recel, id) {
7114 var oneOutArcs = {};
7115 var listeners = recel.that.applier.listeners.sortedListeners;
7116 fluid.each(listeners, function (listener) {
7117 if (listener.isRelay && !fluid.isExcludedChangeSource(transacs[id], listener.cond)) {
7118 var targetId = listener.targetId;
7119 if (targetId !== id) {
7120 oneOutArcs[targetId] = true;
7121 }
7122 }
7123 });
7124 var oneOutArcList = Object.keys(oneOutArcs);
7125 var togo = oneOutArcList.map(function (id) {
7126 return mrec[id];
7127 });
7128 // No edge if the component is not enlisted - it will sort to the end via "completeOnInit"
7129 fluid.remove_if(togo, function (rec) {
7130 return rec === undefined;
7131 });
7132 return togo;
7133 });
7134 };
7135
7136 fluid.sortCompleteLast = function (reca, recb) {
7137 return (reca.completeOnInit ? 1 : 0) - (recb.completeOnInit ? 1 : 0);
7138 };
7139
7140
7141 /** Operate all coordinated transactions by bringing models to their respective initial values, and then commit them all
7142 * @param {Component} that - A representative component of the collection for which the initial transaction is to be operated
7143 * @param {Object} mrec - The global model transaction record for the init transaction. This is a hash indexed by component id
7144 * to a model transaction record, as registered in `fluid.enlistModelComponent`. This has members `that`, `applier`, `complete`.
7145 */
7146 fluid.operateInitialTransaction = function (that, mrec) {
7147 var transId = fluid.allocateGuid();
7148 var transRec = fluid.getModelTransactionRec(that, transId);
7149 var transac;
7150 var transacs = fluid.transform(mrec, function (recel) {
7151 transac = recel.that.applier.initiate(null, "init", transId);
7152 transRec[recel.that.applier.applierId] = {transaction: transac};
7153 return transac;
7154 });
7155 // TODO: This sort has very little effect in any current test (can be replaced by no-op - see FLUID-5339) - but
7156 // at least can't be performed in reverse order ("FLUID-3674 event coordination test" will fail) - need more cases
7157
7158 // Compute the graph of init transaction relays for FLUID-6234 - one day we will have to do better than this, since there
7159 // may be finer structure than per-component - it may be that each piece of model area participates in this relation
7160 // differently. But this will require even more ambitious work such as fragmenting all the initial model values along
7161 // these boundaries.
7162 var outArcs = fluid.computeInitialOutArcs(transacs, mrec);
7163 var arcAccessor = function (mrec) {
7164 return outArcs[mrec.that.id];
7165 };
7166 var recs = fluid.values(mrec);
7167 var components = fluid.stronglyConnected(recs, arcAccessor);
7168 var priorityIndex = 0;
7169 components.forEach(function (component) {
7170 component.forEach(function (recel) {
7171 recel.initPriority = recel.completeOnInit ? Math.Infinity : priorityIndex++;
7172 });
7173 });
7174
7175 recs.sort(function (reca, recb) {
7176 return reca.initPriority - recb.initPriority;
7177 });
7178 recs.forEach(function (recel) {
7179 var that = recel.that;
7180 var transac = transacs[that.id];
7181 if (recel.completeOnInit) {
7182 fluid.initModelEvent(that, that.applier, transac, that.applier.listeners.sortedListeners);
7183 } else {
7184 fluid.each(recel.initModels, function (initModel) {
7185 transac.fireChangeRequest({type: "ADD", segs: [], value: initModel});
7186 fluid.clearLinkCounts(transRec, true);
7187 });
7188 }
7189 var shadow = fluid.shadowForComponent(that);
7190 if (shadow) { // Fix for FLUID-5869 - the component may have been destroyed during its own init transaction
7191 shadow.modelComplete = true; // technically this is a little early, but this flag is only read in fluid.connectModelRelay
7192 }
7193 });
7194
7195 transac.commit(); // committing one representative transaction will commit them all
7196 };
7197
7198 // This modelComponent has now concluded initialisation - commit its initialisation transaction if it is the last such in the wave
7199 fluid.deenlistModelComponent = function (that) {
7200 var instantiator = fluid.getInstantiator(that);
7201 var mrec = instantiator.modelTransactions.init;
7202 if (!mrec[that.id]) { // avoid double evaluation through currently hacked "members" implementation
7203 return;
7204 }
7205 that.model = undefined; // Abuse of the ginger system - in fact it is "currently in evaluation" - we need to return a proper initial model value even if no init occurred yet
7206 mrec[that.id].complete = true; // flag means - "complete as in ready to participate in this transaction"
7207 var incomplete = fluid.find_if(mrec, function (recel) {
7208 return recel.complete !== true;
7209 });
7210 if (!incomplete) {
7211 try { // For FLUID-6195 ensure that exceptions during init relay don't leave the framework unusable
7212 fluid.operateInitialTransaction(that, mrec);
7213 } catch (e) {
7214 fluid.clearTransactions();
7215 throw e;
7216 }
7217 // NB: Don't call fluid.concludeTransaction since "init" is not a standard record - this occurs in commitRelays for the corresponding genuine record as usual
7218 instantiator.modelTransactions.init = {};
7219 }
7220 };
7221
7222 fluid.parseModelReference = function (that, ref) {
7223 var parsed = fluid.parseContextReference(ref);
7224 parsed.segs = that.applier.parseEL(parsed.path);
7225 return parsed;
7226 };
7227
7228 /** Given a string which may represent a reference into a model, parses it into a structure holding the coordinates for resolving the reference. It specially
7229 * detects "references into model material" by looking for the first path segment in the path reference which holds the value "model". Some of its workflow is bypassed
7230 * in the special case of a reference representing an implicit model relay. In this case, ref will definitely be a String, and if it does not refer to model material, rather than
7231 * raising an error, the return structure will include a field <code>nonModel: true</code>
7232 * @param {Component} that - The component holding the reference
7233 * @param {String} name - A human-readable string representing the type of block holding the reference - e.g. "modelListeners"
7234 * @param {String|ModelReference} ref - The model reference to be parsed. This may have already been partially parsed at the original site - that is, a ModelReference is a
7235 * structure containing
7236 * segs: {String[]} An array of model path segments to be dereferenced in the target component (will become `modelSegs` in the final return)
7237 * context: {String} An IoC reference to the component holding the model
7238 * @param {Boolean} implicitRelay - <code>true</code> if the reference was being resolved for an implicit model relay - that is,
7239 * whether it occured within the `model` block itself. In this case, references to non-model material are not a failure and will simply be resolved
7240 * (by the caller) onto their targets (as constants). Otherwise, this function will issue a failure on discovering a reference to non-model material.
7241 * @return {Object} - A structure holding:
7242 * that {Component} The component whose model is the target of the reference. This may end up being constructed as part of the act of resolving the reference
7243 * applier {Component} The changeApplier for the component <code>that</code>. This may end up being constructed as part of the act of resolving the reference
7244 * modelSegs {String[]} An array of path segments into the model of the component
7245 * path {String} the value of <code>modelSegs</code> encoded as an EL path (remove client uses of this in time)
7246 * nonModel {Boolean} Set if <code>implicitRelay</code> was true and the reference was not into a model (modelSegs/path will not be set in this case)
7247 * segs {String[]} Holds the full array of path segments found by parsing the original reference - only useful in <code>nonModel</code> case
7248 */
7249 fluid.parseValidModelReference = function (that, name, ref, implicitRelay) {
7250 var reject = function () {
7251 var failArgs = ["Error in " + name + ": ", ref].concat(fluid.makeArray(arguments));
7252 fluid.fail.apply(null, failArgs);
7253 };
7254 var rejectNonModel = function (value) {
7255 reject(" must be a reference to a component with a ChangeApplier (descended from fluid.modelComponent), instead got ", value);
7256 };
7257 var parsed; // resolve ref into context and modelSegs
7258 if (typeof(ref) === "string") {
7259 if (fluid.isIoCReference(ref)) {
7260 parsed = fluid.parseModelReference(that, ref);
7261 var modelPoint = parsed.segs.indexOf("model");
7262 if (modelPoint === -1) {
7263 if (implicitRelay) {
7264 parsed.nonModel = true;
7265 } else {
7266 reject(" must be a reference into a component model via a path including the segment \"model\"");
7267 }
7268 } else {
7269 parsed.modelSegs = parsed.segs.slice(modelPoint + 1);
7270 parsed.contextSegs = parsed.segs.slice(0, modelPoint);
7271 delete parsed.path;
7272 }
7273 } else {
7274 parsed = {
7275 path: ref,
7276 modelSegs: that.applier.parseEL(ref)
7277 };
7278 }
7279 } else {
7280 if (!fluid.isArrayable(ref.segs)) {
7281 reject(" must contain an entry \"segs\" holding path segments referring a model path within a component");
7282 }
7283 parsed = {
7284 context: ref.context,
7285 modelSegs: fluid.expandOptions(ref.segs, that)
7286 };
7287 }
7288 var contextTarget, target; // resolve target component, which defaults to "that"
7289 if (parsed.context) {
7290 contextTarget = fluid.resolveContext(parsed.context, that);
7291 if (!contextTarget) {
7292 reject(" context must be a reference to an existing component");
7293 }
7294 target = parsed.contextSegs ? fluid.getForComponent(contextTarget, parsed.contextSegs) : contextTarget;
7295 } else {
7296 target = that;
7297 }
7298 if (!parsed.nonModel) {
7299 if (!fluid.isComponent(target)) {
7300 rejectNonModel(target);
7301 }
7302 if (!target.applier) {
7303 fluid.getForComponent(target, ["applier"]);
7304 }
7305 if (!target.applier) {
7306 rejectNonModel(target);
7307 }
7308 }
7309 parsed.that = target;
7310 parsed.applier = target && target.applier;
7311 if (!parsed.path) { // ChangeToApplicable amongst others rely on this
7312 parsed.path = target && target.applier.composeSegments.apply(null, parsed.modelSegs);
7313 }
7314 return parsed;
7315 };
7316
7317 // Gets global record for a particular transaction id, allocating if necessary - looks up applier id to transaction,
7318 // as well as looking up source id (linkId in below) to count/true
7319 // Through poor implementation quality, not every access passes through this function - some look up instantiator.modelTransactions directly
7320 fluid.getModelTransactionRec = function (that, transId) {
7321 var instantiator = fluid.getInstantiator(that);
7322 if (!transId) {
7323 fluid.fail("Cannot get transaction record without transaction id");
7324 }
7325 if (!instantiator) {
7326 return null;
7327 }
7328 var transRec = instantiator.modelTransactions[transId];
7329 if (!transRec) {
7330 transRec = instantiator.modelTransactions[transId] = {
7331 relays: [], // sorted array of relay elements (also appear at top level index by transaction id)
7332 sources: {}, // hash of the global transaction sources (includes "init" but excludes "relay" and "local")
7333 externalChanges: {} // index by applierId to changePath to listener record
7334 };
7335 }
7336 return transRec;
7337 };
7338
7339 fluid.recordChangeListener = function (component, applier, sourceListener, listenerId) {
7340 var shadow = fluid.shadowForComponent(component);
7341 fluid.recordListener(applier.modelChanged, sourceListener, shadow, listenerId);
7342 };
7343
7344 /** Called when a relay listener registered using `fluid.registerDirectChangeRelay` enlists in a transaction. Opens a local
7345 * representative of this transaction on `targetApplier`, creates and stores a "transaction element" within the global transaction
7346 * record keyed by the target applier's id. The transaction element is also pushed onto the `relays` member of the global transaction record - they
7347 * will be sorted by priority here when changes are fired.
7348 * @param {TransactionRecord} transRec - The global record for the current ChangeApplier transaction as retrieved from `fluid.getModelTransactionRec`
7349 * @param {ChangeApplier} targetApplier - The ChangeApplier to which outgoing changes will be applied. A local representative of the transaction will be opened on this applier and returned.
7350 * @param {String} transId - The global id of this transaction
7351 * @param {Object} options - The `options` argument supplied to `fluid.registerDirectChangeRelay`. This will be stored in the returned transaction element
7352 * - note that only the member `update` is ever used in `fluid.model.updateRelays` - TODO: We should thin this out
7353 * @param {Object} npOptions - Namespace and priority options
7354 * namespace {String} [optional] The namespace attached to this relay definition
7355 * priority {String} [optional] The (unparsed) priority attached to this relay definition
7356 * @return {Object} A "transaction element" holding information relevant to this relay's enlistment in the current transaction. This includes fields:
7357 * transaction {Transaction} The local representative of this transaction created on `targetApplier`
7358 * relayCount {Integer} The number of times this relay has been activated in this transaction
7359 * namespace {String} [optional] Namespace for this relay definition
7360 * priority {Priority} The parsed priority definition for this relay
7361 */
7362 fluid.registerRelayTransaction = function (transRec, targetApplier, transId, options, npOptions) {
7363 var newTrans = targetApplier.initiate("relay", null, transId); // non-top-level transaction will defeat postCommit
7364 var transEl = transRec[targetApplier.applierId] = {transaction: newTrans, relayCount: 0, namespace: npOptions.namespace, priority: npOptions.priority, options: options};
7365 transEl.priority = fluid.parsePriority(transEl.priority, transRec.relays.length, false, "model relay");
7366 transRec.relays.push(transEl);
7367 return transEl;
7368 };
7369
7370 // Configure this parameter to tweak the number of relays the model will attempt per transaction before bailing out with an error
7371 fluid.relayRecursionBailout = 100;
7372
7373 // Used with various arg combinations from different sources. For standard "implicit relay" or fully lensed relay,
7374 // the first 4 args will be set, and "options" will be empty
7375
7376 // For a model-dependent relay, this will be used in two halves - firstly, all of the model
7377 // sources will bind to the relay transform document itself. In this case the argument "targetApplier" within "options" will be set.
7378 // In this case, the component known as "target" is really the source - it is a component reference discovered by parsing the
7379 // relay document.
7380
7381 // Secondly, the relay itself will schedule an invalidation (as if receiving change to "*" of its source - which may in most
7382 // cases actually be empty) and play through its transducer. "Source" component itself is never empty, since it is used for listener
7383 // degistration on destruction (check this is correct for external model relay). However, "sourceSegs" may be empty in the case
7384 // there is no "source" component registered for the link. This change is played in a "half-transactional" way - that is, we wait
7385 // for all other changes in the system to settle before playing the relay document, in order to minimise the chances of multiple
7386 // firing and corruption. This is done via the "preCommit" hook registered at top level in establishModelRelay. This listener
7387 // is transactional but it does not require the transaction to conclude in order to fire - it may be reused as many times as
7388 // required within the "overall" transaction whilst genuine (external) changes continue to arrive.
7389
7390 // TODO: Vast overcomplication and generation of closure garbage. SURELY we should be able to convert this into an externalised, arg-ist form
7391 /** Registers a listener operating one leg of a model relay relation, connecting the source and target. Called once or twice from `fluid.connectModelRelay` -
7392 * see the comment there for the three cases involved. Note that in its case iii)B) the applier to bind to is not the one attached to `target` but is instead
7393 * held in `options.targetApplier`.
7394 * @param {Object} target - The target component at the end of the relay.
7395 * @param {String[]} targetSegs - String segments representing the path in the target where outgoing changes are to be fired
7396 * @param {Component|null} source - The source component from where changes will be listened to. May be null if the change source is a relay document.
7397 * @param {String[]} sourceSegs - String segments representing the path in the source component's model at which changes will be listened to
7398 * @param {String} linkId - The unique id of this relay arc. This will be used as a key within the active transaction record to look up dynamic information about
7399 * activation of the link within that transaction (currently just an activation count)
7400 * @param {Function|null} transducer - A function which will be invoked when a change is to be relayed. This is one of the adapters constructed in "makeTransformPackage"
7401 * and is set in all cases other than iii)B) (collecting changes to contextualised relay). Note that this will have a member `cond` as returned from
7402 * `fluid.model.parseRelayCondition` encoding the condition whereby changes should be excluded from the transaction. The rule encoded by the condition
7403 * will be applied by the function within `transducer`.
7404 * @param {Object} options -
7405 * transactional {Boolean} `true` in case iii) - although this only represents `half-transactions`, `false` in others since these are resolved immediately with no granularity
7406 * targetApplier {ChangeApplier} [optional] in case iii)B) holds the applier for the contextualised relay document which outgoing changes should be applied to
7407 * sourceApplier {ChangeApplier} [optional] in case ii) holds the applier for the contextualised relay document on which we listen for outgoing changes
7408 * @param {Object} npOptions - Namespace and priority options
7409 * namespace {String} [optional] The namespace attached to this relay definition
7410 * priority {String} [optional] The (unparsed) priority attached to this relay definition
7411 */
7412 fluid.registerDirectChangeRelay = function (target, targetSegs, source, sourceSegs, linkId, transducer, options, npOptions) {
7413 var targetApplier = options.targetApplier || target.applier; // first branch implies the target is a relay document
7414 var sourceApplier = options.sourceApplier || source.applier; // first branch implies the source is a relay document - listener will be transactional
7415 var applierId = targetApplier.applierId;
7416 targetSegs = fluid.makeArray(targetSegs);
7417 sourceSegs = fluid.makeArray(sourceSegs); // take copies since originals will be trashed
7418 var sourceListener = function (newValue, oldValue, path, changeRequest, trans, applier) {
7419 var transId = trans.id;
7420 var transRec = fluid.getModelTransactionRec(target, transId);
7421 if (applier && trans && !transRec[applier.applierId]) { // don't trash existing record which may contain "options" (FLUID-5397)
7422 transRec[applier.applierId] = {transaction: trans}; // enlist the outer user's original transaction
7423 }
7424 var existing = transRec[applierId];
7425 transRec[linkId] = transRec[linkId] || 0;
7426 // Crude "oscillation prevention" system limits each link to maximum of 2 operations per cycle (presumably in opposite directions)
7427 var relay = true; // TODO: See FLUID-5303 - we currently disable this check entirely to solve FLUID-5293 - perhaps we might remove link counts entirely
7428 if (relay) {
7429 ++transRec[linkId];
7430 if (transRec[linkId] > fluid.relayRecursionBailout) {
7431 fluid.fail("Error in model relay specification at component ", target, " - operated more than " + fluid.relayRecursionBailout + " relays without model value settling - current model contents are ", trans.newHolder.model);
7432 }
7433 if (!existing) {
7434 existing = fluid.registerRelayTransaction(transRec, targetApplier, transId, options, npOptions);
7435 }
7436 if (transducer && !options.targetApplier) {
7437 // TODO: This is just for safety but is still unusual and now abused. The transducer doesn't need the "newValue" since all the transform information
7438 // has been baked into the transform document itself. However, we now rely on this special signalling value to make sure we regenerate transforms in
7439 // the "forwardAdapter"
7440 transducer(existing.transaction, options.sourceApplier ? undefined : newValue, sourceSegs, targetSegs, changeRequest);
7441 } else {
7442 if (changeRequest && changeRequest.type === "DELETE") {
7443 existing.transaction.fireChangeRequest({type: "DELETE", segs: targetSegs});
7444 }
7445 if (newValue !== undefined) {
7446 existing.transaction.fireChangeRequest({type: "ADD", segs: targetSegs, value: newValue});
7447 }
7448 }
7449 }
7450 };
7451 var spec = sourceApplier.modelChanged.addListener({
7452 isRelay: true,
7453 cond: transducer && transducer.cond,
7454 targetId: target.id, // these two fields for debuggability
7455 targetApplierId: targetApplier.id,
7456 segs: sourceSegs,
7457 transactional: options.transactional
7458 }, sourceListener);
7459 if (fluid.passLogLevel(fluid.logLevel.TRACE)) {
7460 fluid.log(fluid.logLevel.TRACE, "Adding relay listener with listenerId " + spec.listenerId + " to source applier with id " +
7461 sourceApplier.applierId + " from target applier with id " + applierId + " for target component with id " + target.id);
7462 }
7463 if (source) { // TODO - we actually may require to register on THREE sources in the case modelRelay is attached to a
7464 // component which is neither source nor target. Note there will be problems if source, say, is destroyed and recreated,
7465 // and holder is not - relay will in that case be lost. Need to integrate relay expressions with IoCSS.
7466 fluid.recordChangeListener(source, sourceApplier, sourceListener, spec.listenerId);
7467 if (target !== source) {
7468 fluid.recordChangeListener(target, sourceApplier, sourceListener, spec.listenerId);
7469 }
7470 }
7471 };
7472
7473 /** Connect a model relay relation between model material. This is called in three scenarios:
7474 * i) from `fluid.parseModelRelay` when parsing an uncontextualised model relay (one with a static transform document), to
7475 * directly connect the source and target of the relay
7476 * ii) from `fluid.parseModelRelay` when parsing a contextualised model relay (one whose transform document depends on other model
7477 * material), to connect updates emitted from the transform document's applier onto the relay ends (both source and target)
7478 * iii) from `fluid.parseImplicitRelay` when parsing model references found within contextualised model relay to bind changes emitted
7479 * from the target of the reference onto the transform document's applier. These may apply directly to another component's model (in its case
7480 * A) or apply to a relay document (in its case B)
7481 *
7482 * This function will make one or two calls to `fluid.registerDirectChangeRelay` in order to set up each leg of any required relay.
7483 * Note that in case iii)B) the component referred to as our argument `target` is actually the "source" of the changes (that is, the one encountered
7484 * while traversing the transform document), and our argument `source` is the component holding the transform, and so
7485 * the call to `fluid.registerDirectChangeRelay` will have `source` and `target` reversed (`fluid.registerDirectChangeRelay` will bind to the `targetApplier`
7486 * in the options rather than source's applier).
7487 * @param {Component} source - The component holding the material giving rise to the relay, or the one referred to by the `source` member
7488 * of the configuration in case ii), if there is one
7489 * @param {Array|null} sourceSegs - An array of parsed string segments of the `source` relay reference in case i), or the offset into the transform
7490 * document of the reference component in case iii), otherwise `null` (case ii))
7491 * @param {Component} target - The component holding the model relay `target` in cases i) and ii), or the component at the other end of
7492 * the model reference in case iii) (in this case in fact a "source" for the changes.
7493 * @param {Array} targetSegs - An array of parsed string segments of the `target` reference in cases i) and ii), or of the model reference in
7494 * case iii)
7495 * @param {Object} options - A structure describing the relay, allowing discrimination of the various cases above. This is derived from the return from
7496 * `fluid.makeTransformPackage` but will have some members filtered in different cases. This contains members:
7497 * update {Function} A function to be called at the end of a "half-transaction" when all pending updates have been applied to the document's applier.
7498 * This discriminates case iii)
7499 * targetApplier {ChangeApplier} The ChangeApplier for the relay document, in case iii)B)
7500 * forwardApplier (ChangeApplier} The ChangeApplier for the relay document, in cases ii) and iii)B) (only used in latter case)
7501 * forwardAdapter {Adapter} A function accepting (transaction, newValue) to pass through the forward leg of the relay. Contains a member `cond` holding the parsed relay condition.
7502 * backwardAdapter {Adapter} A function accepting (transaction, newValue) to pass through the backward leg of the relay. Contains a member `cond` holding the parsed relay condition.
7503 * namespace {String} Namespace for any relay definition
7504 * priority {String} Priority for any relay definition or synthetic "first" for iii)A)
7505 */
7506 fluid.connectModelRelay = function (source, sourceSegs, target, targetSegs, options) {
7507 var linkId = fluid.allocateGuid();
7508 function enlistComponent(component) {
7509 var enlist = fluid.enlistModelComponent(component);
7510
7511 if (enlist.complete) {
7512 var shadow = fluid.shadowForComponent(component);
7513 if (shadow.modelComplete) {
7514 enlist.completeOnInit = true;
7515 }
7516 }
7517 }
7518 enlistComponent(target);
7519 enlistComponent(source); // role of "source" and "target" are swapped in case iii)B)
7520 var npOptions = fluid.filterKeys(options, ["namespace", "priority"]);
7521
7522 if (options.update) { // it is a call for a relay document - ii) or iii)B)
7523 if (options.targetApplier) { // case iii)B)
7524 // We are in the middle of parsing a contextualised relay, and this call has arrived via its parseImplicitRelay.
7525 // register changes from the target model onto changes to the model relay document
7526 fluid.registerDirectChangeRelay(source, sourceSegs, target, targetSegs, linkId, null, {
7527 transactional: false,
7528 targetApplier: options.targetApplier,
7529 update: options.update
7530 }, npOptions);
7531 } else { // case ii), contextualised relay overall output
7532 // Rather than bind source-source, instead register the "half-transactional" listener which binds changes
7533 // from the relay document itself onto the target
7534 fluid.registerDirectChangeRelay(target, targetSegs, source, [], linkId + "-transform", options.forwardAdapter, {transactional: true, sourceApplier: options.forwardApplier}, npOptions);
7535 }
7536 } else { // case i) or iii)A): more efficient, old-fashioned branch where relay is uncontextualised
7537 fluid.registerDirectChangeRelay(target, targetSegs, source, sourceSegs, linkId, options.forwardAdapter, {transactional: false}, npOptions);
7538 fluid.registerDirectChangeRelay(source, sourceSegs, target, targetSegs, linkId, options.backwardAdapter, {transactional: false}, npOptions);
7539 }
7540 };
7541
7542 fluid.parseSourceExclusionSpec = function (targetSpec, sourceSpec) {
7543 targetSpec.excludeSource = fluid.arrayToHash(fluid.makeArray(sourceSpec.excludeSource || (sourceSpec.includeSource ? "*" : undefined)));
7544 targetSpec.includeSource = fluid.arrayToHash(fluid.makeArray(sourceSpec.includeSource));
7545 return targetSpec;
7546 };
7547
7548 /** Determines whether the supplied transaction should have changes not propagated into it as a result of being excluded by a
7549 * condition specification.
7550 * @param {Transaction} transaction - A local ChangeApplier transaction, with member `fullSources` holding all currently active sources
7551 * @param {ConditionSpec} spec - A parsed relay condition specification, as returned from `fluid.model.parseRelayCondition`.
7552 * @return {Boolean} `true` if changes should be excluded from the supplied transaction according to the supplied specification
7553 */
7554 fluid.isExcludedChangeSource = function (transaction, spec) {
7555 if (!spec || !spec.excludeSource) { // mergeModelListeners initModelEvent fabricates a fake spec that bypasses processing
7556 return false;
7557 }
7558 var excluded = spec.excludeSource["*"];
7559 for (var source in transaction.fullSources) {
7560 if (spec.excludeSource[source]) {
7561 excluded = true;
7562 }
7563 if (spec.includeSource[source]) {
7564 excluded = false;
7565 }
7566 }
7567 return excluded;
7568 };
7569
7570 fluid.model.guardedAdapter = function (transaction, cond, func, args) {
7571 if (!fluid.isExcludedChangeSource(transaction, cond) && func !== fluid.model.transform.uninvertibleTransform) {
7572 func.apply(null, args);
7573 }
7574 };
7575
7576 // TODO: This rather crummy function is the only site with a hard use of "path" as String
7577 fluid.transformToAdapter = function (transform, targetPath) {
7578 var basedTransform = {};
7579 basedTransform[targetPath] = transform; // TODO: Faulty with respect to escaping rules
7580 return function (trans, newValue, sourceSegs, targetSegs, changeRequest) {
7581 if (changeRequest && changeRequest.type === "DELETE") {
7582 trans.fireChangeRequest({type: "DELETE", path: targetPath}); // avoid mouse droppings in target document for FLUID-5585
7583 }
7584 // TODO: More efficient model that can only run invalidated portion of transform (need to access changeMap of source transaction)
7585 fluid.model.transformWithRules(newValue, basedTransform, {finalApplier: trans});
7586 };
7587 };
7588
7589 // TODO: sourcePath and targetPath should really be converted to segs to avoid excess work in parseValidModelReference
7590 fluid.makeTransformPackage = function (componentThat, transform, sourcePath, targetPath, forwardCond, backwardCond, namespace, priority) {
7591 var that = {
7592 forwardHolder: {model: transform},
7593 backwardHolder: {model: null}
7594 };
7595 that.generateAdapters = function (trans) {
7596 // can't commit "half-transaction" or events will fire - violate encapsulation in this way
7597 that.forwardAdapterImpl = fluid.transformToAdapter(trans ? trans.newHolder.model : that.forwardHolder.model, targetPath);
7598 if (sourcePath !== null) {
7599 var inverted = fluid.model.transform.invertConfiguration(transform);
7600 if (inverted !== fluid.model.transform.uninvertibleTransform) {
7601 that.backwardHolder.model = inverted;
7602 that.backwardAdapterImpl = fluid.transformToAdapter(that.backwardHolder.model, sourcePath);
7603 } else {
7604 that.backwardAdapterImpl = inverted;
7605 }
7606 }
7607 };
7608 that.forwardAdapter = function (transaction, newValue) { // create a stable function reference for this possibly changing adapter
7609 if (newValue === undefined) {
7610 that.generateAdapters(); // TODO: Quick fix for incorrect scheduling of invalidation/transducing
7611 // "it so happens" that fluid.registerDirectChangeRelay invokes us with empty newValue in the case of invalidation -> transduction
7612 }
7613 fluid.model.guardedAdapter(transaction, forwardCond, that.forwardAdapterImpl, arguments);
7614 };
7615 that.forwardAdapter.cond = forwardCond; // Used when parsing graph in init transaction
7616 // fired from fluid.model.updateRelays via invalidator event
7617 that.runTransform = function (trans) {
7618 trans.commit(); // this will reach the special "half-transactional listener" registered in fluid.connectModelRelay,
7619 // branch with options.targetApplier - by committing the transaction, we update the relay document in bulk and then cause
7620 // it to execute (via "transducer")
7621 trans.reset();
7622 };
7623 that.forwardApplier = fluid.makeHolderChangeApplier(that.forwardHolder);
7624 that.forwardApplier.isRelayApplier = true; // special annotation so these can be discovered in the transaction record
7625 that.invalidator = fluid.makeEventFirer({name: "Invalidator for model relay with applier " + that.forwardApplier.applierId});
7626 if (sourcePath !== null) {
7627 // TODO: backwardApplier is unused
7628 that.backwardApplier = fluid.makeHolderChangeApplier(that.backwardHolder);
7629 that.backwardAdapter = function (transaction) {
7630 fluid.model.guardedAdapter(transaction, backwardCond, that.backwardAdapterImpl, arguments);
7631 };
7632 that.backwardAdapter.cond = backwardCond;
7633 }
7634 that.update = that.invalidator.fire; // necessary so that both routes to fluid.connectModelRelay from here hit the first branch
7635 var implicitOptions = {
7636 targetApplier: that.forwardApplier, // this special field identifies us to fluid.connectModelRelay
7637 update: that.update,
7638 namespace: namespace,
7639 priority: priority,
7640 refCount: 0
7641 };
7642 that.forwardHolder.model = fluid.parseImplicitRelay(componentThat, transform, [], implicitOptions);
7643 that.refCount = implicitOptions.refCount;
7644 that.namespace = namespace;
7645 that.priority = priority;
7646 that.generateAdapters();
7647 that.invalidator.addListener(that.generateAdapters);
7648 that.invalidator.addListener(that.runTransform);
7649 return that;
7650 };
7651
7652 fluid.singleTransformToFull = function (singleTransform) {
7653 var withPath = $.extend(true, {inputPath: ""}, singleTransform);
7654 return {
7655 "": {
7656 transform: withPath
7657 }
7658 };
7659 };
7660
7661 // Convert old-style "relay conditions" to source includes/excludes as used in model listeners
7662 fluid.model.relayConditions = {
7663 initOnly: {includeSource: "init"},
7664 liveOnly: {excludeSource: "init"},
7665 never: {includeSource: []},
7666 always: {}
7667 };
7668
7669 /** Parse a relay condition specification, e.g. of the form `{includeSource: "init"}` or `never` into a hash representation
7670 * suitable for rapid querying.
7671 * @param {String|Object} condition - A relay condition specification, appearing in the section `forward` or `backward` of a
7672 * relay definition
7673 * @return {RelayCondition} The parsed condition, holding members `includeSource` and `excludeSource` each with a hash to `true`
7674 * of referenced sources
7675 */
7676 fluid.model.parseRelayCondition = function (condition) {
7677 if (condition === "initOnly") {
7678 fluid.log(fluid.logLevel.WARN, "The relay condition \"initOnly\" is deprecated: Please use the form 'includeSource: \"init\"' instead");
7679 } else if (condition === "liveOnly") {
7680 fluid.log(fluid.logLevel.WARN, "The relay condition \"liveOnly\" is deprecated: Please use the form 'excludeSource: \"init\"' instead");
7681 }
7682 var exclusionRec;
7683 if (!condition) {
7684 exclusionRec = {};
7685 } else if (typeof(condition) === "string") {
7686 exclusionRec = fluid.model.relayConditions[condition];
7687 if (!exclusionRec) {
7688 fluid.fail("Unrecognised model relay condition string \"" + condition + "\": the supported values are \"never\" or a record with members \"includeSource\" and/or \"excludeSource\"");
7689 }
7690 } else {
7691 exclusionRec = condition;
7692 }
7693 return fluid.parseSourceExclusionSpec({}, exclusionRec);
7694 };
7695
7696 /** Parse a single model relay record as appearing nested within the `modelRelay` block in a model component's
7697 * options. By various calls to `fluid.connectModelRelay` this will set up the structure operating the live
7698 * relay during the component#s lifetime.
7699 * @param {Component} that - The component holding the record, currently instantiating
7700 * @param {Object} mrrec - The model relay record. This must contain either a member `singleTransform` or `transform` and may also contain
7701 * members `namespace`, `path`, `priority`, `forward` and `backward`
7702 * @param {String} key -
7703 */
7704 fluid.parseModelRelay = function (that, mrrec, key) {
7705 var parsedSource = mrrec.source !== undefined ? fluid.parseValidModelReference(that, "modelRelay record member \"source\"", mrrec.source) :
7706 {path: null, modelSegs: null};
7707 var parsedTarget = fluid.parseValidModelReference(that, "modelRelay record member \"target\"", mrrec.target);
7708 var namespace = mrrec.namespace || key;
7709
7710 var transform = mrrec.singleTransform ? fluid.singleTransformToFull(mrrec.singleTransform) : mrrec.transform;
7711 if (!transform) {
7712 fluid.fail("Cannot parse modelRelay record without element \"singleTransform\" or \"transform\":", mrrec);
7713 }
7714 var forwardCond = fluid.model.parseRelayCondition(mrrec.forward), backwardCond = fluid.model.parseRelayCondition(mrrec.backward);
7715
7716 var transformPackage = fluid.makeTransformPackage(that, transform, parsedSource.path, parsedTarget.path, forwardCond, backwardCond, namespace, mrrec.priority);
7717 if (transformPackage.refCount === 0) { // There were no implicit relay elements found in the relay document - it can be relayed directly
7718 // Case i): Bind changes emitted from the relay ends to each other, synchronously
7719 fluid.connectModelRelay(parsedSource.that || that, parsedSource.modelSegs, parsedTarget.that, parsedTarget.modelSegs,
7720 // Primarily, here, we want to get rid of "update" which is what signals to connectModelRelay that this is a invalidatable relay
7721 fluid.filterKeys(transformPackage, ["forwardAdapter", "backwardAdapter", "namespace", "priority"]));
7722 } else {
7723 if (parsedSource.modelSegs) {
7724 fluid.fail("Error in model relay definition: If a relay transform has a model dependency, you can not specify a \"source\" entry - please instead enter this as \"input\" in the transform specification. Definition was ", mrrec, " for component ", that);
7725 }
7726 // Case ii): Binds changes emitted from the relay document itself onto the relay ends (using the "half-transactional system")
7727 fluid.connectModelRelay(that, null, parsedTarget.that, parsedTarget.modelSegs, transformPackage);
7728 }
7729 };
7730
7731 /** Traverses a model document written within a component's options, parsing any IoC references looking for
7732 * i) references to general material, which will be fetched and interpolated now, and ii) "implicit relay" references to the
7733 * model areas of other components, which will be used to set up live synchronisation between the area in this component where
7734 * they appear and their target, as well as setting up initial synchronisation to compute the initial contents.
7735 * This is called in two situations: A) parsing the `model` configuration option for a model component, and B) parsing the
7736 * `transform` member (perhaps derived from `singleTransform`) of a `modelRelay` block for a model component. It calls itself
7737 * recursively as it progresses through the model document material with updated `segs`
7738 * @param {Component} that - The component holding the model document
7739 * @param {Any} modelRec - The model document specification to be parsed
7740 * @param {String[]} segs - The array of string path segments from the root of the entire model document to the point of current parsing
7741 * @param {Object} options - Configuration options (mutable) governing this parse. This is primarily used to hand as the 5th argument to
7742 * `fluid.connectModelRelay` for any model references found, and contains members
7743 * refCount {Integer} An count incremented for every call to `fluid.connectModelRelay` setting up a synchronizing relay for every
7744 * reference to model material encountered
7745 * priority {String} The unparsed priority member attached to this record, or `first` for a parse of `model` (case A)
7746 * namespace {String} [optional] A namespace attached to this transform, if we are parsing a transform
7747 * targetApplier {ChangeApplier} [optional] The ChangeApplier for this transform document, if it is a transform, empty otherwise
7748 * update {Function} [optional] A function to be called on conclusion of a "half-transaction" where all currently pending updates have been applied
7749 * to this transform document. This function will update/regenerate the relay transform functions used to relay changes between the transform
7750 * ends based on the updated document.
7751 * @return {Any} - The resulting model value.
7752 */
7753 fluid.parseImplicitRelay = function (that, modelRec, segs, options) {
7754 var value;
7755 if (fluid.isIoCReference(modelRec)) {
7756 var parsed = fluid.parseValidModelReference(that, "model reference from model (implicit relay)", modelRec, true);
7757 if (parsed.nonModel) {
7758 value = fluid.getForComponent(parsed.that, parsed.segs);
7759 } else {
7760 ++options.refCount; // This count is used from within fluid.makeTransformPackage
7761 fluid.connectModelRelay(that, segs, parsed.that, parsed.modelSegs, options);
7762 }
7763 } else if (fluid.isPrimitive(modelRec) || !fluid.isPlainObject(modelRec)) {
7764 value = modelRec;
7765 } else if (modelRec.expander && fluid.isPlainObject(modelRec.expander)) {
7766 value = fluid.expandOptions(modelRec, that);
7767 } else {
7768 value = fluid.freshContainer(modelRec);
7769 fluid.each(modelRec, function (innerValue, key) {
7770 segs.push(key);
7771 var innerTrans = fluid.parseImplicitRelay(that, innerValue, segs, options);
7772 if (innerTrans !== undefined) {
7773 value[key] = innerTrans;
7774 }
7775 segs.pop();
7776 });
7777 }
7778 return value;
7779 };
7780
7781
7782 // Conclude the transaction by firing to all external listeners in priority order
7783 fluid.model.notifyExternal = function (transRec) {
7784 var allChanges = transRec ? fluid.values(transRec.externalChanges) : [];
7785 fluid.sortByPriority(allChanges);
7786 for (var i = 0; i < allChanges.length; ++i) {
7787 var change = allChanges[i];
7788 var targetApplier = change.args[5]; // NOTE: This argument gets here via fluid.model.storeExternalChange from fluid.notifyModelChanges
7789 if (!targetApplier.destroyed) { // 3rd point of guarding for FLUID-5592
7790 change.listener.apply(null, change.args);
7791 }
7792 }
7793 fluid.clearLinkCounts(transRec, true); // "options" structures for relayCount are aliased
7794 };
7795
7796 fluid.model.commitRelays = function (instantiator, transactionId) {
7797 var transRec = instantiator.modelTransactions[transactionId];
7798 fluid.each(transRec, function (transEl) {
7799 // EXPLAIN: This must commit ALL current transactions, not just those for relays - why?
7800 if (transEl.transaction) { // some entries are links
7801 transEl.transaction.commit("relay");
7802 transEl.transaction.reset();
7803 }
7804 });
7805 };
7806
7807 // Listens to all invalidation to relays, and reruns/applies them if they have been invalidated
7808 fluid.model.updateRelays = function (instantiator, transactionId) {
7809 var transRec = instantiator.modelTransactions[transactionId];
7810 var updates = 0;
7811 fluid.sortByPriority(transRec.relays);
7812 fluid.each(transRec.relays, function (transEl) {
7813 // TODO: We have a bit of a problem here in that we only process updatable relays by priority - plain relays get to act non-transactionally
7814 if (transEl.transaction.changeRecord.changes > 0 && transEl.relayCount < 2 && transEl.options.update) {
7815 transEl.relayCount++;
7816 fluid.clearLinkCounts(transRec);
7817 transEl.options.update(transEl.transaction, transRec);
7818 ++updates;
7819 }
7820 });
7821 return updates;
7822 };
7823
7824 fluid.establishModelRelay = function (that, optionsModel, optionsML, optionsMR, applier) {
7825 var shadow = fluid.shadowForComponent(that);
7826 if (!shadow.modelRelayEstablished) {
7827 shadow.modelRelayEstablished = true;
7828 } else {
7829 fluid.fail("FLUID-5887 failure: Model relay initialised twice on component", that);
7830 }
7831 fluid.mergeModelListeners(that, optionsML);
7832
7833 var enlist = fluid.enlistModelComponent(that);
7834 fluid.each(optionsMR, function (mrrec, key) {
7835 for (var i = 0; i < mrrec.length; ++i) {
7836 fluid.parseModelRelay(that, mrrec[i], key);
7837 }
7838 });
7839
7840 // Note: this particular instance of "refCount" is disused. We only use the count made within fluid.makeTransformPackge
7841 var initModels = fluid.transform(optionsModel, function (modelRec) {
7842 return fluid.parseImplicitRelay(that, modelRec, [], {refCount: 0, priority: "first"});
7843 });
7844 enlist.initModels = initModels;
7845
7846 var instantiator = fluid.getInstantiator(that);
7847
7848 function updateRelays(transaction) {
7849 while (fluid.model.updateRelays(instantiator, transaction.id) > 0) {} // eslint-disable-line no-empty
7850 }
7851
7852 function commitRelays(transaction, applier, code) {
7853 if (code !== "relay") { // don't commit relays if this commit is already a relay commit
7854 fluid.model.commitRelays(instantiator, transaction.id);
7855 }
7856 }
7857
7858 function concludeTransaction(transaction, applier, code) {
7859 if (code !== "relay") {
7860 fluid.model.notifyExternal(instantiator.modelTransactions[transaction.id]);
7861 delete instantiator.modelTransactions[transaction.id];
7862 }
7863 }
7864
7865 applier.preCommit.addListener(updateRelays);
7866 applier.preCommit.addListener(commitRelays);
7867 applier.postCommit.addListener(concludeTransaction);
7868
7869 return null;
7870 };
7871
7872 // supported, PUBLIC API grade
7873 fluid.defaults("fluid.modelComponent", {
7874 gradeNames: ["fluid.component"],
7875 changeApplierOptions: {
7876 relayStyle: true,
7877 cullUnchanged: true
7878 },
7879 members: {
7880 model: "@expand:fluid.initRelayModel({that}, {that}.modelRelay)",
7881 applier: "@expand:fluid.makeHolderChangeApplier({that}, {that}.options.changeApplierOptions)",
7882 modelRelay: "@expand:fluid.establishModelRelay({that}, {that}.options.model, {that}.options.modelListeners, {that}.options.modelRelay, {that}.applier)"
7883 },
7884 mergePolicy: {
7885 model: {
7886 noexpand: true,
7887 func: fluid.arrayConcatPolicy // TODO: bug here in case a model consists of an array
7888 },
7889 modelListeners: fluid.makeMergeListenersPolicy(fluid.arrayConcatPolicy),
7890 modelRelay: fluid.makeMergeListenersPolicy(fluid.arrayConcatPolicy, true)
7891 }
7892 });
7893
7894 fluid.modelChangedToChange = function (args) {
7895 return {
7896 value: args[0],
7897 oldValue: args[1],
7898 path: args[2],
7899 transaction: args[4]
7900 };
7901 };
7902
7903 // Note - has only one call, from resolveModelListener
7904 fluid.event.invokeListener = function (listener, args, localRecord, mergeRecord) {
7905 if (typeof(listener) === "string") {
7906 listener = fluid.event.resolveListener(listener); // just resolves globals
7907 }
7908 return listener.apply(null, args, localRecord, mergeRecord); // can be "false apply" that requires extra context for expansion
7909 };
7910
7911 fluid.resolveModelListener = function (that, record) {
7912 var togo = function () {
7913 if (fluid.isDestroyed(that)) { // first guarding point to resolve FLUID-5592
7914 return;
7915 }
7916 var change = fluid.modelChangedToChange(arguments);
7917 var args = arguments;
7918 var localRecord = {change: change, "arguments": args};
7919 var mergeRecord = {source: Object.keys(change.transaction.sources)}; // cascade for FLUID-5490
7920 if (record.args) {
7921 args = fluid.expandOptions(record.args, that, {}, localRecord);
7922 }
7923 fluid.event.invokeListener(record.listener, fluid.makeArray(args), localRecord, mergeRecord);
7924 };
7925 fluid.event.impersonateListener(record.listener, togo);
7926 return togo;
7927 };
7928
7929 fluid.registerModelListeners = function (that, record, paths, namespace) {
7930 var func = fluid.resolveModelListener(that, record);
7931 fluid.each(record.byTarget, function (parsedArray) {
7932 var parsed = parsedArray[0]; // that, applier are common across all these elements
7933 var spec = {
7934 listener: func, // for initModelEvent
7935 listenerId: fluid.allocateGuid(), // external declarative listeners may often share listener handle, identify here
7936 segsArray: fluid.getMembers(parsedArray, "modelSegs"),
7937 pathArray: fluid.getMembers(parsedArray, "path"),
7938 includeSource: record.includeSource,
7939 excludeSource: record.excludeSource,
7940 priority: fluid.expandOptions(record.priority, that),
7941 transactional: true
7942 };
7943 // update "spec" so that we parse priority information just once
7944 spec = parsed.applier.modelChanged.addListener(spec, func, namespace, record.softNamespace);
7945
7946 fluid.recordChangeListener(that, parsed.applier, func, spec.listenerId);
7947 function initModelEvent() {
7948 if (fluid.isModelComplete(parsed.that)) {
7949 var trans = parsed.applier.initiate(null, "init");
7950 fluid.initModelEvent(that, parsed.applier, trans, [spec]);
7951 trans.commit();
7952 }
7953 }
7954 if (that !== parsed.that && !fluid.isModelComplete(that)) { // TODO: Use FLUID-4883 "latched events" when available
7955 // Don't confuse the end user by firing their listener before the component is constructed
7956 // TODO: Better detection than this is requred - we assume that the target component will not be discovered as part
7957 // of the initial transaction wave, but if it is, it will get a double notification - we really need "wave of explosions"
7958 // since we are currently too early in initialisation of THIS component in order to tell if other will be found
7959 // independently.
7960 var onCreate = fluid.getForComponent(that, ["events", "onCreate"]);
7961 onCreate.addListener(initModelEvent);
7962 }
7963 });
7964 };
7965
7966 fluid.mergeModelListeners = function (that, listeners) {
7967 fluid.each(listeners, function (value, key) {
7968 if (typeof(value) === "string") {
7969 value = {
7970 funcName: value
7971 };
7972 }
7973 // Bypass fluid.event.dispatchListener by means of "standard = false" and enter our custom workflow including expanding "change":
7974 var records = fluid.event.resolveListenerRecord(value, that, "modelListeners", null, false).records;
7975 fluid.each(records, function (record) {
7976 // Aggregate model listeners into groups referring to the same component target.
7977 // We do this so that a single entry will appear in its modelListeners so that they may
7978 // be notified just once per transaction, and also displaced by namespace
7979 record.byTarget = {};
7980 var paths = fluid.makeArray(record.path === undefined ? key : record.path);
7981 fluid.each(paths, function (path) {
7982 var parsed = fluid.parseValidModelReference(that, "modelListeners entry", path);
7983 fluid.pushArray(record.byTarget, parsed.that.id, parsed);
7984 });
7985 var namespace = (record.namespace && !record.softNamespace ? record.namespace : null) || (record.path !== undefined ? key : null);
7986 fluid.registerModelListeners(that, record, paths, namespace);
7987 });
7988 });
7989 };
7990
7991
7992 /** CHANGE APPLIER **/
7993
7994 /* Dispatches a list of changes to the supplied applier */
7995 fluid.fireChanges = function (applier, changes) {
7996 for (var i = 0; i < changes.length; ++i) {
7997 applier.fireChangeRequest(changes[i]);
7998 }
7999 };
8000
8001 fluid.model.isChangedPath = function (changeMap, segs) {
8002 for (var i = 0; i <= segs.length; ++i) {
8003 if (typeof(changeMap) === "string") {
8004 return true;
8005 }
8006 if (i < segs.length && changeMap) {
8007 changeMap = changeMap[segs[i]];
8008 }
8009 }
8010 return false;
8011 };
8012
8013 fluid.model.setChangedPath = function (options, segs, value) {
8014 var notePath = function (record) {
8015 segs.unshift(record);
8016 fluid.model.setSimple(options, segs, value);
8017 segs.shift();
8018 };
8019 if (!fluid.model.isChangedPath(options.changeMap, segs)) {
8020 ++options.changes;
8021 notePath("changeMap");
8022 }
8023 if (!fluid.model.isChangedPath(options.deltaMap, segs)) {
8024 ++options.deltas;
8025 notePath("deltaMap");
8026 }
8027 };
8028
8029 fluid.model.fetchChangeChildren = function (target, i, segs, source, options) {
8030 fluid.each(source, function (value, key) {
8031 segs[i] = key;
8032 fluid.model.applyChangeStrategy(target, key, i, segs, value, options);
8033 segs.length = i;
8034 });
8035 };
8036
8037 // Called with two primitives which are compared for equality. This takes account of "floating point slop" to avoid
8038 // continuing to propagate inverted values as changes
8039 // TODO: replace with a pluggable implementation
8040 fluid.model.isSameValue = function (a, b) {
8041 if (typeof(a) !== "number" || typeof(b) !== "number") {
8042 return a === b;
8043 } else {
8044 // Don't use isNaN because of https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/isNaN#Confusing_special-case_behavior
8045 if (a === b || a !== a && b !== b) { // Either the same concrete number or both NaN
8046 return true;
8047 } else {
8048 var relError = Math.abs((a - b) / b);
8049 return relError < 1e-12; // 64-bit floats have approx 16 digits accuracy, this should deal with most reasonable transforms
8050 }
8051 }
8052 };
8053
8054 fluid.model.applyChangeStrategy = function (target, name, i, segs, source, options) {
8055 var targetSlot = target[name];
8056 var sourceCode = fluid.typeCode(source);
8057 var targetCode = fluid.typeCode(targetSlot);
8058 var changedValue = fluid.NO_VALUE;
8059 if (sourceCode === "primitive") {
8060 if (!fluid.model.isSameValue(targetSlot, source)) {
8061 changedValue = source;
8062 ++options.unchanged;
8063 }
8064 } else if (targetCode !== sourceCode || sourceCode === "array" && source.length !== targetSlot.length) {
8065 // RH is not primitive - array or object and mismatching or any array rewrite
8066 changedValue = fluid.freshContainer(source);
8067 }
8068 if (changedValue !== fluid.NO_VALUE) {
8069 target[name] = changedValue;
8070 if (options.changeMap) {
8071 fluid.model.setChangedPath(options, segs, options.inverse ? "DELETE" : "ADD");
8072 }
8073 }
8074 if (sourceCode !== "primitive") {
8075 fluid.model.fetchChangeChildren(target[name], i + 1, segs, source, options);
8076 }
8077 };
8078
8079 fluid.model.stepTargetAccess = function (target, type, segs, startpos, endpos, options) {
8080 for (var i = startpos; i < endpos; ++i) {
8081 if (!target) {
8082 continue;
8083 }
8084 var oldTrunk = target[segs[i]];
8085 target = fluid.model.traverseWithStrategy(target, segs, i, options[type === "ADD" ? "resolverSetConfig" : "resolverGetConfig"],
8086 segs.length - i - 1);
8087 if (oldTrunk !== target && options.changeMap) {
8088 fluid.model.setChangedPath(options, segs.slice(0, i + 1), "ADD");
8089 }
8090 }
8091 return {root: target, last: segs[endpos]};
8092 };
8093
8094 fluid.model.defaultAccessorConfig = function (options) {
8095 options = options || {};
8096 options.resolverSetConfig = options.resolverSetConfig || fluid.model.escapedSetConfig;
8097 options.resolverGetConfig = options.resolverGetConfig || fluid.model.escapedGetConfig;
8098 return options;
8099 };
8100
8101 // Changes: "MERGE" action abolished
8102 // ADD/DELETE at root can be destructive
8103 // changes tracked in optional final argument holding "changeMap: {}, changes: 0, unchanged: 0"
8104 fluid.model.applyHolderChangeRequest = function (holder, request, options) {
8105 options = fluid.model.defaultAccessorConfig(options);
8106 options.deltaMap = options.changeMap ? {} : null;
8107 options.deltas = 0;
8108 var length = request.segs.length;
8109 var pen, atRoot = length === 0;
8110 if (atRoot) {
8111 pen = {root: holder, last: "model"};
8112 } else {
8113 if (!holder.model) {
8114 holder.model = {};
8115 fluid.model.setChangedPath(options, [], options.inverse ? "DELETE" : "ADD");
8116 }
8117 pen = fluid.model.stepTargetAccess(holder.model, request.type, request.segs, 0, length - 1, options);
8118 }
8119 if (request.type === "ADD") {
8120 var value = request.value;
8121 var segs = fluid.makeArray(request.segs);
8122 fluid.model.applyChangeStrategy(pen.root, pen.last, length - 1, segs, value, options, atRoot);
8123 } else if (request.type === "DELETE") {
8124 if (pen.root && pen.root[pen.last] !== undefined) {
8125 delete pen.root[pen.last];
8126 if (options.changeMap) {
8127 fluid.model.setChangedPath(options, request.segs, "DELETE");
8128 }
8129 }
8130 } else {
8131 fluid.fail("Unrecognised change type of " + request.type);
8132 }
8133 return options.deltas ? options.deltaMap : null;
8134 };
8135
8136 /** Compare two models for equality using a deep algorithm. It is assumed that both models are JSON-equivalent and do
8137 * not contain circular links.
8138 * @param modela The first model to be compared
8139 * @param modelb The second model to be compared
8140 * @param {Object} options - If supplied, will receive a map and summary of the change content between the objects. Structure is:
8141 * changeMap: {Object/String} An isomorphic map of the object structures to values "ADD" or "DELETE" indicating
8142 * that values have been added/removed at that location. Note that in the case the object structure differs at the root, <code>changeMap</code> will hold
8143 * the plain String value "ADD" or "DELETE"
8144 * changes: {Integer} Counts the number of changes between the objects - The two objects are identical iff <code>changes === 0</code>.
8145 * unchanged: {Integer} Counts the number of leaf (primitive) values at which the two objects are identical. Note that the current implementation will
8146 * double-count, this summary should be considered indicative rather than precise.
8147 * @return <code>true</code> if the models are identical
8148 */
8149 // TODO: This algorithm is quite inefficient in that both models will be copied once each
8150 // supported, PUBLIC API function
8151 fluid.model.diff = function (modela, modelb, options) {
8152 options = options || {changes: 0, unchanged: 0, changeMap: {}}; // current algorithm can't avoid the expense of changeMap
8153 var typea = fluid.typeCode(modela);
8154 var typeb = fluid.typeCode(modelb);
8155 var togo;
8156 if (typea === "primitive" && typeb === "primitive") {
8157 togo = fluid.model.isSameValue(modela, modelb);
8158 } else if (typea === "primitive" ^ typeb === "primitive") {
8159 togo = false;
8160 } else {
8161 // Apply both forward and reverse changes - if no changes either way, models are identical
8162 // "ADD" reported in the reverse direction must be accounted as a "DELETE"
8163 var holdera = {
8164 model: fluid.copy(modela)
8165 };
8166 fluid.model.applyHolderChangeRequest(holdera, {value: modelb, segs: [], type: "ADD"}, options);
8167 var holderb = {
8168 model: fluid.copy(modelb)
8169 };
8170 options.inverse = true;
8171 fluid.model.applyHolderChangeRequest(holderb, {value: modela, segs: [], type: "ADD"}, options);
8172 togo = options.changes === 0;
8173 }
8174 if (togo === false && options.changes === 0) { // catch all primitive cases
8175 options.changes = 1;
8176 options.changeMap = modelb === undefined ? "DELETE" : "ADD";
8177 } else if (togo === true && options.unchanged === 0) {
8178 options.unchanged = 1;
8179 }
8180 return togo;
8181 };
8182
8183 fluid.outputMatches = function (matches, outSegs, root) {
8184 fluid.each(root, function (value, key) {
8185 matches.push(outSegs.concat(key));
8186 });
8187 };
8188
8189 // Here we only support for now very simple expressions which have at most one
8190 // wildcard which must appear in the final segment
8191 fluid.matchChanges = function (changeMap, specSegs, newHolder, oldHolder) {
8192 var newRoot = newHolder.model;
8193 var oldRoot = oldHolder.model;
8194 var map = changeMap;
8195 var outSegs = ["model"];
8196 var wildcard = false;
8197 var togo = [];
8198 for (var i = 0; i < specSegs.length; ++i) {
8199 var seg = specSegs[i];
8200 if (seg === "*") {
8201 if (i === specSegs.length - 1) {
8202 wildcard = true;
8203 } else {
8204 fluid.fail("Wildcard specification in modelChanged listener is only supported for the final path segment: " + specSegs.join("."));
8205 }
8206 } else {
8207 outSegs.push(seg);
8208 map = fluid.isPrimitive(map) ? map : map[seg];
8209 newRoot = newRoot ? newRoot[seg] : undefined;
8210 oldRoot = oldRoot ? oldRoot[seg] : undefined;
8211 }
8212 }
8213 if (map) {
8214 if (wildcard) {
8215 if (map === "DELETE") {
8216 fluid.outputMatches(togo, outSegs, oldRoot);
8217 } else if (map === "ADD") {
8218 fluid.outputMatches(togo, outSegs, newRoot);
8219 } else {
8220 fluid.outputMatches(togo, outSegs, map);
8221 }
8222 } else {
8223 togo.push(outSegs);
8224 }
8225 }
8226 return togo;
8227 };
8228
8229 fluid.storeExternalChange = function (transRec, applier, invalidPath, spec, args) {
8230 var pathString = applier.composeSegments.apply(null, invalidPath);
8231 var keySegs = [applier.holder.id, spec.listenerId, (spec.wildcard ? pathString : "")];
8232 var keyString = keySegs.join("|");
8233 // TODO: We think we probably have a bug in that notifications destined for end of transaction are actually continuously emitted during the transaction
8234 // These are unbottled in fluid.concludeTransaction
8235 transRec.externalChanges[keyString] = {listener: spec.listener, namespace: spec.namespace, priority: spec.priority, args: args};
8236 };
8237
8238 fluid.notifyModelChanges = function (listeners, changeMap, newHolder, oldHolder, changeRequest, transaction, applier, that) {
8239 if (!listeners) {
8240 return;
8241 }
8242 var transRec = transaction && fluid.getModelTransactionRec(that, transaction.id);
8243 for (var i = 0; i < listeners.length; ++i) {
8244 var spec = listeners[i];
8245 var multiplePaths = spec.segsArray.length > 1; // does this spec listen on multiple paths? If so, don't rebase arguments and just report once per transaction
8246 for (var j = 0; j < spec.segsArray.length; ++j) {
8247 var invalidPaths = fluid.matchChanges(changeMap, spec.segsArray[j], newHolder, oldHolder);
8248 // We only have multiple invalidPaths here if there is a wildcard
8249 for (var k = 0; k < invalidPaths.length; ++k) {
8250 if (applier.destroyed) { // 2nd guarding point for FLUID-5592
8251 return;
8252 }
8253 var invalidPath = invalidPaths[k];
8254 spec.listener = fluid.event.resolveListener(spec.listener);
8255 var args = [multiplePaths ? newHolder.model : fluid.model.getSimple(newHolder, invalidPath),
8256 multiplePaths ? oldHolder.model : fluid.model.getSimple(oldHolder, invalidPath),
8257 multiplePaths ? [] : invalidPath.slice(1), changeRequest, transaction, applier];
8258 // FLUID-5489: Do not notify of null changes which were reported as a result of invalidating a higher path
8259 // TODO: We can improve greatly on efficiency by i) reporting a special code from fluid.matchChanges which signals the difference between invalidating a higher and lower path,
8260 // ii) improving fluid.model.diff to create fewer intermediate structures and no copies
8261 // TODO: The relay invalidation system is broken and must always be notified (branch 1) - since our old/new value detection is based on the wrong (global) timepoints in the transaction here,
8262 // rather than the "last received model" by the holder of the transform document
8263 if (!spec.isRelay) {
8264 var isNull = fluid.model.diff(args[0], args[1]);
8265 if (isNull) {
8266 continue;
8267 }
8268 var sourceExcluded = fluid.isExcludedChangeSource(transaction, spec);
8269 if (sourceExcluded) {
8270 continue;
8271 }
8272 }
8273 if (transRec && !spec.isRelay && spec.transactional) { // bottle up genuine external changes so we can sort and dedupe them later
8274 fluid.storeExternalChange(transRec, applier, invalidPath, spec, args);
8275 } else {
8276 spec.listener.apply(null, args);
8277 }
8278 }
8279 }
8280 }
8281 };
8282
8283 fluid.bindELMethods = function (applier) {
8284 applier.parseEL = function (EL) {
8285 return fluid.model.pathToSegments(EL, applier.options.resolverSetConfig);
8286 };
8287 applier.composeSegments = function () {
8288 return applier.options.resolverSetConfig.parser.compose.apply(null, arguments);
8289 };
8290 };
8291
8292 fluid.initModelEvent = function (that, applier, trans, listeners) {
8293 fluid.notifyModelChanges(listeners, "ADD", trans.oldHolder, fluid.emptyHolder, null, trans, applier, that);
8294 };
8295
8296 // A standard "empty model" for the purposes of comparing initial state during the primordial transaction
8297 fluid.emptyHolder = fluid.freezeRecursive({ model: undefined });
8298
8299 fluid.preFireChangeRequest = function (applier, changeRequest) {
8300 if (!changeRequest.type) {
8301 changeRequest.type = "ADD";
8302 }
8303 changeRequest.segs = changeRequest.segs || applier.parseEL(changeRequest.path);
8304 };
8305
8306 // Automatically adapts change onto fireChangeRequest
8307 fluid.bindRequestChange = function (that) {
8308 that.change = function (path, value, type, source) {
8309 var changeRequest = {
8310 path: path,
8311 value: value,
8312 type: type,
8313 source: source
8314 };
8315 that.fireChangeRequest(changeRequest);
8316 };
8317 };
8318
8319 // Quick n dirty test to cheaply detect Object versus other JSON types
8320 fluid.isObjectSimple = function (totest) {
8321 return Object.prototype.toString.call(totest) === "[object Object]";
8322 };
8323
8324 fluid.mergeChangeSources = function (target, globalSources) {
8325 if (fluid.isObjectSimple(globalSources)) { // TODO: No test for this branch!
8326 fluid.extend(target, globalSources);
8327 } else {
8328 fluid.each(fluid.makeArray(globalSources), function (globalSource) {
8329 target[globalSource] = true;
8330 });
8331 }
8332 };
8333
8334 fluid.ChangeApplier = function () {};
8335
8336 fluid.makeHolderChangeApplier = function (holder, options) {
8337 options = fluid.model.defaultAccessorConfig(options);
8338 var applierId = fluid.allocateGuid();
8339 var that = new fluid.ChangeApplier();
8340 var name = fluid.isComponent(holder) ? "ChangeApplier for component " + fluid.dumpThat(holder) : "ChangeApplier with id " + applierId;
8341 $.extend(that, {
8342 applierId: applierId,
8343 holder: holder,
8344 listeners: fluid.makeEventFirer({name: "Internal change listeners for " + name}),
8345 transListeners: fluid.makeEventFirer({name: "External change listeners for " + name}),
8346 options: options,
8347 modelChanged: {},
8348 preCommit: fluid.makeEventFirer({name: "preCommit event for " + name}),
8349 postCommit: fluid.makeEventFirer({name: "postCommit event for " + name})
8350 });
8351 that.destroy = function () {
8352 that.preCommit.destroy();
8353 that.postCommit.destroy();
8354 that.destroyed = true;
8355 };
8356 that.modelChanged.addListener = function (spec, listener, namespace, softNamespace) {
8357 if (typeof(spec) === "string") {
8358 spec = {
8359 path: spec
8360 };
8361 } else {
8362 spec = fluid.copy(spec);
8363 }
8364 spec.listenerId = spec.listenerId || fluid.allocateGuid(); // FLUID-5151: don't use identifyListener since event.addListener will use this as a namespace
8365 spec.namespace = namespace;
8366 spec.softNamespace = softNamespace;
8367 if (typeof(listener) === "string") { // The reason for "globalName" is so that listener names can be resolved on first use and not on registration
8368 listener = {globalName: listener};
8369 }
8370 spec.listener = listener;
8371 if (spec.transactional !== false) {
8372 spec.transactional = true;
8373 }
8374 if (!spec.segsArray) { // It's a manual registration
8375 if (spec.path !== undefined) {
8376 spec.segs = spec.segs || that.parseEL(spec.path);
8377 }
8378 if (!spec.segsArray) {
8379 spec.segsArray = [spec.segs];
8380 }
8381 }
8382 if (!spec.isRelay) {
8383 // This acts for listeners registered externally. For relays, the exclusion spec is stored in "cond"
8384 fluid.parseSourceExclusionSpec(spec, spec);
8385 spec.wildcard = fluid.accumulate(fluid.transform(spec.segsArray, function (segs) {
8386 return fluid.contains(segs, "*");
8387 }), fluid.add, 0);
8388 if (spec.wildcard && spec.segsArray.length > 1) {
8389 fluid.fail("Error in model listener specification ", spec, " - you may not supply a wildcard pattern as one of a set of multiple paths to be matched");
8390 }
8391 }
8392 var firer = that[spec.transactional ? "transListeners" : "listeners"];
8393 firer.addListener(spec);
8394 return spec; // return is used in registerModelListeners
8395 };
8396 that.modelChanged.removeListener = function (listener) {
8397 that.listeners.removeListener(listener);
8398 that.transListeners.removeListener(listener);
8399 };
8400 that.fireChangeRequest = function (changeRequest) {
8401 var ation = that.initiate("local", changeRequest.source);
8402 ation.fireChangeRequest(changeRequest);
8403 ation.commit();
8404 };
8405
8406 /**
8407 * Initiate a fresh transaction on this applier, perhaps coordinated with other transactions sharing the same id across the component tree
8408 * Arguments all optional
8409 * @param {String} localSource - "local", "relay" or null Local source identifiers only good for transaction's representative on this applier
8410 * @param {String|Array|Object} globalSources - Global source identifiers common across this transaction, expressed as a single string, an array of strings, or an object with a "toString" method.
8411 * @param {String} transactionId - Global transaction id to enlist with.
8412 * @return {Object} - The component initiating the change.
8413 */
8414 that.initiate = function (localSource, globalSources, transactionId) {
8415 localSource = globalSources === "init" ? null : (localSource || "local"); // supported values for localSource are "local" and "relay" - globalSource of "init" defeats defaulting of localSource to "local"
8416 var defeatPost = localSource === "relay"; // defeatPost is supplied for all non-top-level transactions
8417 var trans = {
8418 instanceId: fluid.allocateGuid(), // for debugging only - the representative of this transction on this applier
8419 id: transactionId || fluid.allocateGuid(), // The global transaction id across all appliers - allocate here if this is the starting point
8420 changeRecord: {
8421 resolverSetConfig: options.resolverSetConfig, // here to act as "options" in applyHolderChangeRequest
8422 resolverGetConfig: options.resolverGetConfig
8423 },
8424 reset: function () {
8425 trans.oldHolder = holder;
8426 trans.newHolder = { model: fluid.copy(holder.model) };
8427 trans.changeRecord.changes = 0;
8428 trans.changeRecord.unchanged = 0; // just for type consistency - we don't use these values in the ChangeApplier
8429 trans.changeRecord.changeMap = {};
8430 },
8431 commit: function (code) {
8432 that.preCommit.fire(trans, that, code);
8433 if (trans.changeRecord.changes > 0) {
8434 var oldHolder = {model: holder.model};
8435 holder.model = trans.newHolder.model;
8436 fluid.notifyModelChanges(that.transListeners.sortedListeners, trans.changeRecord.changeMap, holder, oldHolder, null, trans, that, holder);
8437 }
8438 if (!defeatPost) {
8439 that.postCommit.fire(trans, that, code);
8440 }
8441 },
8442 fireChangeRequest: function (changeRequest) {
8443 fluid.preFireChangeRequest(that, changeRequest);
8444 changeRequest.transactionId = trans.id;
8445 var deltaMap = fluid.model.applyHolderChangeRequest(trans.newHolder, changeRequest, trans.changeRecord);
8446 fluid.notifyModelChanges(that.listeners.sortedListeners, deltaMap, trans.newHolder, holder, changeRequest, trans, that, holder);
8447 },
8448 hasChangeSource: function (source) {
8449 return trans.fullSources[source];
8450 }
8451 };
8452 var transRec = fluid.getModelTransactionRec(holder, trans.id);
8453 if (transRec) {
8454 fluid.mergeChangeSources(transRec.sources, globalSources);
8455 trans.sources = transRec.sources;
8456 trans.fullSources = Object.create(transRec.sources);
8457 trans.fullSources[localSource] = true;
8458 }
8459 trans.reset();
8460 fluid.bindRequestChange(trans);
8461 return trans;
8462 };
8463
8464 fluid.bindRequestChange(that);
8465 fluid.bindELMethods(that);
8466 return that;
8467 };
8468
8469 /**
8470 * Calculates the changes between the model values 'value' and
8471 * 'oldValue' and returns an array of change records. The optional
8472 * argument 'changePathPrefix' is prepended to the change path of
8473 * each record (this is useful for generating change records to be
8474 * applied at a non-root path in a model). The returned array of
8475 * change records may be used with fluid.fireChanges().
8476 *
8477 * @param {Any} value - Model value to compare.
8478 * @param {Any} oldValue - Model value to compare.
8479 * @param {String|Array} [changePathPrefix] - [optional] Path prefix to prepend to change record paths, expressed as a string or an array of string segments.
8480 * @return {Array} - An array of change record objects.
8481 */
8482 fluid.modelPairToChanges = function (value, oldValue, changePathPrefix) {
8483 changePathPrefix = changePathPrefix || "";
8484
8485 // Calculate the diff between value and oldValue
8486 var diffOptions = {changes: 0, unchanged: 0, changeMap: {}};
8487 fluid.model.diff(oldValue, value, diffOptions);
8488
8489 var changes = [];
8490
8491 // Recursively process the diff to generate an array of change
8492 // records, stored in 'changes'
8493 fluid.modelPairToChangesImpl(value,
8494 fluid.pathUtil.parseEL(changePathPrefix),
8495 diffOptions.changeMap, [], changes);
8496
8497 return changes;
8498 };
8499
8500 /**
8501 * This function implements recursive processing for
8502 * fluid.modelPairToChanges(). It builds an array of change
8503 * records, accumulated in the 'changes' argument, by walking the
8504 * 'changeMap' structure and 'value' model value. As we walk down
8505 * the model, our path from the root of the model is recorded in
8506 * the 'changeSegs' argument.
8507 *
8508 * @param {Any} value - Model value
8509 * @param {String[]} changePathPrefixSegs - Path prefix to prepend to change record paths, expressed as an array of string segments.
8510 * @param {String|Object} changeMap - The changeMap structure from fluid.model.diff().
8511 * @param {String[]} changeSegs - Our path relative to the model value root, expressed as an array of string segments.
8512 * @param {Object[]} changes - The accumulated change record objects.
8513 */
8514 fluid.modelPairToChangesImpl = function (value, changePathPrefixSegs, changeMap, changeSegs, changes) {
8515 if (changeMap === "ADD") {
8516 // The whole model value is new
8517 changes.push({
8518 path: changePathPrefixSegs,
8519 value: value,
8520 type: "ADD"
8521 });
8522 } else if (changeMap === "DELETE") {
8523 // The whole model value has been deleted
8524 changes.push({
8525 path: changePathPrefixSegs,
8526 value: null,
8527 type: "DELETE"
8528 });
8529 } else if (fluid.isPlainObject(changeMap, true)) {
8530 // Something within the model value has changed
8531 fluid.each(changeMap, function (change, seg) {
8532 var currentChangeSegs = changeSegs.concat([seg]);
8533 if (change === "ADD") {
8534 changes.push({
8535 path: changePathPrefixSegs.concat(currentChangeSegs),
8536 value: fluid.get(value, currentChangeSegs),
8537 type: "ADD"
8538 });
8539 } else if (change === "DELETE") {
8540 changes.push({
8541 path: changePathPrefixSegs.concat(currentChangeSegs),
8542 value: null,
8543 type: "DELETE"
8544 });
8545 } else if (fluid.isPlainObject(change, true)) {
8546 // Recurse down the tree of changes
8547 fluid.modelPairToChangesImpl(value, changePathPrefixSegs,
8548 change, currentChangeSegs, changes);
8549 }
8550 });
8551 }
8552 };
8553
8554})(jQuery, fluid_3_0_0);
8555;
8556/*
8557Copyright The Infusion copyright holders
8558See the AUTHORS.md file at the top-level directory of this distribution and at
8559https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
8560
8561Licensed under the Educational Community License (ECL), Version 2.0 or the New
8562BSD license. You may not use this file except in compliance with one these
8563Licenses.
8564
8565You may obtain a copy of the ECL 2.0 License and BSD License at
8566https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
8567*/
8568
8569var fluid_3_0_0 = fluid_3_0_0 || {};
8570
8571(function ($, fluid) {
8572 "use strict";
8573
8574 /**
8575 * fluid.remoteModelComponent builds on top of fluid.modelComponent with the purpose of providing a buffer between a
8576 * local and remote model that are attempting to stay in sync. For example a local model is being updated by user
8577 * interaction, this is sent back to a remote server, which in turn tries to update the local model. If additional
8578 * user actions occur during the roundtrip, an infinite loop of updates may occur. fluid.remoteModelComponent solves
8579 * this by restricting reading and writing to a single request at a time, waiting for one request to complete
8580 * before operating the next.
8581 *
8582 * For more detailed documentation, including diagrams outlining the fetch and write workflows, see:
8583 * https://docs.fluidproject.org/infusion/development/RemoteModelAPI.html
8584 */
8585 fluid.defaults("fluid.remoteModelComponent", {
8586 gradeNames: ["fluid.modelComponent"],
8587 events: {
8588 afterFetch: null,
8589 onFetch: null,
8590 onFetchError: null,
8591 afterWrite: null,
8592 onWrite: null,
8593 onWriteError: null
8594 },
8595 members: {
8596 pendingRequests: {
8597 write: null,
8598 fetch: null
8599 }
8600 },
8601 model: {
8602 // an implementor must setup a model relay between the buffered local value and the portions of the
8603 // component's model state that should be updated with the remote source.
8604 local: {},
8605 remote: {},
8606 requestInFlight: false
8607 },
8608 modelListeners: {
8609 "requestInFlight": {
8610 listener: "fluid.remoteModelComponent.launchPendingRequest",
8611 args: ["{that}"]
8612 }
8613 },
8614 listeners: {
8615 "afterFetch.updateModel": {
8616 listener: "fluid.remoteModelComponent.updateModelFromFetch",
8617 args: ["{that}", "{arguments}.0"],
8618 priority: "before:unblock"
8619 },
8620 "afterFetch.unblock": {
8621 listener: "fluid.remoteModelComponent.unblockFetchReq",
8622 args: ["{that}"]
8623 },
8624 "onFetchError.unblock": {
8625 listener: "fluid.remoteModelComponent.unblockFetchReq",
8626 args: ["{that}"]
8627 },
8628 "afterWrite.updateRemoteModel": {
8629 listener: "fluid.remoteModelComponent.updateRemoteFromLocal",
8630 args: ["{that}"]
8631 },
8632 "afterWrite.unblock": {
8633 changePath: "requestInFlight",
8634 value: false,
8635 priority: "after:updateRemoteModel"
8636 },
8637 "onWriteError.unblock": {
8638 changePath: "requestInFlight",
8639 value: false
8640 }
8641 },
8642 invokers: {
8643 fetch: {
8644 funcName: "fluid.remoteModelComponent.fetch",
8645 args: ["{that}"]
8646 },
8647 fetchImpl: "fluid.notImplemented",
8648 write: {
8649 funcName: "fluid.remoteModelComponent.write",
8650 args: ["{that}"]
8651 },
8652 writeImpl: "fluid.notImplemented"
8653 }
8654 });
8655
8656 fluid.remoteModelComponent.launchPendingRequest = function (that) {
8657 if (!that.model.requestInFlight) {
8658 if (that.pendingRequests.fetch) {
8659 that.fetch();
8660 } else if (that.pendingRequests.write) {
8661 that.write();
8662 }
8663 }
8664 };
8665
8666 fluid.remoteModelComponent.updateModelFromFetch = function (that, fetchedModel) {
8667 var remoteChanges = fluid.modelPairToChanges(fetchedModel, that.model.remote, "local");
8668 var localChanges = fluid.modelPairToChanges(that.model.local, that.model.remote, "local");
8669 var changes = remoteChanges.concat(localChanges);
8670
8671 // perform model updates in a single transaction
8672 var transaction = that.applier.initiate();
8673 transaction.fireChangeRequest({path: "local", type: "DELETE"}); // clear old local model
8674 transaction.change("local", that.model.remote); // reset local model to the base for applying changes.
8675 transaction.fireChangeRequest({path: "remote", type: "DELETE"}); // clear old remote model
8676 transaction.change("remote", fetchedModel); // update remote model to fetched changes.
8677 fluid.fireChanges(transaction, changes); // apply changes from remote and local onto base model.
8678 transaction.commit(); // submit transaction
8679 };
8680
8681 fluid.remoteModelComponent.updateRemoteFromLocal = function (that) {
8682 // perform model updates in a single transaction
8683 var transaction = that.applier.initiate();
8684 transaction.fireChangeRequest({path: "remote", type: "DELETE"}); // clear old remote model
8685 transaction.change("remote", that.model.local); // update remote model to local changes.
8686 transaction.commit(); // submit transaction
8687 };
8688
8689 /*
8690 * Similar to fluid.promise.makeSequenceStrategy from FluidPromises.js; however, rather than passing along the
8691 * result from one listener in the sequence to the next, the original payload is always passed to each listener.
8692 * In this way, the synthetic events are handled like typical events, but a promise can be resolved/rejected at the
8693 * end of the sequence.
8694 */
8695 fluid.remoteModelComponent.makeSequenceStrategy = function (payload) {
8696 return {
8697 invokeNext: function (that) {
8698 var lisrec = that.sources[that.index];
8699 lisrec.listener = fluid.event.resolveListener(lisrec.listener);
8700 var value = lisrec.listener.apply(null, [payload, that.options]);
8701 return value;
8702 },
8703 resolveResult: function () {
8704 return payload;
8705 }
8706 };
8707 };
8708
8709 fluid.remoteModelComponent.makeSequence = function (listeners, payload, options) {
8710 var sequencer = fluid.promise.makeSequencer(listeners, options, fluid.remoteModelComponent.makeSequenceStrategy(payload));
8711 fluid.promise.resumeSequence(sequencer);
8712 return sequencer;
8713 };
8714
8715 fluid.remoteModelComponent.fireEventSequence = function (event, payload, options) {
8716 var listeners = fluid.makeArray(event.sortedListeners);
8717 var sequence = fluid.remoteModelComponent.makeSequence(listeners, payload, options);
8718 return sequence.promise;
8719 };
8720
8721 /**
8722 * Adds a fetch request and returns a promise.
8723 *
8724 * Only one request can be in flight (processing) at a time. If a write request is in flight, the fetch will be
8725 * queued. If a fetch request is already in queue/flight, the result of that request will be passed along to the
8726 * current fetch request. When a fetch request is in flight , it will trigger the fetchImpl invoker to perform the
8727 * actual request.
8728 *
8729 * Two synthetic events, onFetch and afterFetch, are fired during the processing of a fetch. onFetch can be used to
8730 * perform any necessary actions before running fetchImpl. afterFetch can be used to perform any necessary actions
8731 * after running fetchImpl (e.g. updating the model, unblocking the queue). If promises returned from onFetch, afterFetch, or
8732 * fetchImpl are rejected, the onFetchError event will be fired.
8733 *
8734 * @param {Object} that - The component itself.
8735 * @return {Promise} - A promise that will be resolved with the fetched value or rejected if there is an error.
8736 */
8737 fluid.remoteModelComponent.fetch = function (that) {
8738 var promise = fluid.promise();
8739 var activePromise;
8740
8741 if (that.pendingRequests.fetch) {
8742 activePromise = that.pendingRequests.fetch;
8743 fluid.promise.follow(activePromise, promise);
8744 } else {
8745 activePromise = promise;
8746 that.pendingRequests.fetch = promise;
8747 }
8748
8749 if (!that.model.requestInFlight) {
8750 var onFetchSeqPromise = fluid.remoteModelComponent.fireEventSequence(that.events.onFetch);
8751 onFetchSeqPromise.then(function () {
8752 that.applier.change("requestInFlight", true);
8753 var reqPromise = that.fetchImpl();
8754 reqPromise.then(function (data) {
8755 var afterFetchSeqPromise = fluid.remoteModelComponent.fireEventSequence(that.events.afterFetch, data);
8756 fluid.promise.follow(afterFetchSeqPromise, activePromise);
8757 }, that.events.onFetchError.fire);
8758
8759 }, that.events.onFetchError.fire);
8760 }
8761 return promise;
8762 };
8763
8764 fluid.remoteModelComponent.unblockFetchReq = function (that) {
8765 that.pendingRequests.fetch = null;
8766 that.applier.change("requestInFlight", false);
8767 };
8768
8769 /**
8770 * Adds a write request and returns a promise.
8771 *
8772 * Only one request can be in flight (processing) at a time. If a fetch or write request is in flight, the write will
8773 * be queued. If a write request is already in queue, the result of that request will be passed along to the current
8774 * write request. When a write request is in flight , it will trigger the writeImpl invoker to perform the
8775 * actual request.
8776 *
8777 * Two synthetic events, onWrite and afterWrite, are fired during the processing of a write. onWrite can be used to
8778 * perform any necessary actions before running writeImpl (e.g. performing a fetch). afterWrite can be used to perform any necessary actions
8779 * after running writeImpl (e.g. unblocking the queue, performing a fetch). If promises returned from onWrite, afterWrite, or
8780 * writeImpl are rejected, the onWriteError event will be fired.
8781 *
8782 * @param {Object} that - The component itself.
8783 * @return {Promise} - A promise that will be resolved when the value is written or rejected if there is an error.
8784 */
8785 fluid.remoteModelComponent.write = function (that) {
8786 var promise = fluid.promise();
8787 var activePromise;
8788
8789 if (that.pendingRequests.write) {
8790 activePromise = that.pendingRequests.write;
8791 fluid.promise.follow(that.pendingRequests.write, promise);
8792 } else {
8793 activePromise = promise;
8794 }
8795
8796 if (that.model.requestInFlight) {
8797 that.pendingRequests.write = activePromise;
8798 } else {
8799 var onWriteSeqPromise = fluid.remoteModelComponent.fireEventSequence(that.events.onWrite);
8800 onWriteSeqPromise.then(function () {
8801 that.applier.change("requestInFlight", true);
8802 that.pendingRequests.write = null;
8803
8804 if (fluid.model.diff(that.model.local, that.model.remote)) {
8805 var afterWriteSeqPromise = fluid.remoteModelComponent.fireEventSequence(that.events.afterWrite, that.model.local);
8806 fluid.promise.follow(afterWriteSeqPromise, activePromise);
8807 } else {
8808 var reqPromise = that.writeImpl(that.model.local);
8809 reqPromise.then(function (data) {
8810 var afterWriteSeqPromise = fluid.remoteModelComponent.fireEventSequence(that.events.afterWrite, data);
8811 fluid.promise.follow(afterWriteSeqPromise, activePromise);
8812 }, that.events.onWriteError.fire);;
8813 }
8814 }, that.events.onWriteError.fire);
8815 }
8816
8817 return promise;
8818 };
8819
8820})(jQuery, fluid_3_0_0);
8821;
8822/*
8823Copyright The Infusion copyright holders
8824See the AUTHORS.md file at the top-level directory of this distribution and at
8825https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
8826
8827Licensed under the Educational Community License (ECL), Version 2.0 or the New
8828BSD license. You may not use this file except in compliance with one these
8829Licenses.
8830
8831You may obtain a copy of the ECL 2.0 License and BSD License at
8832https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
8833*/
8834
8835var fluid_3_0_0 = fluid_3_0_0 || {};
8836
8837(function ($, fluid) {
8838 "use strict";
8839
8840 fluid.registerNamespace("fluid.model.transform");
8841
8842 /** Grade definitions for standard transformation function hierarchy **/
8843
8844 fluid.defaults("fluid.transformFunction", {
8845 gradeNames: "fluid.function"
8846 });
8847
8848 // uses standard layout and workflow involving inputPath - an undefined input value
8849 // will short-circuit the evaluation
8850 fluid.defaults("fluid.standardInputTransformFunction", {
8851 gradeNames: "fluid.transformFunction"
8852 });
8853
8854 fluid.defaults("fluid.standardOutputTransformFunction", {
8855 gradeNames: "fluid.transformFunction"
8856 });
8857
8858 // defines a set of options "inputVariables" referring to its inputs, which are converted
8859 // to functions that the transform may explicitly use to demand the input value
8860 fluid.defaults("fluid.multiInputTransformFunction", {
8861 gradeNames: "fluid.transformFunction"
8862 });
8863
8864 // uses the standard layout and workflow involving inputPath and outputPath
8865 fluid.defaults("fluid.standardTransformFunction", {
8866 gradeNames: ["fluid.standardInputTransformFunction", "fluid.standardOutputTransformFunction"]
8867 });
8868
8869 fluid.defaults("fluid.lens", {
8870 gradeNames: "fluid.transformFunction",
8871 invertConfiguration: null
8872 // this function method returns "inverted configuration" rather than actually performing inversion
8873 // TODO: harmonise with strategy used in VideoPlayer_framework.js
8874 });
8875
8876 /***********************************
8877 * Base utilities for transformers *
8878 ***********************************/
8879
8880 // unsupported, NON-API function
8881 fluid.model.transform.pathToRule = function (inputPath) {
8882 return {
8883 transform: {
8884 type: "fluid.transforms.value",
8885 inputPath: inputPath
8886 }
8887 };
8888 };
8889
8890 // unsupported, NON-API function
8891 fluid.model.transform.literalValueToRule = function (input) {
8892 return {
8893 transform: {
8894 type: "fluid.transforms.literalValue",
8895 input: input
8896 }
8897 };
8898 };
8899
8900 /* Accepts two fully escaped paths, either of which may be empty or null */
8901 fluid.model.composePaths = function (prefix, suffix) {
8902 prefix = prefix === 0 ? "0" : prefix || "";
8903 suffix = suffix === 0 ? "0" : suffix || "";
8904 return !prefix ? suffix : (!suffix ? prefix : prefix + "." + suffix);
8905 };
8906
8907 fluid.model.transform.accumulateInputPath = function (inputPath, transformer, paths) {
8908 if (inputPath !== undefined) {
8909 paths.push(fluid.model.composePaths(transformer.inputPrefix, inputPath));
8910 }
8911 };
8912
8913 fluid.model.transform.accumulateStandardInputPath = function (input, transformSpec, transformer, paths) {
8914 fluid.model.transform.getValue(undefined, transformSpec[input], transformer);
8915 fluid.model.transform.accumulateInputPath(transformSpec[input + "Path"], transformer, paths);
8916 };
8917
8918 fluid.model.transform.accumulateMultiInputPaths = function (inputVariables, transformSpec, transformer, paths) {
8919 fluid.each(inputVariables, function (v, k) {
8920 fluid.model.transform.accumulateStandardInputPath(k, transformSpec, transformer, paths);
8921 });
8922 };
8923
8924 fluid.model.transform.getValue = function (inputPath, value, transformer) {
8925 var togo;
8926 if (inputPath !== undefined) { // NB: We may one day want to reverse the crazy jQuery-like convention that "no path means root path"
8927 togo = fluid.get(transformer.source, fluid.model.composePaths(transformer.inputPrefix, inputPath), transformer.resolverGetConfig);
8928 }
8929 if (togo === undefined) {
8930 // FLUID-5867 - actually helpful behaviour here rather than the insane original default of expecting a short-form value document
8931 togo = fluid.isPrimitive(value) ? value :
8932 ("literalValue" in value ? value.literalValue :
8933 (value.transform === undefined ? value : transformer.expand(value)));
8934 }
8935 return togo;
8936 };
8937
8938 // distinguished value which indicates that a transformation rule supplied a
8939 // non-default output path, and so the user should be prevented from making use of it
8940 // in a compound transform definition
8941 fluid.model.transform.NONDEFAULT_OUTPUT_PATH_RETURN = {};
8942
8943 fluid.model.transform.setValue = function (userOutputPath, value, transformer) {
8944 // avoid crosslinking to input object - this might be controlled by a "nocopy" option in future
8945 var toset = fluid.copy(value);
8946 var outputPath = fluid.model.composePaths(transformer.outputPrefix, userOutputPath);
8947 // TODO: custom resolver config here to create non-hash output model structure
8948 if (toset !== undefined) {
8949 transformer.applier.change(outputPath, toset);
8950 }
8951 return userOutputPath ? fluid.model.transform.NONDEFAULT_OUTPUT_PATH_RETURN : toset;
8952 };
8953
8954 /* Resolves the <key> given as parameter by looking up the path <key>Path in the object
8955 * to be transformed. If not present, it resolves the <key> by using the literal value if primitive,
8956 * or expanding otherwise. <def> defines the default value if unableto resolve the key. If no
8957 * default value is given undefined is returned
8958 */
8959 fluid.model.transform.resolveParam = function (transformSpec, transformer, key, def) {
8960 var val = fluid.model.transform.getValue(transformSpec[key + "Path"], transformSpec[key], transformer);
8961 return (val !== undefined) ? val : def;
8962 };
8963
8964 // Compute a "match score" between two pieces of model material, with 0 indicating a complete mismatch, and
8965 // higher values indicating increasingly good matches
8966 fluid.model.transform.matchValue = function (expected, actual, partialMatches) {
8967 var stats = {changes: 0, unchanged: 0, changeMap: {}};
8968 fluid.model.diff(expected, actual, stats);
8969 // i) a pair with 0 matches counts for 0 in all cases
8970 // ii) without "partial match mode" (the default), we simply count matches, with any mismatch giving 0
8971 // iii) with "partial match mode", a "perfect score" in the top 24 bits is
8972 // penalised for each mismatch, with a positive score of matches store in the bottom 24 bits
8973 return stats.unchanged === 0 ? 0
8974 : (partialMatches ? 0xffffff000000 - 0x1000000 * stats.changes + stats.unchanged :
8975 (stats.changes ? 0 : 0xffffff000000 + stats.unchanged));
8976 };
8977
8978 fluid.model.transform.invertPaths = function (transformSpec, transformer) {
8979 // TODO: this will not behave correctly in the face of compound "input" which contains
8980 // further transforms
8981 var oldOutput = fluid.model.composePaths(transformer.outputPrefix, transformSpec.outputPath);
8982 transformSpec.outputPath = fluid.model.composePaths(transformer.inputPrefix, transformSpec.inputPath);
8983 transformSpec.inputPath = oldOutput;
8984 return transformSpec;
8985 };
8986
8987
8988 // TODO: prefixApplier is a transform which is currently unused and untested
8989 fluid.model.transform.prefixApplier = function (transformSpec, transformer) {
8990 if (transformSpec.inputPrefix) {
8991 transformer.inputPrefixOp.push(transformSpec.inputPrefix);
8992 }
8993 if (transformSpec.outputPrefix) {
8994 transformer.outputPrefixOp.push(transformSpec.outputPrefix);
8995 }
8996 transformer.expand(transformSpec.input);
8997 if (transformSpec.inputPrefix) {
8998 transformer.inputPrefixOp.pop();
8999 }
9000 if (transformSpec.outputPrefix) {
9001 transformer.outputPrefixOp.pop();
9002 }
9003 };
9004
9005 fluid.defaults("fluid.model.transform.prefixApplier", {
9006 gradeNames: ["fluid.transformFunction"]
9007 });
9008
9009 // unsupported, NON-API function
9010 fluid.model.makePathStack = function (transform, prefixName) {
9011 var stack = transform[prefixName + "Stack"] = [];
9012 transform[prefixName] = "";
9013 return {
9014 push: function (prefix) {
9015 var newPath = fluid.model.composePaths(transform[prefixName], prefix);
9016 stack.push(transform[prefixName]);
9017 transform[prefixName] = newPath;
9018 },
9019 pop: function () {
9020 transform[prefixName] = stack.pop();
9021 }
9022 };
9023 };
9024
9025 // unsupported, NON-API function
9026 fluid.model.transform.doTransform = function (transformSpec, transformer, transformOpts) {
9027 var expdef = transformOpts.defaults;
9028 var transformFn = fluid.getGlobalValue(transformOpts.typeName);
9029 if (typeof(transformFn) !== "function") {
9030 fluid.fail("Transformation record specifies transformation function with name " +
9031 transformSpec.type + " which is not a function - ", transformFn);
9032 }
9033 if (!fluid.hasGrade(expdef, "fluid.transformFunction")) {
9034 // If no suitable grade is set up, assume that it is intended to be used as a standardTransformFunction
9035 expdef = fluid.defaults("fluid.standardTransformFunction");
9036 }
9037 var transformArgs = [transformSpec, transformer];
9038 if (fluid.hasGrade(expdef, "fluid.multiInputTransformFunction")) {
9039 var inputs = {};
9040 fluid.each(expdef.inputVariables, function (v, k) {
9041 inputs[k] = function () {
9042 var input = fluid.model.transform.getValue(transformSpec[k + "Path"], transformSpec[k], transformer);
9043 // TODO: This is a mess, null might perfectly well be a possible default
9044 // if no match, assign default if one exists (v != null)
9045 input = (input === undefined && v !== null) ? v : input;
9046 return input;
9047 };
9048 });
9049 transformArgs.unshift(inputs);
9050 }
9051 if (fluid.hasGrade(expdef, "fluid.standardInputTransformFunction")) {
9052 if (!("input" in transformSpec) && !("inputPath" in transformSpec)) {
9053 fluid.fail("Error in transform specification. Either \"input\" or \"inputPath\" must be specified for a standardInputTransformFunction: received ", transformSpec);
9054 }
9055 var expanded = fluid.model.transform.getValue(transformSpec.inputPath, transformSpec.input, transformer);
9056
9057 transformArgs.unshift(expanded);
9058 // if the function has no input, the result is considered undefined, and this is returned
9059 if (expanded === undefined) {
9060 return undefined;
9061 }
9062 }
9063 var transformed = transformFn.apply(null, transformArgs);
9064 if (fluid.hasGrade(expdef, "fluid.standardOutputTransformFunction")) {
9065 // "doOutput" flag is currently set nowhere, but could be used in future
9066 var outputPath = transformSpec.outputPath !== undefined ? transformSpec.outputPath : (transformOpts.doOutput ? "" : undefined);
9067 if (outputPath !== undefined && transformed !== undefined) {
9068 //If outputPath is given in the expander we want to:
9069 // (1) output to the document
9070 // (2) return undefined, to ensure that expanders higher up in the hierarchy doesn't attempt to output it again
9071 fluid.model.transform.setValue(transformSpec.outputPath, transformed, transformer);
9072 transformed = undefined;
9073 }
9074 }
9075 return transformed;
9076 };
9077
9078 // OLD PATHUTIL utilities: Rescued from old DataBinding implementation to support obsolete "schema" scheme for transforms - all of this needs to be rethought
9079 var globalAccept = [];
9080
9081 fluid.registerNamespace("fluid.pathUtil");
9082
9083 /* Parses a path segment, following escaping rules, starting from character index i in the supplied path */
9084 fluid.pathUtil.getPathSegment = function (path, i) {
9085 fluid.pathUtil.getPathSegmentImpl(globalAccept, path, i);
9086 return globalAccept[0];
9087 };
9088 /* Returns just the head segment of an EL path */
9089 fluid.pathUtil.getHeadPath = function (path) {
9090 return fluid.pathUtil.getPathSegment(path, 0);
9091 };
9092
9093 /* Returns all of an EL path minus its first segment - if the path consists of just one segment, returns "" */
9094 fluid.pathUtil.getFromHeadPath = function (path) {
9095 var firstdot = fluid.pathUtil.getPathSegmentImpl(null, path, 0);
9096 return firstdot === path.length ? "" : path.substring(firstdot + 1);
9097 };
9098
9099 /** Determines whether a particular EL path matches a given path specification.
9100 * The specification consists of a path with optional wildcard segments represented by "*".
9101 * @param {String} spec - The specification to be matched
9102 * @param {String} path - The path to be tested
9103 * @param {Boolean} exact - Whether the path must exactly match the length of the specification in
9104 * terms of path segments in order to count as match. If exact is falsy, short specifications will
9105 * match all longer paths as if they were padded out with "*" segments
9106 * @return {Array|null} - An array of {String} path segments which matched the specification, or <code>null</code> if there was no match.
9107 */
9108 fluid.pathUtil.matchPath = function (spec, path, exact) {
9109 var togo = [];
9110 while (true) {
9111 if (((path === "") ^ (spec === "")) && exact) {
9112 return null;
9113 }
9114 // FLUID-4625 - symmetry on spec and path is actually undesirable, but this
9115 // quickly avoids at least missed notifications - improved (but slower)
9116 // implementation should explode composite changes
9117 if (!spec || !path) {
9118 break;
9119 }
9120 var spechead = fluid.pathUtil.getHeadPath(spec);
9121 var pathhead = fluid.pathUtil.getHeadPath(path);
9122 // if we fail to match on a specific component, fail.
9123 if (spechead !== "*" && spechead !== pathhead) {
9124 return null;
9125 }
9126 togo.push(pathhead);
9127 spec = fluid.pathUtil.getFromHeadPath(spec);
9128 path = fluid.pathUtil.getFromHeadPath(path);
9129 }
9130 return togo;
9131 };
9132
9133 // unsupported, NON-API function
9134 fluid.model.transform.expandWildcards = function (transformer, source) {
9135 fluid.each(source, function (value, key) {
9136 var q = transformer.queuedTransforms;
9137 transformer.pathOp.push(fluid.pathUtil.escapeSegment(key.toString()));
9138 for (var i = 0; i < q.length; ++i) {
9139 if (fluid.pathUtil.matchPath(q[i].matchPath, transformer.path, true)) {
9140 var esCopy = fluid.copy(q[i].transformSpec);
9141 if (esCopy.inputPath === undefined || fluid.model.transform.hasWildcard(esCopy.inputPath)) {
9142 esCopy.inputPath = "";
9143 }
9144 // TODO: allow some kind of interpolation for output path
9145 // TODO: Also, we now require outputPath to be specified in these cases for output to be produced as well.. Is that something we want to continue with?
9146 transformer.inputPrefixOp.push(transformer.path);
9147 transformer.outputPrefixOp.push(transformer.path);
9148 var transformOpts = fluid.model.transform.lookupType(esCopy.type);
9149 var result = fluid.model.transform.doTransform(esCopy, transformer, transformOpts);
9150 if (result !== undefined) {
9151 fluid.model.transform.setValue(null, result, transformer);
9152 }
9153 transformer.outputPrefixOp.pop();
9154 transformer.inputPrefixOp.pop();
9155 }
9156 }
9157 if (!fluid.isPrimitive(value)) {
9158 fluid.model.transform.expandWildcards(transformer, value);
9159 }
9160 transformer.pathOp.pop();
9161 });
9162 };
9163
9164 // unsupported, NON-API function
9165 fluid.model.transform.hasWildcard = function (path) {
9166 return typeof(path) === "string" && path.indexOf("*") !== -1;
9167 };
9168
9169 // unsupported, NON-API function
9170 fluid.model.transform.maybePushWildcard = function (transformSpec, transformer) {
9171 var hw = fluid.model.transform.hasWildcard;
9172 var matchPath;
9173 if (hw(transformSpec.inputPath)) {
9174 matchPath = fluid.model.composePaths(transformer.inputPrefix, transformSpec.inputPath);
9175 }
9176 else if (hw(transformer.outputPrefix) || hw(transformSpec.outputPath)) {
9177 matchPath = fluid.model.composePaths(transformer.outputPrefix, transformSpec.outputPath);
9178 }
9179
9180 if (matchPath) {
9181 transformer.queuedTransforms.push({transformSpec: transformSpec, outputPrefix: transformer.outputPrefix, inputPrefix: transformer.inputPrefix, matchPath: matchPath});
9182 return true;
9183 }
9184 return false;
9185 };
9186
9187 fluid.model.sortByKeyLength = function (inObject) {
9188 var keys = fluid.keys(inObject);
9189 return keys.sort(fluid.compareStringLength(true));
9190 };
9191
9192 // Three handler functions operating the (currently) three different processing modes
9193 // unsupported, NON-API function
9194 fluid.model.transform.handleTransformStrategy = function (transformSpec, transformer, transformOpts) {
9195 if (fluid.model.transform.maybePushWildcard(transformSpec, transformer)) {
9196 return;
9197 }
9198 else {
9199 return fluid.model.transform.doTransform(transformSpec, transformer, transformOpts);
9200 }
9201 };
9202 // unsupported, NON-API function
9203 fluid.model.transform.handleInvertStrategy = function (transformSpec, transformer, transformOpts) {
9204 transformSpec = fluid.copy(transformSpec);
9205 // if we have a standardTransformFunction we can switch input and output arguments:
9206 if (fluid.hasGrade(transformOpts.defaults, "fluid.standardTransformFunction")) {
9207 transformSpec = fluid.model.transform.invertPaths(transformSpec, transformer);
9208 }
9209 var invertor = transformOpts.defaults && transformOpts.defaults.invertConfiguration;
9210 if (invertor) {
9211 var inverted = fluid.invokeGlobalFunction(invertor, [transformSpec, transformer]);
9212 transformer.inverted.push(inverted);
9213 } else {
9214 transformer.inverted.push(fluid.model.transform.uninvertibleTransform);
9215 }
9216 };
9217
9218 // unsupported, NON-API function
9219 fluid.model.transform.handleCollectStrategy = function (transformSpec, transformer, transformOpts) {
9220 var defaults = transformOpts.defaults;
9221 var standardInput = fluid.hasGrade(defaults, "fluid.standardInputTransformFunction");
9222 var multiInput = fluid.hasGrade(defaults, "fluid.multiInputTransformFunction");
9223
9224 if (standardInput) {
9225 fluid.model.transform.accumulateStandardInputPath("input", transformSpec, transformer, transformer.inputPaths);
9226 }
9227 if (multiInput) {
9228 fluid.model.transform.accumulateMultiInputPaths(defaults.inputVariables, transformSpec, transformer, transformer.inputPaths);
9229 }
9230 var collector = defaults.collectInputPaths;
9231 if (collector) {
9232 var collected = fluid.makeArray(fluid.invokeGlobalFunction(collector, [transformSpec, transformer]));
9233 Array.prototype.push.apply(transformer.inputPaths, collected); // push all elements of collected onto inputPaths
9234 }
9235 };
9236
9237 fluid.model.transform.lookupType = function (typeName, transformSpec) {
9238 if (!typeName) {
9239 fluid.fail("Transformation record is missing a type name: ", transformSpec);
9240 }
9241 if (typeName.indexOf(".") === -1) {
9242 typeName = "fluid.transforms." + typeName;
9243 }
9244 var defaults = fluid.defaults(typeName);
9245 return { defaults: defaults, typeName: typeName};
9246 };
9247
9248 // unsupported, NON-API function
9249 fluid.model.transform.processRule = function (rule, transformer) {
9250 if (typeof(rule) === "string") {
9251 rule = fluid.model.transform.pathToRule(rule);
9252 }
9253 // special dispensation to allow "literalValue" to escape any value
9254 else if (rule.literalValue !== undefined) {
9255 rule = fluid.model.transform.literalValueToRule(rule.literalValue);
9256 }
9257 var togo;
9258 if (rule.transform) {
9259 var transformSpec, transformOpts;
9260 if (fluid.isArrayable(rule.transform)) {
9261 // if the transform holds an array, each transformer within that is responsible for its own output
9262 var transforms = rule.transform;
9263 togo = undefined;
9264 for (var i = 0; i < transforms.length; ++i) {
9265 transformSpec = transforms[i];
9266 transformOpts = fluid.model.transform.lookupType(transformSpec.type);
9267 transformer.transformHandler(transformSpec, transformer, transformOpts);
9268 }
9269 } else {
9270 // else we just have a normal single transform which will return 'undefined' as a flag to defeat cascading output
9271 transformSpec = rule.transform;
9272 transformOpts = fluid.model.transform.lookupType(transformSpec.type);
9273 togo = transformer.transformHandler(transformSpec, transformer, transformOpts);
9274 }
9275 }
9276 // if rule is an array, save path for later use in schema strategy on final applier (so output will be interpreted as array)
9277 if (fluid.isArrayable(rule)) {
9278 transformer.collectedFlatSchemaOpts = transformer.collectedFlatSchemaOpts || {};
9279 transformer.collectedFlatSchemaOpts[transformer.outputPrefix] = "array";
9280 }
9281 fluid.each(rule, function (value, key) {
9282 if (key !== "transform") {
9283 transformer.outputPrefixOp.push(key);
9284 var togo = transformer.expand(value, transformer);
9285 // Value expanders and arrays as rules implicitly output, unless they have nothing (undefined) to output
9286 if (togo !== undefined) {
9287 fluid.model.transform.setValue(null, togo, transformer);
9288 // ensure that expanders further up does not try to output this value as well.
9289 togo = undefined;
9290 }
9291 transformer.outputPrefixOp.pop();
9292 }
9293 });
9294 return togo;
9295 };
9296
9297 // unsupported, NON-API function
9298 // 3rd arg is disused by the framework and always defaults to fluid.model.transform.processRule
9299 fluid.model.transform.makeStrategy = function (transformer, handleFn, transformFn) {
9300 transformFn = transformFn || fluid.model.transform.processRule;
9301 transformer.expand = function (rules) {
9302 return transformFn(rules, transformer);
9303 };
9304 transformer.outputPrefixOp = fluid.model.makePathStack(transformer, "outputPrefix");
9305 transformer.inputPrefixOp = fluid.model.makePathStack(transformer, "inputPrefix");
9306 transformer.transformHandler = handleFn;
9307 };
9308
9309 /* A special, empty, transform document representing the inversion of a transformation which does not not have an inverse
9310 */
9311 fluid.model.transform.uninvertibleTransform = Object.freeze({});
9312
9313 /** Accepts a transformation document, and returns its inverse if all of its constituent transforms have inverses
9314 * defined via their individual invertConfiguration functions, or else `fluid.model.transform.uninvertibleTransform`
9315 * if any of them do not.
9316 * Note that this algorithm will give faulty results in many cases of compound transformation documents.
9317 * @param {Transform} rules - The model transformation document to be inverted
9318 * @return {Transform} The inverse transformation document if it can be computed easily, or
9319 * `fluid.model.transform.uninvertibleTransform` if it is clear that it cannot.
9320 */
9321 fluid.model.transform.invertConfiguration = function (rules) {
9322 var transformer = {
9323 inverted: []
9324 };
9325 fluid.model.transform.makeStrategy(transformer, fluid.model.transform.handleInvertStrategy);
9326 transformer.expand(rules);
9327 var invertible = transformer.inverted.indexOf(fluid.model.transform.uninvertibleTransform) === -1;
9328 return invertible ? {
9329 transform: transformer.inverted
9330 } : fluid.model.transform.uninvertibleTransform;
9331 };
9332
9333 /** Compute the paths which will be read from the input document of the supplied transformation if it were operated.
9334 *
9335 * @param {Transform} rules - The transformation for which the input paths are to be computed.
9336 * @return {Array} - An array of paths which will be read by the document.
9337 */
9338 fluid.model.transform.collectInputPaths = function (rules) {
9339 var transformer = {
9340 inputPaths: []
9341 };
9342 fluid.model.transform.makeStrategy(transformer, fluid.model.transform.handleCollectStrategy);
9343 transformer.expand(rules);
9344 // Deduplicate input paths
9345 var inputPathHash = fluid.arrayToHash(transformer.inputPaths);
9346 return Object.keys(inputPathHash);
9347 };
9348
9349 // unsupported, NON-API function
9350 fluid.model.transform.flatSchemaStrategy = function (flatSchema, getConfig) {
9351 var keys = fluid.model.sortByKeyLength(flatSchema);
9352 return function (root, segment, index, segs) {
9353 var path = getConfig.parser.compose.apply(null, segs.slice(0, index));
9354 // TODO: clearly this implementation could be much more efficient
9355 for (var i = 0; i < keys.length; ++i) {
9356 var key = keys[i];
9357 if (fluid.pathUtil.matchPath(key, path, true) !== null) {
9358 return flatSchema[key];
9359 }
9360 }
9361 };
9362 };
9363
9364 // unsupported, NON-API function
9365 fluid.model.transform.defaultSchemaValue = function (schemaValue) {
9366 var type = fluid.isPrimitive(schemaValue) ? schemaValue : schemaValue.type;
9367 return type === "array" ? [] : {};
9368 };
9369
9370 // unsupported, NON-API function
9371 fluid.model.transform.isomorphicSchemaStrategy = function (source, getConfig) {
9372 return function (root, segment, index, segs) {
9373 var existing = fluid.get(source, segs.slice(0, index), getConfig);
9374 return fluid.isArrayable(existing) ? "array" : "object";
9375 };
9376 };
9377
9378 // unsupported, NON-API function
9379 fluid.model.transform.decodeStrategy = function (source, options, getConfig) {
9380 if (options.isomorphic) {
9381 return fluid.model.transform.isomorphicSchemaStrategy(source, getConfig);
9382 }
9383 else if (options.flatSchema) {
9384 return fluid.model.transform.flatSchemaStrategy(options.flatSchema, getConfig);
9385 }
9386 };
9387
9388 // unsupported, NON-API function
9389 fluid.model.transform.schemaToCreatorStrategy = function (strategy) {
9390 return function (root, segment, index, segs) {
9391 if (root[segment] === undefined) {
9392 var schemaValue = strategy(root, segment, index, segs);
9393 root[segment] = fluid.model.transform.defaultSchemaValue(schemaValue);
9394 return root[segment];
9395 }
9396 };
9397 };
9398
9399 /* Transforms a model by a sequence of rules. Parameters as for fluid.model.transform,
9400 * only with an array accepted for "rules"
9401 */
9402 fluid.model.transform.sequence = function (source, rules, options) {
9403 for (var i = 0; i < rules.length; ++i) {
9404 source = fluid.model.transform(source, rules[i], options);
9405 }
9406 return source;
9407 };
9408
9409 fluid.model.compareByPathLength = function (changea, changeb) {
9410 var pdiff = changea.path.length - changeb.path.length;
9411 return pdiff === 0 ? changea.sequence - changeb.sequence : pdiff;
9412 };
9413
9414 /* Fires an accumulated set of change requests in increasing order of target pathlength */
9415 fluid.model.fireSortedChanges = function (changes, applier) {
9416 changes.sort(fluid.model.compareByPathLength);
9417 fluid.fireChanges(applier, changes);
9418 };
9419
9420 /**
9421 * Transforms a model based on a specified expansion rules objects.
9422 * Rules objects take the form of:
9423 * {
9424 * "target.path": "value.el.path" || {
9425 * transform: {
9426 * type: "transform.function.path",
9427 * ...
9428 * }
9429 * }
9430 * }
9431 *
9432 * @param {Object} source - the model to transform
9433 * @param {Object} rules - a rules object containing instructions on how to transform the model
9434 * @param {Object} options - a set of rules governing the transformations. At present this may contain
9435 * the values <code>isomorphic: true</code> indicating that the output model is to be governed by the
9436 * same schema found in the input model, or <code>flatSchema</code> holding a flat schema object which
9437 * consists of a hash of EL path specifications with wildcards, to the values "array"/"object" defining
9438 * the schema to be used to construct missing trunk values.
9439 * @return {Any} The transformed model.
9440 */
9441 fluid.model.transformWithRules = function (source, rules, options) {
9442 options = options || {};
9443
9444 var getConfig = fluid.model.escapedGetConfig;
9445 var setConfig = fluid.model.escapedSetConfig;
9446
9447 var schemaStrategy = fluid.model.transform.decodeStrategy(source, options, getConfig);
9448
9449 var transformer = {
9450 source: source,
9451 target: {
9452 // TODO: This should default to undefined to allow return of primitives, etc.
9453 model: schemaStrategy ? fluid.model.transform.defaultSchemaValue(schemaStrategy(null, "", 0, [""])) : {}
9454 },
9455 resolverGetConfig: getConfig,
9456 resolverSetConfig: setConfig,
9457 collectedFlatSchemaOpts: undefined, // to hold options for flat schema collected during transforms
9458 queuedChanges: [],
9459 queuedTransforms: [] // TODO: This is used only by wildcard applier - explain its operation
9460 };
9461 fluid.model.transform.makeStrategy(transformer, fluid.model.transform.handleTransformStrategy);
9462 transformer.applier = {
9463 fireChangeRequest: function (changeRequest) {
9464 changeRequest.sequence = transformer.queuedChanges.length;
9465 transformer.queuedChanges.push(changeRequest);
9466 }
9467 };
9468 fluid.bindRequestChange(transformer.applier);
9469
9470 transformer.expand(rules);
9471
9472 var rootSetConfig = fluid.copy(setConfig);
9473 // Modify schemaStrategy if we collected flat schema options for the setConfig of finalApplier
9474 if (transformer.collectedFlatSchemaOpts !== undefined) {
9475 $.extend(transformer.collectedFlatSchemaOpts, options.flatSchema);
9476 schemaStrategy = fluid.model.transform.flatSchemaStrategy(transformer.collectedFlatSchemaOpts, getConfig);
9477 }
9478 rootSetConfig.strategies = [fluid.model.defaultFetchStrategy, schemaStrategy ? fluid.model.transform.schemaToCreatorStrategy(schemaStrategy)
9479 : fluid.model.defaultCreatorStrategy];
9480 transformer.finalApplier = options.finalApplier || fluid.makeHolderChangeApplier(transformer.target, {resolverSetConfig: rootSetConfig});
9481
9482 if (transformer.queuedTransforms.length > 0) {
9483 transformer.typeStack = [];
9484 transformer.pathOp = fluid.model.makePathStack(transformer, "path");
9485 fluid.model.transform.expandWildcards(transformer, source);
9486 }
9487 fluid.model.fireSortedChanges(transformer.queuedChanges, transformer.finalApplier);
9488 return transformer.target.model;
9489 };
9490
9491 $.extend(fluid.model.transformWithRules, fluid.model.transform);
9492 fluid.model.transform = fluid.model.transformWithRules;
9493
9494 /* Utility function to produce a standard options transformation record for a single set of rules */
9495 fluid.transformOne = function (rules) {
9496 return {
9497 transformOptions: {
9498 transformer: "fluid.model.transformWithRules",
9499 config: rules
9500 }
9501 };
9502 };
9503
9504 /* Utility function to produce a standard options transformation record for multiple rules to be applied in sequence */
9505 fluid.transformMany = function (rules) {
9506 return {
9507 transformOptions: {
9508 transformer: "fluid.model.transform.sequence",
9509 config: rules
9510 }
9511 };
9512 };
9513
9514})(jQuery, fluid_3_0_0);
9515;
9516/*
9517Copyright The Infusion copyright holders
9518See the AUTHORS.md file at the top-level directory of this distribution and at
9519https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
9520
9521Licensed under the Educational Community License (ECL), Version 2.0 or the New
9522BSD license. You may not use this file except in compliance with one these
9523Licenses.
9524
9525You may obtain a copy of the ECL 2.0 License and BSD License at
9526https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
9527*/
9528
9529var fluid_3_0_0 = fluid_3_0_0 || {};
9530
9531(function ($, fluid) {
9532 "use strict";
9533
9534 fluid.registerNamespace("fluid.model.transform");
9535 fluid.registerNamespace("fluid.transforms");
9536
9537 /**********************************
9538 * Standard transformer functions *
9539 **********************************/
9540
9541 fluid.defaults("fluid.transforms.value", {
9542 gradeNames: "fluid.standardTransformFunction",
9543 invertConfiguration: "fluid.identity"
9544 });
9545
9546 fluid.transforms.value = fluid.identity;
9547
9548 // Export the use of the "value" transform under the "identity" name for FLUID-5293
9549 fluid.transforms.identity = fluid.transforms.value;
9550 fluid.defaults("fluid.transforms.identity", {
9551 gradeNames: "fluid.transforms.value"
9552 });
9553
9554 // A helpful utility function to be used when a transform's inverse is the identity
9555 fluid.transforms.invertToIdentity = function (transformSpec) {
9556 transformSpec.type = "fluid.transforms.identity";
9557 return transformSpec;
9558 };
9559
9560 fluid.defaults("fluid.transforms.literalValue", {
9561 gradeNames: "fluid.standardOutputTransformFunction"
9562 });
9563
9564 fluid.transforms.literalValue = function (transformSpec) {
9565 return transformSpec.input;
9566 };
9567
9568 fluid.defaults("fluid.transforms.stringToNumber", {
9569 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
9570 invertConfiguration: "fluid.transforms.stringToNumber.invert"
9571 });
9572
9573 fluid.transforms.stringToNumber = function (value) {
9574 var newValue = Number(value);
9575 return isNaN(newValue) ? undefined : newValue;
9576 };
9577
9578 fluid.transforms.stringToNumber.invert = function (transformSpec) {
9579 transformSpec.type = "fluid.transforms.numberToString";
9580 return transformSpec;
9581 };
9582
9583 fluid.defaults("fluid.transforms.numberToString", {
9584 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
9585 invertConfiguration: "fluid.transforms.numberToString.invert"
9586 });
9587
9588 fluid.transforms.numberToString = function (value, transformSpec) {
9589 if (typeof value === "number") {
9590 if (typeof transformSpec.scale === "number" && !isNaN(transformSpec.scale)) {
9591 var rounded = fluid.roundToDecimal(value, transformSpec.scale, transformSpec.method);
9592 return rounded.toString();
9593 } else {
9594 return value.toString();
9595 }
9596 }
9597 };
9598
9599 fluid.transforms.numberToString.invert = function (transformSpec) {
9600 transformSpec.type = "fluid.transforms.stringToNumber";
9601 return transformSpec;
9602 };
9603
9604 fluid.defaults("fluid.transforms.count", {
9605 gradeNames: "fluid.standardTransformFunction"
9606 });
9607
9608 fluid.transforms.count = function (value) {
9609 return fluid.makeArray(value).length;
9610 };
9611
9612
9613 fluid.defaults("fluid.transforms.round", {
9614 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
9615 invertConfiguration: "fluid.transforms.invertToIdentity"
9616 });
9617
9618 fluid.transforms.round = function (value, transformSpec) {
9619 // validation of scale is handled by roundToDecimal
9620 return fluid.roundToDecimal(value, transformSpec.scale, transformSpec.method);
9621 };
9622
9623 fluid.defaults("fluid.transforms.delete", {
9624 gradeNames: "fluid.transformFunction"
9625 });
9626
9627 fluid.transforms["delete"] = function (transformSpec, transformer) {
9628 var outputPath = fluid.model.composePaths(transformer.outputPrefix, transformSpec.outputPath);
9629 transformer.applier.change(outputPath, null, "DELETE");
9630 };
9631
9632
9633 fluid.defaults("fluid.transforms.firstValue", {
9634 gradeNames: "fluid.standardOutputTransformFunction"
9635 });
9636
9637 fluid.transforms.firstValue = function (transformSpec, transformer) {
9638 if (!transformSpec.values || !transformSpec.values.length) {
9639 fluid.fail("firstValue transformer requires an array of values at path named \"values\", supplied", transformSpec);
9640 }
9641 for (var i = 0; i < transformSpec.values.length; i++) {
9642 var value = transformSpec.values[i];
9643 // TODO: problem here - all of these transforms will have their side-effects (setValue) even if only one is chosen
9644 var expanded = transformer.expand(value);
9645 if (expanded !== undefined) {
9646 return expanded;
9647 }
9648 }
9649 };
9650
9651 fluid.defaults("fluid.transforms.linearScale", {
9652 gradeNames: ["fluid.multiInputTransformFunction",
9653 "fluid.standardTransformFunction",
9654 "fluid.lens" ],
9655 invertConfiguration: "fluid.transforms.linearScale.invert",
9656 inputVariables: {
9657 factor: 1,
9658 offset: 0
9659 }
9660 });
9661
9662 /* simple linear transformation */
9663 fluid.transforms.linearScale = function (input, extraInputs) {
9664 var factor = extraInputs.factor();
9665 var offset = extraInputs.offset();
9666
9667 if (typeof(input) !== "number" || typeof(factor) !== "number" || typeof(offset) !== "number") {
9668 return undefined;
9669 }
9670 return input * factor + offset;
9671 };
9672
9673 /* TODO: This inversion doesn't work if the value and factors are given as paths in the source model */
9674 fluid.transforms.linearScale.invert = function (transformSpec) {
9675 // delete the factor and offset paths if present
9676 delete transformSpec.factorPath;
9677 delete transformSpec.offsetPath;
9678
9679 if (transformSpec.factor !== undefined) {
9680 transformSpec.factor = (transformSpec.factor === 0) ? 0 : 1 / transformSpec.factor;
9681 }
9682 if (transformSpec.offset !== undefined) {
9683 transformSpec.offset = -transformSpec.offset * (transformSpec.factor !== undefined ? transformSpec.factor : 1);
9684 }
9685 return transformSpec;
9686 };
9687
9688 fluid.defaults("fluid.transforms.binaryOp", {
9689 gradeNames: [ "fluid.multiInputTransformFunction", "fluid.standardOutputTransformFunction" ],
9690 inputVariables: {
9691 left: null,
9692 right: null
9693 }
9694 });
9695
9696 fluid.transforms.binaryLookup = {
9697 "===": function (a, b) { return fluid.model.isSameValue(a, b); },
9698 "!==": function (a, b) { return !fluid.model.isSameValue(a, b); },
9699 "<=": function (a, b) { return a <= b; },
9700 "<": function (a, b) { return a < b; },
9701 ">=": function (a, b) { return a >= b; },
9702 ">": function (a, b) { return a > b; },
9703 "+": function (a, b) { return a + b; },
9704 "-": function (a, b) { return a - b; },
9705 "*": function (a, b) { return a * b; },
9706 "/": function (a, b) { return a / b; },
9707 "%": function (a, b) { return a % b; },
9708 "&&": function (a, b) { return a && b; },
9709 "||": function (a, b) { return a || b; }
9710 };
9711
9712 fluid.transforms.binaryOp = function (inputs, transformSpec, transformer) {
9713 var left = inputs.left();
9714 var right = inputs.right();
9715
9716 var operator = fluid.model.transform.getValue(undefined, transformSpec.operator, transformer);
9717
9718 var fun = fluid.transforms.binaryLookup[operator];
9719 return (fun === undefined || left === undefined || right === undefined) ?
9720 undefined : fun(left, right);
9721 };
9722
9723 fluid.defaults("fluid.transforms.condition", {
9724 gradeNames: [ "fluid.multiInputTransformFunction", "fluid.standardOutputTransformFunction" ],
9725 inputVariables: {
9726 "true": null,
9727 "false": null,
9728 "condition": null
9729 }
9730 });
9731
9732 fluid.transforms.condition = function (inputs) {
9733 var condition = inputs.condition();
9734 if (condition === null) {
9735 return undefined;
9736 }
9737
9738 return inputs[condition ? "true" : "false"]();
9739 };
9740
9741 fluid.defaults("fluid.transforms.valueMapper", {
9742 gradeNames: ["fluid.lens"],
9743 invertConfiguration: "fluid.transforms.valueMapper.invert",
9744 collectInputPaths: "fluid.transforms.valueMapper.collect"
9745 });
9746
9747 /* unsupported, NON-API function
9748 * sorts by the object's 'matchValue' property, where higher is better.
9749 * Tiebreaking is done via the `index` property, where a lower index takes priority
9750 */
9751 fluid.model.transform.compareMatches = function (speca, specb) {
9752 var matchDiff = specb.matchValue - speca.matchValue;
9753 return matchDiff === 0 ? speca.index - specb.index : matchDiff; // tiebreak using 'index'
9754 };
9755
9756 fluid.transforms.valueMapper = function (transformSpec, transformer) {
9757 if (!transformSpec.match) {
9758 fluid.fail("valueMapper requires an array or hash of matches at path named \"match\", supplied ", transformSpec);
9759 }
9760 var value = fluid.model.transform.getValue(transformSpec.defaultInputPath, transformSpec.defaultInput, transformer);
9761
9762 var matchedEntry = (fluid.isArrayable(transformSpec.match)) ? // long form with array of records?
9763 fluid.transforms.valueMapper.longFormMatch(value, transformSpec, transformer) :
9764 transformSpec.match[value];
9765
9766 if (matchedEntry === undefined) { // if no matches found, default to noMatch
9767 matchedEntry = transformSpec.noMatch;
9768 }
9769
9770 if (matchedEntry === undefined) { // if there was no noMatch directive, return undefined
9771 return;
9772 }
9773
9774 var outputPath = matchedEntry.outputPath === undefined ? transformSpec.defaultOutputPath : matchedEntry.outputPath;
9775 transformer.outputPrefixOp.push(outputPath);
9776
9777 var outputValue;
9778 if (fluid.isPrimitive(matchedEntry)) {
9779 outputValue = matchedEntry;
9780 } else if (matchedEntry.outputUndefinedValue) { // if outputUndefinedValue is set, outputValue `undefined`
9781 outputValue = undefined;
9782 } else {
9783 // get value from outputValue. If none is found set the outputValue to be that of defaultOutputValue (or undefined)
9784 outputValue = fluid.model.transform.resolveParam(matchedEntry, transformer, "outputValue", undefined);
9785 outputValue = (outputValue === undefined) ? transformSpec.defaultOutputValue : outputValue;
9786 }
9787 // output if we have a path and something to output
9788 if (typeof(outputPath) === "string" && outputValue !== undefined) {
9789 fluid.model.transform.setValue(undefined, outputValue, transformer, transformSpec.merge);
9790 outputValue = undefined; // make sure we don't also return value
9791 }
9792 transformer.outputPrefixOp.pop();
9793 return outputValue;
9794 };
9795
9796 // unsupported, NON-API function
9797 fluid.transforms.valueMapper.longFormMatch = function (valueFromDefaultPath, transformSpec, transformer) {
9798 var o = transformSpec.match;
9799 if (o.length === 0) {
9800 fluid.fail("valueMapper supplied empty list of matches: ", transformSpec);
9801 }
9802 var matchPower = [];
9803 for (var i = 0; i < o.length; ++i) {
9804 var option = o[i];
9805 var value = option.inputPath ?
9806 fluid.model.transform.getValue(option.inputPath, undefined, transformer) : valueFromDefaultPath;
9807
9808 var matchValue = fluid.model.transform.matchValue(option.inputValue, value, option.partialMatches);
9809 matchPower[i] = {index: i, matchValue: matchValue};
9810 }
9811 matchPower.sort(fluid.model.transform.compareMatches);
9812 return matchPower[0].matchValue <= 0 ? undefined : o[matchPower[0].index];
9813 };
9814
9815 fluid.transforms.valueMapper.invert = function (transformSpec, transformer) {
9816 var match = [];
9817 var togo = {
9818 type: "fluid.transforms.valueMapper",
9819 match: match
9820 };
9821 var isArray = fluid.isArrayable(transformSpec.match);
9822
9823 togo.defaultInputPath = fluid.model.composePaths(transformer.outputPrefix, transformSpec.defaultOutputPath);
9824 togo.defaultOutputPath = fluid.model.composePaths(transformer.inputPrefix, transformSpec.defaultInputPath);
9825
9826 var def = fluid.firstDefined;
9827 fluid.each(transformSpec.match, function (option, key) {
9828 if (option.outputUndefinedValue === true) {
9829 return; // don't attempt to invert undefined output value entries
9830 }
9831 var outOption = {};
9832 var origInputValue = def(isArray ? option.inputValue : key, transformSpec.defaultInputValue);
9833 if (origInputValue === undefined) {
9834 fluid.fail("Failure inverting configuration for valueMapper - inputValue could not be resolved for record " + key + ": ", transformSpec);
9835 }
9836 outOption.outputValue = origInputValue;
9837 outOption.inputValue = !isArray && fluid.isPrimitive(option) ?
9838 option : def(option.outputValue, transformSpec.defaultOutputValue);
9839
9840 if (option.outputPath) {
9841 outOption.inputPath = fluid.model.composePaths(transformer.outputPrefix, def(option.outputPath, transformSpec.outputPath));
9842 }
9843 if (option.inputPath) {
9844 outOption.outputPath = fluid.model.composePaths(transformer.inputPrefix, def(option.inputPath, transformSpec.inputPath));
9845 }
9846 match.push(outOption);
9847 });
9848 return togo;
9849 };
9850
9851 fluid.transforms.valueMapper.collect = function (transformSpec, transformer) {
9852 var togo = [];
9853 fluid.model.transform.accumulateStandardInputPath("defaultInput", transformSpec, transformer, togo);
9854 fluid.each(transformSpec.match, function (option) {
9855 fluid.model.transform.accumulateInputPath(option.inputPath, transformer, togo);
9856 });
9857 return togo;
9858 };
9859
9860 /* -------- arrayToSetMembership and setMembershipToArray ---------------- */
9861
9862 fluid.defaults("fluid.transforms.arrayToSetMembership", {
9863 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
9864 invertConfiguration: "fluid.transforms.arrayToSetMembership.invert"
9865 });
9866
9867
9868 fluid.transforms.arrayToSetMembership = function (value, transformSpec, transformer) {
9869 var output = {};
9870 var options = transformSpec.options;
9871
9872 if (!value || !fluid.isArrayable(value)) {
9873 fluid.fail("arrayToSetMembership didn't find array at inputPath nor passed as value.", transformSpec);
9874 }
9875 if (!options) {
9876 fluid.fail("arrayToSetMembership requires an options block set");
9877 }
9878
9879 if (transformSpec.presentValue === undefined) {
9880 transformSpec.presentValue = true;
9881 }
9882
9883 if (transformSpec.missingValue === undefined) {
9884 transformSpec.missingValue = false;
9885 }
9886
9887 fluid.each(options, function (outPath, key) {
9888 // write to output object the value <presentValue> or <missingValue> depending on whether key is found in user input
9889 var outVal = (value.indexOf(key) !== -1) ? transformSpec.presentValue : transformSpec.missingValue;
9890 fluid.set(output, outPath, outVal, transformer.resolverSetConfig);
9891 });
9892 return output;
9893 };
9894
9895 /*
9896 * NON-API function; Copies the entire transformSpec with the following modifications:
9897 * * A new type is set (from argument)
9898 * * each [key]=value entry in the options is swapped to be: [value]=key
9899 */
9900 fluid.transforms.arrayToSetMembership.invertWithType = function (transformSpec, transformer, newType) {
9901 transformSpec.type = newType;
9902 var newOptions = {};
9903 fluid.each(transformSpec.options, function (path, oldKey) {
9904 newOptions[path] = oldKey;
9905 });
9906 transformSpec.options = newOptions;
9907 return transformSpec;
9908 };
9909
9910 fluid.transforms.arrayToSetMembership.invert = function (transformSpec, transformer) {
9911 return fluid.transforms.arrayToSetMembership.invertWithType(transformSpec, transformer,
9912 "fluid.transforms.setMembershipToArray");
9913 };
9914
9915 fluid.defaults("fluid.transforms.setMembershipToArray", {
9916 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
9917 invertConfiguration: "fluid.transforms.setMembershipToArray.invert"
9918 });
9919
9920 fluid.transforms.setMembershipToArray = function (input, transformSpec, transformer) {
9921 var options = transformSpec.options;
9922
9923 if (!options) {
9924 fluid.fail("setMembershipToArray requires an options block specified");
9925 }
9926
9927 if (transformSpec.presentValue === undefined) {
9928 transformSpec.presentValue = true;
9929 }
9930
9931 if (transformSpec.missingValue === undefined) {
9932 transformSpec.missingValue = false;
9933 }
9934
9935 var outputArr = [];
9936 fluid.each(options, function (outputVal, key) {
9937 var value = fluid.get(input, key, transformer.resolverGetConfig);
9938 if (value === transformSpec.presentValue) {
9939 outputArr.push(outputVal);
9940 }
9941 });
9942 return outputArr;
9943 };
9944
9945 fluid.transforms.setMembershipToArray.invert = function (transformSpec, transformer) {
9946 return fluid.transforms.arrayToSetMembership.invertWithType(transformSpec, transformer,
9947 "fluid.transforms.arrayToSetMembership");
9948 };
9949
9950 /* -------- deindexIntoArrayByKey and indexArrayByKey -------------------- */
9951
9952 /*
9953 * Transforms the given array to an object.
9954 * Uses the transformSpec.options.key values from each object within the array as new keys.
9955 *
9956 * For example, with transformSpec.key = "name" and an input object like this:
9957 *
9958 * {
9959 * b: [
9960 * { name: b1, v: v1 },
9961 * { name: b2, v: v2 }
9962 * ]
9963 * }
9964 *
9965 * The output will be:
9966 * {
9967 * b: {
9968 * b1: {
9969 * v: v1
9970 * }
9971 * },
9972 * {
9973 * b2: {
9974 * v: v2
9975 * }
9976 * }
9977 * }
9978 */
9979 fluid.model.transform.applyPaths = function (operation, pathOp, paths) {
9980 for (var i = 0; i < paths.length; ++i) {
9981 if (operation === "push") {
9982 pathOp.push(paths[i]);
9983 } else {
9984 pathOp.pop();
9985 }
9986 }
9987 };
9988
9989 fluid.model.transform.expandInnerValues = function (inputPath, outputPath, transformer, innerValues) {
9990 var inputPrefixOp = transformer.inputPrefixOp;
9991 var outputPrefixOp = transformer.outputPrefixOp;
9992 var apply = fluid.model.transform.applyPaths;
9993
9994 apply("push", inputPrefixOp, inputPath);
9995 apply("push", outputPrefixOp, outputPath);
9996 var expanded = {};
9997 fluid.each(innerValues, function (innerValue) {
9998 var expandedInner = transformer.expand(innerValue);
9999 if (!fluid.isPrimitive(expandedInner)) {
10000 $.extend(true, expanded, expandedInner);
10001 } else {
10002 expanded = expandedInner;
10003 }
10004 });
10005 apply("pop", outputPrefixOp, outputPath);
10006 apply("pop", inputPrefixOp, inputPath);
10007
10008 return expanded;
10009 };
10010
10011
10012 fluid.defaults("fluid.transforms.indexArrayByKey", {
10013 gradeNames: ["fluid.standardTransformFunction", "fluid.lens" ],
10014 invertConfiguration: "fluid.transforms.indexArrayByKey.invert"
10015 });
10016
10017 /* Transforms an array of objects into an object of objects, by indexing using the option "key" which must be supplied within the transform specification.
10018 * The key of each element will be taken from the value held in each each original object's member derived from the option value in "key" - this member should
10019 * exist in each array element. The member with name agreeing with "key" and its value will be removed from each original object before inserting into the returned
10020 * object.
10021 * For example,
10022 * <code>fluid.transforms.indexArrayByKey([{k: "e1", b: 1, c: 2}, {k: "e2", b: 2: c: 3}], {key: "k"})</code> will output the object
10023 * <code>{e1: {b: 1, c: 2}, e2: {b: 2: c, 3}</code>
10024 * Note: This transform frequently arises in the context of data which arose in XML form, which often represents "morally indexed" data in repeating array-like
10025 * constructs where the indexing key is held, for example, in an attribute.
10026 */
10027 fluid.transforms.indexArrayByKey = function (arr, transformSpec, transformer) {
10028 if (transformSpec.key === undefined) {
10029 fluid.fail("indexArrayByKey requires a 'key' option.", transformSpec);
10030 }
10031 if (!fluid.isArrayable(arr)) {
10032 fluid.fail("indexArrayByKey didn't find array at inputPath.", transformSpec);
10033 }
10034 var newHash = {};
10035 var pivot = transformSpec.key;
10036
10037 fluid.each(arr, function (v, k) {
10038 // check that we have a pivot entry in the object and it's a valid type:
10039 var newKey = v[pivot];
10040 var keyType = typeof(newKey);
10041 if (keyType !== "string" && keyType !== "boolean" && keyType !== "number") {
10042 fluid.fail("indexArrayByKey encountered untransformable array due to missing or invalid key", v);
10043 }
10044 // use the value of the key element as key and use the remaining content as value
10045 var content = fluid.copy(v);
10046 delete content[pivot];
10047 // fix sub Arrays if needed:
10048 if (transformSpec.innerValue) {
10049 content = fluid.model.transform.expandInnerValues([transformer.inputPrefix, transformSpec.inputPath, k.toString()],
10050 [transformSpec.outputPath, newKey], transformer, transformSpec.innerValue);
10051 }
10052 newHash[newKey] = content;
10053 });
10054 return newHash;
10055 };
10056
10057 fluid.transforms.indexArrayByKey.invert = function (transformSpec) {
10058 transformSpec.type = "fluid.transforms.deindexIntoArrayByKey";
10059 // invert transforms from innerValue as well:
10060 // TODO: The Model Transformations framework should be capable of this, but right now the
10061 // issue is that we use a "private contract" to operate the "innerValue" slot. We need to
10062 // spend time thinking of how this should be formalised
10063 if (transformSpec.innerValue) {
10064 var innerValue = transformSpec.innerValue;
10065 for (var i = 0; i < innerValue.length; ++i) {
10066 var inverted = fluid.model.transform.invertConfiguration(innerValue[i]);
10067 if (inverted === fluid.model.transform.uninvertibleTransform) {
10068 return inverted;
10069 } else {
10070 innerValue[i] = inverted;
10071 }
10072 }
10073 }
10074 return transformSpec;
10075 };
10076
10077
10078 fluid.defaults("fluid.transforms.deindexIntoArrayByKey", {
10079 gradeNames: [ "fluid.standardTransformFunction", "fluid.lens" ],
10080 invertConfiguration: "fluid.transforms.deindexIntoArrayByKey.invert"
10081 });
10082
10083 /*
10084 * Transforms an object of objects into an array of objects, by deindexing by the option "key" which must be supplied within the transform specification.
10085 * The key of each object will become split out into a fresh value in each array element which will be given the key held in the transformSpec option "key".
10086 * For example:
10087 * <code>fluid.transforms.deindexIntoArrayByKey({e1: {b: 1, c: 2}, e2: {b: 2: c, 3}, {key: "k"})</code> will output the array
10088 * <code>[{k: "e1", b: 1, c: 2}, {k: "e2", b: 2: c: 3}]</code>
10089 *
10090 * This performs the inverse transform of fluid.transforms.indexArrayByKey.
10091 */
10092 fluid.transforms.deindexIntoArrayByKey = function (hash, transformSpec, transformer) {
10093 if (transformSpec.key === undefined) {
10094 fluid.fail("deindexIntoArrayByKey requires a \"key\" option.", transformSpec);
10095 }
10096
10097 var newArray = [];
10098 var pivot = transformSpec.key;
10099
10100 fluid.each(hash, function (v, k) {
10101 var content = {};
10102 content[pivot] = k;
10103 if (transformSpec.innerValue) {
10104 v = fluid.model.transform.expandInnerValues([transformSpec.inputPath, k], [transformSpec.outputPath, newArray.length.toString()],
10105 transformer, transformSpec.innerValue);
10106 }
10107 $.extend(true, content, v);
10108 newArray.push(content);
10109 });
10110 return newArray;
10111 };
10112
10113 fluid.transforms.deindexIntoArrayByKey.invert = function (transformSpec) {
10114 transformSpec.type = "fluid.transforms.indexArrayByKey";
10115 // invert transforms from innerValue as well:
10116 // TODO: The Model Transformations framework should be capable of this, but right now the
10117 // issue is that we use a "private contract" to operate the "innerValue" slot. We need to
10118 // spend time thinking of how this should be formalised
10119 if (transformSpec.innerValue) {
10120 var innerValue = transformSpec.innerValue;
10121 for (var i = 0; i < innerValue.length; ++i) {
10122 innerValue[i] = fluid.model.transform.invertConfiguration(innerValue[i]);
10123 }
10124 }
10125 return transformSpec;
10126 };
10127
10128 fluid.defaults("fluid.transforms.limitRange", {
10129 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10130 invertConfiguration: "fluid.transforms.invertToIdentity"
10131 });
10132
10133 fluid.transforms.limitRange = function (value, transformSpec) {
10134 var min = transformSpec.min;
10135 if (min !== undefined) {
10136 var excludeMin = transformSpec.excludeMin || 0;
10137 min += excludeMin;
10138 if (value < min) {
10139 value = min;
10140 }
10141 }
10142 var max = transformSpec.max;
10143 if (max !== undefined) {
10144 var excludeMax = transformSpec.excludeMax || 0;
10145 max -= excludeMax;
10146 if (value > max) {
10147 value = max;
10148 }
10149 }
10150 return value;
10151 };
10152
10153 fluid.defaults("fluid.transforms.indexOf", {
10154 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10155 invertConfiguration: "fluid.transforms.indexOf.invert"
10156 });
10157
10158 fluid.transforms.indexOf = function (value, transformSpec) {
10159 // We do not allow a positive number as 'notFound' value, as it threatens invertibility
10160 if (typeof (transformSpec.notFound) === "number" && transformSpec.notFound >= 0) {
10161 fluid.fail("A positive number is not allowed as 'notFound' value for indexOf");
10162 }
10163 var offset = fluid.transforms.parseIndexationOffset(transformSpec.offset, "indexOf");
10164 var array = fluid.makeArray(transformSpec.array);
10165 var originalIndex = array.indexOf(value);
10166 return originalIndex === -1 && transformSpec.notFound ? transformSpec.notFound : originalIndex + offset;
10167 };
10168
10169 fluid.transforms.indexOf.invert = function (transformSpec, transformer) {
10170 var togo = fluid.transforms.invertArrayIndexation(transformSpec, transformer);
10171 togo.type = "fluid.transforms.dereference";
10172 return togo;
10173 };
10174
10175 fluid.defaults("fluid.transforms.dereference", {
10176 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10177 invertConfiguration: "fluid.transforms.dereference.invert"
10178 });
10179
10180 fluid.transforms.dereference = function (value, transformSpec) {
10181 if (typeof (value) !== "number") {
10182 return undefined;
10183 }
10184 var offset = fluid.transforms.parseIndexationOffset(transformSpec.offset, "dereference");
10185 var array = fluid.makeArray(transformSpec.array);
10186 var index = value + offset;
10187 return array[index];
10188 };
10189
10190 fluid.transforms.dereference.invert = function (transformSpec, transformer) {
10191 var togo = fluid.transforms.invertArrayIndexation(transformSpec, transformer);
10192 togo.type = "fluid.transforms.indexOf";
10193 return togo;
10194 };
10195
10196 fluid.transforms.parseIndexationOffset = function (offset, transformName) {
10197 var parsedOffset = 0;
10198 if (offset !== undefined) {
10199 parsedOffset = fluid.parseInteger(offset);
10200 if (isNaN(parsedOffset)) {
10201 fluid.fail(transformName + " requires the value of \"offset\" to be an integer or a string that can be converted to an integer. " + offset + " is invalid.");
10202 }
10203 }
10204 return parsedOffset;
10205 };
10206
10207 fluid.transforms.invertArrayIndexation = function (transformSpec) {
10208 if (!isNaN(Number(transformSpec.offset))) {
10209 transformSpec.offset = Number(transformSpec.offset) * (-1);
10210 }
10211 return transformSpec;
10212 };
10213
10214 fluid.defaults("fluid.transforms.stringTemplate", {
10215 gradeNames: "fluid.standardOutputTransformFunction"
10216 });
10217
10218 fluid.transforms.stringTemplate = function (transformSpec) {
10219 return fluid.stringTemplate(transformSpec.template, transformSpec.terms);
10220 };
10221
10222 fluid.defaults("fluid.transforms.free", {
10223 gradeNames: "fluid.transformFunction"
10224 });
10225
10226 fluid.transforms.free = function (transformSpec) {
10227 var args = fluid.makeArray(transformSpec.args);
10228 return fluid.invokeGlobalFunction(transformSpec.func, args);
10229 };
10230
10231 fluid.defaults("fluid.transforms.quantize", {
10232 gradeNames: "fluid.standardTransformFunction",
10233 collectInputPaths: "fluid.transforms.quantize.collect"
10234 });
10235
10236 /*
10237 * Quantize function maps a continuous range into discrete values. Given an input, it will
10238 * be matched into a discrete bucket and the corresponding output will be done.
10239 */
10240 fluid.transforms.quantize = function (value, transformSpec, transformer) {
10241 if (!transformSpec.ranges || !transformSpec.ranges.length) {
10242 fluid.fail("fluid.transforms.quantize should have a key called ranges containing an array defining ranges to quantize");
10243 }
10244 // TODO: error checking that upper bounds are all numbers and increasing
10245 for (var i = 0; i < transformSpec.ranges.length; i++) {
10246 var rangeSpec = transformSpec.ranges[i];
10247 if (value <= rangeSpec.upperBound || rangeSpec.upperBound === undefined && value >= Number.NEGATIVE_INFINITY) {
10248 return fluid.isPrimitive(rangeSpec.output) ? rangeSpec.output : transformer.expand(rangeSpec.output);
10249 }
10250 }
10251 };
10252
10253 fluid.transforms.quantize.collect = function (transformSpec, transformer) {
10254 transformSpec.ranges.forEach(function (rangeSpec) {
10255 if (!fluid.isPrimitive(rangeSpec.output)) {
10256 transformer.expand(rangeSpec.output);
10257 }
10258 });
10259 };
10260
10261 /**
10262 * inRange transformer checks whether a value is within a given range and returns `true` if it is,
10263 * and `false` if it's not.
10264 *
10265 * The range is defined by the two inputs: "min" and "max" (both inclusive). If one of these inputs
10266 * is not present it is treated as -Infinity and +Infinity, respectively - In other words, if no
10267 * `min` value is defined, any value below or equal to the given `max` value will result in `true`.
10268 */
10269 fluid.defaults("fluid.transforms.inRange", {
10270 gradeNames: "fluid.standardTransformFunction"
10271 });
10272
10273 fluid.transforms.inRange = function (value, transformSpec) {
10274 return (transformSpec.min === undefined || transformSpec.min <= value) &&
10275 (transformSpec.max === undefined || transformSpec.max >= value) ? true : false;
10276 };
10277
10278 /**
10279 *
10280 * Convert a string to a Boolean, for example, when working with HTML form element values.
10281 *
10282 * The following are all false: undefined, null, "", "0", "false", false, 0
10283 *
10284 * Everything else is true.
10285 *
10286 * @param {String} value - The value to be interpreted.
10287 * @return {Boolean} The interpreted value.
10288 */
10289 fluid.transforms.stringToBoolean = function (value) {
10290 if (value) {
10291 return !(value === "0" || value === "false");
10292 }
10293 else {
10294 return false;
10295 }
10296 };
10297
10298 fluid.transforms.stringToBoolean.invert = function (transformSpec) {
10299 transformSpec.type = "fluid.transforms.booleanToString";
10300 return transformSpec;
10301 };
10302
10303 fluid.defaults("fluid.transforms.stringToBoolean", {
10304 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10305 invertConfiguration: "fluid.transforms.stringToBoolean.invert"
10306 });
10307
10308 /**
10309 *
10310 * Convert any value into a stringified boolean, i. e. either "true" or "false". Anything that evaluates to
10311 * true (1, true, "non empty string", {}, et. cetera) returns "true". Anything else (0, false, null, et. cetera)
10312 * returns "false".
10313 *
10314 * @param {Any} value - The value to be converted to a stringified Boolean.
10315 * @return {String} - A stringified boolean representation of the value.
10316 */
10317 fluid.transforms.booleanToString = function (value) {
10318 return value ? "true" : "false";
10319 };
10320
10321 fluid.transforms.booleanToString.invert = function (transformSpec) {
10322 transformSpec.type = "fluid.transforms.stringToBoolean";
10323 return transformSpec;
10324 };
10325
10326 fluid.defaults("fluid.transforms.booleanToString", {
10327 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10328 invertConfiguration: "fluid.transforms.booleanToString.invert"
10329 });
10330
10331 /**
10332 *
10333 * Transform stringified JSON to an object using `JSON.parse`. Returns `undefined` if the JSON string is invalid.
10334 *
10335 * @param {String} value - The stringified JSON to be converted to an object.
10336 * @return {Any} - The parsed value of the string, or `undefined` if it can't be parsed.
10337 */
10338 fluid.transforms.JSONstringToObject = function (value) {
10339 try {
10340 return JSON.parse(value);
10341 }
10342 catch (e) {
10343 return undefined;
10344 }
10345 };
10346
10347 fluid.transforms.JSONstringToObject.invert = function (transformSpec) {
10348 transformSpec.type = "fluid.transforms.objectToJSONString";
10349 return transformSpec;
10350 };
10351
10352 fluid.defaults("fluid.transforms.JSONstringToObject", {
10353 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10354 invertConfiguration: "fluid.transforms.JSONstringToObject.invert"
10355 });
10356
10357 /**
10358 *
10359 * Transform an object to a string using `JSON.stringify`. You can pass the `space` option to be used
10360 * as part of your transform, as in:
10361 *
10362 * ```
10363 * "": {
10364 * transform: {
10365 * funcName: "fluid.transforms.objectToJSONString",
10366 * inputPath: "",
10367 * space: 2
10368 * }
10369 * }
10370 * ```
10371 *
10372 * The default value for `space` is 0, which disables spacing and line breaks.
10373 *
10374 * @param {Object} value - An object to be converted to stringified JSON.
10375 * @param {Object} transformSpec - An object describing the transformation spec, see above.
10376 * @return {String} - A string representation of the object.
10377 *
10378 */
10379 fluid.transforms.objectToJSONString = function (value, transformSpec) {
10380 var space = transformSpec.space || 0;
10381 return JSON.stringify(value, null, space);
10382 };
10383
10384 fluid.transforms.objectToJSONString.invert = function (transformSpec) {
10385 transformSpec.type = "fluid.transforms.JSONstringToObject";
10386 return transformSpec;
10387 };
10388
10389 fluid.defaults("fluid.transforms.objectToJSONString", {
10390 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10391 invertConfiguration: "fluid.transforms.objectToJSONString.invert"
10392 });
10393
10394 /**
10395 *
10396 * Transform a string to a date using the Date constructor. Accepts (among other things) the date and dateTime
10397 * values returned by HTML5 date and dateTime inputs.
10398 *
10399 * A string that cannot be parsed will be treated as `undefined`.
10400 *
10401 * Note: This function allows you to create Date objects from an ISO 8601 string such as `2017-01-23T08:51:25.891Z`.
10402 * It is intended to provide a consistent mechanism for recreating Date objects stored as strings. Although the
10403 * framework currently works as expected with Date objects stored in the model, this is very likely to change. If
10404 * you are working with Date objects in your model, your best option for ensuring your code continues to work in the
10405 * future is to handle serialisation and deserialisation yourself, for example, by using this transform and one of
10406 * its inverse transforms, `fluid.transforms.dateToString` or `fluid.transforms.dateTimeToString`. See the Infusion
10407 * documentation for details about supported model values:
10408 *
10409 * http://docs.fluidproject.org/infusion/development/FrameworkConcepts.html#model-objects
10410 *
10411 * @param {String} value - The String value to be transformed into a Date object.
10412 * @return {Date} - A date object, or `undefined`.
10413 *
10414 */
10415 fluid.transforms.stringToDate = function (value) {
10416 var date = new Date(value);
10417 return isNaN(date.getTime()) ? undefined : date;
10418 };
10419
10420 fluid.transforms.stringToDate.invert = function (transformSpec) {
10421 transformSpec.type = "fluid.transforms.dateToString";
10422 return transformSpec;
10423 };
10424
10425 fluid.defaults("fluid.transforms.stringToDate", {
10426 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10427 invertConfiguration: "fluid.transforms.stringToDate.invert"
10428 });
10429
10430 /**
10431 *
10432 * Transform a Date object into a date string using its toISOString method. Strips the "time" portion away to
10433 * produce date strings that are suitable for use with both HTML5 "date" inputs and JSON Schema "date" format
10434 * string validation, for example: `2016-11-23`
10435 *
10436 * If you wish to preserve the time, use `fluid.transforms.dateTimeToString` instead.
10437 *
10438 * A non-date object will be treated as `undefined`.
10439 *
10440 * Note: This function allows you to seralise Date objects (not including time information) as ISO 8601 strings such
10441 * as `2017-01-23`. It is intended to provide a consistent mechanism for storing Date objects in a model. Although
10442 * the framework currently works as expected with Date objects stored in the model, this is very likely to change.
10443 * If you are working with Date objects in your model, your best option for ensuring your code continues to work in
10444 * the future is to handle serialisation and deserialisation yourself, for example, by using this transform and its
10445 * inverse, `fluid.transforms.stringToDate`. See the Infusion documentation for details about supported model
10446 * values:
10447 *
10448 * http://docs.fluidproject.org/infusion/development/FrameworkConcepts.html#model-objects
10449 *
10450 * @param {Date} value - The Date object to be transformed into an ISO 8601 string.
10451 * @return {String} - A {String} value representing the date, or `undefined` if the date is invalid.
10452 *
10453 */
10454 fluid.transforms.dateToString = function (value) {
10455 if (value instanceof Date) {
10456 var isoString = value.toISOString(); // A string like "2016-09-26T08:05:57.462Z"
10457 var dateString = isoString.substring(0, isoString.indexOf("T")); // A string like "2016-09-26"
10458 return dateString;
10459 }
10460 else {
10461 return undefined;
10462 }
10463 };
10464
10465 fluid.transforms.dateToString.invert = function (transformSpec) {
10466 transformSpec.type = "fluid.transforms.stringToDate";
10467 return transformSpec;
10468 };
10469
10470 fluid.defaults("fluid.transforms.dateToString", {
10471 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10472 invertConfiguration: "fluid.transforms.dateToString.invert"
10473 });
10474
10475 /**
10476 *
10477 * Transform a Date object into a date/time string using its toISOString method. Results in date strings that are
10478 * suitable for use with both HTML5 "dateTime" inputs and JSON Schema "date-time" format string validation, for\
10479 * example: `2016-11-23T13:05:24.079Z`
10480 *
10481 * A non-date object will be treated as `undefined`.
10482 *
10483 * Note: This function allows you to seralise Date objects (including time information) as ISO 8601 strings such as
10484 * `2017-01-23T08:51:25.891Z`. It is intended to provide a consistent mechanism for storing Date objects in a model.
10485 * Although the framework currently works as expected with Date objects stored in the model, this is very likely to
10486 * change. If you are working with Date objects in your model, your best option for ensuring your code continues to
10487 * work in the future is to handle serialisation and deserialisation yourself, for example, by using this function
10488 * and its inverse, `fluid.transforms.stringToDate`. See the Infusion documentation for details about supported
10489 * model values:
10490 *
10491 * http://docs.fluidproject.org/infusion/development/FrameworkConcepts.html#model-objects
10492 *
10493 * @param {Date} value - The Date object to be transformed into an ISO 8601 string.
10494 * @return {String} - A {String} value representing the date and time, or `undefined` if the date/time are invalid.
10495 *
10496 */
10497 fluid.transforms.dateTimeToString = function (value) {
10498 return value instanceof Date ? value.toISOString() : undefined;
10499 };
10500
10501 fluid.defaults("fluid.transforms.dateTimeToString", {
10502 gradeNames: ["fluid.standardTransformFunction", "fluid.lens"],
10503 invertConfiguration: "fluid.transforms.dateToString.invert"
10504 });
10505})(jQuery, fluid_3_0_0);
10506;
10507/*
10508Copyright The Infusion copyright holders
10509See the AUTHORS.md file at the top-level directory of this distribution and at
10510https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
10511
10512Licensed under the Educational Community License (ECL), Version 2.0 or the New
10513BSD license. You may not use this file except in compliance with one these
10514Licenses.
10515
10516You may obtain a copy of the ECL 2.0 License and BSD License at
10517https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
10518*/
10519
10520var fluid_3_0_0 = fluid_3_0_0 || {};
10521var fluid = fluid || fluid_3_0_0;
10522
10523(function ($, fluid) {
10524 "use strict";
10525
10526 // $().fluid("selectable", args)
10527 // $().fluid("selectable".that()
10528 // $().fluid("pager.pagerBar", args)
10529 // $().fluid("reorderer", options)
10530
10531 /** Create a "bridge" from code written in the Fluid standard "that-ist" style,
10532 * to the standard JQuery UI plugin architecture specified at http://docs.jquery.com/UI/Guidelines .
10533 * Every Fluid component corresponding to the top-level standard signature (JQueryable, options)
10534 * will automatically convert idiomatically to the JQuery UI standard via this adapter.
10535 * Any return value which is a primitive or array type will become the return value
10536 * of the "bridged" function - however, where this function returns a general hash
10537 * (object) this is interpreted as forming part of the Fluid "return that" pattern,
10538 * and the function will instead be bridged to "return this" as per JQuery standard,
10539 * permitting chaining to occur. However, as a courtesy, the particular "this" returned
10540 * will be augmented with a function that() which will allow the original return
10541 * value to be retrieved if desired.
10542 * @param {String} name - The name under which the "plugin space" is to be injected into JQuery
10543 * @param {Object} peer - The root of the namespace corresponding to the peer object.
10544 * @return {Function} - A JQuery UI plugin function.
10545 */
10546
10547 fluid.thatistBridge = function (name, peer) {
10548
10549 var togo = function (funcname) {
10550 var segs = funcname.split(".");
10551 var move = peer;
10552 for (var i = 0; i < segs.length; ++i) {
10553 move = move[segs[i]];
10554 }
10555 var args = [this];
10556 if (arguments.length === 2) {
10557 args = args.concat($.makeArray(arguments[1]));
10558 }
10559 var ret = move.apply(null, args);
10560 this.that = function () {
10561 return ret;
10562 };
10563 var type = typeof(ret);
10564 return !ret || type === "string" || type === "number" || type === "boolean" ||
10565 (ret && ret.length !== undefined) ? ret : this;
10566 };
10567 $.fn[name] = togo;
10568 return togo;
10569 };
10570
10571 fluid.thatistBridge("fluid", fluid);
10572 fluid.thatistBridge("fluid_3_0_0", fluid_3_0_0);
10573
10574/*************************************************************************
10575 * Tabindex normalization - compensate for browser differences in naming
10576 * and function of "tabindex" attribute and tabbing order.
10577 */
10578
10579 // -- Private functions --
10580
10581
10582 var normalizeTabindexName = function () {
10583 return $.browser.msie ? "tabIndex" : "tabindex";
10584 };
10585
10586 var canHaveDefaultTabindex = function (elements) {
10587 if (elements.length <= 0) {
10588 return false;
10589 }
10590
10591 return $(elements[0]).is("a, input, button, select, area, textarea, object");
10592 };
10593
10594 var getValue = function (elements) {
10595 if (elements.length <= 0) {
10596 return undefined;
10597 }
10598
10599 if (!fluid.tabindex.hasAttr(elements)) {
10600 return canHaveDefaultTabindex(elements) ? Number(0) : undefined;
10601 }
10602
10603 // Get the attribute and return it as a number value.
10604 var value = elements.attr(normalizeTabindexName());
10605 return Number(value);
10606 };
10607
10608 var setValue = function (elements, toIndex) {
10609 return elements.each(function (i, item) {
10610 $(item).attr(normalizeTabindexName(), toIndex);
10611 });
10612 };
10613
10614 // -- Public API --
10615
10616 /**
10617 * Gets the value of the tabindex attribute for the first item, or sets the tabindex value of all elements
10618 * if toIndex is specified.
10619 *
10620 * @param {jQuery} target - The target element.
10621 * @param {String|Number} toIndex - (Optional) the tabIndex value to set on the target.
10622 * @return {Any} - The result of the underlying "get" or "set" operation.
10623 */
10624 fluid.tabindex = function (target, toIndex) {
10625 target = $(target);
10626 if (toIndex !== null && toIndex !== undefined) {
10627 return setValue(target, toIndex);
10628 } else {
10629 return getValue(target);
10630 }
10631 };
10632
10633 /*
10634 * Removes the tabindex attribute altogether from each element.
10635 */
10636 fluid.tabindex.remove = function (target) {
10637 target = $(target);
10638 return target.each(function (i, item) {
10639 $(item).removeAttr(normalizeTabindexName());
10640 });
10641 };
10642
10643 /*
10644 * Determines if an element actually has a tabindex attribute present.
10645 */
10646 fluid.tabindex.hasAttr = function (target) {
10647 target = $(target);
10648 if (target.length <= 0) {
10649 return false;
10650 }
10651 var togo = target.map(
10652 function () {
10653 var attributeNode = this.getAttributeNode(normalizeTabindexName());
10654 return attributeNode ? attributeNode.specified : false;
10655 }
10656 );
10657 return togo.length === 1 ? togo[0] : togo;
10658 };
10659
10660 /*
10661 * Determines if an element either has a tabindex attribute or is naturally tab-focussable.
10662 */
10663 fluid.tabindex.has = function (target) {
10664 target = $(target);
10665 return fluid.tabindex.hasAttr(target) || canHaveDefaultTabindex(target);
10666 };
10667
10668 // Keyboard navigation
10669 // Public, static constants needed by the rest of the library.
10670 fluid.a11y = $.a11y || {};
10671
10672 fluid.a11y.orientation = {
10673 HORIZONTAL: 0,
10674 VERTICAL: 1,
10675 BOTH: 2
10676 };
10677
10678 var UP_DOWN_KEYMAP = {
10679 next: $.ui.keyCode.DOWN,
10680 previous: $.ui.keyCode.UP
10681 };
10682
10683 var LEFT_RIGHT_KEYMAP = {
10684 next: $.ui.keyCode.RIGHT,
10685 previous: $.ui.keyCode.LEFT
10686 };
10687
10688 // Private functions.
10689 var unwrap = function (element) {
10690 return element.jquery ? element[0] : element; // Unwrap the element if it's a jQuery.
10691 };
10692
10693
10694 var makeElementsTabFocussable = function (elements) {
10695 // If each element doesn't have a tabindex, or has one set to a negative value, set it to 0.
10696 elements.each(function (idx, item) {
10697 item = $(item);
10698 if (!item.fluid("tabindex.has") || item.fluid("tabindex") < 0) {
10699 item.fluid("tabindex", 0);
10700 }
10701 });
10702 };
10703
10704 // Public API.
10705 /*
10706 * Makes all matched elements available in the tab order by setting their tabindices to "0".
10707 */
10708 fluid.tabbable = function (target) {
10709 target = $(target);
10710 makeElementsTabFocussable(target);
10711 };
10712
10713 /***********************************************************************
10714 * Selectable functionality - geometrising a set of nodes such that they
10715 * can be navigated (by setting focus) using a set of directional keys
10716 */
10717
10718 var CONTEXT_KEY = "selectionContext";
10719 var NO_SELECTION = -32768;
10720
10721 var cleanUpWhenLeavingContainer = function (selectionContext) {
10722 if (selectionContext.activeItemIndex !== NO_SELECTION) {
10723 if (selectionContext.options.onLeaveContainer) {
10724 selectionContext.options.onLeaveContainer(
10725 selectionContext.selectables[selectionContext.activeItemIndex]
10726 );
10727 } else if (selectionContext.options.onUnselect) {
10728 selectionContext.options.onUnselect(
10729 selectionContext.selectables[selectionContext.activeItemIndex]
10730 );
10731 }
10732 }
10733
10734 if (!selectionContext.options.rememberSelectionState) {
10735 selectionContext.activeItemIndex = NO_SELECTION;
10736 }
10737 };
10738
10739 /*
10740 * Does the work of selecting an element and delegating to the client handler.
10741 */
10742 var drawSelection = function (elementToSelect, handler) {
10743 if (handler) {
10744 handler(elementToSelect);
10745 }
10746 };
10747
10748 /*
10749 * Does does the work of unselecting an element and delegating to the client handler.
10750 */
10751 var eraseSelection = function (selectedElement, handler) {
10752 if (handler && selectedElement) {
10753 handler(selectedElement);
10754 }
10755 };
10756
10757 var unselectElement = function (selectedElement, selectionContext) {
10758 eraseSelection(selectedElement, selectionContext.options.onUnselect);
10759 };
10760
10761 var selectElement = function (elementToSelect, selectionContext) {
10762 // It's possible that we're being called programmatically, in which case we should clear any previous selection.
10763 unselectElement(selectionContext.selectedElement(), selectionContext);
10764
10765 elementToSelect = unwrap(elementToSelect);
10766 var newIndex = selectionContext.selectables.index(elementToSelect);
10767
10768 // Next check if the element is a known selectable. If not, do nothing.
10769 if (newIndex === -1) {
10770 return;
10771 }
10772
10773 // Select the new element.
10774 selectionContext.activeItemIndex = newIndex;
10775 drawSelection(elementToSelect, selectionContext.options.onSelect);
10776 };
10777
10778 var selectableFocusHandler = function (selectionContext) {
10779 return function (evt) {
10780 // FLUID-3590: newer browsers (FF 3.6, Webkit 4) have a form of "bug" in that they will go bananas
10781 // on attempting to move focus off an element which has tabindex dynamically set to -1.
10782 $(evt.target).fluid("tabindex", 0);
10783 selectElement(evt.target, selectionContext);
10784
10785 // Force focus not to bubble on some browsers.
10786 return evt.stopPropagation();
10787 };
10788 };
10789
10790 var selectableBlurHandler = function (selectionContext) {
10791 return function (evt) {
10792 $(evt.target).fluid("tabindex", selectionContext.options.selectablesTabindex);
10793 unselectElement(evt.target, selectionContext);
10794
10795 // Force blur not to bubble on some browsers.
10796 return evt.stopPropagation();
10797 };
10798 };
10799
10800 var reifyIndex = function (sc_that) {
10801 var elements = sc_that.selectables;
10802 if (sc_that.activeItemIndex >= elements.length) {
10803 sc_that.activeItemIndex = (sc_that.options.noWrap ? elements.length - 1 : 0);
10804 }
10805 if (sc_that.activeItemIndex < 0 && sc_that.activeItemIndex !== NO_SELECTION) {
10806 sc_that.activeItemIndex = (sc_that.options.noWrap ? 0 : elements.length - 1);
10807 }
10808 if (sc_that.activeItemIndex >= 0) {
10809 fluid.focus(elements[sc_that.activeItemIndex]);
10810 }
10811 };
10812
10813 var prepareShift = function (selectionContext) {
10814 // FLUID-3590: FF 3.6 and Safari 4.x won't fire blur() when programmatically moving focus.
10815 var selElm = selectionContext.selectedElement();
10816 if (selElm) {
10817 fluid.blur(selElm);
10818 }
10819
10820 unselectElement(selectionContext.selectedElement(), selectionContext);
10821 if (selectionContext.activeItemIndex === NO_SELECTION) {
10822 selectionContext.activeItemIndex = -1;
10823 }
10824 };
10825
10826 var focusNextElement = function (selectionContext) {
10827 prepareShift(selectionContext);
10828 ++selectionContext.activeItemIndex;
10829 reifyIndex(selectionContext);
10830 };
10831
10832 var focusPreviousElement = function (selectionContext) {
10833 prepareShift(selectionContext);
10834 --selectionContext.activeItemIndex;
10835 reifyIndex(selectionContext);
10836 };
10837
10838 var arrowKeyHandler = function (selectionContext, keyMap) {
10839 return function (evt) {
10840 if (evt.which === keyMap.next) {
10841 focusNextElement(selectionContext);
10842 evt.preventDefault();
10843 } else if (evt.which === keyMap.previous) {
10844 focusPreviousElement(selectionContext);
10845 evt.preventDefault();
10846 }
10847 };
10848 };
10849
10850 var getKeyMapForDirection = function (direction) {
10851 // Determine the appropriate mapping for next and previous based on the specified direction.
10852 var keyMap;
10853 if (direction === fluid.a11y.orientation.HORIZONTAL) {
10854 keyMap = LEFT_RIGHT_KEYMAP;
10855 }
10856 else if (direction === fluid.a11y.orientation.VERTICAL) {
10857 // Assume vertical in any other case.
10858 keyMap = UP_DOWN_KEYMAP;
10859 }
10860
10861 return keyMap;
10862 };
10863
10864 var tabKeyHandler = function (selectionContext) {
10865 return function (evt) {
10866 if (evt.which !== $.ui.keyCode.TAB) {
10867 return;
10868 }
10869 cleanUpWhenLeavingContainer(selectionContext);
10870
10871 // Catch Shift-Tab and note that focus is on its way out of the container.
10872 if (evt.shiftKey) {
10873 selectionContext.focusIsLeavingContainer = true;
10874 }
10875 };
10876 };
10877
10878 var containerFocusHandler = function (selectionContext) {
10879 return function (evt) {
10880 var shouldOrig = selectionContext.options.autoSelectFirstItem;
10881 var shouldSelect = typeof(shouldOrig) === "function" ? shouldOrig() : shouldOrig;
10882
10883 // Override the autoselection if we're on the way out of the container.
10884 if (selectionContext.focusIsLeavingContainer) {
10885 shouldSelect = false;
10886 }
10887
10888 // This target check works around the fact that sometimes focus bubbles, even though it shouldn't.
10889 if (shouldSelect && evt.target === selectionContext.container.get(0)) {
10890 if (selectionContext.activeItemIndex === NO_SELECTION) {
10891 selectionContext.activeItemIndex = 0;
10892 }
10893 fluid.focus(selectionContext.selectables[selectionContext.activeItemIndex]);
10894 }
10895
10896 // Force focus not to bubble on some browsers.
10897 return evt.stopPropagation();
10898 };
10899 };
10900
10901 var containerBlurHandler = function (selectionContext) {
10902 return function (evt) {
10903 selectionContext.focusIsLeavingContainer = false;
10904
10905 // Force blur not to bubble on some browsers.
10906 return evt.stopPropagation();
10907 };
10908 };
10909
10910 var makeElementsSelectable = function (container, defaults, userOptions) {
10911
10912 var options = $.extend(true, {}, defaults, userOptions);
10913
10914 var keyMap = getKeyMapForDirection(options.direction);
10915
10916 var selectableElements = options.selectableElements ? options.selectableElements :
10917 container.find(options.selectableSelector);
10918
10919 // Context stores the currently active item(undefined to start) and list of selectables.
10920 var that = {
10921 container: container,
10922 activeItemIndex: NO_SELECTION,
10923 selectables: selectableElements,
10924 focusIsLeavingContainer: false,
10925 options: options
10926 };
10927
10928 that.selectablesUpdated = function (focusedItem) {
10929 // Remove selectables from the tab order and add focus/blur handlers
10930 if (typeof(that.options.selectablesTabindex) === "number") {
10931 that.selectables.fluid("tabindex", that.options.selectablesTabindex);
10932 }
10933 that.selectables.off("focus." + CONTEXT_KEY);
10934 that.selectables.off("blur." + CONTEXT_KEY);
10935 that.selectables.on("focus." + CONTEXT_KEY, selectableFocusHandler(that));
10936 that.selectables.on("blur." + CONTEXT_KEY, selectableBlurHandler(that));
10937 if (keyMap && that.options.noBubbleListeners) {
10938 that.selectables.off("keydown." + CONTEXT_KEY);
10939 that.selectables.on("keydown." + CONTEXT_KEY, arrowKeyHandler(that, keyMap));
10940 }
10941 if (focusedItem) {
10942 selectElement(focusedItem, that);
10943 }
10944 else {
10945 reifyIndex(that);
10946 }
10947 };
10948
10949 that.refresh = function () {
10950 if (!that.options.selectableSelector) {
10951 fluid.fail("Cannot refresh selectable context which was not initialised by a selector");
10952 }
10953 that.selectables = container.find(options.selectableSelector);
10954 that.selectablesUpdated();
10955 };
10956
10957 that.selectedElement = function () {
10958 return that.activeItemIndex < 0 ? null : that.selectables[that.activeItemIndex];
10959 };
10960
10961 // Add various handlers to the container.
10962 if (keyMap && !that.options.noBubbleListeners) {
10963 container.keydown(arrowKeyHandler(that, keyMap));
10964 }
10965 container.keydown(tabKeyHandler(that));
10966 container.focus(containerFocusHandler(that));
10967 container.blur(containerBlurHandler(that));
10968
10969 that.selectablesUpdated();
10970
10971 return that;
10972 };
10973
10974 /*
10975 * Makes all matched elements selectable with the arrow keys.
10976 * Supply your own handlers object with onSelect: and onUnselect: properties for custom behaviour.
10977 * Options provide configurability, including direction: and autoSelectFirstItem:
10978 * Currently supported directions are jQuery.a11y.directions.HORIZONTAL and VERTICAL.
10979 */
10980 fluid.selectable = function (target, options) {
10981 target = $(target);
10982 var that = makeElementsSelectable(target, fluid.selectable.defaults, options);
10983 fluid.setScopedData(target, CONTEXT_KEY, that);
10984 return that;
10985 };
10986
10987 /*
10988 * Selects the specified element.
10989 */
10990 fluid.selectable.select = function (target, toSelect) {
10991 fluid.focus(toSelect);
10992 };
10993
10994 /*
10995 * Selects the next matched element.
10996 */
10997 fluid.selectable.selectNext = function (target) {
10998 target = $(target);
10999 focusNextElement(fluid.getScopedData(target, CONTEXT_KEY));
11000 };
11001
11002 /*
11003 * Selects the previous matched element.
11004 */
11005 fluid.selectable.selectPrevious = function (target) {
11006 target = $(target);
11007 focusPreviousElement(fluid.getScopedData(target, CONTEXT_KEY));
11008 };
11009
11010 /*
11011 * Returns the currently selected item wrapped as a jQuery object.
11012 */
11013 fluid.selectable.currentSelection = function (target) {
11014 target = $(target);
11015 var that = fluid.getScopedData(target, CONTEXT_KEY);
11016 return $(that.selectedElement());
11017 };
11018
11019 fluid.selectable.defaults = {
11020 direction: fluid.a11y.orientation.VERTICAL,
11021 selectablesTabindex: -1,
11022 autoSelectFirstItem: true,
11023 rememberSelectionState: true,
11024 selectableSelector: ".selectable",
11025 selectableElements: null,
11026 onSelect: null,
11027 onUnselect: null,
11028 onLeaveContainer: null,
11029 noWrap: false
11030 };
11031
11032 /********************************************************************
11033 * Activation functionality - declaratively associating actions with
11034 * a set of keyboard bindings.
11035 */
11036
11037 var checkForModifier = function (binding, evt) {
11038 // If no modifier was specified, just return true.
11039 if (!binding.modifier) {
11040 return true;
11041 }
11042
11043 var modifierKey = binding.modifier;
11044 var isCtrlKeyPresent = modifierKey && evt.ctrlKey;
11045 var isAltKeyPresent = modifierKey && evt.altKey;
11046 var isShiftKeyPresent = modifierKey && evt.shiftKey;
11047
11048 return isCtrlKeyPresent || isAltKeyPresent || isShiftKeyPresent;
11049 };
11050
11051 /* Constructs a raw "keydown"-facing handler, given a binding entry. This
11052 * checks whether the key event genuinely triggers the event and forwards it
11053 * to any "activateHandler" registered in the binding.
11054 */
11055 var makeActivationHandler = function (binding) {
11056 return function (evt) {
11057 var target = evt.target;
11058 if (!fluid.enabled(target)) {
11059 return;
11060 }
11061// The following 'if' clause works in the real world, but there's a bug in the jQuery simulation
11062// that causes keyboard simulation to fail in Safari, causing our tests to fail:
11063// http://ui.jquery.com/bugs/ticket/3229
11064// The replacement 'if' clause works around this bug.
11065// When this issue is resolved, we should revert to the original clause.
11066// if (evt.which === binding.key && binding.activateHandler && checkForModifier(binding, evt)) {
11067 var code = evt.which ? evt.which : evt.keyCode;
11068 if (code === binding.key && binding.activateHandler && checkForModifier(binding, evt)) {
11069 var event = $.Event("fluid-activate");
11070 $(target).trigger(event, [binding.activateHandler]);
11071 if (event.isDefaultPrevented()) {
11072 evt.preventDefault();
11073 }
11074 }
11075 };
11076 };
11077
11078 var makeElementsActivatable = function (elements, onActivateHandler, defaultKeys, options) {
11079 // Create bindings for each default key.
11080 var bindings = [];
11081 $(defaultKeys).each(function (index, key) {
11082 bindings.push({
11083 modifier: null,
11084 key: key,
11085 activateHandler: onActivateHandler
11086 });
11087 });
11088
11089 // Merge with any additional key bindings.
11090 if (options && options.additionalBindings) {
11091 bindings = bindings.concat(options.additionalBindings);
11092 }
11093
11094 fluid.initEnablement(elements);
11095
11096 // Add listeners for each key binding.
11097 for (var i = 0; i < bindings.length; ++i) {
11098 var binding = bindings[i];
11099 elements.keydown(makeActivationHandler(binding));
11100 }
11101 elements.on("fluid-activate", function (evt, handler) {
11102 handler = handler || onActivateHandler;
11103 return handler ? handler(evt) : null;
11104 });
11105 };
11106
11107 /*
11108 * Makes all matched elements activatable with the Space and Enter keys.
11109 * Provide your own handler function for custom behaviour.
11110 * Options allow you to provide a list of additionalActivationKeys.
11111 */
11112 fluid.activatable = function (target, fn, options) {
11113 target = $(target);
11114 makeElementsActivatable(target, fn, fluid.activatable.defaults.keys, options);
11115 };
11116
11117 /*
11118 * Activates the specified element.
11119 */
11120 fluid.activate = function (target) {
11121 $(target).trigger("fluid-activate");
11122 };
11123
11124 // Public Defaults.
11125 fluid.activatable.defaults = {
11126 keys: [$.ui.keyCode.ENTER, $.ui.keyCode.SPACE]
11127 };
11128
11129
11130})(jQuery, fluid_3_0_0);
11131;
11132/*
11133Copyright The Infusion copyright holders
11134See the AUTHORS.md file at the top-level directory of this distribution and at
11135https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
11136
11137Licensed under the Educational Community License (ECL), Version 2.0 or the New
11138BSD license. You may not use this file except in compliance with one these
11139Licenses.
11140
11141You may obtain a copy of the ECL 2.0 License and BSD License at
11142https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
11143*/
11144
11145/** This file contains functions which depend on the presence of a DOM document
11146 * and which depend on the contents of Fluid.js **/
11147
11148var fluid_3_0_0 = fluid_3_0_0 || {};
11149
11150(function ($, fluid) {
11151 "use strict";
11152
11153 fluid.defaults("fluid.viewComponent", {
11154 gradeNames: ["fluid.modelComponent"],
11155 initFunction: "fluid.initView",
11156 argumentMap: {
11157 container: 0,
11158 options: 1
11159 },
11160 members: { // Used to allow early access to DOM binder via IoC, but to also avoid triggering evaluation of selectors
11161 dom: "@expand:fluid.initDomBinder({that}, {that}.options.selectors)"
11162 }
11163 });
11164
11165 // unsupported, NON-API function
11166 fluid.dumpSelector = function (selectable) {
11167 return typeof (selectable) === "string" ? selectable :
11168 selectable.selector ? selectable.selector : "";
11169 };
11170
11171 // unsupported, NON-API function
11172 // NOTE: this function represents a temporary strategy until we have more integrated IoC debugging.
11173 // It preserves the 1.3 and previous framework behaviour for the 1.x releases, but provides a more informative
11174 // diagnostic - in fact, it is perfectly acceptable for a component's creator to return no value and
11175 // the failure is really in assumptions in fluid.initLittleComponent. Revisit this issue for 2.0
11176 fluid.diagnoseFailedView = function (componentName, that, options, args) {
11177 if (!that && fluid.hasGrade(options, "fluid.viewComponent")) {
11178 var container = fluid.wrap(args[1]);
11179 var message1 = "Instantiation of view component with type " + componentName + " failed, since ";
11180 if (!container) {
11181 fluid.fail(message1 + " container argument is empty");
11182 }
11183 else if (container.length === 0) {
11184 fluid.fail(message1 + "selector \"", fluid.dumpSelector(args[1]), "\" did not match any markup in the document");
11185 } else {
11186 fluid.fail(message1 + " component creator function did not return a value");
11187 }
11188 }
11189 };
11190
11191 fluid.checkTryCatchParameter = function () {
11192 var location = window.location || { search: "", protocol: "file:" };
11193 var GETparams = location.search.slice(1).split("&");
11194 return fluid.find(GETparams, function (param) {
11195 if (param.indexOf("notrycatch") === 0) {
11196 return true;
11197 }
11198 }) === true;
11199 };
11200
11201 fluid.notrycatch = fluid.checkTryCatchParameter();
11202
11203
11204 /**
11205 * Wraps an object in a jQuery if it isn't already one. This function is useful since
11206 * it ensures to wrap a null or otherwise falsy argument to itself, rather than the
11207 * often unhelpful jQuery default of returning the overall document node.
11208 *
11209 * @param {Object} obj - the object to wrap in a jQuery
11210 * @param {jQuery} [userJQuery] - the jQuery object to use for the wrapping, optional - use the current jQuery if absent
11211 * @return {jQuery} - The wrapped object.
11212 */
11213 fluid.wrap = function (obj, userJQuery) {
11214 userJQuery = userJQuery || $;
11215 return ((!obj || obj.jquery) ? obj : userJQuery(obj));
11216 };
11217
11218 /**
11219 * If obj is a jQuery, this function will return the first DOM element within it. Otherwise, the object will be returned unchanged.
11220 *
11221 * @param {jQuery} obj - The jQuery instance to unwrap into a pure DOM element.
11222 * @return {Object} - The unwrapped object.
11223 */
11224 fluid.unwrap = function (obj) {
11225 return obj && obj.jquery ? obj[0] : obj;
11226 };
11227
11228 /**
11229 * Fetches a single container element and returns it as a jQuery.
11230 *
11231 * @param {String|jQuery|element} containerSpec - an id string, a single-element jQuery, or a DOM element specifying a unique container
11232 * @param {Boolean} fallible - <code>true</code> if an empty container is to be reported as a valid condition
11233 * @param {jQuery} [userJQuery] - the jQuery object to use for the wrapping, optional - use the current jQuery if absent
11234 * @return {jQuery} - A single-element jQuery container.
11235 */
11236 fluid.container = function (containerSpec, fallible, userJQuery) {
11237 var selector = containerSpec.selector || containerSpec;
11238 if (userJQuery) {
11239 containerSpec = fluid.unwrap(containerSpec);
11240 }
11241 var container = fluid.wrap(containerSpec, userJQuery);
11242 if (fallible && (!container || container.length === 0)) {
11243 return null;
11244 }
11245
11246 if (!container || !container.jquery || container.length !== 1) {
11247 if (typeof (containerSpec) !== "string") {
11248 containerSpec = container.selector;
11249 }
11250 var count = container.length !== undefined ? container.length : 0;
11251 fluid.fail((count > 1 ? "More than one (" + count + ") container elements were"
11252 : "No container element was") + " found for selector " + containerSpec);
11253 }
11254 if (!fluid.isDOMNode(container[0])) {
11255 fluid.fail("fluid.container was supplied a non-jQueryable element");
11256 }
11257
11258 // To address FLUID-5966, manually adding back the selector and context properties that were removed from jQuery v3.0.
11259 // ( see: https://jquery.com/upgrade-guide/3.0/#breaking-change-deprecated-context-and-selector-properties-removed )
11260 // In most cases the "selector" property will already be restored through the DOM binder;
11261 // however, when a selector or pure jQuery element is supplied directly as a component's container, we need to add them
11262 // if it is possible to infer them. This feature is rarely used but is crucial for the prefs framework infrastructure
11263 // in Panels.js fluid.prefs.subPanel.resetDomBinder
11264 container.selector = selector;
11265 container.context = container.context || containerSpec.ownerDocument || document;
11266
11267 return container;
11268 };
11269
11270 /**
11271 * Creates a new DOM Binder instance, used to locate elements in the DOM by name.
11272 *
11273 * @param {Object} container - the root element in which to locate named elements
11274 * @param {Object} selectors - a collection of named jQuery selectors
11275 * @return {Object} - The new DOM binder.
11276 */
11277 fluid.createDomBinder = function (container, selectors) {
11278 // don't put on a typename to avoid confusing primitive visitComponentChildren
11279 var that = {
11280 id: fluid.allocateGuid(),
11281 cache: {}
11282 };
11283 var userJQuery = container.constructor;
11284
11285 function cacheKey(name, thisContainer) {
11286 return fluid.allocateSimpleId(thisContainer) + "-" + name;
11287 }
11288
11289 function record(name, thisContainer, result) {
11290 that.cache[cacheKey(name, thisContainer)] = result;
11291 }
11292
11293 that.locate = function (name, localContainer) {
11294 var selector, thisContainer, togo;
11295
11296 selector = selectors[name];
11297 if (selector === undefined) {
11298 return undefined;
11299 }
11300 thisContainer = localContainer ? $(localContainer) : container;
11301 if (!thisContainer) {
11302 fluid.fail("DOM binder invoked for selector " + name + " without container");
11303 }
11304 if (selector === "") {
11305 togo = thisContainer;
11306 }
11307 else if (!selector) {
11308 togo = userJQuery();
11309 }
11310 else {
11311 if (typeof (selector) === "function") {
11312 togo = userJQuery(selector.call(null, fluid.unwrap(thisContainer)));
11313 } else {
11314 togo = userJQuery(selector, thisContainer);
11315 }
11316 }
11317
11318 if (!togo.selector) {
11319 togo.selector = selector;
11320 togo.context = thisContainer;
11321 }
11322 togo.selectorName = name;
11323 record(name, thisContainer, togo);
11324 return togo;
11325 };
11326 that.fastLocate = function (name, localContainer) {
11327 var thisContainer = localContainer ? localContainer : container;
11328 var key = cacheKey(name, thisContainer);
11329 var togo = that.cache[key];
11330 return togo ? togo : that.locate(name, localContainer);
11331 };
11332 that.clear = function () {
11333 that.cache = {};
11334 };
11335 that.refresh = function (names, localContainer) {
11336 var thisContainer = localContainer ? localContainer : container;
11337 if (typeof names === "string") {
11338 names = [names];
11339 }
11340 if (thisContainer.length === undefined) {
11341 thisContainer = [thisContainer];
11342 }
11343 for (var i = 0; i < names.length; ++i) {
11344 for (var j = 0; j < thisContainer.length; ++j) {
11345 that.locate(names[i], thisContainer[j]);
11346 }
11347 }
11348 };
11349 that.resolvePathSegment = that.locate;
11350
11351 return that;
11352 };
11353
11354 /* Expect that jQuery selector query has resulted in a non-empty set of
11355 * results. If none are found, this function will fail with a diagnostic message,
11356 * with the supplied message prepended.
11357 */
11358 fluid.expectFilledSelector = function (result, message) {
11359 if (result && result.length === 0 && result.jquery) {
11360 fluid.fail(message + ": selector \"" + result.selector + "\" with name " + result.selectorName +
11361 " returned no results in context " + fluid.dumpEl(result.context));
11362 }
11363 };
11364
11365 /**
11366 * The central initialiation method called as the first act of every Fluid
11367 * component. This function automatically merges user options with defaults,
11368 * attaches a DOM Binder to the instance, and configures events.
11369 *
11370 * @param {String} componentName - The unique "name" of the component, which will be used
11371 * to fetch the default options from store. By recommendation, this should be the global
11372 * name of the component's creator function.
11373 * @param {jQueryable} containerSpec - A specifier for the single root "container node" in the
11374 * DOM which will house all the markup for this component.
11375 * @param {Object} userOptions - The user configuration options for this component.
11376 * @param {Object} localOptions - The local configuration options for this component. Unsupported, see comments for initLittleComponent.
11377 * @return {Object|null} - The newly created component, or `null` id the container does not exist.
11378 */
11379 fluid.initView = function (componentName, containerSpec, userOptions, localOptions) {
11380 var container = fluid.container(containerSpec, true);
11381 fluid.expectFilledSelector(container, "Error instantiating component with name \"" + componentName);
11382 if (!container) {
11383 return null;
11384 }
11385 // Need to ensure container is set early, without relying on an IoC mechanism - rethink this with asynchrony
11386 var receiver = function (that) {
11387 that.container = container;
11388 };
11389 var that = fluid.initLittleComponent(componentName, userOptions, localOptions || {gradeNames: ["fluid.viewComponent"]}, receiver);
11390
11391 if (!that.dom) {
11392 fluid.initDomBinder(that);
11393 }
11394 // TODO: cannot afford a mutable container - put this into proper workflow
11395 var userJQuery = that.options.jQuery; // Do it a second time to correct for jQuery injection
11396 // if (userJQuery) {
11397 // container = fluid.container(containerSpec, true, userJQuery);
11398 // }
11399 fluid.log("Constructing view component " + componentName + " with container " + container.constructor.expando +
11400 (userJQuery ? " user jQuery " + userJQuery.expando : "") + " env: " + $.expando);
11401
11402 return that;
11403 };
11404
11405 /**
11406 * Creates a new DOM Binder instance for the specified component and mixes it in.
11407 *
11408 * @param {Object} that - The component instance to attach the new DOM Binder to.
11409 * @param {Object} selectors - a collection of named jQuery selectors
11410 * @return {Object} - The DOM for the component.
11411 */
11412 fluid.initDomBinder = function (that, selectors) {
11413 if (!that.container) {
11414 fluid.fail("fluid.initDomBinder called for component with typeName " + that.typeName +
11415 " without an initialised container - this has probably resulted from placing \"fluid.viewComponent\" in incorrect position in grade merging order. " +
11416 " Make sure to place it to the right of any non-view grades in the gradeNames list to ensure that it overrides properly: resolved gradeNames is ", that.options.gradeNames, " for component ", that);
11417 }
11418 that.dom = fluid.createDomBinder(that.container, selectors || that.options.selectors || {});
11419 that.locate = that.dom.locate;
11420 return that.dom;
11421 };
11422
11423 // DOM Utilities.
11424
11425 /**
11426 * Finds the nearest ancestor of the element that matches a predicate
11427 * @param {Element} element - DOM element
11428 * @param {Function} test - A function (predicate) accepting a DOM element, returning a truthy value representing a match
11429 * @return {Element|undefined} - The first element parent for which the predicate returns truthy - or undefined if no parent matches
11430 */
11431 fluid.findAncestor = function (element, test) {
11432 element = fluid.unwrap(element);
11433 while (element) {
11434 if (test(element)) {
11435 return element;
11436 }
11437 element = element.parentNode;
11438 }
11439 };
11440
11441 fluid.findForm = function (node) {
11442 return fluid.findAncestor(node, function (element) {
11443 return element.nodeName.toLowerCase() === "form";
11444 });
11445 };
11446
11447 /* A utility with the same signature as jQuery.text and jQuery.html, but without the API irregularity
11448 * that treats a single argument of undefined as different to no arguments */
11449 // in jQuery 1.7.1, jQuery pulled the same dumb trick with $.text() that they did with $.val() previously,
11450 // see comment in fluid.value below
11451 fluid.each(["text", "html"], function (method) {
11452 fluid[method] = function (node, newValue) {
11453 node = $(node);
11454 return newValue === undefined ? node[method]() : node[method](newValue);
11455 };
11456 });
11457
11458 /* A generalisation of jQuery.val to correctly handle the case of acquiring and
11459 * setting the value of clustered radio button/checkbox sets, potentially, given
11460 * a node corresponding to just one element.
11461 */
11462 fluid.value = function (nodeIn, newValue) {
11463 var node = fluid.unwrap(nodeIn);
11464 var multiple = false;
11465 if (node.nodeType === undefined && node.length > 1) {
11466 node = node[0];
11467 multiple = true;
11468 }
11469 if ("input" !== node.nodeName.toLowerCase() || !/radio|checkbox/.test(node.type)) {
11470 // resist changes to contract of jQuery.val() in jQuery 1.5.1 (see FLUID-4113)
11471 return newValue === undefined ? $(node).val() : $(node).val(newValue);
11472 }
11473 var name = node.name;
11474 if (name === undefined) {
11475 fluid.fail("Cannot acquire value from node " + fluid.dumpEl(node) + " which does not have name attribute set");
11476 }
11477 var elements;
11478 if (multiple) {
11479 elements = nodeIn;
11480 } else {
11481 elements = node.ownerDocument.getElementsByName(name);
11482 var scope = fluid.findForm(node);
11483 elements = $.grep(elements, function (element) {
11484 if (element.name !== name) {
11485 return false;
11486 }
11487 return !scope || fluid.dom.isContainer(scope, element);
11488 });
11489 }
11490 if (newValue !== undefined) {
11491 if (typeof(newValue) === "boolean") {
11492 newValue = (newValue ? "true" : "false");
11493 }
11494 // jQuery gets this partially right, but when dealing with radio button array will
11495 // set all of their values to "newValue" rather than setting the checked property
11496 // of the corresponding control.
11497 $.each(elements, function () {
11498 this.checked = (newValue instanceof Array ?
11499 newValue.indexOf(this.value) !== -1 : newValue === this.value);
11500 });
11501 } else { // this part jQuery will not do - extracting value from <input> array
11502 var checked = $.map(elements, function (element) {
11503 return element.checked ? element.value : null;
11504 });
11505 return node.type === "radio" ? checked[0] : checked;
11506 }
11507 };
11508
11509
11510 fluid.BINDING_ROOT_KEY = "fluid-binding-root";
11511
11512 /* Recursively find any data stored under a given name from a node upwards
11513 * in its DOM hierarchy **/
11514
11515 fluid.findData = function (elem, name) {
11516 while (elem) {
11517 var data = $.data(elem, name);
11518 if (data) {
11519 return data;
11520 }
11521 elem = elem.parentNode;
11522 }
11523 };
11524
11525 fluid.bindFossils = function (node, data, fossils) {
11526 $.data(node, fluid.BINDING_ROOT_KEY, {data: data, fossils: fossils});
11527 };
11528
11529 fluid.boundPathForNode = function (node, fossils) {
11530 node = fluid.unwrap(node);
11531 var key = node.name || node.id;
11532 var record = fossils[key];
11533 return record ? record.EL : null;
11534 };
11535
11536 /* relevant, the changed value received at the given DOM node */
11537 fluid.applyBoundChange = function (node, newValue, applier) {
11538 node = fluid.unwrap(node);
11539 if (newValue === undefined) {
11540 newValue = fluid.value(node);
11541 }
11542 if (node.nodeType === undefined && node.length > 0) {
11543 node = node[0];
11544 } // assume here that they share name and parent
11545 var root = fluid.findData(node, fluid.BINDING_ROOT_KEY);
11546 if (!root) {
11547 fluid.fail("Bound data could not be discovered in any node above " + fluid.dumpEl(node));
11548 }
11549 var name = node.name;
11550 var fossil = root.fossils[name];
11551 if (!fossil) {
11552 fluid.fail("No fossil discovered for name " + name + " in fossil record above " + fluid.dumpEl(node));
11553 }
11554 if (typeof(fossil.oldvalue) === "boolean") { // deal with the case of an "isolated checkbox"
11555 newValue = newValue[0] ? true : false;
11556 }
11557 var EL = root.fossils[name].EL;
11558 if (applier) {
11559 applier.fireChangeRequest({path: EL, value: newValue, source: "DOM:" + node.id});
11560 } else {
11561 fluid.set(root.data, EL, newValue);
11562 }
11563 };
11564
11565
11566 /*
11567 * Returns a jQuery object given the id of a DOM node. In the case the element
11568 * is not found, will return an empty list.
11569 */
11570 fluid.jById = function (id, dokkument) {
11571 dokkument = dokkument && dokkument.nodeType === 9 ? dokkument : document;
11572 var element = fluid.byId(id, dokkument);
11573 var togo = element ? $(element) : [];
11574 togo.selector = "#" + id;
11575 togo.context = dokkument;
11576 return togo;
11577 };
11578
11579 /**
11580 * Returns an DOM element quickly, given an id
11581 *
11582 * @param {Object} id - the id of the DOM node to find
11583 * @param {Document} dokkument - the document in which it is to be found (if left empty, use the current document)
11584 * @return {Object} - The DOM element with this id, or null, if none exists in the document.
11585 */
11586 fluid.byId = function (id, dokkument) {
11587 dokkument = dokkument && dokkument.nodeType === 9 ? dokkument : document;
11588 var el = dokkument.getElementById(id);
11589 if (el) {
11590 // Use element id property here rather than attribute, to work around FLUID-3953
11591 if (el.id !== id) {
11592 fluid.fail("Problem in document structure - picked up element " +
11593 fluid.dumpEl(el) + " for id " + id +
11594 " without this id - most likely the element has a name which conflicts with this id");
11595 }
11596 return el;
11597 } else {
11598 return null;
11599 }
11600 };
11601
11602 /**
11603 * Returns the id attribute from a jQuery or pure DOM element.
11604 *
11605 * @param {jQuery|Element} element - the element to return the id attribute for.
11606 * @return {String} - The id attribute of the element.
11607 */
11608 fluid.getId = function (element) {
11609 return fluid.unwrap(element).id;
11610 };
11611
11612 /*
11613 * Allocate an id to the supplied element if it has none already, by a simple
11614 * scheme resulting in ids "fluid-id-nnnn" where nnnn is an increasing integer.
11615 */
11616 fluid.allocateSimpleId = function (element) {
11617 element = fluid.unwrap(element);
11618 if (!element || fluid.isPrimitive(element)) {
11619 return null;
11620 }
11621
11622 if (!element.id) {
11623 var simpleId = "fluid-id-" + fluid.allocateGuid();
11624 element.id = simpleId;
11625 }
11626 return element.id;
11627 };
11628
11629 /**
11630 * Returns the document to which an element belongs, or the element itself if it is already a document
11631 *
11632 * @param {jQuery|Element} element - The element to return the document for
11633 * @return {Document} - The document in which it is to be found
11634 */
11635 fluid.getDocument = function (element) {
11636 var node = fluid.unwrap(element);
11637 // DOCUMENT_NODE - guide to node types at https://developer.mozilla.org/en/docs/Web/API/Node/nodeType
11638 return node.nodeType === 9 ? node : node.ownerDocument;
11639 };
11640
11641 fluid.defaults("fluid.ariaLabeller", {
11642 gradeNames: ["fluid.viewComponent"],
11643 labelAttribute: "aria-label",
11644 liveRegionMarkup: "<div class=\"liveRegion fl-hidden-accessible\" aria-live=\"polite\"></div>",
11645 liveRegionId: "fluid-ariaLabeller-liveRegion",
11646 invokers: {
11647 generateLiveElement: {
11648 funcName: "fluid.ariaLabeller.generateLiveElement",
11649 args: "{that}"
11650 },
11651 update: {
11652 funcName: "fluid.ariaLabeller.update",
11653 args: ["{that}", "{arguments}.0"]
11654 }
11655 },
11656 listeners: {
11657 onCreate: {
11658 func: "{that}.update",
11659 args: [null]
11660 }
11661 }
11662 });
11663
11664 fluid.ariaLabeller.update = function (that, newOptions) {
11665 newOptions = newOptions || that.options;
11666 that.container.attr(that.options.labelAttribute, newOptions.text);
11667 if (newOptions.dynamicLabel) {
11668 var live = fluid.jById(that.options.liveRegionId);
11669 if (live.length === 0) {
11670 live = that.generateLiveElement();
11671 }
11672 live.text(newOptions.text);
11673 }
11674 };
11675
11676 fluid.ariaLabeller.generateLiveElement = function (that) {
11677 var liveEl = $(that.options.liveRegionMarkup);
11678 liveEl.prop("id", that.options.liveRegionId);
11679 $("body").append(liveEl);
11680 return liveEl;
11681 };
11682
11683 var LABEL_KEY = "aria-labelling";
11684
11685 fluid.getAriaLabeller = function (element) {
11686 element = $(element);
11687 var that = fluid.getScopedData(element, LABEL_KEY);
11688 return that;
11689 };
11690
11691 /* Manages an ARIA-mediated label attached to a given DOM element. An
11692 * aria-labelledby attribute and target node is fabricated in the document
11693 * if they do not exist already, and a "little component" is returned exposing a method
11694 * "update" that allows the text to be updated. */
11695
11696 fluid.updateAriaLabel = function (element, text, options) {
11697 options = $.extend({}, options || {}, {text: text});
11698 var that = fluid.getAriaLabeller(element);
11699 if (!that) {
11700 that = fluid.ariaLabeller(element, options);
11701 fluid.setScopedData(element, LABEL_KEY, that);
11702 } else {
11703 that.update(options);
11704 }
11705 return that;
11706 };
11707
11708 /* "Global Dismissal Handler" for the entire page. Attaches a click handler to the
11709 * document root that will cause dismissal of any elements (typically dialogs) which
11710 * have registered themselves. Dismissal through this route will automatically clean up
11711 * the record - however, the dismisser themselves must take care to deregister in the case
11712 * dismissal is triggered through the dialog interface itself. This component can also be
11713 * automatically configured by fluid.deadMansBlur by means of the "cancelByDefault" option */
11714
11715 var dismissList = {};
11716
11717 $(document).click(function (event) {
11718 var target = fluid.resolveEventTarget(event);
11719 while (target) {
11720 if (dismissList[target.id]) {
11721 return;
11722 }
11723 target = target.parentNode;
11724 }
11725 fluid.each(dismissList, function (dismissFunc, key) {
11726 dismissFunc(event);
11727 delete dismissList[key];
11728 });
11729 });
11730 // TODO: extend a configurable equivalent of the above dealing with "focusin" events
11731
11732 /* Accepts a free hash of nodes and an optional "dismissal function".
11733 * If dismissFunc is set, this "arms" the dismissal system, such that when a click
11734 * is received OUTSIDE any of the hierarchy covered by "nodes", the dismissal function
11735 * will be executed.
11736 */
11737 fluid.globalDismissal = function (nodes, dismissFunc) {
11738 fluid.each(nodes, function (node) {
11739 // Don't bother to use the real id if it is from a foreign document - we will never receive events
11740 // from it directly in any case - and foreign documents may be under the control of malign fiends
11741 // such as tinyMCE who allocate the same id to everything
11742 var id = fluid.unwrap(node).ownerDocument === document ? fluid.allocateSimpleId(node) : fluid.allocateGuid();
11743 if (dismissFunc) {
11744 dismissList[id] = dismissFunc;
11745 }
11746 else {
11747 delete dismissList[id];
11748 }
11749 });
11750 };
11751
11752 /* Provides an abstraction for determing the current time.
11753 * This is to provide a fix for FLUID-4762, where IE6 - IE8
11754 * do not support Date.now().
11755 */
11756 fluid.now = function () {
11757 return Date.now ? Date.now() : (new Date()).getTime();
11758 };
11759
11760
11761 /* Sets an interation on a target control, which morally manages a "blur" for
11762 * a possibly composite region.
11763 * A timed blur listener is set on the control, which waits for a short period of
11764 * time (options.delay, defaults to 150ms) to discover whether the reason for the
11765 * blur interaction is that either a focus or click is being serviced on a nominated
11766 * set of "exclusions" (options.exclusions, a free hash of elements or jQueries).
11767 * If no such event is received within the window, options.handler will be called
11768 * with the argument "control", to service whatever interaction is required of the
11769 * blur.
11770 */
11771
11772 fluid.deadMansBlur = function (control, options) {
11773 // TODO: This should be rewritten as a proper component
11774 var that = {options: $.extend(true, {}, fluid.defaults("fluid.deadMansBlur"), options)};
11775 that.blurPending = false;
11776 that.lastCancel = 0;
11777 that.canceller = function (event) {
11778 fluid.log("Cancellation through " + event.type + " on " + fluid.dumpEl(event.target));
11779 that.lastCancel = fluid.now();
11780 that.blurPending = false;
11781 };
11782 that.noteProceeded = function () {
11783 fluid.globalDismissal(that.options.exclusions);
11784 };
11785 that.reArm = function () {
11786 fluid.globalDismissal(that.options.exclusions, that.proceed);
11787 };
11788 that.addExclusion = function (exclusions) {
11789 fluid.globalDismissal(exclusions, that.proceed);
11790 };
11791 that.proceed = function (event) {
11792 fluid.log("Direct proceed through " + event.type + " on " + fluid.dumpEl(event.target));
11793 that.blurPending = false;
11794 that.options.handler(control);
11795 };
11796 fluid.each(that.options.exclusions, function (exclusion) {
11797 exclusion = $(exclusion);
11798 fluid.each(exclusion, function (excludeEl) {
11799 $(excludeEl).on("focusin", that.canceller).
11800 on("fluid-focus", that.canceller).
11801 click(that.canceller).mousedown(that.canceller);
11802 // Mousedown is added for FLUID-4212, as a result of Chrome bug 6759, 14204
11803 });
11804 });
11805 if (!that.options.cancelByDefault) {
11806 $(control).on("focusout", function (event) {
11807 fluid.log("Starting blur timer for element " + fluid.dumpEl(event.target));
11808 var now = fluid.now();
11809 fluid.log("back delay: " + (now - that.lastCancel));
11810 if (now - that.lastCancel > that.options.backDelay) {
11811 that.blurPending = true;
11812 }
11813 setTimeout(function () {
11814 if (that.blurPending) {
11815 that.options.handler(control);
11816 }
11817 }, that.options.delay);
11818 });
11819 }
11820 else {
11821 that.reArm();
11822 }
11823 return that;
11824 };
11825
11826 fluid.defaults("fluid.deadMansBlur", {
11827 gradeNames: "fluid.function",
11828 delay: 150,
11829 backDelay: 100
11830 });
11831
11832})(jQuery, fluid_3_0_0);
11833;
11834/*
11835Copyright The Infusion copyright holders
11836See the AUTHORS.md file at the top-level directory of this distribution and at
11837https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
11838
11839Licensed under the Educational Community License (ECL), Version 2.0 or the New
11840BSD license. You may not use this file except in compliance with one these
11841Licenses.
11842
11843You may obtain a copy of the ECL 2.0 License and BSD License at
11844https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
11845*/
11846
11847var fluid_3_0_0 = fluid_3_0_0 || {};
11848
11849(function ($, fluid) {
11850 "use strict";
11851
11852 /** NOTE: All contents of this file are DEPRECATED and no entry point should be considered a supported API **/
11853
11854 fluid.explodeLocalisedName = function (fileName, locale, defaultLocale) {
11855 var lastDot = fileName.lastIndexOf(".");
11856 if (lastDot === -1 || lastDot === 0) {
11857 lastDot = fileName.length;
11858 }
11859 var baseName = fileName.substring(0, lastDot);
11860 var extension = fileName.substring(lastDot);
11861
11862 var segs = locale.split("_");
11863
11864 var exploded = fluid.transform(segs, function (seg, index) {
11865 var shortSegs = segs.slice(0, index + 1);
11866 return baseName + "_" + shortSegs.join("_") + extension;
11867 });
11868 if (defaultLocale) {
11869 exploded.unshift(baseName + "_" + defaultLocale + extension);
11870 }
11871 return exploded;
11872 };
11873
11874 /** Framework-global caching state for fluid.fetchResources **/
11875
11876 var resourceCache = {};
11877
11878 var pendingClass = {};
11879
11880 /** Accepts a hash of structures with free keys, where each entry has either
11881 * href/url or nodeId set - on completion, callback will be called with the populated
11882 * structure with fetched resource text in the field "resourceText" for each
11883 * entry. Each structure may contain "options" holding raw options to be forwarded
11884 * to jQuery.ajax().
11885 */
11886
11887 fluid.fetchResources = function (resourceSpecs, callback, options) {
11888 var that = {
11889 options: fluid.copy(options || {})
11890 };
11891 that.resourceSpecs = resourceSpecs;
11892 that.callback = callback;
11893 that.operate = function () {
11894 fluid.fetchResources.fetchResourcesImpl(that);
11895 };
11896 fluid.each(resourceSpecs, function (resourceSpec, key) {
11897 resourceSpec.recurseFirer = fluid.makeEventFirer({name: "I/O completion for resource \"" + key + "\""});
11898 resourceSpec.recurseFirer.addListener(that.operate);
11899 if (resourceSpec.url && !resourceSpec.href) {
11900 resourceSpec.href = resourceSpec.url;
11901 }
11902
11903 // If options.defaultLocale is set, it will replace any
11904 // defaultLocale set on an individual resourceSpec
11905 if (that.options.defaultLocale) {
11906 resourceSpec.defaultLocale = that.options.defaultLocale;
11907 }
11908 if (!resourceSpec.locale) {
11909 resourceSpec.locale = resourceSpec.defaultLocale;
11910 }
11911
11912 });
11913 if (that.options.amalgamateClasses) {
11914 fluid.fetchResources.amalgamateClasses(resourceSpecs, that.options.amalgamateClasses, that.operate);
11915 }
11916 fluid.fetchResources.explodeForLocales(resourceSpecs);
11917 that.operate();
11918 return that;
11919 };
11920
11921 fluid.fetchResources.explodeForLocales = function (resourceSpecs) {
11922 fluid.each(resourceSpecs, function (resourceSpec, key) {
11923 if (resourceSpec.locale) {
11924 var exploded = fluid.explodeLocalisedName(resourceSpec.href, resourceSpec.locale, resourceSpec.defaultLocale);
11925 for (var i = 0; i < exploded.length; ++i) {
11926 var newKey = key + "$localised-" + i;
11927 var newRecord = $.extend(true, {}, resourceSpec, {
11928 href: exploded[i],
11929 localeExploded: true
11930 });
11931 resourceSpecs[newKey] = newRecord;
11932 }
11933 resourceSpec.localeExploded = exploded.length;
11934 }
11935 });
11936 return resourceSpecs;
11937 };
11938
11939 fluid.fetchResources.condenseOneResource = function (resourceSpecs, resourceSpec, key, localeCount) {
11940 var localeSpecs = [resourceSpec];
11941 for (var i = 0; i < localeCount; ++i) {
11942 var localKey = key + "$localised-" + i;
11943 localeSpecs.unshift(resourceSpecs[localKey]);
11944 delete resourceSpecs[localKey];
11945 }
11946 var lastNonError = fluid.find_if(localeSpecs, function (spec) {
11947 return !spec.fetchError;
11948 });
11949 if (lastNonError) {
11950 resourceSpecs[key] = lastNonError;
11951 }
11952 };
11953
11954 fluid.fetchResources.condenseForLocales = function (resourceSpecs) {
11955 fluid.each(resourceSpecs, function (resourceSpec, key) {
11956 if (typeof(resourceSpec.localeExploded) === "number") {
11957 fluid.fetchResources.condenseOneResource(resourceSpecs, resourceSpec, key, resourceSpec.localeExploded);
11958 }
11959 });
11960 };
11961
11962 fluid.fetchResources.notifyResources = function (that, resourceSpecs, callback) {
11963 fluid.fetchResources.condenseForLocales(resourceSpecs);
11964 callback(resourceSpecs);
11965 };
11966
11967 /*
11968 * This function is unsupported: It is not really intended for use by implementors.
11969 */
11970 // Add "synthetic" elements of *this* resourceSpec list corresponding to any
11971 // still pending elements matching the PROLEPTICK CLASS SPECIFICATION supplied
11972 fluid.fetchResources.amalgamateClasses = function (specs, classes, operator) {
11973 fluid.each(classes, function (clazz) {
11974 var pending = pendingClass[clazz];
11975 fluid.each(pending, function (pendingrec, canon) {
11976 specs[clazz + "!" + canon] = pendingrec;
11977 pendingrec.recurseFirer.addListener(operator);
11978 });
11979 });
11980 };
11981
11982 /*
11983 * This function is unsupported: It is not really intended for use by implementors.
11984 */
11985 fluid.fetchResources.timeSuccessCallback = function (resourceSpec) {
11986 if (resourceSpec.timeSuccess && resourceSpec.options && resourceSpec.options.success) {
11987 var success = resourceSpec.options.success;
11988 resourceSpec.options.success = function () {
11989 var startTime = new Date();
11990 var ret = success.apply(null, arguments);
11991 fluid.log("External callback for URL " + resourceSpec.href + " completed - callback time: " +
11992 (new Date().getTime() - startTime.getTime()) + "ms");
11993 return ret;
11994 };
11995 }
11996 };
11997
11998 // TODO: Integrate punch-through from old Engage implementation
11999 function canonUrl(url) {
12000 return url;
12001 }
12002
12003 fluid.fetchResources.clearResourceCache = function (url) {
12004 if (url) {
12005 delete resourceCache[canonUrl(url)];
12006 }
12007 else {
12008 fluid.clear(resourceCache);
12009 }
12010 };
12011
12012 /*
12013 * This function is unsupported: It is not really intended for use by implementors.
12014 */
12015 fluid.fetchResources.handleCachedRequest = function (resourceSpec, response, fetchError) {
12016 var canon = canonUrl(resourceSpec.href);
12017 var cached = resourceCache[canon];
12018 if (cached.$$firer$$) {
12019 fluid.log("Handling request for " + canon + " from cache");
12020 var fetchClass = resourceSpec.fetchClass;
12021 if (fetchClass && pendingClass[fetchClass]) {
12022 fluid.log("Clearing pendingClass entry for class " + fetchClass);
12023 delete pendingClass[fetchClass][canon];
12024 }
12025 var result = {response: response, fetchError: fetchError};
12026 resourceCache[canon] = result;
12027 cached.fire(response, fetchError);
12028 }
12029 };
12030
12031 /*
12032 * This function is unsupported: It is not really intended for use by implementors.
12033 */
12034 fluid.fetchResources.completeRequest = function (thisSpec) {
12035 thisSpec.queued = false;
12036 thisSpec.completeTime = new Date();
12037 fluid.log("Request to URL " + thisSpec.href + " completed - total elapsed time: " +
12038 (thisSpec.completeTime.getTime() - thisSpec.initTime.getTime()) + "ms");
12039 thisSpec.recurseFirer.fire();
12040 };
12041
12042 /*
12043 * This function is unsupported: It is not really intended for use by implementors.
12044 */
12045 fluid.fetchResources.makeResourceCallback = function (thisSpec) {
12046 return {
12047 success: function (response) {
12048 thisSpec.resourceText = response;
12049 thisSpec.resourceKey = thisSpec.href;
12050 if (thisSpec.forceCache) {
12051 fluid.fetchResources.handleCachedRequest(thisSpec, response);
12052 }
12053 fluid.fetchResources.completeRequest(thisSpec);
12054 },
12055 error: function (response, textStatus, errorThrown) {
12056 thisSpec.fetchError = {
12057 status: response.status,
12058 textStatus: response.textStatus,
12059 errorThrown: errorThrown
12060 };
12061 if (thisSpec.forceCache) {
12062 fluid.fetchResources.handleCachedRequest(thisSpec, null, thisSpec.fetchError);
12063 }
12064 fluid.fetchResources.completeRequest(thisSpec);
12065 }
12066
12067 };
12068 };
12069
12070
12071 /*
12072 * This function is unsupported: It is not really intended for use by implementors.
12073 */
12074 fluid.fetchResources.issueCachedRequest = function (resourceSpec, options) {
12075 var canon = canonUrl(resourceSpec.href);
12076 var cached = resourceCache[canon];
12077 if (!cached) {
12078 fluid.log("First request for cached resource with url " + canon);
12079 cached = fluid.makeEventFirer({name: "cache notifier for resource URL " + canon});
12080 cached.$$firer$$ = true;
12081 resourceCache[canon] = cached;
12082 var fetchClass = resourceSpec.fetchClass;
12083 if (fetchClass) {
12084 if (!pendingClass[fetchClass]) {
12085 pendingClass[fetchClass] = {};
12086 }
12087 pendingClass[fetchClass][canon] = resourceSpec;
12088 }
12089 options.cache = false; // TODO: Getting weird "not modified" issues on Firefox
12090 $.ajax(options);
12091 }
12092 else {
12093 if (!cached.$$firer$$) {
12094 if (cached.response) {
12095 options.success(cached.response);
12096 } else {
12097 options.error(cached.fetchError);
12098 }
12099 }
12100 else {
12101 fluid.log("Request for cached resource which is in flight: url " + canon);
12102 cached.addListener(function (response, fetchError) {
12103 if (response) {
12104 options.success(response);
12105 } else {
12106 options.error(fetchError);
12107 }
12108 });
12109 }
12110 }
12111 };
12112
12113 /*
12114 * This function is unsupported: It is not really intended for use by implementors.
12115 */
12116 // Compose callbacks in such a way that the 2nd, marked "external" will be applied
12117 // first if it exists, but in all cases, the first, marked internal, will be
12118 // CALLED WITHOUT FAIL
12119 fluid.fetchResources.composeCallbacks = function (internal, external) {
12120 return external ? (internal ?
12121 function () {
12122 try {
12123 external.apply(null, arguments);
12124 }
12125 catch (e) {
12126 fluid.log("Exception applying external fetchResources callback: " + e);
12127 }
12128 internal.apply(null, arguments); // call the internal callback without fail
12129 } : external ) : internal;
12130 };
12131
12132 // unsupported, NON-API function
12133 fluid.fetchResources.composePolicy = function (target, source) {
12134 return fluid.fetchResources.composeCallbacks(target, source);
12135 };
12136
12137 fluid.defaults("fluid.fetchResources.issueRequest", {
12138 mergePolicy: {
12139 success: fluid.fetchResources.composePolicy,
12140 error: fluid.fetchResources.composePolicy,
12141 url: "reverse"
12142 }
12143 });
12144
12145 // unsupported, NON-API function
12146 fluid.fetchResources.issueRequest = function (resourceSpec, key) {
12147 var thisCallback = fluid.fetchResources.makeResourceCallback(resourceSpec);
12148 var options = {
12149 url: resourceSpec.href,
12150 success: thisCallback.success,
12151 error: thisCallback.error,
12152 dataType: resourceSpec.dataType || "text"
12153 };
12154 fluid.fetchResources.timeSuccessCallback(resourceSpec);
12155 options = fluid.merge(fluid.defaults("fluid.fetchResources.issueRequest").mergePolicy,
12156 options, resourceSpec.options);
12157 resourceSpec.queued = true;
12158 resourceSpec.initTime = new Date();
12159 fluid.log("Request with key " + key + " queued for " + resourceSpec.href);
12160
12161 if (resourceSpec.forceCache) {
12162 fluid.fetchResources.issueCachedRequest(resourceSpec, options);
12163 }
12164 else {
12165 $.ajax(options);
12166 }
12167 };
12168
12169
12170 fluid.fetchResources.fetchResourcesImpl = function (that) {
12171 var complete = true;
12172 var resourceSpecs = that.resourceSpecs;
12173 for (var key in resourceSpecs) {
12174 var resourceSpec = resourceSpecs[key];
12175 if (resourceSpec.href && !resourceSpec.completeTime) {
12176 if (!resourceSpec.queued) {
12177 fluid.fetchResources.issueRequest(resourceSpec, key);
12178 }
12179 if (resourceSpec.queued) {
12180 complete = false;
12181 }
12182 }
12183 else if (resourceSpec.nodeId && !resourceSpec.resourceText) {
12184 var node = document.getElementById(resourceSpec.nodeId);
12185 // upgrade this to somehow detect whether node is "armoured" somehow
12186 // with comment or CDATA wrapping
12187 resourceSpec.resourceText = fluid.dom.getElementText(node);
12188 resourceSpec.resourceKey = resourceSpec.nodeId;
12189 }
12190 }
12191 if (complete && that.callback && !that.callbackCalled) {
12192 that.callbackCalled = true;
12193 // Always defer notification in an anti-Zalgo scheme to ease problems like FLUID-6202
12194 // In time this will be resolved by i) latched events, ii) global async ginger world
12195 setTimeout(function () {
12196 fluid.fetchResources.notifyResources(that, resourceSpecs, that.callback);
12197 }, 1);
12198 }
12199 };
12200
12201 // TODO: This framework function is a stop-gap before the "ginger world" is capable of
12202 // asynchronous instantiation. It currently performs very poor fidelity expansion of a
12203 // component's options to discover "resources" only held in the static environment
12204 fluid.fetchResources.primeCacheFromResources = function (componentName) {
12205 var resources = fluid.defaults(componentName).resources;
12206 var expanded = (fluid.expandOptions ? fluid.expandOptions : fluid.identity)(fluid.copy(resources));
12207 fluid.fetchResources(expanded);
12208 };
12209
12210 /** Utilities invoking requests for expansion **/
12211 fluid.registerNamespace("fluid.expander");
12212
12213 /*
12214 * This function is unsupported: It is not really intended for use by implementors.
12215 */
12216 fluid.expander.makeDefaultFetchOptions = function (successdisposer, failid, options) {
12217 return $.extend(true, {dataType: "text"}, options, {
12218 success: function (response, environmentdisposer) {
12219 var json = JSON.parse(response);
12220 environmentdisposer(successdisposer(json));
12221 },
12222 error: function (response, textStatus) {
12223 fluid.log("Error fetching " + failid + ": " + textStatus);
12224 }
12225 });
12226 };
12227
12228 /*
12229 * This function is unsupported: It is not really intended for use by implementors.
12230 */
12231 fluid.expander.makeFetchExpander = function (options) {
12232 return { expander: {
12233 type: "fluid.expander.deferredFetcher",
12234 href: options.url,
12235 options: fluid.expander.makeDefaultFetchOptions(options.disposer, options.url, options.options),
12236 resourceSpecCollector: "{resourceSpecCollector}",
12237 fetchKey: options.fetchKey
12238 }};
12239 };
12240
12241 fluid.expander.deferredFetcher = function (deliverer, source, expandOptions) {
12242 var expander = source.expander;
12243 var spec = fluid.copy(expander);
12244 // fetch the "global" collector specified in the external environment to receive
12245 // this resourceSpec
12246 var collector = fluid.expand(expander.resourceSpecCollector, expandOptions);
12247 delete spec.type;
12248 delete spec.resourceSpecCollector;
12249 delete spec.fetchKey;
12250 var environmentdisposer = function (disposed) {
12251 deliverer(disposed);
12252 };
12253 // replace the callback which is there (taking 2 arguments) with one which
12254 // directly responds to the request, passing in the result and OUR "disposer" -
12255 // which once the user has processed the response (say, parsing JSON and repackaging)
12256 // finally deposits it in the place of the expander in the tree to which this reference
12257 // has been stored at the point this expander was evaluated.
12258 spec.options.success = function (response) {
12259 expander.options.success(response, environmentdisposer);
12260 };
12261 var key = expander.fetchKey || fluid.allocateGuid();
12262 collector[key] = spec;
12263 return fluid.NO_VALUE;
12264 };
12265
12266
12267})(jQuery, fluid_3_0_0);
12268;
12269/*
12270Copyright The Infusion copyright holders
12271See the AUTHORS.md file at the top-level directory of this distribution and at
12272https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
12273
12274Licensed under the Educational Community License (ECL), Version 2.0 or the New
12275BSD license. You may not use this file except in compliance with one these
12276Licenses.
12277
12278You may obtain a copy of the ECL 2.0 License and BSD License at
12279https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
12280*/
12281
12282var fluid_3_0_0 = fluid_3_0_0 || {};
12283
12284(function ($, fluid) {
12285 "use strict";
12286
12287 fluid.defaults("fluid.messageResolver", {
12288 gradeNames: ["fluid.component"],
12289 mergePolicy: {
12290 messageBase: "nomerge",
12291 parents: "nomerge"
12292 },
12293 resolveFunc: fluid.stringTemplate,
12294 parseFunc: fluid.identity,
12295 messageBase: {},
12296 members: {
12297 messageBase: "@expand:{that}.options.parseFunc({that}.options.messageBase)"
12298 },
12299 invokers: {
12300 lookup: "fluid.messageResolver.lookup({that}, {arguments}.0)", // messagecodes
12301 resolve: "fluid.messageResolver.resolve({that}, {arguments}.0, {arguments}.1)" // messagecodes, args
12302 },
12303 parents: []
12304 });
12305
12306 /**
12307 *
12308 * Look up the first matching message template, starting with the current grade and working up through its parents.
12309 * Returns both the template for the message and the function used to resolve the localised value. By default
12310 * the resolve function is `fluid.stringTemplate`, and the template returned uses its syntax.
12311 *
12312 * @param {Object} that - The component itself.
12313 * @param {Array} messagecodes - One or more message codes to look up templates for.
12314 * @return {Object} - An object that contains`template` and `resolveFunc` members (see above).
12315 *
12316 */
12317 fluid.messageResolver.lookup = function (that, messagecodes) {
12318 var resolved = fluid.messageResolver.resolveOne(that.messageBase, messagecodes);
12319 if (resolved === undefined) {
12320 return fluid.find(that.options.parents, function (parent) {
12321 return parent ? parent.lookup(messagecodes) : undefined;
12322 });
12323 } else {
12324 return {template: resolved, resolveFunc: that.options.resolveFunc};
12325 }
12326 };
12327
12328 /**
12329 *
12330 * Look up the first message that corresponds to a message code found in `messageCodes`. Then, resolve its
12331 * localised value. By default, supports variable substitutions using `fluid.stringTemplate`.
12332 *
12333 * @param {Object} that - The component itself.
12334 * @param {Array} messagecodes - A list of message codes to look for.
12335 * @param {Object} args - A map of variables that may potentially be used as part of the final output.
12336 * @return {String} - The final message, localised, with any variables found in `args`.
12337 *
12338 */
12339 fluid.messageResolver.resolve = function (that, messagecodes, args) {
12340 if (!messagecodes) {
12341 return "[No messagecodes provided]";
12342 }
12343 messagecodes = fluid.makeArray(messagecodes);
12344 var looked = that.lookup(messagecodes);
12345 return looked ? looked.resolveFunc(looked.template, args) :
12346 "[Message string for key " + messagecodes[0] + " not found]";
12347 };
12348
12349
12350 // unsupported, NON-API function
12351 fluid.messageResolver.resolveOne = function (messageBase, messagecodes) {
12352 for (var i = 0; i < messagecodes.length; ++i) {
12353 var code = messagecodes[i];
12354 var message = messageBase[code];
12355 if (message !== undefined) {
12356 return message;
12357 }
12358 }
12359 };
12360
12361 /**
12362 *
12363 * Converts a data structure consisting of a mapping of keys to message strings, into a "messageLocator" function
12364 * which maps an array of message codes, to be tried in sequence until a key is found, and an array of substitution
12365 * arguments, into a substituted message string.
12366 *
12367 * @param {Object} messageBase - A body of messages to wrap in a resolver function.
12368 * @param {Function} resolveFunc (Optional) - A "resolver" function to use instead of the default `fluid.stringTemplate`.
12369 * @return {Function} - A "messageLocator" function (see above).
12370 *
12371 */
12372 fluid.messageLocator = function (messageBase, resolveFunc) {
12373 var resolver = fluid.messageResolver({messageBase: messageBase, resolveFunc: resolveFunc});
12374 return function (messagecodes, args) {
12375 return resolver.resolve(messagecodes, args);
12376 };
12377 };
12378
12379 /**
12380 *
12381 * Resolve a "message source", which is either itself a resolver, or an object representing a bundle of messages
12382 * and the associated resolution function.
12383 *
12384 * When passing a "data" object, it is expected to have a `type` element that is set to `data`, and to have a
12385 * `messages` array and a `resolveFunc` function that can be used to resolve messages.
12386 *
12387 * A "resolver" is expected to be an object with a `type` element that is set to `resolver` that exposes a `resolve`
12388 * function.
12389 *
12390 * @param {Object} messageSource - See above.
12391 * @return {Function|String} - A resolve function or a `String` representing the final resolved output.
12392 *
12393 */
12394 fluid.resolveMessageSource = function (messageSource) {
12395 if (messageSource.type === "data") {
12396 if (messageSource.url === undefined) {
12397 return fluid.messageLocator(messageSource.messages, messageSource.resolveFunc);
12398 }
12399 else {
12400 // TODO: fetch via AJAX, and convert format if necessary
12401 }
12402 }
12403 else if (messageSource.type === "resolver") {
12404 return messageSource.resolver.resolve;
12405 }
12406 };
12407})(jQuery, fluid_3_0_0);
12408;
12409/*
12410Copyright The Infusion copyright holders
12411See the AUTHORS.md file at the top-level directory of this distribution and at
12412https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
12413
12414Licensed under the Educational Community License (ECL), Version 2.0 or the New
12415BSD license. You may not use this file except in compliance with one these
12416Licenses.
12417
12418You may obtain a copy of the ECL 2.0 License and BSD License at
12419https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
12420*/
12421
12422var fluid_3_0_0 = fluid_3_0_0 || {};
12423
12424(function ($, fluid) {
12425
12426 "use strict";
12427
12428 /**
12429 * A configurable component to allow users to load multiple resources via AJAX requests.
12430 * The resources can be localised by means of options `locale`, `defaultLocale`. Once all
12431 * resources are loaded, the event `onResourceLoaded` will be fired, which can be used
12432 * to time the creation of components dependent on the resources.
12433 *
12434 * @param {Object} options - The component options.
12435 */
12436 fluid.defaults("fluid.resourceLoader", {
12437 gradeNames: ["fluid.component"],
12438 listeners: {
12439 "onCreate.loadResources": {
12440 listener: "fluid.resourceLoader.loadResources",
12441 args: ["{that}", {expander: {func: "{that}.resolveResources"}}]
12442 }
12443 },
12444 defaultLocale: null,
12445 locale: null,
12446 terms: {}, // Must be supplied by integrators
12447 resources: {}, // Must be supplied by integrators
12448 resourceOptions: {},
12449 // Unsupported, non-API option
12450 invokers: {
12451 transformURL: {
12452 funcName: "fluid.stringTemplate",
12453 args: ["{arguments}.0", "{that}.options.terms"]
12454 },
12455 resolveResources: {
12456 funcName: "fluid.resourceLoader.resolveResources",
12457 args: "{that}"
12458 }
12459 },
12460 events: {
12461 onResourcesLoaded: null
12462 }
12463 });
12464
12465 fluid.resourceLoader.resolveResources = function (that) {
12466 var mapped = fluid.transform(that.options.resources, that.transformURL);
12467
12468 return fluid.transform(mapped, function (url) {
12469 var resourceSpec = {url: url, forceCache: true, options: that.options.resourceOptions};
12470 return $.extend(resourceSpec, fluid.filterKeys(that.options, ["defaultLocale", "locale"]));
12471 });
12472 };
12473
12474 fluid.resourceLoader.loadResources = function (that, resources) {
12475 fluid.fetchResources(resources, function () {
12476 that.resources = resources;
12477 that.events.onResourcesLoaded.fire(resources);
12478 });
12479 };
12480
12481})(jQuery, fluid_3_0_0);
12482;
12483/*
12484Copyright The Infusion copyright holders
12485See the AUTHORS.md file at the top-level directory of this distribution and at
12486https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
12487
12488Licensed under the Educational Community License (ECL), Version 2.0 or the New
12489BSD license. You may not use this file except in compliance with one these
12490Licenses.
12491
12492You may obtain a copy of the ECL 2.0 License and BSD License at
12493https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
12494*/
12495
12496/*
12497 The contents of this file were adapted from ViewComponentSupport.js and ComponentGraph.js in fluid-authoring
12498 See: https://github.com/fluid-project/fluid-authoring/blob/FLUID-4884/src/js/ViewComponentSupport.js
12499 https://github.com/fluid-project/fluid-authoring/blob/FLUID-4884/src/js/ComponentGraph.js
12500*/
12501
12502var fluid_3_0_0 = fluid_3_0_0 || {};
12503
12504(function ($, fluid) {
12505 "use strict";
12506
12507 /**
12508 * A variant of fluid.viewComponent that bypasses the wacky "initView" and variant signature
12509 * workflow, sourcing instead its "container" from an option of that name, so that this argument
12510 * can participate in standard ginger resolution. This enables useful results such as a component
12511 * which can render its own container into the DOM on startup, whilst the container remains immutable.
12512 */
12513 fluid.defaults("fluid.newViewComponent", {
12514 gradeNames: ["fluid.modelComponent"],
12515 members: {
12516 // 3rd argument is throwaway to force evaluation of container
12517 dom: "@expand:fluid.initDomBinder({that}, {that}.options.selectors, {that}.container)",
12518 container: "@expand:fluid.container({that}.options.container)"
12519 }
12520 });
12521
12522 /**
12523 * Used to add an element to a parent container. Internally it can use either of jQuery's prepend or append methods.
12524 *
12525 * @param {jQuery|DOMElement|Selector} parentContainer - any jQueryable selector representing the parent element to
12526 * inject the `elm` into.
12527 * @param {DOMElement|jQuery} elm - a DOM element or jQuery element to be added to the parent.
12528 * @param {String} method - (optional) a string representing the method to use to add the `elm` to the
12529 * `parentContainer`. The method can be "append" (default), "prepend", or "html" (will
12530 * replace the contents).
12531 */
12532 fluid.newViewComponent.addToParent = function (parentContainer, elm, method) {
12533 method = method || "append";
12534 $(parentContainer)[method](elm);
12535 };
12536
12537 /**
12538 * Similar to fluid.newViewComponent; however, it will render its own markup including its container, into a
12539 * specified parent container.
12540 */
12541 fluid.defaults("fluid.containerRenderingView", {
12542 gradeNames: ["fluid.newViewComponent"],
12543 container: "@expand:{that}.renderContainer()",
12544 // The DOM element which this component should inject its markup into on startup
12545 parentContainer: "fluid.notImplemented", // must be overridden
12546 injectionType: "append",
12547 invokers: {
12548 renderMarkup: "fluid.identity({that}.options.markup.container)",
12549 renderContainer: "fluid.containerRenderingView.renderContainer({that}, {that}.renderMarkup, {that}.addToParent)",
12550 addToParent: {
12551 funcName: "fluid.newViewComponent.addToParent",
12552 args: ["{that}.options.parentContainer", "{arguments}.0", "{that}.options.injectionType"]
12553 }
12554 }
12555 });
12556
12557 /**
12558 * Renders the components markup and inserts it into the parent container based on the addToParent method
12559 *
12560 * @param {Component} that - the component
12561 * @param {Function} renderMarkup - a function returning the components container markup to be inserted into the
12562 * parentContainer element
12563 * @param {Function} addToParent - a function that inserts the container into the DOM
12564 * @return {DOMElement} - the container
12565 */
12566 fluid.containerRenderingView.renderContainer = function (that, renderMarkup, addToParent) {
12567 fluid.log("Rendering container for " + that.id);
12568 var containerMarkup = renderMarkup();
12569 var container = $(containerMarkup);
12570 addToParent(container);
12571 return container;
12572 };
12573
12574 /**
12575 * Similar to fluid.newViewComponent; however, it will fetch a template and render it into the container.
12576 *
12577 * The template path must be supplied either via a top level `template` option or directly to the
12578 * `resources.template` option. The path may optionally include "terms" to use as tokens which will be resolved
12579 * from values specified in the `terms` option.
12580 *
12581 * The template is fetched on creation and rendered into the container after it has been fetched. After rendering
12582 * the `afterRender` event is fired.
12583 */
12584 fluid.defaults("fluid.templateRenderingView", {
12585 gradeNames: ["fluid.newViewComponent", "fluid.resourceLoader"],
12586 resources: {
12587 template: "fluid.notImplemented"
12588 },
12589 injectionType: "append",
12590 events: {
12591 afterRender: null
12592 },
12593 listeners: {
12594 "onResourcesLoaded.render": "{that}.render",
12595 "onResourcesLoaded.afterRender": {
12596 listener: "{that}.events.afterRender",
12597 args: ["{that}"],
12598 priority: "after:render"
12599 }
12600 },
12601 invokers: {
12602 render: {
12603 funcName: "fluid.newViewComponent.addToParent",
12604 args: ["{that}.container", "{that}.resources.template.resourceText", "{that}.options.injectionType"]
12605 }
12606 },
12607 distributeOptions: {
12608 "mapTemplateSource": {
12609 source: "{that}.options.template",
12610 removeSource: true,
12611 target: "{that}.options.resources.template"
12612 }
12613 }
12614 });
12615
12616})(jQuery, fluid_3_0_0);
12617;
12618/*
12619Copyright The Infusion copyright holders
12620See the AUTHORS.md file at the top-level directory of this distribution and at
12621https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
12622
12623Licensed under the Educational Community License (ECL), Version 2.0 or the New
12624BSD license. You may not use this file except in compliance with one these
12625Licenses.
12626
12627You may obtain a copy of the ECL 2.0 License and BSD License at
12628https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
12629*/
12630
12631var fluid_3_0_0 = fluid_3_0_0 || {};
12632
12633(function ($, fluid) {
12634 "use strict";
12635
12636 /*
12637 * Provides a grade for creating and interacting with a Mutation Observer to listen/respond to DOM changes.
12638 */
12639 fluid.defaults("fluid.mutationObserver", {
12640 gradeNames: ["fluid.viewComponent"],
12641 events: {
12642 onNodeAdded: null,
12643 onNodeRemoved: null,
12644 onAttributeChanged: null
12645 },
12646 listeners: {
12647 "onDestroy.disconnect": "{that}.disconnect"
12648 },
12649 members: {
12650 observer: {
12651 expander: {
12652 func: "{that}.createObserver"
12653 }
12654 }
12655 },
12656 defaultObserveConfig: {
12657 attributes: true,
12658 childList: true,
12659 subtree: true
12660 },
12661 invokers: {
12662 observe: {
12663 funcName: "fluid.mutationObserver.observe",
12664 args: ["{that}", "{arguments}.0", "{arguments}.1"]
12665 },
12666 disconnect: {
12667 "this": "{that}.observer",
12668 method: "disconnect"
12669 },
12670 takeRecords: {
12671 "this": "{that}.observer",
12672 method: "takeRecords"
12673 },
12674 createObserver: {
12675 funcName: "fluid.mutationObserver.createObserver",
12676 args: ["{that}"]
12677 }
12678 }
12679 });
12680
12681 /**
12682 * A Mutation Observer; allows for tracking changes to the DOM.
12683 * A mutation observer is created with a callback function, configured through its `observe` method.
12684 * See: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
12685 *
12686 * @typedef {Object} MutationObserver
12687 */
12688
12689 /**
12690 * Instantiates a mutation observer, defining the callback function which relays the observations to component
12691 * events. The configuration passed to the observe function will determine what mutations are observed and what
12692 * information is returned. See `fluid.mutationObserver.observe` about configuring the mutation observer.
12693 *
12694 * Event Mapping:
12695 * onNodeAdded - fired for each added node, includes the node and mutation record
12696 * onNodeRemoved - fired for each removed node, includes the node and mutation record
12697 * onAttributeChanged - fired for each attribute change, includes the node and mutation record
12698 *
12699 * @param {Component} that - an instance of `fluid.mutationObserver`
12700 *
12701 * @return {MutationObserver} - the created mutation observer`
12702 */
12703 fluid.mutationObserver.createObserver = function (that) {
12704 var observer = new MutationObserver(function (mutationRecords) {
12705 fluid.each(mutationRecords, function (mutationRecord) {
12706 // IE11 doesn't support forEach on NodeLists and NodeLists aren't real arrays so using fluid.each
12707 // will iterate over the object properties. Therefore we use a for loop to iterate over the nodes.
12708 for (var i = 0; i < mutationRecord.addedNodes.length; i++) {
12709 that.events.onNodeAdded.fire(mutationRecord.addedNodes[i], mutationRecord);
12710 }
12711 for (var j = 0; j < mutationRecord.removedNodes.length; j++) {
12712 that.events.onNodeRemoved.fire(mutationRecord.removedNodes[j], mutationRecord);
12713 }
12714 if (mutationRecord.type === "attributes") {
12715 that.events.onAttributeChanged.fire(mutationRecord.target, mutationRecord);
12716 }
12717 });
12718 });
12719
12720 return observer;
12721 };
12722
12723 /**
12724 * Starts observing the DOM changes. Optionally takes in a target and configuration for setting up the specific
12725 * observation. The observe method may be called multiple times; however, if the same observer is set on the same
12726 * node, the old one will be replaced. If an observation is disconnected, the observe method will need to be called
12727 * again to re-instate the mutation observation.
12728 * See: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
12729 *
12730 * @param {Component} that - an instance of `fluid.mutationObserver`
12731 * @param {DOMElement|jQuery} target - a DOM element or jQuery element to be observed. By default the component's
12732 * container element is used.
12733 * @param {Object} options - config options to pass to the observations. This specifies which mutations should be
12734 * reported. See: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit
12735 * By default the config specified at `that.options.defaultObserveConfig` is used.
12736 */
12737 fluid.mutationObserver.observe = function (that, target, options) {
12738 target = fluid.unwrap(target || that.container);
12739 that.observer.observe(target, options || that.options.defaultObserveConfig);
12740 };
12741
12742
12743})(jQuery, fluid_3_0_0);
12744;
12745/*
12746Copyright The Infusion copyright holders
12747See the AUTHORS.md file at the top-level directory of this distribution and at
12748https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
12749
12750Licensed under the Educational Community License (ECL), Version 2.0 or the New
12751BSD license. You may not use this file except in compliance with one these
12752Licenses.
12753
12754You may obtain a copy of the ECL 2.0 License and BSD License at
12755https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
12756*/
12757
12758var fluid_3_0_0 = fluid_3_0_0 || {};
12759
12760(function ($, fluid) {
12761 "use strict";
12762
12763 /*******************************************************************************
12764 * fluid.textNodeParser
12765 *
12766 * Parses out the text nodes from a DOM element and its descendants
12767 *******************************************************************************/
12768
12769 fluid.defaults("fluid.textNodeParser", {
12770 gradeNames: ["fluid.component"],
12771 events: {
12772 onParsedTextNode: null,
12773 afterParse: null
12774 },
12775 invokers: {
12776 parse: {
12777 funcName: "fluid.textNodeParser.parse",
12778 args: ["{that}", "{arguments}.0", "{arguments}.1", "{that}.events.afterParse.fire"]
12779 },
12780 hasTextToRead: "fluid.textNodeParser.hasTextToRead",
12781 isWord: "fluid.textNodeParser.isWord",
12782 getLang: "fluid.textNodeParser.getLang"
12783 }
12784 });
12785
12786 /**
12787 * Tests if a string is a word; i.e. it has a value and is not only whitespace.
12788 * inspired by https://stackoverflow.com/a/2031143
12789 *
12790 * @param {String} str - the String to test
12791 *
12792 * @return {Boolean} - `true` if a word, `false` otherwise.
12793 */
12794 fluid.textNodeParser.isWord = function (str) {
12795 return fluid.isValue(str) && /\S/.test(str);
12796 };
12797
12798 /**
12799 * Determines if there is text in an element that should be read.
12800 * Will return false in the following conditions:
12801 * - elm is falsey (undefined, null, etc.)
12802 * - elm's offsetHeight is 0 (e.g. display none set on itself or its parent)
12803 * - elm has no text or only whitespace
12804 *
12805 * NOTE: Text added by pseudo elements (e.g. :before, :after) are not considered.
12806 * NOTE: This method is not supported in IE 11 because innerText returns the text for some hidden elements
12807 * that is inconsistent with modern browsers.
12808 *
12809 * @param {jQuery|DomElement} elm - either a DOM node or a jQuery element
12810 *
12811 * @return {Boolean} - returns true if there is rendered text within the element and false otherwise.
12812 * (See rules above)
12813 */
12814 fluid.textNodeParser.hasTextToRead = function (elm) {
12815 elm = fluid.unwrap(elm);
12816
12817 return elm &&
12818 !!elm.offsetHeight &&
12819 fluid.textNodeParser.isWord(elm.innerText);
12820 };
12821
12822 /**
12823 * Similar to `fluid.textNodeParser.hasTextToRead` except adds the following condition:
12824 * - elm or its parent has `aria-hidden="true"` set.
12825 *
12826 * @param {jQuery|DomElement} elm - either a DOM node or a jQuery element
12827 *
12828 * @return {Boolean} - returns true if there is rendered text within the element and false otherwise.
12829 * (See rules above)
12830 */
12831 fluid.textNodeParser.hasVisibleText = function (elm) {
12832 return fluid.textNodeParser.hasTextToRead(elm) && !$(elm).closest("[aria-hidden=\"true\"]").length;
12833 };
12834
12835 /**
12836 * Uses jQuery's `closest` method to find the closest element with a lang attribute, and returns the value.
12837 *
12838 * @param {jQuery|DomElement} elm - either a DOM node or a jQuery element
12839 *
12840 * @return {String|Undefined} - a valid BCP 47 language code if found, otherwise undefined.
12841 */
12842 fluid.textNodeParser.getLang = function (elm) {
12843 return $(elm).closest("[lang]").attr("lang");
12844 };
12845
12846 /**
12847 * Recursively parses a DOM element and it's sub elements and fires the `onParsedTextNode` event for each text node
12848 * found. The event is fired with the text node, language and index of the text node in the list of its parent's
12849 * child nodes..
12850 *
12851 * Note: elements that return `false` to `that.hasTextToRead` are ignored.
12852 *
12853 * @param {Component} that - an instance of `fluid.textNodeParser`
12854 * @param {jQuery|DomElement} elm - the DOM node to parse
12855 * @param {String} lang - a valid BCP 47 language code.
12856 * @param {Event} afterParseEvent - the event to fire after parsing has completed.
12857 */
12858 fluid.textNodeParser.parse = function (that, elm, lang, afterParseEvent) {
12859 elm = fluid.unwrap(elm);
12860 lang = lang || that.getLang(elm);
12861
12862 if (that.hasTextToRead(elm)) {
12863 var childNodes = elm.childNodes;
12864 var elementLang = elm.getAttribute("lang") || lang;
12865
12866 $.each(childNodes, function (childIndex, childNode) {
12867 if (childNode.nodeType === Node.TEXT_NODE) {
12868 that.events.onParsedTextNode.fire(childNode, elementLang, childIndex);
12869 } else if (childNode.nodeType === Node.ELEMENT_NODE) {
12870 fluid.textNodeParser.parse(that, childNode, elementLang);
12871 }
12872 });
12873 }
12874
12875 if (afterParseEvent) {
12876 afterParseEvent(that);
12877 }
12878 };
12879
12880})(jQuery, fluid_3_0_0);
12881
12882//# sourceMappingURL=infusion-framework-no-jquery.js.map
\No newline at end of file