Reusable Accessible Mapping Platform

API Docs for: 4.0.0
Show:

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

  1. /* global define, window, XMLHttpRequest, ActiveXObject, XSLTProcessor, console, $, document, jQuery */
  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. */
  27. define(["dojo/_base/array", "dojo/_base/lang", "dojo/topic", "dojo/Deferred", "esri/geometry/Extent"],
  28. function (dojoArray, dojoLang, topic, Deferred, Extent) {
  29. "use strict";
  30.  
  31. return {
  32. /**
  33. * Checks if the console exists, if not, redefine the console and all console methods to
  34. * a function that does nothing. Useful for IE which does not have the console until the
  35. * debugger is opened.
  36. *
  37. * @method checkConsole
  38. * @static
  39. */
  40. checkConsole: function () {
  41. var noop = function () { },
  42. methods = [
  43. 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',
  44. 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
  45. 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',
  46. 'timeStamp', 'trace', 'warn'
  47. ],
  48. length = methods.length,
  49. console = (window.console = window.console || {}),
  50. method;
  51.  
  52. while (length--) {
  53. method = methods[length];
  54.  
  55. // Only stub undefined methods.
  56. if (!console[method]) {
  57. console[method] = noop;
  58. }
  59. }
  60. },
  61.  
  62. // String Functions
  63.  
  64. /**
  65. * Returns an String that has it's angle brackets ('<' and '>') escaped (replaced by '&lt;' and '&gt;').
  66. * This will effectively cause the String to be displayed in plain text when embedded in an HTML page.
  67. *
  68. * @method escapeHtml
  69. * @static
  70. * @param {String} html String to escape
  71. * @return {String} escapeHtml Escaped string
  72. */
  73. escapeHtml: function (html) {
  74. return html.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
  75. },
  76.  
  77. /**
  78. * Returns true if the given String is a number
  79. *
  80. * @method isNumber
  81. * @static
  82. * @param {String} input The string to check
  83. * @return {boolean} True if number
  84. */
  85. isNumber: function (input) {
  86. return isFinite(String(input).trim() || NaN);
  87. },
  88.  
  89. /**
  90. * Parse the given String into a boolean. Returns true if the String
  91. * is the word "true" (case insensitive). False otherwise.
  92. *
  93. * @method parseBool
  94. * @static
  95. * @param {String} str The string to check
  96. * @return {boolean} True if `true`
  97. */
  98. parseBool: function (str) {
  99. return (str.toLowerCase() === 'true');
  100. },
  101.  
  102. // Deferred
  103.  
  104. /**
  105. * Executes the callback function only after all the deferred Objects in the
  106. * given deferredList has resolved.
  107. *
  108. * @method afterAll
  109. * @static
  110. * @param {array} deferredList A list of Deferred objects
  111. * @param {function} callback The callback to be executed
  112. */
  113. afterAll: function (deferredList, callback, context) {
  114. if (deferredList.length === 0) {
  115. callback();
  116. return;
  117. }
  118.  
  119. var completed = 0; // Keeps track of the number of deferred that has resolved
  120. dojoArray.forEach(deferredList, function (deferred) {
  121. deferred.then(function () {
  122. completed++;
  123. if (completed === deferredList.length) {
  124. callback.call(context);
  125. }
  126. });
  127. });
  128. },
  129.  
  130. // Serialization
  131.  
  132. /**
  133. * Converts an array into a '+' separated String that can be used as
  134. * the query parameter of a URL.
  135. *
  136. * arrayToQuery(["abc", 123, "efg"]) -> "abc+123+efg"
  137. *
  138. * *__NOTE:__ the array should only contain primitives, objects will not be serialized
  139. * properly.*
  140. *
  141. * @method arrayToQuery
  142. * @static
  143. * @param {array} array An array of primitives to be serialized
  144. * @return {String} A serialized representation of the given array
  145. */
  146. arrayToQuery: function (array) {
  147. return array.join("+");
  148. },
  149.  
  150. /**
  151. * Converts a query String generated by arrayToQuery into an array object.
  152. * The array object will only contain Strings.
  153. *
  154. * queryToArray("abc+123+efg") -> ["abc", "123", "efg"]
  155. *
  156. * @method queryToArray
  157. * @static
  158. * @param {String} query A query string to be converted
  159. * @return {String} A resulting array of strings
  160. */
  161. queryToArray: function (query) {
  162. return query.split("+");
  163. },
  164.  
  165. // Event handling
  166.  
  167. /**
  168. * A convenience method that wraps around Dojo's subscribe method to allow
  169. * a scope to hitched to the given callback function.
  170. *
  171. * @method subscribe
  172. * @static
  173. * @param {String} name Event name
  174. * @param {function} callback The callback to be executed
  175. * @param {object} scope Scope of the callback
  176. */
  177. subscribe: function (name, callback, scope) {
  178. if (this.isUndefined(scope)) {
  179. topic.subscribe(name, callback);
  180. } else {
  181. topic.subscribe(name, dojoLang.hitch(scope, callback));
  182. }
  183. },
  184.  
  185. /**
  186. * Subscribes to an event, after the event has occurred, the handle is
  187. * removed.
  188. *
  189. * @method subscribeOnce
  190. * @static
  191. * @param {String} name Event name
  192. * @param {function} callback The callback to be executed
  193. */
  194. subscribeOnce: function (name, callback) {
  195. var handle = null,
  196. wrapper = function (evt) {
  197. handle.remove();
  198. callback(evt);
  199. };
  200.  
  201. return (handle = topic.subscribe(name, wrapper));
  202. },
  203.  
  204. /**
  205. * Subscribes to a set of events, executes the callback when any of the events fire, then removes the handle.
  206. *
  207. * @method subscribeOnceAny
  208. * @static
  209. * @param {String} names An array of event names
  210. * @param {Function} callback The callback to be executed
  211. */
  212. subscribeOnceAny: function (names, callback) {
  213. var handles = [];
  214.  
  215. function wrapper(evt) {
  216. dojoArray.forEach(handles, function (handle) {
  217. handle.remove();
  218. });
  219.  
  220. callback(evt);
  221. }
  222.  
  223. dojoArray.forEach(names, dojoLang.hitch(this,
  224. function (name) {
  225. handles.push(this.subscribeOnce(name, wrapper));
  226. }));
  227. },
  228.  
  229. /**
  230. * Given an array of event names published by topic.publish, call the given
  231. * callback function after ALL of the given events have occurred. An array of
  232. * arguments is passed to the callback function, the arguments are those returned
  233. * by the events (in the order that the events appear in the array).
  234. *
  235. * #####Example
  236. *
  237. * Assume somewhere a module publishes a "click" event:
  238. *
  239. * topic.publish("click", { mouseX: 10, mouseY: 50 });
  240. *
  241. * and somewhere else another module publishes a "finishLoading" event:
  242. *
  243. * topic.publish("finishLoading", { loadedPictures: [pic1, pic2] });
  244. *
  245. * Then if one wants to do something (e.g. display pictures) only after the pictures
  246. * have been loaded AND the user clicked somewhere, then:
  247. *
  248. * - args[0] will be the object returned by the "click" event
  249. * - which in this case will be: { mouseX: 10, mouseY: 50 }
  250. * - args[1] will be the object returned by the "finishLoading" event
  251. * - which in this case will be: { loadedPictures: [pic1, pic2] }
  252. *
  253. *
  254. * subscribe(["click", "finishLoading"], function(args) {
  255. * doSomething();
  256. * });
  257. *
  258. * *__NOTE:__
  259. * If one of the events fires multiple times before the other event, the object
  260. * passed by this function to the callback will be the object returned when the
  261. * event FIRST fired (subsequent firings of the same event are ignored). Also, if
  262. * some event do not return an object, it will also be excluded in the arguments to
  263. * the callback function. So be careful! For example, say you subscribed to the events:
  264. * "evt1", "evt2", "evt3". "evt1" returns an object (call it "evt1Obj"), "evt2" does not,
  265. * "evt3" returns two objects (call it "evt3Obj-1" and "evt3Obj-2" respectively).
  266. * Then the array passed to the callback will be: ["evt1Obj", "evt3Obj-1", "evt3Obj-2"].*
  267. *
  268. * @method subscribeAll
  269. * @static
  270. * @param {array} nameArray An array of Strings containing the names of events to subscribe to
  271. * @param {function} callback The callback to be executed
  272. */
  273. subscribeAll: function (nameArray, callback) {
  274. // Keeps track of the status of all the events being subscribed to
  275. var events = [];
  276.  
  277. dojoArray.forEach(nameArray, function (eventName, i) {
  278. events.push({
  279. fired: false,
  280. args: null
  281. });
  282.  
  283. topic.subscribe(eventName, function () {
  284. // If this is the fire time the event fired
  285. if (!events[i].fired) {
  286. // Mark the event has fired and capture it's arguments (if any)
  287. events[i].fired = true;
  288. events[i].args = Array.prototype.slice.call(arguments);
  289.  
  290. // Check if all events have fired
  291. if (dojoArray.every(events, function (event) {
  292. return event.fired;
  293. })) {
  294. // If so construct an array with arguments from the events
  295. var eventArgs = [];
  296. dojoArray.forEach(events, function (event) {
  297. eventArgs.append(event.args);
  298. });
  299. callback(eventArgs);
  300. }
  301. }
  302. });
  303. });
  304. },
  305.  
  306. // Specialized Variables *
  307.  
  308. /**
  309. * Creates an object that acts like a lazy variable (i.e. a variable whose value is only
  310. * resolved the first time it is retrieved, not when it is assigned). The value given to
  311. * the lazy variable should be the return value of the given initFunc. The returned object
  312. * has two methods:
  313. *
  314. * - get - returns the value of the variable, if it is the first time get is called, the
  315. * the initFunc will be called to resolve the value of the variable.
  316. * - reset - forces the variable to call the initFunc again the next time get is called
  317. *
  318. * @method createLazyVariable
  319. * @static
  320. * @param {function} initFunc A function to call to resolve the variable value
  321. * @return {Object} The lazy variable
  322. */
  323. createLazyVariable: function (initFunc) {
  324. var value = null;
  325. return {
  326. reset: function () {
  327. value = null;
  328. },
  329.  
  330. get: function () {
  331. if (value == null) {
  332. value = initFunc();
  333. }
  334. return value;
  335. }
  336. };
  337. },
  338.  
  339. // FUNCTION DECORATORS
  340.  
  341. /**
  342. * Returns a function that has the same functionality as the given function, but
  343. * can only be executed once (subsequent execution does nothing).
  344. *
  345. * @method once
  346. * @static
  347. * @param {function} func Function to be decorated
  348. * @return {function} Decorated function that can be executed once
  349. */
  350. once: function (func) {
  351. var ran = false;
  352. return function () {
  353. if (!ran) {
  354. func();
  355. ran = true;
  356. }
  357. };
  358. },
  359.  
  360. // MISCELLANEOUS
  361.  
  362. /**
  363. * Returns true if the given obj is undefined, false otherwise.
  364. *
  365. * @method isUndefined
  366. * @static
  367. * @param {object} obj Object to be checked
  368. * @return {boolean} True if the given object is undefined, false otherwise
  369. */
  370. isUndefined: function (obj) {
  371. return (typeof obj === 'undefined');
  372. },
  373.  
  374. /**
  375. * Compares two graphic objects.
  376. *
  377. * @method compareGraphics
  378. * @static
  379. * @param {Object} one Graphic object
  380. * @param {Object} two Graphic object
  381. * @return {boolean} True if the objects represent the same feature
  382. */
  383. compareGraphics: function (one, two) {
  384. var oneKey = "0",
  385. twoKey = "1",
  386. objectIdField,
  387. oneLayer,
  388. twoLayer;
  389.  
  390. if (one && two &&
  391. $.isFunction(one.getLayer) && $.isFunction(two.getLayer)) {
  392. oneLayer = one.getLayer();
  393. twoLayer = two.getLayer();
  394. objectIdField = oneLayer.objectIdField;
  395. oneKey = oneLayer.url + one.attributes[objectIdField];
  396. twoKey = twoLayer.url + two.attributes[objectIdField];
  397. }
  398.  
  399. return oneKey === twoKey;
  400. },
  401.  
  402. /**
  403. * Returns the width of the scrollbar in pixels. Since different browsers render scrollbars differently, the width may vary.
  404. *
  405. * @method scrollbarWidth
  406. * @static
  407. * @return {int} The width of the scrollbar in pixels
  408. * @for Util
  409. */
  410. scrollbarWidth: function () {
  411. var $inner = jQuery('<div style="width: 100%; height:200px;">test</div>'),
  412. $outer = jQuery('<div style="width:200px;height:150px; position: absolute; top: 0; left: 0; visibility: hidden; overflow:hidden;"></div>').append($inner),
  413. inner = $inner[0],
  414. outer = $outer[0],
  415. width1, width2;
  416.  
  417. jQuery('body').append(outer);
  418. width1 = inner.offsetWidth;
  419. $outer.css('overflow', 'scroll');
  420. width2 = outer.clientWidth;
  421. $outer.remove();
  422.  
  423. return (width1 - width2);
  424. },
  425.  
  426. /**
  427. * Checks if the height of the scrollable content of the body is taller than its height;
  428. * if so, offset the content horizontally to accommodate for the scrollbar assuming target's width is
  429. * set to "100%".
  430. *
  431. * @method adjustWidthForSrollbar
  432. * @static
  433. * @param {jObject} body A DOM node with a scrollbar (or not)
  434. * @param {jObject} targets An array of jObjects to add the offset to
  435. */
  436. adjustWidthForSrollbar: function (body, targets) {
  437. var offset = body.innerHeight() < body[0].scrollHeight ? this.scrollbarWidth() : 0;
  438.  
  439. dojoArray.map(targets, function (target) {
  440. target.css({
  441. right: offset
  442. });
  443. });
  444. },
  445.  
  446. /**
  447. * Waits until a given function is available and executes a callback function.
  448. *
  449. * @method executeOnLoad
  450. * @static
  451. * @param {Object} target an object on which to wait for function to appear
  452. * @param {function} func A function whose availability in question
  453. * @param {function} callback The callback function to be executed after func is available
  454. */
  455. executeOnLoad: function (target, func, callback) {
  456. var deferred = new Deferred(),
  457. handle;
  458.  
  459. deferred.then(function () {
  460. window.clearInterval(handle);
  461. //console.log("deffered resolved");
  462.  
  463. callback();
  464. });
  465.  
  466. handle = window.setInterval(function () {
  467. if ($.isFunction(target[func])) {
  468. deferred.resolve(true);
  469. }
  470. }, 500);
  471. },
  472.  
  473. /**
  474. * Loops through all object properties and applies a given function to each. Resolves the given deferred when done.
  475. *
  476. * @method executeOnDone
  477. * @static
  478. * @param {object} o Object to look through
  479. * @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.
  480. * @param {object} d A deferred to be resolved when all properties have been processed.
  481. */
  482. executeOnDone: function (o, func, d) {
  483. var counter = 0,
  484. arr = [],
  485. deferred;
  486.  
  487. function fnOnDeferredCancel() {
  488. d.cancel();
  489. }
  490.  
  491. function fnOnDeferredThen() {
  492. counter--;
  493. if (counter === 0) {
  494. d.resolve(true);
  495. }
  496. }
  497.  
  498. d = d || new Deferred();
  499.  
  500. for (var q in o) {
  501. if (o.hasOwnProperty(q)) {
  502. arr.push(o[q]);
  503. }
  504. }
  505.  
  506. counter = arr.length;
  507.  
  508. arr.forEach(function (p) {
  509. deferred = new Deferred(fnOnDeferredCancel);
  510.  
  511. deferred.then(fnOnDeferredThen);
  512.  
  513. func(p, deferred);
  514. });
  515.  
  516. if (counter === 0) {
  517. d.resolve(true);
  518. }
  519. },
  520.  
  521. /**
  522. * Generates an rfc4122 version 4 compliant guid.
  523. * Taken from here: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
  524. *
  525. * @method guid
  526. * @static
  527. * @return {String} The generated guid string
  528. */
  529. guid: function () {
  530. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
  531. var r = Math.random() * 16 | 0,
  532. v = c === 'x' ? r : (r & 0x3 | 0x8);
  533. return v.toString(16);
  534. });
  535. },
  536.  
  537. /**
  538. * Returns an appropriate where clause depending on whether the query
  539. * is a String (returns a where clause with CASE INSENSITIVE comparison)
  540. * or an integer.
  541. *
  542. * @method getWhereClause
  543. * @static
  544. * @param {String} varName ???
  545. * @param {String | Number} query A query string
  546. * @return {String} The generated "where" clause
  547. */
  548. getWhereClause: function (varName, query) {
  549. if (this.isNumber(query)) {
  550. return String.format("{0}={1}", varName, query);
  551. }
  552. return String.format("Upper({0})=Upper(\'{1}\')", varName, query);
  553. },
  554.  
  555. /**
  556. * Converts html into text by replacing
  557. * all html tags with their appropriate special characters
  558. *
  559. * @method stripHtml
  560. * @static
  561. * @param {String} html HTML to be converted to text
  562. * @return {String} The HTML in text form
  563. */
  564. stripHtml: function (html) {
  565. var tmp = document.createElement("DIV");
  566. // jquery .text function converts html into text by replacing
  567. // all html tags with their appropriate special characters
  568. $(tmp).text(html);
  569. return tmp.textContent || tmp.innerText || "";
  570. },
  571.  
  572. // Query geometry
  573. /*
  574. * Create a new extent based on the current map size, a point (X/Y coordinates), and a pixel tolerance value.
  575. * @method pointToExtent
  576. * @param {Object} map The map control
  577. * @param {Object} point The location on screen (X/Y coordinates)
  578. * @param {Number} toleranceInPixel A value indicating how many screen pixels the extent should be from the point
  579. * @returns {Object} a new extent calculated from the given parameters
  580. *
  581. */
  582. pointToExtent: function (map, point, toleranceInPixel) {
  583. var pixelWidth = map.extent.getWidth() / map.width,
  584. toleraceInMapCoords = toleranceInPixel * pixelWidth;
  585.  
  586. return new Extent(point.x - toleraceInMapCoords,
  587. point.y - toleraceInMapCoords,
  588. point.x + toleraceInMapCoords,
  589. point.y + toleraceInMapCoords,
  590. map.spatialReference);
  591. },
  592. /**
  593. * Checks if the string ends with the supplied suffix.
  594. *
  595. * @method endsWith
  596. * @static
  597. * @param {String} str String to be evaluated
  598. * @param {String} suffix Ending string to be matched
  599. * @return {boolean} True if suffix matches
  600. */
  601. endsWith: function (str, suffix) {
  602. return str.indexOf(suffix, str.length - suffix.length) !== -1;
  603. },
  604.  
  605. /**
  606. * Recursively merge JSON objects into a target object.
  607. * The merge will also merge array elements.
  608. *
  609. * @method mergeRecursive
  610. * @static
  611. */
  612. mergeRecursive: function () {
  613. function isDOMNode(v) {
  614. if (v === null) {
  615. return false;
  616. }
  617. if (typeof v !== 'object') {
  618. return false;
  619. }
  620. if (!('nodeName' in v)) {
  621. return false;
  622. }
  623. var nn = v.nodeName;
  624. try {
  625. v.nodeName = 'is readonly?';
  626. } catch (e) {
  627. return true;
  628. }
  629. if (v.nodeName === nn) {
  630. return true;
  631. }
  632. v.nodeName = nn;
  633. return false;
  634. }
  635.  
  636. // _mergeRecursive does the actual job with two arguments.
  637. // @param {Object} destination JSON object to have other objects merged into. Parameter is modified by the function.
  638. // @param {Object} sourceArray Param array of JSON objects to merge into the source
  639. // @return {Ojbect} merged result object (points to destination variable)
  640. var _mergeRecursive = function (dst, src) {
  641. if (isDOMNode(src) || typeof src !== 'object' || src === null) {
  642. return dst;
  643. }
  644.  
  645. for (var p in src) {
  646. if (src.hasOwnProperty(p)) {
  647. if ($.isArray(src[p])) {
  648. if (dst[p] === undefined) {
  649. dst[p] = [];
  650. }
  651. $.merge(dst[p], src[p]);
  652. continue;
  653. }
  654.  
  655. if (src[p] === undefined) {
  656. continue;
  657. }
  658. if (typeof src[p] !== 'object' || src[p] === null) {
  659. dst[p] = src[p];
  660. } else if (typeof dst[p] !== 'object' || dst[p] === null) {
  661. dst[p] = _mergeRecursive(src[p].constructor === Array ? [] : {}, src[p]);
  662. } else {
  663. _mergeRecursive(dst[p], src[p]);
  664. }
  665. }
  666. }
  667. return dst;
  668. }, out;
  669.  
  670. // Loop through arguments and merge them into the first argument.
  671. out = arguments[0];
  672. if (typeof out !== 'object' || out === null) {
  673. return out;
  674. }
  675. for (var i = 1, il = arguments.length; i < il; i++) {
  676. _mergeRecursive(out, arguments[i]);
  677. }
  678. return out;
  679. },
  680.  
  681. /**
  682. * Applies supplied xslt to supplied xml. IE always returns a String; others may return a documentFragment or a jObject.
  683. *
  684. * @method transformXML
  685. * @static
  686. * @param {String} xmlurl Location of the xml file
  687. * @param {String} xslurl Location of the xslt file
  688. * @param {Function} callback The callback to be executed
  689. * @param {Boolean} returnFragment True if you want a document fragment returned (doesn't work in IE)}
  690. */
  691. transformXML: function (xmlurl, xslurl, callback, returnFragment) {
  692. var xmld = new Deferred(),
  693. xsld = new Deferred(),
  694. xml, xsl,
  695. dlist = [xmld, xsld],
  696. result,
  697. error,
  698. that = this;
  699.  
  700. that.afterAll(dlist, function () {
  701. if (!error) {
  702. result = applyXSLT(xml, xsl);
  703. }
  704. callback(error, result);
  705. });
  706.  
  707. // Transform XML using XSLT
  708. function applyXSLT(xmlString, xslString) {
  709. var output;
  710. if (window.ActiveXObject || window.hasOwnProperty("ActiveXObject")) { // IE
  711. var xslt = new ActiveXObject("Msxml2.XSLTemplate"),
  712. xmlDoc = new ActiveXObject("Msxml2.DOMDocument"),
  713. xslDoc = new ActiveXObject("Msxml2.FreeThreadedDOMDocument"),
  714. xslProc;
  715.  
  716. xmlDoc.loadXML(xmlString);
  717. xslDoc.loadXML(xslString);
  718. xslt.stylesheet = xslDoc;
  719. xslProc = xslt.createProcessor();
  720. xslProc.input = xmlDoc;
  721. xslProc.transform();
  722. output = xslProc.output;
  723. } else { // Chrome/FF/Others
  724. var xsltProcessor = new XSLTProcessor();
  725. xsltProcessor.importStylesheet(xslString);
  726. output = xsltProcessor.transformToFragment(xmlString, document);
  727.  
  728. // turn a document fragment into a proper jQuery object
  729. if (!returnFragment) {
  730. output = ($('body')
  731. .append(output)
  732. .children().last())
  733. .detach();
  734. }
  735. }
  736. return output;
  737. }
  738.  
  739. // Distinguish between XML/XSL deferred objects to resolve and set response
  740. function resolveDeferred(filename, responseObj) {
  741. if (filename.endsWith(".xsl")) {
  742. xsl = responseObj.responseText;
  743. xsld.resolve();
  744. } else {
  745. xml = responseObj.responseText;
  746. xmld.resolve();
  747. }
  748. }
  749. /*
  750. function loadXMLFileIE9(filename) {
  751. var xdr = new XDomainRequest();
  752. xdr.contentType = "text/plain";
  753. xdr.open("GET", filename);
  754. xdr.onload = function () {
  755. resolveDeferred(filename, xdr);
  756. };
  757. xdr.onprogress = function () { };
  758. xdr.ontimeout = function () { };
  759. xdr.onerror = function () {
  760. error = true;
  761. resolveDeferred(filename, xdr);
  762. };
  763. window.setTimeout(function () {
  764. xdr.send();
  765. }, 0);
  766. }
  767. */
  768. // IE10+
  769. function loadXMLFileIE(filename) {
  770. var xhttp = new XMLHttpRequest();
  771. xhttp.open("GET", filename);
  772. try {
  773. xhttp.responseType = "msxml-document";
  774. } catch (err) { } // Helping IE11
  775. xhttp.onreadystatechange = function () {
  776. if (xhttp.readyState === 4) {
  777. if (xhttp.status !== 200) {
  778. error = true;
  779. }
  780. resolveDeferred(filename, xhttp);
  781. }
  782. };
  783. xhttp.send("");
  784. }
  785.  
  786. if ('withCredentials' in new XMLHttpRequest() && "ActiveXObject" in window) { // IE10 and above
  787. loadXMLFileIE(xmlurl);
  788. loadXMLFileIE(xslurl);
  789. } else if (window.XDomainRequest) { // IE9 and below
  790. /*
  791. loadXMLFileIE9(xmlurl);
  792. loadXMLFileIE9(xslurl);
  793. */
  794. // dataType need to be set to "text" for xml doc requests.
  795. $.ajax({
  796. type: "GET",
  797. url: xmlurl,
  798. dataType: "text",
  799. cache: false,
  800. success: function (data) {
  801. xml = data;
  802. xmld.resolve();
  803. },
  804. error: function () {
  805. error = true;
  806. xmld.resolve();
  807. }
  808. });
  809.  
  810. $.ajax({
  811. type: "GET",
  812. url: xslurl,
  813. dataType: "text",
  814. cache: false,
  815. success: function (data) {
  816. xsl = data;
  817. xsld.resolve();
  818. },
  819. error: function () {
  820. error = true;
  821. xsld.resolve();
  822. }
  823. });
  824. } else { // Good browsers (Chrome/FF)
  825. $.ajax({
  826. type: "GET",
  827. url: xmlurl,
  828. dataType: "xml",
  829. cache: false,
  830. success: function (data) {
  831. xml = data;
  832. xmld.resolve();
  833. },
  834. error: function () {
  835. error = true;
  836. xmld.resolve();
  837. }
  838. });
  839.  
  840. $.ajax({
  841. type: "GET",
  842. url: xslurl,
  843. dataType: "xml",
  844. cache: false,
  845. success: function (data) {
  846. xsl = data;
  847. xsld.resolve();
  848. },
  849. error: function () {
  850. error = true;
  851. xsld.resolve();
  852. }
  853. });
  854. }
  855. },
  856.  
  857. /**
  858. * [settings.linkLists]: false
  859. */
  860. keyboardSortable: function (ulNodes, settings) {
  861. settings = dojoLang.mixin({
  862. linkLists: false,
  863.  
  864. onStart: function () { },
  865. onUpdate: function () { },
  866. onStop: function () { }
  867. }, settings);
  868.  
  869. ulNodes.each(function (index, _ulNode) {
  870. var ulNode = $(_ulNode),
  871. liNodes = ulNode.find("> li"),
  872. sortHandleNodes = liNodes.find(".sort-handle"),
  873. isReordering = false,
  874. grabbed;
  875.  
  876. // Reset focus, set aria attributes, and styling
  877. function reorderReset(handle, liNodes, liNode) {
  878. handle.focus();
  879. liNodes.attr("aria-dropeffect", "move");
  880. liNode.attr("aria-grabbed", "true").removeAttr("aria-dropeffect");
  881. }
  882.  
  883. sortHandleNodes
  884. .focusout(function (event) {
  885. var node = $(this).closest("li");
  886.  
  887. // if the list is not being reordered right now, release list item
  888. if (node.hasClass("list-item-grabbed") && !isReordering) {
  889. liNodes.removeAttr("aria-dropeffect");
  890. node
  891. .removeClass("list-item-grabbed")
  892. .attr({ "aria-selected": false, "aria-grabbed": false });
  893.  
  894. grabbed = false;
  895.  
  896. console.log("Keyboard Sortable: OnStop -> ", event);
  897. settings.onStop.call(null, event, { item: null });
  898. }
  899. })
  900. .on("keyup", function (event) {
  901. var liNode = $(this).closest("li"),
  902. liId = liNode[0].id,
  903. liIdArray = ulNode.sortable("toArray"),
  904. liIndex = dojoArray.indexOf(liIdArray, liId);
  905.  
  906. // Toggle grabbed state and aria attributes (13 = enter, 32 = space bar)
  907. if (event.which === 13 || event.which === 32) {
  908. if (grabbed) {
  909. liNodes.removeAttr("aria-dropeffect");
  910. liNode
  911. .attr("aria-grabbed", "false")
  912. .removeClass("list-item-grabbed");
  913.  
  914. console.log("Keyboard Sortable: OnStop -> ", liNode);
  915. settings.onStop.call(null, event, { item: liNode });
  916.  
  917. grabbed = false;
  918. } else {
  919. liNodes.attr("aria-dropeffect", "move");
  920. liNode
  921. .attr("aria-grabbed", "true")
  922. .removeAttr("aria-dropeffect")
  923. .addClass("list-item-grabbed");
  924.  
  925. console.log("Keyboard Sortable: OnStart -> ", liNode);
  926. settings.onStart.call(null, event, { item: liNode });
  927.  
  928. grabbed = true;
  929. }
  930. // Keyboard up (38) and down (40)
  931. } else if (event.which === 38) {
  932. if (grabbed) {
  933. // Don't move up if first layer in list
  934. if (liIndex > 0) {
  935. isReordering = true;
  936.  
  937. liNode.prev().before(liNode);
  938.  
  939. reorderReset($(this), liNodes, liNode);
  940.  
  941. grabbed = true;
  942. liIndex -= 1;
  943.  
  944. console.log("Keyboard Sortable: OnUpdate -> ", liNode);
  945. settings.onUpdate.call(null, event, { item: liNode });
  946.  
  947. isReordering = false;
  948. }
  949. } else {
  950. // if lists are linked, jump to the last item of the previous list, if any
  951. if (settings.linkLists &&
  952. liIndex === 0 &&
  953. index !== 0) {
  954. liNode = $(ulNodes[index - 1]).find("> li:last");
  955. } else {
  956. liNode = liNode.prev();
  957. }
  958.  
  959. liNode.find(":tabbable:first").focus();
  960. }
  961. } else if (event.which === 40) {
  962. if (grabbed) {
  963. // Don't move down if last layer in list
  964. if (liIndex < liNodes.length - 1) {
  965. isReordering = true;
  966.  
  967. liNode.next().after(liNode);
  968.  
  969. reorderReset($(this), liNodes, liNode);
  970.  
  971. grabbed = true;
  972. liIndex += 1;
  973.  
  974. console.log("Keyboard Sortable: OnUpdate -> ", liNode);
  975. settings.onUpdate.call(null, event, { item: liNode });
  976.  
  977. isReordering = false;
  978. }
  979. } else {
  980. // if lists are linked, jump to the first item of the next list, if any
  981. if (settings.linkLists &&
  982. liIndex === liNodes.length - 1 &&
  983. index < ulNodes.length - 1) {
  984. liNode = $(ulNodes[index + 1]).find("> li:first");
  985. } else {
  986. liNode = liNode.next();
  987. }
  988.  
  989. liNode.find(":tabbable:first").focus();
  990. }
  991. }
  992. });
  993. });
  994. }
  995. };
  996. });