Reusable Accessible Mapping Platform

API Docs for: 5.3.1
Show:

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

  1. /* global define, window, XMLHttpRequest, ActiveXObject, XSLTProcessor, console, $, document, jQuery, FileReader, Btoa */
  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 (scope) {
  207. topic.subscribe(name, dojoLang.hitch(scope, callback));
  208. } else {
  209. topic.subscribe(name, 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. console.warn('Think twice about using isDefined: consider checking explicitly or using a falsy test instead');
  400. return (typeof obj === 'undefined');
  401. },
  402.  
  403. /**
  404. * Compares two graphic objects.
  405. *
  406. * @method compareGraphics
  407. * @static
  408. * @param {Object} one Graphic object
  409. * @param {Object} two Graphic object
  410. * @return {boolean} True if the objects represent the same feature
  411. */
  412. compareGraphics: function (one, two) {
  413. var oneKey = "0",
  414. twoKey = "1",
  415. objectIdField,
  416. oneLayer,
  417. twoLayer;
  418.  
  419. if (one && two &&
  420. $.isFunction(one.getLayer) && $.isFunction(two.getLayer)) {
  421. oneLayer = one.getLayer();
  422. twoLayer = two.getLayer();
  423. objectIdField = oneLayer.objectIdField;
  424. oneKey = oneLayer.sourceLayerId + one.attributes[objectIdField];
  425. twoKey = twoLayer.sourceLayerId + two.attributes[objectIdField];
  426. }
  427.  
  428. return oneKey === twoKey;
  429. },
  430.  
  431. /**
  432. * Returns the width of the scrollbar in pixels. Since different browsers render scrollbars differently, the width may vary.
  433. *
  434. * @method scrollbarWidth
  435. * @static
  436. * @return {int} The width of the scrollbar in pixels
  437. * @for Util
  438. */
  439. scrollbarWidth: function () {
  440. var $inner = jQuery('<div style="width: 100%; height:200px;">test</div>'),
  441. $outer = jQuery('<div style="width:200px;height:150px; position: absolute; top: 0; left: 0; visibility: hidden; overflow:hidden;"></div>').append($inner),
  442. inner = $inner[0],
  443. outer = $outer[0],
  444. width1, width2;
  445.  
  446. jQuery('body').append(outer);
  447. width1 = inner.offsetWidth;
  448. $outer.css('overflow', 'scroll');
  449. width2 = outer.clientWidth;
  450. $outer.remove();
  451.  
  452. return (width1 - width2);
  453. },
  454.  
  455. /**
  456. * Checks if the height of the scrollable content of the body is taller than its height;
  457. * if so, offset the content horizontally to accommodate for the scrollbar assuming target's width is
  458. * set to "100%".
  459. *
  460. * @method adjustWidthForSrollbar
  461. * @static
  462. * @param {jObject} body A DOM node with a scrollbar (or not)
  463. * @param {jObject} targets An array of jObjects to add the offset to
  464. */
  465. adjustWidthForSrollbar: function (body, targets) {
  466. var offset = body.innerHeight() < body[0].scrollHeight ? this.scrollbarWidth() : 0;
  467.  
  468. dojoArray.map(targets, function (target) {
  469. target.css({
  470. right: offset
  471. });
  472. });
  473. },
  474.  
  475. /**
  476. * Waits until a given function is available and executes a callback function.
  477. *
  478. * @method executeOnLoad
  479. * @static
  480. * @param {Object} target an object on which to wait for function to appear
  481. * @param {function} func A function whose availability in question
  482. * @param {function} callback The callback function to be executed after func is available
  483. */
  484. executeOnLoad: function (target, func, callback) {
  485. var deferred = new Deferred(),
  486. handle;
  487.  
  488. deferred.then(function () {
  489. window.clearInterval(handle);
  490. //console.log("deferred resolved");
  491.  
  492. callback();
  493. });
  494.  
  495. handle = window.setInterval(function () {
  496. if ($.isFunction(target[func])) {
  497. deferred.resolve(true);
  498. }
  499. }, 500);
  500. },
  501.  
  502. /**
  503. * Loops through all object properties and applies a given function to each. Resolves the given deferred when done.
  504. *
  505. * @method executeOnDone
  506. * @static
  507. * @param {object} o Object to look through
  508. * @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.
  509. * @param {object} d A deferred to be resolved when all properties have been processed.
  510. */
  511. executeOnDone: function (o, func, d) {
  512. var counter = 0,
  513. arr = [],
  514. deferred;
  515.  
  516. function fnOnDeferredCancel() {
  517. d.cancel();
  518. }
  519.  
  520. function fnOnDeferredThen() {
  521. counter--;
  522. if (counter === 0) {
  523. d.resolve(true);
  524. }
  525. }
  526.  
  527. d = d || new Deferred();
  528.  
  529. for (var q in o) {
  530. if (o.hasOwnProperty(q)) {
  531. arr.push(o[q]);
  532. }
  533. }
  534.  
  535. counter = arr.length;
  536.  
  537. arr.forEach(function (p) {
  538. deferred = new Deferred(fnOnDeferredCancel);
  539.  
  540. deferred.then(fnOnDeferredThen);
  541.  
  542. func(p, deferred);
  543. });
  544.  
  545. if (counter === 0) {
  546. d.resolve(true);
  547. }
  548. },
  549.  
  550. /**
  551. * Generates an rfc4122 version 4 compliant guid.
  552. * Taken from here: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
  553. *
  554. * @method guid
  555. * @static
  556. * @return {String} The generated guid string
  557. */
  558. // TODO: check if there is new/better code for guid generation
  559. guid: function () {
  560. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
  561. var r = Math.random() * 16 | 0,
  562. v = c === 'x' ? r : (r & 0x3 | 0x8);
  563. return v.toString(16);
  564. });
  565. },
  566.  
  567. /**
  568. * Returns an appropriate where clause depending on whether the query
  569. * is a String (returns a where clause with CASE INSENSITIVE comparison)
  570. * or an integer.
  571. *
  572. * @method getWhereClause
  573. * @static
  574. * @param {String} varName ???
  575. * @param {String | Number} query A query string
  576. * @return {String} The generated "where" clause
  577. */
  578. getWhereClause: function (varName, query) {
  579. if (this.isNumber(query)) {
  580. return String.format("{0}={1}", varName, query);
  581. }
  582. return String.format("Upper({0})=Upper(\'{1}\')", varName, query);
  583. },
  584.  
  585. /**
  586. * Converts html into text by replacing
  587. * all html tags with their appropriate special characters
  588. *
  589. * @method stripHtml
  590. * @static
  591. * @param {String} html HTML to be converted to text
  592. * @return {String} The HTML in text form
  593. */
  594. stripHtml: function (html) {
  595. var tmp = document.createElement("DIV");
  596. // jquery .text function converts html into text by replacing
  597. // all html tags with their appropriate special characters
  598. $(tmp).text(html);
  599. return tmp.textContent || tmp.innerText || "";
  600. },
  601.  
  602. // Query geometry
  603. /*
  604. * Create a new extent based on the current map size, a point (X/Y coordinates), and a pixel tolerance value.
  605. * @method pointToExtent
  606. * @static
  607. * @param {Object} map The map control
  608. * @param {Object} point The location on screen (X/Y coordinates)
  609. * @param {Number} toleranceInPixel A value indicating how many screen pixels the extent should be from the point
  610. * @returns {Object} a new extent calculated from the given parameters
  611. *
  612. */
  613. pointToExtent: function (map, point, toleranceInPixel) {
  614. var pixelWidth = map.extent.getWidth() / map.width,
  615. toleraceInMapCoords = toleranceInPixel * pixelWidth;
  616.  
  617. return new Extent(point.x - toleraceInMapCoords,
  618. point.y - toleraceInMapCoords,
  619. point.x + toleraceInMapCoords,
  620. point.y + toleraceInMapCoords,
  621. map.spatialReference);
  622. },
  623.  
  624. /**
  625. * Create boudingbox graphic for a bounding box extent
  626. *
  627. * @method createGraphic
  628. * @static
  629. * @param {esri/geometry/Extent} extent of a bounding box
  630. * @return {esri/Graphic} An ESRI graphic object represents a bouding box
  631. */
  632. createGraphic: function (extent) {
  633. return new Graphic({
  634. geometry: extent,
  635. symbol: {
  636. color: [255, 0, 0, 64],
  637. outline: {
  638. color: [240, 128, 128, 255],
  639. width: 1,
  640. type: "esriSLS",
  641. style: "esriSLSSolid"
  642. },
  643. type: "esriSFS",
  644. style: "esriSFSSolid"
  645. }
  646. });
  647. },
  648.  
  649. /**
  650. * Checks if the string ends with the supplied suffix.
  651. *
  652. * @method endsWith
  653. * @static
  654. * @param {String} str String to be evaluated
  655. * @param {String} suffix Ending string to be matched
  656. * @return {boolean} True if suffix matches
  657. */
  658. endsWith: function (str, suffix) {
  659. return str.indexOf(suffix, str.length - suffix.length) !== -1;
  660. },
  661.  
  662. /**
  663. * Recursively merge JSON objects into a target object.
  664. * The merge will also merge array elements.
  665. *
  666. * @method mergeRecursive
  667. * @static
  668. */
  669. mergeRecursive: function () {
  670. function isDOMNode(v) {
  671. if (v === null) {
  672. return false;
  673. }
  674. if (typeof v !== 'object') {
  675. return false;
  676. }
  677. if (!('nodeName' in v)) {
  678. return false;
  679. }
  680. var nn = v.nodeName;
  681. try {
  682. v.nodeName = 'is readonly?';
  683. } catch (e) {
  684. return true;
  685. }
  686. if (v.nodeName === nn) {
  687. return true;
  688. }
  689. v.nodeName = nn;
  690. return false;
  691. }
  692.  
  693. // _mergeRecursive does the actual job with two arguments.
  694. // @param {Object} destination JSON object to have other objects merged into. Parameter is modified by the function.
  695. // @param {Object} sourceArray Param array of JSON objects to merge into the source
  696. // @return {Ojbect} merged result object (points to destination variable)
  697. var _mergeRecursive = function (dst, src) {
  698. if (isDOMNode(src) || typeof src !== 'object' || src === null) {
  699. return dst;
  700. }
  701.  
  702. for (var p in src) {
  703. if (src.hasOwnProperty(p)) {
  704. if ($.isArray(src[p])) {
  705. if (dst[p] === undefined) {
  706. dst[p] = [];
  707. }
  708. $.merge(dst[p], src[p]);
  709. continue;
  710. }
  711.  
  712. if (src[p] === undefined) {
  713. continue;
  714. }
  715. if (typeof src[p] !== 'object' || src[p] === null) {
  716. dst[p] = src[p];
  717. } else if (typeof dst[p] !== 'object' || dst[p] === null) {
  718. dst[p] = _mergeRecursive(src[p].constructor === Array ? [] : {}, src[p]);
  719. } else {
  720. _mergeRecursive(dst[p], src[p]);
  721. }
  722. }
  723. }
  724. return dst;
  725. }, out;
  726.  
  727. // Loop through arguments and merge them into the first argument.
  728. out = arguments[0];
  729. if (typeof out !== 'object' || out === null) {
  730. return out;
  731. }
  732. for (var i = 1, il = arguments.length; i < il; i++) {
  733. _mergeRecursive(out, arguments[i]);
  734. }
  735. return out;
  736. },
  737.  
  738. /**
  739. * Applies supplied xslt to supplied xml. IE always returns a String; others may return a documentFragment or a jObject.
  740. *
  741. * @method transformXML
  742. * @static
  743. * @param {String} xmlurl Location of the xml file
  744. * @param {String} xslurl Location of the xslt file
  745. * @param {Function} callback The callback to be executed
  746. * @param {Boolean} returnFragment True if you want a document fragment returned (doesn't work in IE)}
  747. */
  748. transformXML: function (xmlurl, xslurl, callback, returnFragment, params) {
  749. var xmld = new Deferred(),
  750. xsld = new Deferred(),
  751. xml, xsl,
  752. dlist = [xmld, xsld],
  753. result,
  754. error,
  755. that = this;
  756.  
  757. that.afterAll(dlist, function () {
  758. if (!error) {
  759. result = applyXSLT(xml, xsl);
  760. }
  761. callback(error, result);
  762. });
  763.  
  764. // Transform XML using XSLT
  765. function applyXSLT(xmlString, xslString) {
  766. var output, i;
  767. if (window.ActiveXObject || window.hasOwnProperty("ActiveXObject")) { // IE
  768. var xslt = new ActiveXObject("Msxml2.XSLTemplate"),
  769. xmlDoc = new ActiveXObject("Msxml2.DOMDocument"),
  770. xslDoc = new ActiveXObject("Msxml2.FreeThreadedDOMDocument"),
  771. xslProc;
  772.  
  773. xmlDoc.loadXML(xmlString);
  774. xslDoc.loadXML(xslString);
  775. xslt.stylesheet = xslDoc;
  776. xslProc = xslt.createProcessor();
  777. xslProc.input = xmlDoc;
  778. // [patched from ECDMP] Add parameters to xsl document (addParameter = ie only)
  779. if (params) {
  780. for (i = 0; i < params.length; i++) {
  781. xslProc.addParameter(params[i].key, params[i].value, "");
  782. }
  783. }
  784. xslProc.transform();
  785. output = xslProc.output;
  786. } else { // Chrome/FF/Others
  787. var xsltProcessor = new XSLTProcessor();
  788. xsltProcessor.importStylesheet(xslString);
  789. // [patched from ECDMP] Add parameters to xsl document (setParameter = Chrome/FF/Others)
  790. if (params) {
  791. for (i = 0; i < params.length; i++) {
  792. xsltProcessor.setParameter(null, params[i].key, params[i].value || "");
  793. }
  794. }
  795. output = xsltProcessor.transformToFragment(xmlString, document);
  796.  
  797. // turn a document fragment into a proper jQuery object
  798. if (!returnFragment) {
  799. output = ($('body')
  800. .append(output)
  801. .children().last())
  802. .detach();
  803. }
  804. }
  805. return output;
  806. }
  807.  
  808. // Distinguish between XML/XSL deferred objects to resolve and set response
  809. function resolveDeferred(filename, responseObj) {
  810. console.log('resolving');
  811. console.log(filename);
  812. console.log(responseObj.responseText);
  813. if (filename.endsWith(".xsl")) {
  814. xsl = responseObj.responseText;
  815. xsld.resolve();
  816. } else {
  817. xml = responseObj.responseText;
  818. xmld.resolve();
  819. }
  820. }
  821. function loadXMLFileIE9(filename) {
  822. var xdr = new window.XDomainRequest();
  823. xdr.open("GET", filename);
  824. xdr.onload = function () { resolveDeferred(filename, xdr); };
  825. xdr.ontimeout = function () { console.log('xdr timeout'); };
  826. xdr.onprogress = function () { console.log('xdr progress'); };
  827. xdr.onerror = function (e) {
  828. console.log(e);
  829. error = true;
  830. resolveDeferred(filename, xdr);
  831. };
  832. window.setTimeout(function () { xdr.send(); }, 0);
  833. }
  834. // IE10+
  835. function loadXMLFileIE(filename) {
  836. var xhttp = new XMLHttpRequest();
  837. xhttp.open("GET", filename);
  838. try {
  839. xhttp.responseType = "msxml-document";
  840. } catch (err) { } // Helping IE11
  841. xhttp.onreadystatechange = function () {
  842. if (xhttp.readyState === 4) {
  843. if (xhttp.status !== 200) {
  844. error = true;
  845. }
  846. resolveDeferred(filename, xhttp);
  847. }
  848. };
  849. xhttp.send("");
  850. }
  851.  
  852. if ('withCredentials' in new XMLHttpRequest() && "ActiveXObject" in window) { // IE10 and above
  853. loadXMLFileIE(xmlurl);
  854. loadXMLFileIE(xslurl);
  855. } else if (window.XDomainRequest) {
  856. // for IE9 use loadXMLFileIE9 for the metadata XML -- uses XDomainRequest internally and allows cross domain requests
  857. // use regular XHR for the XSLT as that should be located on the same server as RAMP
  858. // XDR will break on same origin requests unless the same response headers are set allowing cross domain
  859. // FIXME: delete all this as soon as IE9 is off the support list as it is fragile and unpleasant to maintain
  860. loadXMLFileIE9(xmlurl);
  861. loadXMLFileIE(xslurl);
  862. } else { // Good browsers (Chrome/FF)
  863. $.ajax({
  864. type: "GET",
  865. url: xmlurl,
  866. dataType: "xml",
  867. cache: false,
  868. success: function (data) {
  869. xml = data;
  870. xmld.resolve();
  871. },
  872. error: function () {
  873. error = true;
  874. xmld.resolve();
  875. }
  876. });
  877.  
  878. $.ajax({
  879. type: "GET",
  880. url: xslurl,
  881. dataType: "xml",
  882. cache: false,
  883. success: function (data) {
  884. xsl = data;
  885. xsld.resolve();
  886. },
  887. error: function () {
  888. error = true;
  889. xsld.resolve();
  890. }
  891. });
  892. }
  893. },
  894.  
  895. /**
  896. * Parses a file using the FileReader API. Wraps readAsText and returns a promise.
  897. *
  898. * @method readFileAsText
  899. * @static
  900. * @param {File} file a dom file object to be read
  901. * @return {Object} a promise which sends a string containing the file output if successful
  902. */
  903. readFileAsText: _wrapFileCallInPromise('readAsText'),
  904.  
  905. /**
  906. * Parses a file using the FileReader API. Wraps readAsBinaryString and returns a promise.
  907. *
  908. * @method readFileAsBinary
  909. * @static
  910. * @param {File} file a dom file object to be read
  911. * @return {Object} a promise which sends a string containing the file output if successful
  912. */
  913. readFileAsBinary: _wrapFileCallInPromise('readAsBinary'),
  914.  
  915. /**
  916. * Parses a file using the FileReader API. Wraps readAsArrayBuffer and returns a promise.
  917. *
  918. * @method readFileAsArrayBuffer
  919. * @static
  920. * @param {File} file a dom file object to be read
  921. * @return {Object} a promise which sends an ArrayBuffer containing the file output if successful
  922. */
  923. readFileAsArrayBuffer: _wrapFileCallInPromise('readAsArrayBuffer'),
  924.  
  925. /**
  926. * Augments lists of items to be sortable using keyboard.
  927. *
  928. * @method keyboardSortable
  929. * @param {Array} ulNodes An array of <ul> tags containing a number of <li> tags to be made keyboard sortable.
  930. * @param {Object} [settings] Additional settings
  931. * @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.
  932. * @param {Object} [settings.onStart] A callback function to be called when the user initiates sorting process
  933. * @param {Object} [settings.onUpdate] A callback function to be called when the user moves the item around
  934. * @param {Object} [settings.onStop] A callback function to be called when the user commits the item to its new place ending the sorting process
  935. * @static
  936. */
  937. keyboardSortable: function (ulNodes, settings) {
  938. settings = dojoLang.mixin({
  939. linkLists: false,
  940.  
  941. onStart: function () { },
  942. onUpdate: function () { },
  943. onStop: function () { }
  944. }, settings);
  945.  
  946. ulNodes.each(function (index, _ulNode) {
  947. var ulNode = $(_ulNode),
  948. liNodes = ulNode.find("> li"),
  949. //sortHandleNodes = liNodes.find(".sort-handle"),
  950. isReordering = false,
  951. grabbed;
  952.  
  953. // Reset focus, set aria attributes, and styling
  954. function reorderReset(handle, liNodes, liNode) {
  955. handle.focus();
  956. liNodes.attr("aria-dropeffect", "move");
  957. liNode.attr("aria-grabbed", "true").removeAttr("aria-dropeffect");
  958. }
  959.  
  960. // try to remove event handlers to prevent double initialization
  961. ulNode
  962. .off("focusout", ".sort-handle")
  963. .off("keyup", ".sort-handle");
  964.  
  965. ulNode
  966. .on("focusout", ".sort-handle", function (event) {
  967. var node = $(this).closest("li");
  968.  
  969. // if the list is not being reordered right now, release list item
  970. if (node.hasClass("list-item-grabbed") && !isReordering) {
  971. liNodes.removeAttr("aria-dropeffect");
  972. node
  973. .removeClass("list-item-grabbed")
  974. .attr({ "aria-selected": false, "aria-grabbed": false });
  975.  
  976. grabbed = false;
  977.  
  978. console.log("Keyboard Sortable: OnStop -> ", event);
  979. settings.onStop.call(null, event, { item: null });
  980. }
  981. })
  982. .on("keyup", ".sort-handle", function (event) {
  983. var liNode = $(this).closest("li"),
  984. liId = liNode[0].id,
  985. liIdArray = ulNode.sortable("toArray"),
  986. liIndex = dojoArray.indexOf(liIdArray, liId);
  987.  
  988. // Toggle grabbed state and aria attributes (13 = enter, 32 = space bar)
  989. if (event.which === 13 || event.which === 32) {
  990. if (grabbed) {
  991. liNodes.removeAttr("aria-dropeffect");
  992. liNode
  993. .attr("aria-grabbed", "false")
  994. .removeClass("list-item-grabbed");
  995.  
  996. console.log("Keyboard Sortable: OnStop -> ", liNode);
  997. settings.onStop.call(null, event, { item: liNode });
  998.  
  999. grabbed = false;
  1000. } else {
  1001. liNodes.attr("aria-dropeffect", "move");
  1002. liNode
  1003. .attr("aria-grabbed", "true")
  1004. .removeAttr("aria-dropeffect")
  1005. .addClass("list-item-grabbed");
  1006.  
  1007. console.log("Keyboard Sortable: OnStart -> ", liNode);
  1008. settings.onStart.call(null, event, { item: liNode });
  1009.  
  1010. grabbed = true;
  1011. }
  1012. // Keyboard up (38) and down (40)
  1013. } else if (event.which === 38) {
  1014. if (grabbed) {
  1015. // Don't move up if first layer in list
  1016. if (liIndex > 0) {
  1017. isReordering = true;
  1018.  
  1019. liNode.prev().before(liNode);
  1020.  
  1021. reorderReset($(this), liNodes, liNode);
  1022.  
  1023. grabbed = true;
  1024. liIndex -= 1;
  1025.  
  1026. console.log("Keyboard Sortable: OnUpdate -> ", liNode);
  1027. settings.onUpdate.call(null, event, { item: liNode });
  1028.  
  1029. isReordering = false;
  1030. }
  1031. } else {
  1032. // if lists are linked, jump to the last item of the previous list, if any
  1033. if (settings.linkLists &&
  1034. liIndex === 0 &&
  1035. index !== 0) {
  1036. liNode = $(ulNodes[index - 1]).find("> li:last");
  1037. } else {
  1038. liNode = liNode.prev();
  1039. }
  1040.  
  1041. liNode.find(":tabbable:first").focus();
  1042. }
  1043. } else if (event.which === 40) {
  1044. if (grabbed) {
  1045. // Don't move down if last layer in list
  1046. if (liIndex < liNodes.length - 1) {
  1047. isReordering = true;
  1048.  
  1049. liNode.next().after(liNode);
  1050.  
  1051. reorderReset($(this), liNodes, liNode);
  1052.  
  1053. grabbed = true;
  1054. liIndex += 1;
  1055.  
  1056. console.log("Keyboard Sortable: OnUpdate -> ", liNode);
  1057. settings.onUpdate.call(null, event, { item: liNode });
  1058.  
  1059. isReordering = false;
  1060. }
  1061. } else {
  1062. // if lists are linked, jump to the first item of the next list, if any
  1063. if (settings.linkLists &&
  1064. liIndex === liNodes.length - 1 &&
  1065. index < ulNodes.length - 1) {
  1066. liNode = $(ulNodes[index + 1]).find("> li:first");
  1067. } else {
  1068. liNode = liNode.next();
  1069. }
  1070.  
  1071. liNode.find(":tabbable:first").focus();
  1072. }
  1073. }
  1074. });
  1075. });
  1076. },
  1077.  
  1078. /**
  1079. * Takes an array of timelines and their generator functions, clear and recreates timelines optionally preserving the play position.
  1080. * ####Example of tls parameter
  1081. *
  1082. * [
  1083. * {
  1084. * timeline: {timeline},
  1085. * generator: {function}
  1086. * }
  1087. * ]
  1088. *
  1089. *
  1090. * @method resetTimelines
  1091. * @static
  1092. * @param {Array} tls An array of objects containing timeline objects and their respective generator functions
  1093. * @param {Boolean} keepPosition Indicates if the timeline should be set in the play position it was in before the reset
  1094. */
  1095. resetTimelines: function (tls, keepPosition) {
  1096. var position;
  1097.  
  1098. tls.forEach(function (tl) {
  1099. position = tl.timeLine.time(); // preserve timeline position
  1100. tl.timeLine.seek(0).clear();
  1101.  
  1102. tl.generator.call();
  1103.  
  1104. if (keepPosition) {
  1105. tl.timeLine.seek(position);
  1106. }
  1107. });
  1108. },
  1109.  
  1110. /**
  1111. * Checks if two spatial reference objects are equivalent. Handles both wkid and wkt definitions
  1112. *
  1113. * @method isSpatialRefEqual
  1114. * @static
  1115. * @param {Esri/SpatialReference} sr1 First {{#crossLink "Esri/SpatialReference"}}{{/crossLink}} to compare
  1116. * @param {Esri/SpatialReference} sr2 Second {{#crossLink "Esri/SpatialReference"}}{{/crossLink}} to compare
  1117. * @return {Boolean} true if the two spatial references are equivalent. False otherwise.
  1118. */
  1119. isSpatialRefEqual: function (sr1, sr2) {
  1120. if ((sr1.wkid) && (sr2.wkid)) {
  1121. //both SRs have wkids
  1122. return sr1.wkid === sr2.wkid;
  1123. } else if ((sr1.wkt) && (sr2.wkt)) {
  1124. //both SRs have wkt's
  1125. return sr1.wkt === sr2.wkt;
  1126. } else {
  1127. //not enough info provided or mismatch between wkid and wkt.
  1128. return false;
  1129. }
  1130. },
  1131.  
  1132. /**
  1133. * Checks if the given dom node is present in the dom.
  1134. *
  1135. * @method containsInDom
  1136. * @static
  1137. * @param {Object} el DOM node to check
  1138. * @return {Boolean} true if the given node is in the dom
  1139. */
  1140. containsInDom: function (el) {
  1141. return $.contains(document.documentElement, el);
  1142. },
  1143.  
  1144. styleBrowseFilesButton: function (nodes) {
  1145. var input,
  1146. button;
  1147.  
  1148. function focusIn(event) {
  1149. event.data.button.not(".disabled").addClass("btn-focus btn-hover");
  1150. }
  1151.  
  1152. function focusOut(event) {
  1153. event.data.button.removeClass("btn-focus btn-hover btn-active");
  1154. }
  1155.  
  1156. function mouseEnter(event) {
  1157. event.data.button.not(".disabled").addClass("btn-hover");
  1158. }
  1159.  
  1160. function mouseLeave(event) {
  1161. event.data.button.removeClass("btn-hover btn-active");
  1162. }
  1163.  
  1164. function mouseDown(event) {
  1165. event.data.button.not(".disabled").addClass("btn-focus btn-hover btn-active");
  1166. }
  1167.  
  1168. function mouseUp(event) {
  1169. event.data.button.removeClass("btn-active");
  1170. }
  1171.  
  1172. nodes.each(function (i, node) {
  1173. node = $(node);
  1174. input = node.find("input[type='file']");
  1175. button = node.find(".browse-button");
  1176.  
  1177. input
  1178. .on("focusin", { button: button }, focusIn)
  1179. .on("focusout", { button: button }, focusOut)
  1180.  
  1181. .on("mouseenter", { button: button }, mouseEnter)
  1182. .on("mouseleave", { button: button }, mouseLeave)
  1183.  
  1184. .on("mousedown", { button: button }, mouseDown)
  1185. .on("mouseup", { button: button }, mouseUp)
  1186. ;
  1187. });
  1188. },
  1189.  
  1190. /**
  1191. * Detects either cell or line delimiter in a given csv data.
  1192. *
  1193. * @method detectDelimiter
  1194. * @static
  1195. * @param {String} data csv content
  1196. * @param {String} [type] Type of the delimiter to detect. Possible values "cell" or "line". Defaults to "cell".
  1197. * @return {String} Detected delimiter
  1198. */
  1199. detectDelimiter: function (data, type) {
  1200. var count = 0,
  1201. detected,
  1202.  
  1203. escapeDelimiters = ['|', '^'],
  1204. delimiters = {
  1205. cell: [',', ';', '\t', '|', '^'],
  1206. line: ['\r\n', '\r', '\n']
  1207. };
  1208.  
  1209. type = type !== "cell" && type !== "line" ? "cell" : type;
  1210.  
  1211. delimiters[type].forEach(function (delimiter) {
  1212. var needle = delimiter,
  1213. matches;
  1214.  
  1215. if (escapeDelimiters.indexOf(delimiter) !== -1) {
  1216. needle = '\\' + needle;
  1217. }
  1218.  
  1219. matches = data.match(new RegExp(needle, 'g'));
  1220. if (matches && matches.length > count) {
  1221. count = matches.length;
  1222. detected = delimiter;
  1223. }
  1224. });
  1225.  
  1226. console.log(type + " delimiter detected: '" + (detected || delimiters[type][0]) + "'");
  1227.  
  1228. return (detected || delimiters[type][0]);
  1229. },
  1230.  
  1231. /**
  1232. * Converts HEX colour to RGB.
  1233. * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb/5624139#5624139
  1234. *
  1235. * @method hexToRgb
  1236. * @static
  1237. * @param {String} hex hex colour code
  1238. * @return {Object} object containing r, g, and b components of the supplied colour
  1239. */
  1240. hexToRgb: function (hex) {
  1241. // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
  1242. var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
  1243. hex = hex.replace(shorthandRegex, function (m, r, g, b) {
  1244. return r + r + g + g + b + b;
  1245. });
  1246.  
  1247. var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  1248. return result ? {
  1249. r: parseInt(result[1], 16),
  1250. g: parseInt(result[2], 16),
  1251. b: parseInt(result[3], 16)
  1252. } : null;
  1253. },
  1254.  
  1255. /**
  1256. * Converts RGB colour to HEX.
  1257. * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb/5624139#5624139
  1258. *
  1259. * @method rgbToHex
  1260. * @static
  1261. * @param {Number} r r component
  1262. * @param {Number} g g component
  1263. * @param {Number} b b component
  1264. * @return {Object} hex colour code
  1265. */
  1266. rgbToHex: function (r, g, b) {
  1267. return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  1268. },
  1269.  
  1270. resetFormElement: function (e) {
  1271. e.wrap('<form>').closest('form').get(0).reset();
  1272. e.unwrap();
  1273. },
  1274.  
  1275. setSelectOptions: function (select, options, append) {
  1276. //var optionsNode;
  1277.  
  1278. if (!append) {
  1279. select.empty();
  1280. //select.append(optionsNode);
  1281. }
  1282.  
  1283. options.forEach(function (option) {
  1284. select.append($("<option/>", {
  1285. value: option.value,
  1286. text: option.text
  1287. }));
  1288. });
  1289. },
  1290.  
  1291. // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22
  1292. /**
  1293. * Base64 encoding for unicode text.
  1294. *
  1295. * @method b64EncodeUnicode
  1296. * @param {String} str a string to encode
  1297. * @return {String} encoded string
  1298. */
  1299. b64EncodeUnicode: function (str) {
  1300. return new Btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
  1301. return String.fromCharCode('0x' + p1);
  1302. })).a;
  1303. }
  1304. };
  1305. });