Reusable Accessible Mapping Platform

API Docs for: 3.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 varialbe
  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 accomodate 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. * Applies supplied xslt to supplied xml. IE always returns a String; others may return a documentFragment or a jObject.
  607. *
  608. * @method transformXML
  609. * @static
  610. * @param {String} xmlurl Location of the xml file
  611. * @param {String} xslurl Location of the xslt file
  612. * @param {Function} callback The callback to be executed
  613. * @param {Boolean} returnFragment True if you want a document fragment returned (doesn't work in IE)}
  614. */
  615. transformXML: function (xmlurl, xslurl, callback, returnFragment) {
  616. var xmld = new Deferred(),
  617. xsld = new Deferred(),
  618. xml, xsl,
  619. dlist = [xmld, xsld],
  620. result,
  621. error,
  622. that = this;
  623.  
  624. that.afterAll(dlist, function () {
  625. if (!error) {
  626. result = applyXSLT(xml, xsl);
  627. }
  628. callback(error, result);
  629. });
  630.  
  631. // Transform XML using XSLT
  632. function applyXSLT(xmlString, xslString) {
  633. var output;
  634. if (window.ActiveXObject || window.hasOwnProperty("ActiveXObject")) { // IE
  635. var xslt = new ActiveXObject("Msxml2.XSLTemplate"),
  636. xmlDoc = new ActiveXObject("Msxml2.DOMDocument"),
  637. xslDoc = new ActiveXObject("Msxml2.FreeThreadedDOMDocument"),
  638. xslProc;
  639.  
  640. xmlDoc.loadXML(xmlString);
  641. xslDoc.loadXML(xslString);
  642. xslt.stylesheet = xslDoc;
  643. xslProc = xslt.createProcessor();
  644. xslProc.input = xmlDoc;
  645. xslProc.transform();
  646. output = xslProc.output;
  647. } else { // Chrome/FF/Others
  648. var xsltProcessor = new XSLTProcessor();
  649. xsltProcessor.importStylesheet(xslString);
  650. output = xsltProcessor.transformToFragment(xmlString, document);
  651.  
  652. // turn a document fragment into a proper jQuery object
  653. if (!returnFragment) {
  654. output = ($('body')
  655. .append(output)
  656. .children().last())
  657. .detach();
  658. }
  659. }
  660. return output;
  661. }
  662.  
  663. // Distinguish between XML/XSL deferred objects to resolve and set response
  664. function resolveDeferred(filename, responseObj) {
  665. if (filename.endsWith(".xsl")) {
  666. xsl = responseObj.responseText;
  667. xsld.resolve();
  668. } else {
  669. xml = responseObj.responseText;
  670. xmld.resolve();
  671. }
  672. }
  673. /*
  674. function loadXMLFileIE9(filename) {
  675. var xdr = new XDomainRequest();
  676. xdr.contentType = "text/plain";
  677. xdr.open("GET", filename);
  678. xdr.onload = function () {
  679. resolveDeferred(filename, xdr);
  680. };
  681. xdr.onprogress = function () { };
  682. xdr.ontimeout = function () { };
  683. xdr.onerror = function () {
  684. error = true;
  685. resolveDeferred(filename, xdr);
  686. };
  687. window.setTimeout(function () {
  688. xdr.send();
  689. }, 0);
  690.  
  691. }
  692. */
  693. // IE10+
  694. function loadXMLFileIE(filename) {
  695. var xhttp = new XMLHttpRequest();
  696. xhttp.open("GET", filename);
  697. try {
  698. xhttp.responseType = "msxml-document";
  699. } catch (err) { } // Helping IE11
  700. xhttp.onreadystatechange = function () {
  701. if (xhttp.readyState === 4) {
  702. if (xhttp.status !== 200) {
  703. error = true;
  704. }
  705. resolveDeferred(filename, xhttp);
  706. }
  707. };
  708. xhttp.send("");
  709. }
  710.  
  711.  
  712. if ('withCredentials' in new XMLHttpRequest() && "ActiveXObject" in window) { // IE10 and above
  713. loadXMLFileIE(xmlurl);
  714. loadXMLFileIE(xslurl);
  715. } else if (window.XDomainRequest) { // IE9 and below
  716. /*
  717. loadXMLFileIE9(xmlurl);
  718. loadXMLFileIE9(xslurl);
  719. */
  720. // dataType need to be set to "text" for xml doc requests.
  721. $.ajax({
  722. type: "GET",
  723. url: xmlurl,
  724. dataType: "text",
  725. cache: false,
  726. success: function (data) {
  727. xml = data;
  728. xmld.resolve();
  729. },
  730. error: function () {
  731. error = true;
  732. xmld.resolve();
  733. }
  734. });
  735.  
  736. $.ajax({
  737. type: "GET",
  738. url: xslurl,
  739. dataType: "text",
  740. cache: false,
  741. success: function (data) {
  742. xsl = data;
  743. xsld.resolve();
  744. },
  745. error: function () {
  746. error = true;
  747. xsld.resolve();
  748. }
  749. });
  750. } else { // Good browsers (Chrome/FF)
  751.  
  752. $.ajax({
  753. type: "GET",
  754. url: xmlurl,
  755. dataType: "xml",
  756. cache: false,
  757. success: function (data) {
  758. xml = data;
  759. xmld.resolve();
  760. },
  761. error: function () {
  762. error = true;
  763. xmld.resolve();
  764. }
  765. });
  766.  
  767. $.ajax({
  768. type: "GET",
  769. url: xslurl,
  770. dataType: "xml",
  771. cache: false,
  772. success: function (data) {
  773. xsl = data;
  774. xsld.resolve();
  775. },
  776. error: function () {
  777. error = true;
  778. xsld.resolve();
  779. }
  780. });
  781. }
  782. },
  783.  
  784. /**
  785. * [settings.linkLists]: false
  786. */
  787. keyboardSortable: function (ulNodes, settings) {
  788. settings = dojoLang.mixin({
  789. linkLists: false,
  790.  
  791. onStart: function () { },
  792. onUpdate: function () { },
  793. onStop: function () { }
  794. }, settings);
  795.  
  796. ulNodes.each(function (index, _ulNode) {
  797. var ulNode = $(_ulNode),
  798. liNodes = ulNode.find("> li"),
  799. sortHandleNodes = liNodes.find(".sort-handle"),
  800. isReordering = false,
  801. grabbed;
  802.  
  803. // Reset focus, set aria attributes, and styling
  804. function reorderReset(handle, liNodes, liNode) {
  805. handle.focus();
  806. liNodes.attr("aria-dropeffect", "move");
  807. liNode.attr("aria-grabbed", "true").removeAttr("aria-dropeffect");
  808. }
  809.  
  810. sortHandleNodes
  811. .focusout(function (event) {
  812. var node = $(this).closest("li");
  813.  
  814. // if the list is not being reordered right now, release list item
  815. if (node.hasClass("list-item-grabbed") && !isReordering) {
  816. liNodes.removeAttr("aria-dropeffect");
  817. node
  818. .removeClass("list-item-grabbed")
  819. .attr({ "aria-selected": false, "aria-grabbed": false });
  820.  
  821. grabbed = false;
  822.  
  823. console.log("Keyboard Sortable: OnStop -> ", event);
  824. settings.onStop.call(null, event, { item: null });
  825. }
  826. })
  827. .on("keyup", function (event) {
  828. var liNode = $(this).closest("li"),
  829. liId = liNode[0].id,
  830. liIdArray = ulNode.sortable("toArray"),
  831. liIndex = dojoArray.indexOf(liIdArray, liId);
  832.  
  833. // Toggle grabbed state and aria attributes (13 = enter, 32 = space bar)
  834. if (event.which === 13 || event.which === 32) {
  835. if (grabbed) {
  836. liNodes.removeAttr("aria-dropeffect");
  837. liNode
  838. .attr("aria-grabbed", "false")
  839. .removeClass("list-item-grabbed");
  840.  
  841. console.log("Keyboard Sortable: OnStop -> ", liNode);
  842. settings.onStop.call(null, event, { item: liNode });
  843.  
  844. grabbed = false;
  845. } else {
  846. liNodes.attr("aria-dropeffect", "move");
  847. liNode
  848. .attr("aria-grabbed", "true")
  849. .removeAttr("aria-dropeffect")
  850. .addClass("list-item-grabbed");
  851.  
  852. console.log("Keyboard Sortable: OnStart -> ", liNode);
  853. settings.onStart.call(null, event, { item: liNode });
  854.  
  855. grabbed = true;
  856. }
  857. // Keyboard up (38) and down (40)
  858. } else if (event.which === 38) {
  859. if (grabbed) {
  860. // Don't move up if first layer in list
  861. if (liIndex > 0) {
  862. isReordering = true;
  863.  
  864. liNode.prev().before(liNode);
  865.  
  866. reorderReset($(this), liNodes, liNode);
  867.  
  868. grabbed = true;
  869. liIndex -= 1;
  870.  
  871. console.log("Keyboard Sortable: OnUpdate -> ", liNode);
  872. settings.onUpdate.call(null, event, { item: liNode });
  873.  
  874. isReordering = false;
  875. }
  876. } else {
  877. // if lists are linked, jump to the last item of the previous list, if any
  878. if (settings.linkLists &&
  879. liIndex === 0 &&
  880. index !== 0) {
  881. liNode = $(ulNodes[index - 1]).find("> li:last");
  882. } else {
  883. liNode = liNode.prev();
  884. }
  885.  
  886. liNode.find(":tabbable:first").focus();
  887. }
  888. } else if (event.which === 40) {
  889. if (grabbed) {
  890. // Don't move down if last layer in list
  891. if (liIndex < liNodes.length - 1) {
  892. isReordering = true;
  893.  
  894. liNode.next().after(liNode);
  895.  
  896. reorderReset($(this), liNodes, liNode);
  897.  
  898. grabbed = true;
  899. liIndex += 1;
  900.  
  901. console.log("Keyboard Sortable: OnUpdate -> ", liNode);
  902. settings.onUpdate.call(null, event, { item: liNode });
  903.  
  904. isReordering = false;
  905. }
  906. } else {
  907. // if lists are linked, jump to the first item of the next list, if any
  908. if (settings.linkLists &&
  909. liIndex === liNodes.length - 1 &&
  910. index < ulNodes.length - 1) {
  911. liNode = $(ulNodes[index + 1]).find("> li:first");
  912. } else {
  913. liNode = liNode.next();
  914. }
  915.  
  916. liNode.find(":tabbable:first").focus();
  917. }
  918. }
  919. });
  920. });
  921. }
  922. };
  923. });