HtmlStore.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. define("dojox/data/HtmlStore", ["dojo/_base/declare", "dojo/_base/array", "dojo/_base/lang", "dojo/dom", "dojo/_base/xhr", "dojo/_base/window",
  2. "dojo/data/util/simpleFetch", "dojo/data/util/filter", "dojox/xml/parser"],
  3. function(declare, array, lang, dom, xhr, winUtil, simpleFetch, filter, xmlParser) {
  4. var HtmlStore = declare("dojox.data.HtmlStore", null, {
  5. constructor: function(/*Object*/args){
  6. // summary:
  7. // Initializer for the HTML table store.
  8. // description:
  9. // The HtmlStore can be created in one of two ways: a) by parsing an existing
  10. // table or list DOM node on the current page or b) by referencing an external url and giving
  11. // the id of the table or list in that page. The remote url will be parsed as an html page.
  12. //
  13. // The HTML table or list should be of the following form:
  14. //
  15. // | <table id="myTable">
  16. // | <thead>
  17. // | <tr>
  18. // | <th>Attribute1</th>
  19. // | <th>Attribute2</th>
  20. // | </tr>
  21. // | </thead>
  22. // | <tbody>
  23. // | <tr>
  24. // | <td>Value1.1</td>
  25. // | <td>Value1.2</td>
  26. // | </tr>
  27. // | <tr>
  28. // | <td>Value2.1</td>
  29. // | <td>Value2.2</td>
  30. // | </tr>
  31. // | </tbody>
  32. // | </table>
  33. //
  34. // -or-
  35. //
  36. // | <ul id="myUnorderedList">
  37. // | <li>Value.1</li>
  38. // | <li>Value.2</li>
  39. // | </ul>
  40. //
  41. // -or-
  42. //
  43. // | <ol id="myOrderedList">
  44. // | <li>Value.1</li>
  45. // | <li>Value.2</li>
  46. // | </ol>
  47. //
  48. // args:
  49. // An anonymous object to initialize properties. It expects the following values:
  50. // dataId: The id of the HTML table to use.
  51. // OR
  52. // url: The url of the remote page to load
  53. // dataId: The id of the table element in the remote page
  54. // and the option:
  55. // trimWhitespace: Trim off any surrounding whitespace from the headers (attribute
  56. // names) and text content of the items in question. Default is false for
  57. // backwards compatibility.
  58. if(args && "urlPreventCache" in args){
  59. this.urlPreventCache = args.urlPreventCache?true:false;
  60. }
  61. if(args && "trimWhitespace" in args){
  62. this.trimWhitespace = args.trimWhitespace?true:false;
  63. }
  64. if(args.url){
  65. if(!args.dataId){
  66. throw new Error("dojo.data.HtmlStore: Cannot instantiate using url without an id!");
  67. }
  68. this.url = args.url;
  69. this.dataId = args.dataId;
  70. }else{
  71. if(args.dataId){
  72. this.dataId = args.dataId;
  73. }
  74. }
  75. if(args && "fetchOnCreate" in args){
  76. this.fetchOnCreate = args.fetchOnCreate?true:false;
  77. }
  78. if(this.fetchOnCreate && this.dataId){
  79. this.fetch();
  80. }
  81. },
  82. // url: [public] string
  83. // The URL from which to load an HTML document for data loading
  84. url: "",
  85. // dataId: [public] string
  86. // The id in the document for an element from which to get the data.
  87. dataId: "",
  88. // trimWhitepace: [public] boolean
  89. // Boolean flag to denote if the store should trim whitepace around
  90. // header and data content of a node. This matters if reformatters
  91. // alter the white spacing around the tags. The default is false for
  92. // backwards compat.
  93. trimWhitespace: false,
  94. // urlPreventCache: [public] boolean
  95. // Flag to denote if peventCache should be used on xhrGet calls.
  96. urlPreventCache: false,
  97. // fetchOnCreate: [public] boolean
  98. // Flag to denote if it should try to load from a data id (nested in the page)
  99. // The moment the store is created, instead of waiting for first
  100. // fetch call.
  101. fetchOnCreate: false,
  102. _indexItems: function(){
  103. // summary:
  104. // Function to index items found under the id.
  105. // tags:
  106. // private
  107. this._getHeadings();
  108. if(this._rootNode.rows){//tables
  109. if(this._rootNode.tBodies && this._rootNode.tBodies.length > 0){
  110. this._rootNode = this._rootNode.tBodies[0];
  111. }
  112. var i;
  113. for(i=0; i<this._rootNode.rows.length; i++){
  114. this._rootNode.rows[i]._ident = i+1;
  115. }
  116. }else{//lists
  117. var c=1;
  118. for(i=0; i<this._rootNode.childNodes.length; i++){
  119. if(this._rootNode.childNodes[i].nodeType === 1){
  120. this._rootNode.childNodes[i]._ident = c;
  121. c++;
  122. }
  123. }
  124. }
  125. },
  126. _getHeadings: function(){
  127. // summary:
  128. // Function to load the attribute names from the table header so that the
  129. // attributes (cells in a row), can have a reasonable name.
  130. // For list items, returns single implicit heading, ["name"]
  131. this._headings = [];
  132. if(this._rootNode.tHead){
  133. array.forEach(this._rootNode.tHead.rows[0].cells, lang.hitch(this, function(th){
  134. var text = xmlParser.textContent(th);
  135. this._headings.push(this.trimWhitespace?lang.trim(text):text);
  136. }));
  137. }else{
  138. this._headings = ["name"];
  139. }
  140. },
  141. _getAllItems: function(){
  142. // summary:
  143. // Function to return all rows in the table as an array of items.
  144. var items = [];
  145. var i;
  146. if(this._rootNode.rows){//table
  147. for(i=0; i<this._rootNode.rows.length; i++){
  148. items.push(this._rootNode.rows[i]);
  149. }
  150. }else{ //list
  151. for(i=0; i<this._rootNode.childNodes.length; i++){
  152. if(this._rootNode.childNodes[i].nodeType === 1){
  153. items.push(this._rootNode.childNodes[i]);
  154. }
  155. }
  156. }
  157. return items; //array
  158. },
  159. _assertIsItem: function(/* item */ item){
  160. // summary:
  161. // This function tests whether the item passed in is indeed an item in the store.
  162. // item:
  163. // The item to test for being contained by the store.
  164. if(!this.isItem(item)){
  165. throw new Error("dojo.data.HtmlStore: a function was passed an item argument that was not an item");
  166. }
  167. },
  168. _assertIsAttribute: function(/* String */ attribute){
  169. // summary:
  170. // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
  171. // attribute:
  172. // The attribute to test for being contained by the store.
  173. //
  174. // returns:
  175. // Returns the index (column) that the attribute resides in the row.
  176. if(typeof attribute !== "string"){
  177. throw new Error("dojo.data.HtmlStore: a function was passed an attribute argument that was not an attribute name string");
  178. return -1;
  179. }
  180. return array.indexOf(this._headings, attribute); //int
  181. },
  182. /***************************************
  183. dojo.data.api.Read API
  184. ***************************************/
  185. getValue: function( /* item */ item,
  186. /* attribute-name-string */ attribute,
  187. /* value? */ defaultValue){
  188. // summary:
  189. // See dojo.data.api.Read.getValue()
  190. var values = this.getValues(item, attribute);
  191. return (values.length > 0)?values[0]:defaultValue; //Object || int || Boolean
  192. },
  193. getValues: function(/* item */ item,
  194. /* attribute-name-string */ attribute){
  195. // summary:
  196. // See dojo.data.api.Read.getValues()
  197. this._assertIsItem(item);
  198. var index = this._assertIsAttribute(attribute);
  199. if(index>-1){
  200. var text;
  201. if(item.cells){
  202. text = xmlParser.textContent(item.cells[index]);
  203. }else{//return Value for lists
  204. text = xmlParser.textContent(item);
  205. }
  206. return [this.trimWhitespace?lang.trim(text):text];
  207. }
  208. return []; //Array
  209. },
  210. getAttributes: function(/* item */ item){
  211. // summary:
  212. // See dojo.data.api.Read.getAttributes()
  213. this._assertIsItem(item);
  214. var attributes = [];
  215. for(var i=0; i<this._headings.length; i++){
  216. if(this.hasAttribute(item, this._headings[i]))
  217. attributes.push(this._headings[i]);
  218. }
  219. return attributes; //Array
  220. },
  221. hasAttribute: function( /* item */ item,
  222. /* attribute-name-string */ attribute){
  223. // summary:
  224. // See dojo.data.api.Read.hasAttribute()
  225. return this.getValues(item, attribute).length > 0;
  226. },
  227. containsValue: function(/* item */ item,
  228. /* attribute-name-string */ attribute,
  229. /* anything */ value){
  230. // summary:
  231. // See dojo.data.api.Read.containsValue()
  232. var regexp = undefined;
  233. if(typeof value === "string"){
  234. regexp = filter.patternToRegExp(value, false);
  235. }
  236. return this._containsValue(item, attribute, value, regexp); //boolean.
  237. },
  238. _containsValue: function( /* item */ item,
  239. /* attribute-name-string */ attribute,
  240. /* anything */ value,
  241. /* RegExp?*/ regexp){
  242. // summary:
  243. // Internal function for looking at the values contained by the item.
  244. // description:
  245. // Internal function for looking at the values contained by the item. This
  246. // function allows for denoting if the comparison should be case sensitive for
  247. // strings or not (for handling filtering cases where string case should not matter)
  248. //
  249. // item:
  250. // The data item to examine for attribute values.
  251. // attribute:
  252. // The attribute to inspect.
  253. // value:
  254. // The value to match.
  255. // regexp:
  256. // Optional regular expression generated off value if value was of string type to handle wildcarding.
  257. // If present and attribute values are string, then it can be used for comparison instead of 'value'
  258. var values = this.getValues(item, attribute);
  259. for(var i = 0; i < values.length; ++i){
  260. var possibleValue = values[i];
  261. if(typeof possibleValue === "string" && regexp){
  262. return (possibleValue.match(regexp) !== null);
  263. }else{
  264. //Non-string matching.
  265. if(value === possibleValue){
  266. return true; // Boolean
  267. }
  268. }
  269. }
  270. return false; // Boolean
  271. },
  272. isItem: function(/* anything */ something){
  273. // summary:
  274. // See dojo.data.api.Read.isItem()
  275. return something && dom.isDescendant(something, this._rootNode);
  276. },
  277. isItemLoaded: function(/* anything */ something){
  278. // summary:
  279. // See dojo.data.api.Read.isItemLoaded()
  280. return this.isItem(something);
  281. },
  282. loadItem: function(/* Object */ keywordArgs){
  283. // summary:
  284. // See dojo.data.api.Read.loadItem()
  285. this._assertIsItem(keywordArgs.item);
  286. },
  287. _fetchItems: function(request, fetchHandler, errorHandler){
  288. // summary:
  289. // Fetch items (XML elements) that match to a query
  290. // description:
  291. // If '_fetchUrl' is specified, it is used to load an XML document
  292. // with a query string.
  293. // Otherwise and if 'url' is specified, the XML document is
  294. // loaded and list XML elements that match to a query (set of element
  295. // names and their text attribute values that the items to contain).
  296. // A wildcard, "*" can be used to query values to match all
  297. // occurrences.
  298. // If '_rootItem' is specified, it is used to fetch items.
  299. // request:
  300. // A request object
  301. // fetchHandler:
  302. // A function to call for fetched items
  303. // errorHandler:
  304. // A function to call on error
  305. if(this._rootNode){
  306. this._finishFetchItems(request, fetchHandler, errorHandler);
  307. }else{
  308. if(!this.url){
  309. this._rootNode = dom.byId(this.dataId);
  310. this._indexItems();
  311. this._finishFetchItems(request, fetchHandler, errorHandler);
  312. }else{
  313. var getArgs = {
  314. url: this.url,
  315. handleAs: "text",
  316. preventCache: this.urlPreventCache
  317. };
  318. var self = this;
  319. var getHandler = xhr.get(getArgs);
  320. getHandler.addCallback(function(data){
  321. var findNode = function(node, id){
  322. if(node.id == id){
  323. return node; //object
  324. }
  325. if(node.childNodes){
  326. for(var i=0; i<node.childNodes.length; i++){
  327. var returnNode = findNode(node.childNodes[i], id);
  328. if(returnNode){
  329. return returnNode; //object
  330. }
  331. }
  332. }
  333. return null; //null
  334. }
  335. var d = document.createElement("div");
  336. d.innerHTML = data;
  337. self._rootNode = findNode(d, self.dataId);
  338. self._indexItems();
  339. self._finishFetchItems(request, fetchHandler, errorHandler);
  340. });
  341. getHandler.addErrback(function(error){
  342. errorHandler(error, request);
  343. });
  344. }
  345. }
  346. },
  347. _finishFetchItems: function(request, fetchHandler, errorHandler){
  348. // summary:
  349. // Internal function for processing the passed in request and locating the requested items.
  350. var items = [];
  351. var arrayOfAllItems = this._getAllItems();
  352. if(request.query){
  353. var ignoreCase = request.queryOptions ? request.queryOptions.ignoreCase : false;
  354. items = [];
  355. //See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the
  356. //same value for each item examined. Much more efficient.
  357. var regexpList = {};
  358. var key;
  359. var value;
  360. for(key in request.query){
  361. value = request.query[key]+'';
  362. if(typeof value === "string"){
  363. regexpList[key] = filter.patternToRegExp(value, ignoreCase);
  364. }
  365. }
  366. for(var i = 0; i < arrayOfAllItems.length; ++i){
  367. var match = true;
  368. var candidateItem = arrayOfAllItems[i];
  369. for(key in request.query){
  370. value = request.query[key]+'';
  371. if(!this._containsValue(candidateItem, key, value, regexpList[key])){
  372. match = false;
  373. }
  374. }
  375. if(match){
  376. items.push(candidateItem);
  377. }
  378. }
  379. fetchHandler(items, request);
  380. }else{
  381. // We want a copy to pass back in case the parent wishes to sort the array. We shouldn't allow resort
  382. // of the internal list so that multiple callers can get listsand sort without affecting each other.
  383. if(arrayOfAllItems.length> 0){
  384. items = arrayOfAllItems.slice(0,arrayOfAllItems.length);
  385. }
  386. fetchHandler(items, request);
  387. }
  388. },
  389. getFeatures: function(){
  390. // summary:
  391. // See dojo.data.api.Read.getFeatures()
  392. return {
  393. 'dojo.data.api.Read': true,
  394. 'dojo.data.api.Identity': true
  395. };
  396. },
  397. close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
  398. // summary:
  399. // See dojo.data.api.Read.close()
  400. // nothing to do here!
  401. },
  402. getLabel: function(/* item */ item){
  403. // summary:
  404. // See dojo.data.api.Read.getLabel()
  405. if(this.isItem(item)){
  406. if(item.cells){
  407. return "Item #" + this.getIdentity(item);
  408. }else{
  409. return this.getValue(item,"name");
  410. }
  411. }
  412. return undefined;
  413. },
  414. getLabelAttributes: function(/* item */ item){
  415. // summary:
  416. // See dojo.data.api.Read.getLabelAttributes()
  417. if(item.cells){
  418. return null;
  419. }else{
  420. return ["name"];
  421. }
  422. },
  423. /***************************************
  424. dojo.data.api.Identity API
  425. ***************************************/
  426. getIdentity: function(/* item */ item){
  427. // summary:
  428. // See dojo.data.api.Identity.getIdentity()
  429. this._assertIsItem(item);
  430. if(this.hasAttribute(item, "name")){
  431. return this.getValue(item,"name");
  432. }else{
  433. return item._ident;
  434. }
  435. },
  436. getIdentityAttributes: function(/* item */ item){
  437. // summary:
  438. // See dojo.data.api.Identity.getIdentityAttributes()
  439. //Identity isn't taken from a public attribute.
  440. return null;
  441. },
  442. fetchItemByIdentity: function(keywordArgs){
  443. // summary:
  444. // See dojo.data.api.Identity.fetchItemByIdentity()
  445. var identity = keywordArgs.identity;
  446. var self = this;
  447. var item = null;
  448. var scope = null;
  449. if(!this._rootNode){
  450. if(!this.url){
  451. this._rootNode = dom.byId(this.dataId);
  452. this._indexItems();
  453. if(self._rootNode.rows){ //Table
  454. item = this._rootNode.rows[identity + 1];
  455. }else{ //Lists
  456. for(var i = 0; i < self._rootNode.childNodes.length; i++){
  457. if(self._rootNode.childNodes[i].nodeType === 1 && identity === xmlParser.textContent(self._rootNode.childNodes[i])){
  458. item = self._rootNode.childNodes[i];
  459. }
  460. }
  461. }
  462. if(keywordArgs.onItem){
  463. scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
  464. keywordArgs.onItem.call(scope, item);
  465. }
  466. }else{
  467. var getArgs = {
  468. url: this.url,
  469. handleAs: "text"
  470. };
  471. var getHandler = xhr.get(getArgs);
  472. getHandler.addCallback(function(data){
  473. var findNode = function(node, id){
  474. if(node.id == id){
  475. return node; //object
  476. }
  477. if(node.childNodes){
  478. for(var i=0; i<node.childNodes.length; i++){
  479. var returnNode = findNode(node.childNodes[i], id);
  480. if(returnNode){
  481. return returnNode; //object
  482. }
  483. }
  484. }
  485. return null; //null
  486. }
  487. var d = document.createElement("div");
  488. d.innerHTML = data;
  489. self._rootNode = findNode(d, self.dataId);
  490. self._indexItems();
  491. if(self._rootNode.rows && identity <= self._rootNode.rows.length){ //Table
  492. item = self._rootNode.rows[identity-1];
  493. }else{ //List
  494. for(var i = 0; i < self._rootNode.childNodes.length; i++){
  495. if(self._rootNode.childNodes[i].nodeType === 1 && identity === xmlParser.textContent(self._rootNode.childNodes[i])){
  496. item = self._rootNode.childNodes[i];
  497. break;
  498. }
  499. }
  500. }
  501. if(keywordArgs.onItem){
  502. scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
  503. keywordArgs.onItem.call(scope, item);
  504. }
  505. });
  506. getHandler.addErrback(function(error){
  507. if(keywordArgs.onError){
  508. scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
  509. keywordArgs.onError.call(scope, error);
  510. }
  511. });
  512. }
  513. }else{
  514. if(this._rootNode.rows[identity+1]){
  515. item = this._rootNode.rows[identity+1];
  516. if(keywordArgs.onItem){
  517. scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
  518. keywordArgs.onItem.call(scope, item);
  519. }
  520. }
  521. }
  522. }
  523. });
  524. lang.extend(HtmlStore, simpleFetch);
  525. return HtmlStore;
  526. });