Reusable Accessible Mapping Platform

API Docs for: 5.0.0
Show:

File: src\js\RAMP\Utils\util.js

  1. /* global define, window, XMLHttpRequest, ActiveXObject, XSLTProcessor, console, $, document, jQuery, FileReader */
  2. /* jshint bitwise:false */
  3.  
  4. /**
  5. * Utility module containing useful static classes.
  6. *
  7. * @module Utils
  8. * @main Utils
  9. */
  10.  
  11. /**
  12. * A set of functions used by at least one module in this project. The functions
  13. * are generic enough that they may become useful for other modules later or functions
  14. * that are shared amongst multiple modules should be added here.
  15. *
  16. * *__NOTE__: None of these functions require the global configuration object. (i.e. they
  17. * are not exclusive to RAMP). For functions that depend on the global configuration
  18. * object, place them in ramp.js.*
  19. *
  20. * @class Util
  21. * @static
  22. * @uses dojo/_base/array
  23. * @uses dojo/_base/lang
  24. * @uses dojo/topic
  25. * @uses dojo/Deferred
  26. * @uses esri/geometry/Extent
  27. * @uses esri/graphic
  28. */
  29. define(["dojo/_base/array", "dojo/_base/lang", "dojo/topic", "dojo/Deferred", "esri/geometry/Extent", "esri/graphic"],
  30. function (dojoArray, dojoLang, topic, Deferred, Extent, Graphic) {
  31. "use strict";
  32.  
  33. /**
  34. * Helper function for wrapping File API calls in Promise objects. Used for building a series of helpers which
  35. * call different file read methods.
  36. *
  37. * @method _wrapFileCallInPromise
  38. * @private
  39. * @param {String} readMethod a string indicating the FileReader method to call
  40. * @return {Function} a function which accepts a {File} object and returns a Promise
  41. */
  42. function _wrapFileCallInPromise(readMethod) {
  43. return function (file) {
  44. var reader = new FileReader(),
  45. def = new Deferred();
  46.  
  47. reader.onloadend = function (e) { def.resolve(e.target.result); };
  48. reader.onerror = function (e) { def.reject(e.target.error); };
  49. try {
  50. reader[readMethod](file);
  51. } catch (e) {
  52. def.reject(e);
  53. }
  54.  
  55. return def.promise;
  56. };
  57. }
  58.  
  59. return {
  60. /**
  61. * Checks if the console exists, if not, redefine the console and all console methods to
  62. * a function that does nothing. Useful for IE which does not have the console until the
  63. * debugger is opened.
  64. *
  65. * @method checkConsole
  66. * @static
  67. */
  68. checkConsole: function () {
  69. var noop = function () { },
  70. methods = [
  71. 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',
  72. 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
  73. 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',
  74. 'timeStamp', 'trace', 'warn'
  75. ],
  76. length = methods.length,
  77. console = (window.console = window.console || {}),
  78. method;
  79.  
  80. while (length--) {
  81. method = methods[length];
  82.  
  83. // Only stub undefined methods.
  84. if (!console[method]) {
  85. console[method] = noop;
  86. }
  87. }
  88. },
  89.  
  90. // String Functions
  91.  
  92. /**
  93. * Returns an String that has it's angle brackets ('<' and '>') escaped (replaced by '&lt;' and '&gt;').
  94. * This will effectively cause the String to be displayed in plain text when embedded in an HTML page.
  95. *
  96. * @method escapeHtml
  97. * @static
  98. * @param {String} html String to escape
  99. * @return {String} escapeHtml Escaped string
  100. */
  101. escapeHtml: function (html) {
  102. return html.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
  103. },
  104.  
  105. /**
  106. * Returns true if the given String is a number
  107. *
  108. * @method isNumber
  109. * @static
  110. * @param {String} input The string to check
  111. * @return {boolean} True if number
  112. */
  113. isNumber: function (input) {
  114. return isFinite(String(input).trim() || NaN);
  115. },
  116.  
  117. /**
  118. * Parse the given String into a boolean. Returns true if the String
  119. * is the word "true" (case insensitive). False otherwise.
  120. *
  121. * @method parseBool
  122. * @static
  123. * @param {String} str The string to check
  124. * @return {boolean} True if `true`
  125. */
  126. parseBool: function (str) {
  127. return (str.toLowerCase() === 'true');
  128. },
  129.  
  130. // Deferred
  131.  
  132. /**
  133. * Executes the callback function only after all the deferred Objects in the
  134. * given deferredList has resolved.
  135. *
  136. * @method afterAll
  137. * @static
  138. * @param {array} deferredList A list of Deferred objects
  139. * @param {function} callback The callback to be executed
  140. */
  141. afterAll: function (deferredList, callback, context) {
  142. if (deferredList.length === 0) {
  143. callback();
  144. return;
  145. }
  146.  
  147. var completed = 0; // Keeps track of the number of deferred that has resolved
  148. dojoArray.forEach(deferredList, function (deferred) {
  149. deferred.then(function () {
  150. completed++;
  151. if (completed === deferredList.length) {
  152. callback.call(context);
  153. }
  154. });
  155. });
  156. },
  157.  
  158. // Serialization
  159.  
  160. /**
  161. * Converts an array into a '+' separated String that can be used as
  162. * the query parameter of a URL.
  163. *
  164. * arrayToQuery(["abc", 123, "efg"]) -> "abc+123+efg"
  165. *
  166. * *__NOTE:__ the array should only contain primitives, objects will not be serialized
  167. * properly.*
  168. *
  169. * @method arrayToQuery
  170. * @static
  171. * @param {array} array An array of primitives to be serialized
  172. * @return {String} A serialized representation of the given array
  173. */
  174. arrayToQuery: function (array) {
  175. return array.join("+");
  176. },
  177.  
  178. /**
  179. * Converts a query String generated by arrayToQuery into an array object.
  180. * The array object will only contain Strings.
  181. *
  182. * queryToArray("abc+123+efg") -> ["abc", "123", "efg"]
  183. *
  184. * @method queryToArray
  185. * @static
  186. * @param {String} query A query string to be converted
  187. * @return {String} A resulting array of strings
  188. */
  189. queryToArray: function (query) {
  190. return query.split("+");
  191. },
  192.  
  193. // Event handling
  194.  
  195. /**
  196. * A convenience method that wraps around Dojo's subscribe method to allow
  197. * a scope to hitched to the given callback function.
  198. *
  199. * @method subscribe
  200. * @static
  201. * @param {String} name Event name
  202. * @param {function} callback The callback to be executed
  203. * @param {object} scope Scope of the callback
  204. */
  205. subscribe: function (name, callback, scope) {
  206. if (this.isUndefined(scope)) {
  207. topic.subscribe(name, callback);
  208. } else {
  209. topic.subscribe(name, dojoLang.hitch(scope, callback));
  210. }
  211. },
  212.  
  213. /**
  214. * Subscribes to an event, after the event has occurred, the handle is
  215. * removed.
  216. *
  217. * @method subscribeOnce
  218. * @static
  219. * @param {String} name Event name
  220. * @param {function} callback The callback to be executed
  221. */
  222. subscribeOnce: function (name, callback) {
  223. var handle = null,
  224. wrapper = function (evt) {
  225. handle.remove();
  226. callback(evt);
  227. };
  228.  
  229. return (handle = topic.subscribe(name, wrapper));
  230. },
  231.  
  232. /**
  233. * Subscribes to a set of events, executes the callback when any of the events fire, then removes the handle.
  234. *
  235. * @method subscribeOnceAny
  236. * @static
  237. * @param {String} names An array of event names
  238. * @param {Function} callback The callback to be executed
  239. */
  240. subscribeOnceAny: function (names, callback) {
  241. var handles = [];
  242.  
  243. function wrapper(evt) {
  244. dojoArray.forEach(handles, function (handle) {
  245. handle.remove();
  246. });
  247.  
  248. callback(evt);
  249. }
  250.  
  251. dojoArray.forEach(names, dojoLang.hitch(this,
  252. function (name) {
  253. handles.push(this.subscribeOnce(name, wrapper));
  254. }));
  255. },
  256.  
  257. /**
  258. * Given an array of event names published by topic.publish, call the given
  259. * callback function after ALL of the given events have occurred. An array of
  260. * arguments is passed to the callback function, the arguments are those returned
  261. * by the events (in the order that the events appear in the array).
  262. *
  263. * #####Example
  264. *
  265. * Assume somewhere a module publishes a "click" event:
  266. *
  267. * topic.publish("click", { mouseX: 10, mouseY: 50 });
  268. *
  269. * and somewhere else another module publishes a "finishLoading" event:
  270. *
  271. * topic.publish("finishLoading", { loadedPictures: [pic1, pic2] });
  272. *
  273. * Then if one wants to do something (e.g. display pictures) only after the pictures
  274. * have been loaded AND the user clicked somewhere, then:
  275. *
  276. * - args[0] will be the object returned by the "click" event
  277. * - which in this case will be: { mouseX: 10, mouseY: 50 }
  278. * - args[1] will be the object returned by the "finishLoading" event
  279. * - which in this case will be: { loadedPictures: [pic1, pic2] }
  280. *
  281. *
  282. * subscribe(["click", "finishLoading"], function(args) {
  283. * doSomething();
  284. * });
  285. *
  286. * *__NOTE:__
  287. * If one of the events fires multiple times before the other event, the object
  288. * passed by this function to the callback will be the object returned when the
  289. * event FIRST fired (subsequent firings of the same event are ignored). Also, if
  290. * some event do not return an object, it will also be excluded in the arguments to
  291. * the callback function. So be careful! For example, say you subscribed to the events:
  292. * "evt1", "evt2", "evt3". "evt1" returns an object (call it "evt1Obj"), "evt2" does not,
  293. * "evt3" returns two objects (call it "evt3Obj-1" and "evt3Obj-2" respectively).
  294. * Then the array passed to the callback will be: ["evt1Obj", "evt3Obj-1", "evt3Obj-2"].*
  295. *
  296. * @method subscribeAll
  297. * @static
  298. * @param {array} nameArray An array of Strings containing the names of events to subscribe to
  299. * @param {function} callback The callback to be executed
  300. */
  301. subscribeAll: function (nameArray, callback) {
  302. // Keeps track of the status of all the events being subscribed to
  303. var events = [];
  304.  
  305. dojoArray.forEach(nameArray, function (eventName, i) {
  306. events.push({
  307. fired: false,
  308. args: null
  309. });
  310.  
  311. topic.subscribe(eventName, function () {
  312. // If this is the fire time the event fired
  313. if (!events[i].fired) {
  314. // Mark the event has fired and capture it's arguments (if any)
  315. events[i].fired = true;
  316. events[i].args = Array.prototype.slice.call(arguments);
  317.  
  318. // Check if all events have fired
  319. if (dojoArray.every(events, function (event) {
  320. return event.fired;
  321. })) {
  322. // If so construct an array with arguments from the events
  323. var eventArgs = [];
  324. dojoArray.forEach(events, function (event) {
  325. eventArgs.append(event.args);
  326. });
  327. callback(eventArgs);
  328. }
  329. }
  330. });
  331. });
  332. },
  333.  
  334. // Specialized Variables *
  335.  
  336. /**
  337. * Creates an object that acts like a lazy variable (i.e. a variable whose value is only
  338. * resolved the first time it is retrieved, not when it is assigned). The value given to
  339. * the lazy variable should be the return value of the given initFunc. The returned object
  340. * has two methods:
  341. *
  342. * - get - returns the value of the variable, if it is the first time get is called, the
  343. * the initFunc will be called to resolve the value of the variable.
  344. * - reset - forces the variable to call the initFunc again the next time get is called
  345. *
  346. * @method createLazyVariable
  347. * @static
  348. * @param {function} initFunc A function to call to resolve the variable value
  349. * @return {Object} The lazy variable
  350. */
  351. createLazyVariable: function (initFunc) {
  352. var value = null;
  353. return {
  354. reset: function () {
  355. value = null;
  356. },
  357.  
  358. get: function () {
  359. if (value == null) {
  360. value = initFunc();
  361. }
  362. return value;
  363. }
  364. };
  365. },
  366.  
  367. // FUNCTION DECORATORS
  368.  
  369. /**
  370. * Returns a function that has the same functionality as the given function, but
  371. * can only be executed once (subsequent execution does nothing).
  372. *
  373. * @method once
  374. * @static
  375. * @param {function} func Function to be decorated
  376. * @return {function} Decorated function that can be executed once
  377. */
  378. once: function (func) {
  379. var ran = false;
  380. return function () {
  381. if (!ran) {
  382. func();
  383. ran = true;
  384. }
  385. };
  386. },
  387.  
  388. // MISCELLANEOUS
  389.  
  390. /**
  391. * Returns true if the given obj is undefined, false otherwise.
  392. *
  393. * @method isUndefined
  394. * @static
  395. * @param {object} obj Object to be checked
  396. * @return {boolean} True if the given object is undefined, false otherwise
  397. */
  398. isUndefined: function (obj) {
  399. return (typeof obj === 'undefined');
  400. },
  401.  
  402. /**
  403. * Compares two graphic objects.
  404. *
  405. * @method compareGraphics
  406. * @static
  407. * @param {Object} one Graphic object
  408. * @param {Object} two Graphic object
  409. * @return {boolean} True if the objects represent the same feature
  410. */
  411. compareGraphics: function (one, two) {
  412. var oneKey = "0",
  413. twoKey = "1",
  414. objectIdField,
  415. oneLayer,
  416. twoLayer;
  417.  
  418. if (one && two &&
  419. $.isFunction(one.getLayer) && $.isFunction(two.getLayer)) {
  420. oneLayer = one.getLayer();
  421. twoLayer = two.getLayer();
  422. objectIdField = oneLayer.objectIdField;
  423. oneKey = oneLayer.sourceLayerId + one.attributes[objectIdField];
  424. twoKey = twoLayer.sourceLayerId + two.attributes[objectIdField];
  425. }
  426.  
  427. return oneKey === twoKey;
  428. },
  429.  
  430. /**
  431. * Returns the width of the scrollbar in pixels. Since different browsers render scrollbars differently, the width may vary.
  432. *
  433. * @method scrollbarWidth
  434. * @static
  435. * @return {int} The width of the scrollbar in pixels
  436. * @for Util
  437. */
  438. scrollbarWidth: function () {
  439. var $inner = jQuery('<div style="width: 100%; height:200px;">test</div>'),
  440. $outer = jQuery('<div style="width:200px;height:150px; position: absolute; top: 0; left: 0; visibility: hidden; overflow:hidden;"></div>').append($inner),
  441. inner = $inner[0],
  442. outer = $outer[0],
  443. width1, width2;
  444.  
  445. jQuery('body').append(outer);
  446. width1 = inner.offsetWidth;
  447. $outer.css('overflow', 'scroll');
  448. width2 = outer.clientWidth;
  449. $outer.remove();
  450.  
  451. return (width1 - width2);
  452. },
  453.  
  454. /**
  455. * Checks if the height of the scrollable content of the body is taller than its height;
  456. * if so, offset the content horizontally to accommodate for the scrollbar assuming target's width is
  457. * set to "100%".
  458. *
  459. * @method adjustWidthForSrollbar
  460. * @static
  461. * @param {jObject} body A DOM node with a scrollbar (or not)
  462. * @param {jObject} targets An array of jObjects to add the offset to
  463. */
  464. adjustWidthForSrollbar: function (body, targets) {
  465. var offset = body.innerHeight() < body[0].scrollHeight ? this.scrollbarWidth() : 0;
  466.  
  467. dojoArray.map(targets, function (target) {
  468. target.css({
  469. right: offset
  470. });
  471. });
  472. },
  473.  
  474. /**
  475. * Waits until a given function is available and executes a callback function.
  476. *
  477. * @method executeOnLoad
  478. * @static
  479. * @param {Object} target an object on which to wait for function to appear
  480. * @param {function} func A function whose availability in question
  481. * @param {function} callback The callback function to be executed after func is available
  482. */
  483. executeOnLoad: function (target, func, callback) {
  484. var deferred = new Deferred(),
  485. handle;
  486.  
  487. deferred.then(function () {
  488. window.clearInterval(handle);
  489. //console.log("deffered resolved");
  490.  
  491. callback();
  492. });
  493.  
  494. handle = window.setInterval(function () {
  495. if ($.isFunction(target[func])) {
  496. deferred.resolve(true);
  497. }
  498. }, 500);
  499. },
  500.  
  501. /**
  502. * Loops through all object properties and applies a given function to each. Resolves the given deferred when done.
  503. *
  504. * @method executeOnDone
  505. * @static
  506. * @param {object} o Object to look through
  507. * @param {function} func A function to be executed with each object propety. Accepts two parameters: property and deferred to be resolved when it's done.
  508. * @param {object} d A deferred to be resolved when all properties have been processed.
  509. */
  510. executeOnDone: function (o, func, d) {
  511. var counter = 0,
  512. arr = [],
  513. deferred;
  514.  
  515. function fnOnDeferredCancel() {
  516. d.cancel();
  517. }
  518.  
  519. function fnOnDeferredThen() {
  520. counter--;
  521. if (counter === 0) {
  522. d.resolve(true);
  523. }
  524. }
  525.  
  526. d = d || new Deferred();
  527.  
  528. for (var q in o) {
  529. if (o.hasOwnProperty(q)) {
  530. arr.push(o[q]);
  531. }
  532. }
  533.  
  534. counter = arr.length;
  535.  
  536. arr.forEach(function (p) {
  537. deferred = new Deferred(fnOnDeferredCancel);
  538.  
  539. deferred.then(fnOnDeferredThen);
  540.  
  541. func(p, deferred);
  542. });
  543.  
  544. if (counter === 0) {
  545. d.resolve(true);
  546. }
  547. },
  548.  
  549. /**
  550. * Generates an rfc4122 version 4 compliant guid.
  551. * Taken from here: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
  552. *
  553. * @method guid
  554. * @static
  555. * @return {String} The generated guid string
  556. */
  557. guid: function () {
  558. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
  559. var r = Math.random() * 16 | 0,
  560. v = c === 'x' ? r : (r & 0x3 | 0x8);
  561. return v.toString(16);
  562. });
  563. },
  564.  
  565. /**
  566. * Returns an appropriate where clause depending on whether the query
  567. * is a String (returns a where clause with CASE INSENSITIVE comparison)
  568. * or an integer.
  569. *
  570. * @method getWhereClause
  571. * @static
  572. * @param {String} varName ???
  573. * @param {String | Number} query A query string
  574. * @return {String} The generated "where" clause
  575. */
  576. getWhereClause: function (varName, query) {
  577. if (this.isNumber(query)) {
  578. return String.format("{0}={1}", varName, query);
  579. }
  580. return String.format("Upper({0})=Upper(\'{1}\')", varName, query);
  581. },
  582.  
  583. /**
  584. * Converts html into text by replacing
  585. * all html tags with their appropriate special characters
  586. *
  587. * @method stripHtml
  588. * @static
  589. * @param {String} html HTML to be converted to text
  590. * @return {String} The HTML in text form
  591. */
  592. stripHtml: function (html) {
  593. var tmp = document.createElement("DIV");
  594. // jquery .text function converts html into text by replacing
  595. // all html tags with their appropriate special characters
  596. $(tmp).text(html);
  597. return tmp.textContent || tmp.innerText || "";
  598. },
  599.  
  600. // Query geometry
  601. /*
  602. * Create a new extent based on the current map size, a point (X/Y coordinates), and a pixel tolerance value.
  603. * @method pointToExtent
  604. * @static
  605. * @param {Object} map The map control
  606. * @param {Object} point The location on screen (X/Y coordinates)
  607. * @param {Number} toleranceInPixel A value indicating how many screen pixels the extent should be from the point
  608. * @returns {Object} a new extent calculated from the given parameters
  609. *
  610. */
  611. pointToExtent: function (map, point, toleranceInPixel) {
  612. var pixelWidth = map.extent.getWidth() / map.width,
  613. toleraceInMapCoords = toleranceInPixel * pixelWidth;
  614.  
  615. return new Extent(point.x - toleraceInMapCoords,
  616. point.y - toleraceInMapCoords,
  617. point.x + toleraceInMapCoords,
  618. point.y + toleraceInMapCoords,
  619. map.spatialReference);
  620. },
  621.  
  622. /**
  623. * Create boudingbox graphic for a bounding box extent
  624. *
  625. * @method createGraphic
  626. * @static
  627. * @param {esri/geometry/Extent} extent of a bounding box
  628. * @return {esri/Graphic} An ESRI graphic object represents a bouding box
  629. */
  630. createGraphic: function (extent) {
  631. return new Graphic({
  632. geometry: extent,
  633. symbol: {
  634. color: [255, 0, 0, 64],
  635. outline: {
  636. color: [240, 128, 128, 255],
  637. width: 1,
  638. type: "esriSLS",
  639. style: "esriSLSSolid"
  640. },
  641. type: "esriSFS",
  642. style: "esriSFSSolid"
  643. }
  644. });
  645. },
  646.  
  647. /**
  648. * Checks if the string ends with the supplied suffix.
  649. *
  650. * @method endsWith
  651. * @static
  652. * @param {String} str String to be evaluated
  653. * @param {String} suffix Ending string to be matched
  654. * @return {boolean} True if suffix matches
  655. */
  656. endsWith: function (str, suffix) {
  657. return str.indexOf(suffix, str.length - suffix.length) !== -1;
  658. },
  659.  
  660. /**
  661. * Recursively merge JSON objects into a target object.
  662. * The merge will also merge array elements.
  663. *
  664. * @method mergeRecursive
  665. * @static
  666. */
  667. mergeRecursive: function () {
  668. function isDOMNode(v) {
  669. if (v === null) {
  670. return false;
  671. }
  672. if (typeof v !== 'object') {
  673. return false;
  674. }
  675. if (!('nodeName' in v)) {
  676. return false;
  677. }
  678. var nn = v.nodeName;
  679. try {
  680. v.nodeName = 'is readonly?';
  681. } catch (e) {
  682. return true;
  683. }
  684. if (v.nodeName === nn) {
  685. return true;
  686. }
  687. v.nodeName = nn;
  688. return false;
  689. }
  690.  
  691. // _mergeRecursive does the actual job with two arguments.
  692. // @param {Object} destination JSON object to have other objects merged into. Parameter is modified by the function.
  693. // @param {Object} sourceArray Param array of JSON objects to merge into the source
  694. // @return {Ojbect} merged result object (points to destination variable)
  695. var _mergeRecursive = function (dst, src) {
  696. if (isDOMNode(src) || typeof src !== 'object' || src === null) {
  697. return dst;
  698. }
  699.  
  700. for (var p in src) {
  701. if (src.hasOwnProperty(p)) {
  702. if ($.isArray(src[p])) {
  703. if (dst[p] === undefined) {
  704. dst[p] = [];
  705. }
  706. $.merge(dst[p], src[p]);
  707. continue;
  708. }
  709.  
  710. if (src[p] === undefined) {
  711. continue;
  712. }
  713. if (typeof src[p] !== 'object' || src[p] === null) {
  714. dst[p] = src[p];
  715. } else if (typeof dst[p] !== 'object' || dst[p] === null) {
  716. dst[p] = _mergeRecursive(src[p].constructor === Array ? [] : {}, src[p]);
  717. } else {
  718. _mergeRecursive(dst[p], src[p]);
  719. }
  720. }
  721. }
  722. return dst;
  723. }, out;
  724.  
  725. // Loop through arguments and merge them into the first argument.
  726. out = arguments[0];
  727. if (typeof out !== 'object' || out === null) {
  728. return out;
  729. }
  730. for (var i = 1, il = arguments.length; i < il; i++) {
  731. _mergeRecursive(out, arguments[i]);
  732. }
  733. return out;
  734. },
  735.  
  736. /**
  737. * Applies supplied xslt to supplied xml. IE always returns a String; others may return a documentFragment or a jObject.
  738. *
  739. * @method transformXML
  740. * @static
  741. * @param {String} xmlurl Location of the xml file
  742. * @param {String} xslurl Location of the xslt file
  743. * @param {Function} callback The callback to be executed
  744. * @param {Boolean} returnFragment True if you want a document fragment returned (doesn't work in IE)}
  745. */
  746. transformXML: function (xmlurl, xslurl, callback, returnFragment, params) {
  747. var xmld = new Deferred(),
  748. xsld = new Deferred(),
  749. xml, xsl,
  750. dlist = [xmld, xsld],
  751. result,
  752. error,
  753. that = this;
  754.  
  755. that.afterAll(dlist, function () {
  756. if (!error) {
  757. result = applyXSLT(xml, xsl);
  758. }
  759. callback(error, result);
  760. });
  761.  
  762. // Transform XML using XSLT
  763. function applyXSLT(xmlString, xslString) {
  764. var output, i;
  765. if (window.ActiveXObject || window.hasOwnProperty("ActiveXObject")) { // IE
  766. var xslt = new ActiveXObject("Msxml2.XSLTemplate"),
  767. xmlDoc = new ActiveXObject("Msxml2.DOMDocument"),
  768. xslDoc = new ActiveXObject("Msxml2.FreeThreadedDOMDocument"),
  769. xslProc;
  770.  
  771. xmlDoc.loadXML(xmlString);
  772. xslDoc.loadXML(xslString);
  773. xslt.stylesheet = xslDoc;
  774. xslProc = xslt.createProcessor();
  775. xslProc.input = xmlDoc;
  776. // [patched from ECDMP] Add parameters to xsl document (addParameter = ie only)
  777. if (params) {
  778. for (i = 0; i < params.length; i++) {
  779. xslProc.addParameter(params[i].key, params[i].value, "");
  780. }
  781. }
  782. xslProc.transform();
  783. output = xslProc.output;
  784. } else { // Chrome/FF/Others
  785. var xsltProcessor = new XSLTProcessor();
  786. xsltProcessor.importStylesheet(xslString);
  787. // [patched from ECDMP] Add parameters to xsl document (setParameter = Chrome/FF/Others)
  788. if (params) {
  789. for (i = 0; i < params.length; i++) {
  790. xsltProcessor.setParameter(null, params[i].key, params[i].value);
  791. }
  792. }
  793. output = xsltProcessor.transformToFragment(xmlString, document);
  794.  
  795. // turn a document fragment into a proper jQuery object
  796. if (!returnFragment) {
  797. output = ($('body')
  798. .append(output)
  799. .children().last())
  800. .detach();
  801. }
  802. }
  803. return output;
  804. }
  805.  
  806. // Distinguish between XML/XSL deferred objects to resolve and set response
  807. function resolveDeferred(filename, responseObj) {
  808. if (filename.endsWith(".xsl")) {
  809. xsl = responseObj.responseText;
  810. xsld.resolve();
  811. } else {
  812. xml = responseObj.responseText;
  813. xmld.resolve();
  814. }
  815. }
  816. /*
  817. function loadXMLFileIE9(filename) {
  818. var xdr = new XDomainRequest();
  819. xdr.contentType = "text/plain";
  820. xdr.open("GET", filename);
  821. xdr.onload = function () {
  822. resolveDeferred(filename, xdr);
  823. };
  824. xdr.onprogress = function () { };
  825. xdr.ontimeout = function () { };
  826. xdr.onerror = function () {
  827. error = true;
  828. resolveDeferred(filename, xdr);
  829. };
  830. window.setTimeout(function () {
  831. xdr.send();
  832. }, 0);
  833. }
  834. */
  835. // IE10+
  836. function loadXMLFileIE(filename) {
  837. var xhttp = new XMLHttpRequest();
  838. xhttp.open("GET", filename);
  839. try {
  840. xhttp.responseType = "msxml-document";
  841. } catch (err) { } // Helping IE11
  842. xhttp.onreadystatechange = function () {
  843. if (xhttp.readyState === 4) {
  844. if (xhttp.status !== 200) {
  845. error = true;
  846. }
  847. resolveDeferred(filename, xhttp);
  848. }
  849. };
  850. xhttp.send("");
  851. }
  852.  
  853. if ('withCredentials' in new XMLHttpRequest() && "ActiveXObject" in window) { // IE10 and above
  854. loadXMLFileIE(xmlurl);
  855. loadXMLFileIE(xslurl);
  856. } else if (window.XDomainRequest) { // IE9 and below
  857. /*
  858. loadXMLFileIE9(xmlurl);
  859. loadXMLFileIE9(xslurl);
  860. */
  861. // dataType need to be set to "text" for xml doc requests.
  862. $.ajax({
  863. type: "GET",
  864. url: xmlurl,
  865. dataType: "text",
  866. cache: false,
  867. success: function (data) {
  868. xml = data;
  869. xmld.resolve();
  870. },
  871. error: function () {
  872. error = true;
  873. xmld.resolve();
  874. }
  875. });
  876.  
  877. $.ajax({
  878. type: "GET",
  879. url: xslurl,
  880. dataType: "text",
  881. cache: false,
  882. success: function (data) {
  883. xsl = data;
  884. xsld.resolve();
  885. },
  886. error: function () {
  887. error = true;
  888. xsld.resolve();
  889. }
  890. });
  891. } else { // Good browsers (Chrome/FF)
  892. $.ajax({
  893. type: "GET",
  894. url: xmlurl,
  895. dataType: "xml",
  896. cache: false,
  897. success: function (data) {
  898. xml = data;
  899. xmld.resolve();
  900. },
  901. error: function () {
  902. error = true;
  903. xmld.resolve();
  904. }
  905. });
  906.  
  907. $.ajax({
  908. type: "GET",
  909. url: xslurl,
  910. dataType: "xml",
  911. cache: false,
  912. success: function (data) {
  913. xsl = data;
  914. xsld.resolve();
  915. },
  916. error: function () {
  917. error = true;
  918. xsld.resolve();
  919. }
  920. });
  921. }
  922. },
  923.  
  924. /**
  925. * Parses a file using the FileReader API. Wraps readAsText and returns a promise.
  926. *
  927. * @method readFileAsText
  928. * @static
  929. * @param {File} file a dom file object to be read
  930. * @return {Object} a promise which sends a string containing the file output if successful
  931. */
  932. readFileAsText: _wrapFileCallInPromise('readAsText'),
  933.  
  934. /**
  935. * Parses a file using the FileReader API. Wraps readAsBinaryString and returns a promise.
  936. *
  937. * @method readFileAsBinary
  938. * @static
  939. * @param {File} file a dom file object to be read
  940. * @return {Object} a promise which sends a string containing the file output if successful
  941. */
  942. readFileAsBinary: _wrapFileCallInPromise('readAsBinary'),
  943.  
  944. /**
  945. * Parses a file using the FileReader API. Wraps readAsArrayBuffer and returns a promise.
  946. *
  947. * @method readFileAsArrayBuffer
  948. * @static
  949. * @param {File} file a dom file object to be read
  950. * @return {Object} a promise which sends an ArrayBuffer containing the file output if successful
  951. */
  952. readFileAsArrayBuffer: _wrapFileCallInPromise('readAsArrayBuffer'),
  953.  
  954. /**
  955. * Augments lists of items to be sortable using keyboard.
  956. *
  957. * @method keyboardSortable
  958. * @param {Array} ulNodes An array of <ul> tags containing a number of <li> tags to be made keyboard sortable.
  959. * @param {Object} [settings] Additional settings
  960. * @param {Object} [settings.linkLists] Indicates if the supplied lists (if more than one) should be linked - items could be moved from one to another; Update: this is wrong - items can't be moved from list to list right now, but the keyboard focus can be moved between lists - need to fix.
  961. * @param {Object} [settings.onStart] A callback function to be called when the user initiates sorting process
  962. * @param {Object} [settings.onUpdate] A callback function to be called when the user moves the item around
  963. * @param {Object} [settings.onStop] A callback function to be called when the user commits the item to its new place ending the sorting process
  964. * @static
  965. */
  966. keyboardSortable: function (ulNodes, settings) {
  967. settings = dojoLang.mixin({
  968. linkLists: false,
  969.  
  970. onStart: function () { },
  971. onUpdate: function () { },
  972. onStop: function () { }
  973. }, settings);
  974.  
  975. ulNodes.each(function (index, _ulNode) {
  976. var ulNode = $(_ulNode),
  977. liNodes = ulNode.find("> li"),
  978. //sortHandleNodes = liNodes.find(".sort-handle"),
  979. isReordering = false,
  980. grabbed;
  981.  
  982. // Reset focus, set aria attributes, and styling
  983. function reorderReset(handle, liNodes, liNode) {
  984. handle.focus();
  985. liNodes.attr("aria-dropeffect", "move");
  986. liNode.attr("aria-grabbed", "true").removeAttr("aria-dropeffect");
  987. }
  988.  
  989. // try to remove event handlers to prevent double initialization
  990. ulNode
  991. .off("focusout", ".sort-handle")
  992. .off("keyup", ".sort-handle");
  993.  
  994. ulNode
  995. .on("focusout", ".sort-handle", function (event) {
  996. var node = $(this).closest("li");
  997.  
  998. // if the list is not being reordered right now, release list item
  999. if (node.hasClass("list-item-grabbed") && !isReordering) {
  1000. liNodes.removeAttr("aria-dropeffect");
  1001. node
  1002. .removeClass("list-item-grabbed")
  1003. .attr({ "aria-selected": false, "aria-grabbed": false });
  1004.  
  1005. grabbed = false;
  1006.  
  1007. console.log("Keyboard Sortable: OnStop -> ", event);
  1008. settings.onStop.call(null, event, { item: null });
  1009. }
  1010. })
  1011. .on("keyup", ".sort-handle", function (event) {
  1012. var liNode = $(this).closest("li"),
  1013. liId = liNode[0].id,
  1014. liIdArray = ulNode.sortable("toArray"),
  1015. liIndex = dojoArray.indexOf(liIdArray, liId);
  1016.  
  1017. // Toggle grabbed state and aria attributes (13 = enter, 32 = space bar)
  1018. if (event.which === 13 || event.which === 32) {
  1019. if (grabbed) {
  1020. liNodes.removeAttr("aria-dropeffect");
  1021. liNode
  1022. .attr("aria-grabbed", "false")
  1023. .removeClass("list-item-grabbed");
  1024.  
  1025. console.log("Keyboard Sortable: OnStop -> ", liNode);
  1026. settings.onStop.call(null, event, { item: liNode });
  1027.  
  1028. grabbed = false;
  1029. } else {
  1030. liNodes.attr("aria-dropeffect", "move");
  1031. liNode
  1032. .attr("aria-grabbed", "true")
  1033. .removeAttr("aria-dropeffect")
  1034. .addClass("list-item-grabbed");
  1035.  
  1036. console.log("Keyboard Sortable: OnStart -> ", liNode);
  1037. settings.onStart.call(null, event, { item: liNode });
  1038.  
  1039. grabbed = true;
  1040. }
  1041. // Keyboard up (38) and down (40)
  1042. } else if (event.which === 38) {
  1043. if (grabbed) {
  1044. // Don't move up if first layer in list
  1045. if (liIndex > 0) {
  1046. isReordering = true;
  1047.  
  1048. liNode.prev().before(liNode);
  1049.  
  1050. reorderReset($(this), liNodes, liNode);
  1051.  
  1052. grabbed = true;
  1053. liIndex -= 1;
  1054.  
  1055. console.log("Keyboard Sortable: OnUpdate -> ", liNode);
  1056. settings.onUpdate.call(null, event, { item: liNode });
  1057.  
  1058. isReordering = false;
  1059. }
  1060. } else {
  1061. // if lists are linked, jump to the last item of the previous list, if any
  1062. if (settings.linkLists &&
  1063. liIndex === 0 &&
  1064. index !== 0) {
  1065. liNode = $(ulNodes[index - 1]).find("> li:last");
  1066. } else {
  1067. liNode = liNode.prev();
  1068. }
  1069.  
  1070. liNode.find(":tabbable:first").focus();
  1071. }
  1072. } else if (event.which === 40) {
  1073. if (grabbed) {
  1074. // Don't move down if last layer in list
  1075. if (liIndex < liNodes.length - 1) {
  1076. isReordering = true;
  1077.  
  1078. liNode.next().after(liNode);
  1079.  
  1080. reorderReset($(this), liNodes, liNode);
  1081.  
  1082. grabbed = true;
  1083. liIndex += 1;
  1084.  
  1085. console.log("Keyboard Sortable: OnUpdate -> ", liNode);
  1086. settings.onUpdate.call(null, event, { item: liNode });
  1087.  
  1088. isReordering = false;
  1089. }
  1090. } else {
  1091. // if lists are linked, jump to the first item of the next list, if any
  1092. if (settings.linkLists &&
  1093. liIndex === liNodes.length - 1 &&
  1094. index < ulNodes.length - 1) {
  1095. liNode = $(ulNodes[index + 1]).find("> li:first");
  1096. } else {
  1097. liNode = liNode.next();
  1098. }
  1099.  
  1100. liNode.find(":tabbable:first").focus();
  1101. }
  1102. }
  1103. });
  1104. });
  1105. },
  1106.  
  1107. /**
  1108. * Takes an array of timelines and their generator functions, clear and recreates timelines optionally preserving the play position.
  1109. * ####Example of tls parameter
  1110. *
  1111. * [
  1112. * {
  1113. * timeline: {timeline},
  1114. * generator: {function}
  1115. * }
  1116. * ]
  1117. *
  1118. *
  1119. * @method resetTimelines
  1120. * @static
  1121. * @param {Array} tls An array of objects containing timeline objects and their respective generator functions
  1122. * @param {Boolean} keepPosition Indicates if the timeline should be set in the play position it was in before the reset
  1123. */
  1124. resetTimelines: function (tls, keepPosition) {
  1125. var position;
  1126.  
  1127. tls.forEach(function (tl) {
  1128. position = tl.timeLine.time(); // preserve timeline position
  1129. tl.timeLine.seek(0).clear();
  1130.  
  1131. tl.generator.call();
  1132.  
  1133. if (keepPosition) {
  1134. tl.timeLine.seek(position);
  1135. }
  1136. });
  1137. },
  1138.  
  1139. /**
  1140. * Checks if two spatial reference objects are equivalent. Handles both wkid and wkt definitions
  1141. *
  1142. * @method isSpatialRefEqual
  1143. * @static
  1144. * @param {Esri/SpatialReference} sr1 First {{#crossLink "Esri/SpatialReference"}}{{/crossLink}} to compare
  1145. * @param {Esri/SpatialReference} sr2 Second {{#crossLink "Esri/SpatialReference"}}{{/crossLink}} to compare
  1146. * @return {Boolean} true if the two spatial references are equivalent. False otherwise.
  1147. */
  1148. isSpatialRefEqual: function (sr1, sr2) {
  1149. if ((sr1.wkid) && (sr2.wkid)) {
  1150. //both SRs have wkids
  1151. return sr1.wkid === sr2.wkid;
  1152. } else if ((sr1.wkt) && (sr2.wkt)) {
  1153. //both SRs have wkt's
  1154. return sr1.wkt === sr2.wkt;
  1155. } else {
  1156. //not enough info provided or mismatch between wkid and wkt.
  1157. return false;
  1158. }
  1159. },
  1160.  
  1161. /**
  1162. * Checks if the given dom node is present in the dom.
  1163. *
  1164. * @method containsInDom
  1165. * @static
  1166. * @param {Object} el DOM node to check
  1167. * @return {Boolean} true if the given node is in the dom
  1168. */
  1169. containsInDom: function (el) {
  1170. return $.contains(document.documentElement, el);
  1171. },
  1172.  
  1173. styleBrowseFilesButton: function (nodes) {
  1174. var input,
  1175. button;
  1176.  
  1177. function focusIn(event) {
  1178. event.data.button.not(".disabled").addClass("btn-focus btn-hover");
  1179. }
  1180.  
  1181. function focusOut(event) {
  1182. event.data.button.removeClass("btn-focus btn-hover btn-active");
  1183. }
  1184.  
  1185. function mouseEnter(event) {
  1186. event.data.button.not(".disabled").addClass("btn-hover");
  1187. }
  1188.  
  1189. function mouseLeave(event) {
  1190. event.data.button.removeClass("btn-hover btn-active");
  1191. }
  1192.  
  1193. function mouseDown(event) {
  1194. event.data.button.not(".disabled").addClass("btn-focus btn-hover btn-active");
  1195. }
  1196.  
  1197. function mouseUp(event) {
  1198. event.data.button.removeClass("btn-active");
  1199. }
  1200.  
  1201. nodes.each(function (i, node) {
  1202. node = $(node);
  1203. input = node.find("input[type='file']");
  1204. button = node.find(".browse-button");
  1205.  
  1206. input
  1207. .on("focusin", { button: button }, focusIn)
  1208. .on("focusout", { button: button }, focusOut)
  1209.  
  1210. .on("mouseenter", { button: button }, mouseEnter)
  1211. .on("mouseleave", { button: button }, mouseLeave)
  1212.  
  1213. .on("mousedown", { button: button }, mouseDown)
  1214. .on("mouseup", { button: button }, mouseUp)
  1215. ;
  1216. });
  1217. },
  1218.  
  1219. /**
  1220. * Detects either cell or line delimiter in a given csv data.
  1221. *
  1222. * @method detectDelimiter
  1223. * @static
  1224. * @param {String} data csv content
  1225. * @param {String} [type] Type of the delimiter to detect. Possible values "cell" or "line". Defaults to "cell".
  1226. * @return {String} Detected delimiter
  1227. */
  1228. detectDelimiter: function (data, type) {
  1229. var count = 0,
  1230. detected,
  1231.  
  1232. escapeDelimiters = ['|', '^'],
  1233. delimiters = {
  1234. cell: [',', ';', '\t', '|', '^'],
  1235. line: ['\r\n', '\r', '\n']
  1236. };
  1237.  
  1238. type = type !== "cell" && type !== "line" ? "cell" : type;
  1239.  
  1240. delimiters[type].forEach(function (delimiter) {
  1241. var needle = delimiter,
  1242. matches;
  1243.  
  1244. if (escapeDelimiters.indexOf(delimiter) !== -1) {
  1245. needle = '\\' + needle;
  1246. }
  1247.  
  1248. matches = data.match(new RegExp(needle, 'g'));
  1249. if (matches && matches.length > count) {
  1250. count = matches.length;
  1251. detected = delimiter;
  1252. }
  1253. });
  1254.  
  1255. console.log(type + " delimiter detected: '" + (detected || delimiters[type][0]) + "'");
  1256.  
  1257. return (detected || delimiters[type][0]);
  1258. },
  1259.  
  1260. /**
  1261. * Converts HEX colour to RGB.
  1262. * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb/5624139#5624139
  1263. *
  1264. * @method hexToRgb
  1265. * @static
  1266. * @param {String} hex hex colour code
  1267. * @return {Object} ojbect containing r, g, and b components of the supplied colour
  1268. */
  1269. hexToRgb: function (hex) {
  1270. // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
  1271. var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
  1272. hex = hex.replace(shorthandRegex, function (m, r, g, b) {
  1273. return r + r + g + g + b + b;
  1274. });
  1275.  
  1276. var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  1277. return result ? {
  1278. r: parseInt(result[1], 16),
  1279. g: parseInt(result[2], 16),
  1280. b: parseInt(result[3], 16)
  1281. } : null;
  1282. },
  1283.  
  1284. /**
  1285. * Converts RGB colour to HEX.
  1286. * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb/5624139#5624139
  1287. *
  1288. * @method rgbToHex
  1289. * @static
  1290. * @param {Number} r r component
  1291. * @param {Number} g g component
  1292. * @param {Number} b b component
  1293. * @return {Object} hex colour code
  1294. */
  1295. rgbToHex: function (r, g, b) {
  1296. return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  1297. }
  1298. };
  1299. });