PageRenderTime 49ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

/media/foundry/2.1/scripts_/textboxlist.js

https://bitbucket.org/pastor399/newcastleunifc
JavaScript | 815 lines | 453 code | 250 blank | 112 comment | 60 complexity | 8e8b2a1bba5377dc73f25d398cc76880 MD5 | raw file
  1. (function(){
  2. // module factory: start
  3. var moduleFactory = function($) {
  4. // module body: start
  5. var module = this;
  6. var exports = function() {
  7. // Constants
  8. var KEYCODE = {
  9. BACKSPACE: 8,
  10. COMMA: 188,
  11. DELETE: 46,
  12. DOWN: 40,
  13. ENTER: 13,
  14. ESCAPE: 27,
  15. LEFT: 37,
  16. RIGHT: 39,
  17. SPACE: 32,
  18. TAB: 9,
  19. UP: 38
  20. };
  21. // Templates
  22. $.template("textboxlist/item", '<div class="textboxlist-item" data-textboxlist-item><span class="textboxlist-itemContent" data-textboxlist-itemContent>[%== html %]</span><a class="textboxlist-itemRemoveButton" href="javascript: void(0);" data-textboxlist-itemRemoveButton></a></div>');
  23. $.template("textboxlist/itemContent", '[%= title %]<input type="hidden" name="items" value="[%= id %]"/>');
  24. $.template("textboxlist/itemContent", '[%= title %]<input type="hidden" name="items" value="[%= id %]"/>');
  25. $.Controller("TextboxList",
  26. {
  27. pluginName: "textboxlist",
  28. defaultOptions: {
  29. view: {
  30. item: 'textboxlist/item',
  31. itemContent: 'textboxlist/itemContent'
  32. },
  33. plugin: {},
  34. // Options
  35. unique: true,
  36. caseSensitive: false,
  37. max: null,
  38. // Events
  39. filterItem: null,
  40. "{item}" : "[data-textboxlist-item]",
  41. "{itemContent}" : "[data-textboxlist-itemContent]",
  42. "{itemRemoveButton}": "[data-textboxlist-itemRemoveButton]",
  43. "{textField}" : "[data-textboxlist-textField]"
  44. },
  45. hostname: "textboxList"
  46. },
  47. function(self) {
  48. return {
  49. init: function() {
  50. // Go through existing item
  51. // and reconstruct item data.
  52. self.item().each(function(){
  53. var item = $(this),
  54. itemContent = item.find(self.itemContent.selector);
  55. self.createItem({
  56. id: item.data("id") || (function(){
  57. var id = $.uid("item-");
  58. item.data("id", id);
  59. return id;
  60. })(),
  61. title: item.data("title") || $.trim(itemContent.text()),
  62. html: itemContent.html()
  63. });
  64. });
  65. // Determine if there's autocomplete
  66. if (self.options.plugin.autocomplete || self.element.data("query")) {
  67. self.addPlugin("autocomplete");
  68. }
  69. // Prevent form submission
  70. self.on("keypress", self.textField(), function(event){
  71. if (event.keyCode==KEYCODE.ENTER) return event.preventDefault();
  72. });
  73. },
  74. items: {},
  75. itemsByTitle: {},
  76. getItemKey: function(title){
  77. return (self.options.caseSensitive) ? title : title.toLowerCase();
  78. },
  79. filterItem: function(item) {
  80. var options = self.options;
  81. // Use custom filter if provided
  82. var filterItem = options.filterItem;
  83. if ($.isFunction(filterItem)) {
  84. item = filterItem.call(self, item);
  85. }
  86. var items = self.itemsByTitle,
  87. newItem = false;
  88. // If item is a string,
  89. if ($._.isString(item) && item!=="") {
  90. var title = item,
  91. key = self.getItemKey(title);
  92. item =
  93. (items.hasOwnProperty(key)) ?
  94. // Get existing item
  95. self.itemsByTitle[key] :
  96. // Or create a new one
  97. (function(){
  98. var item = {id: $.uid("item-"), title: title, key: self.getItemKey(title)};
  99. newItem = true;
  100. return item;
  101. })();
  102. }
  103. // If item content is not created, then make one.
  104. if (item.html===undefined) {
  105. item.html = self.view.itemContent(true, item);
  106. }
  107. // If items should be unique
  108. if (options.unique &&
  109. // and this item has already been added to the list
  110. (self.items.hasOwnProperty(item.id) ||
  111. // or item of the same title already exists
  112. (newItem && items.hasOwnProperty[item.key])
  113. )
  114. )
  115. {
  116. // Then don't create this item anymore
  117. return null;
  118. }
  119. return item;
  120. },
  121. createItem: function(item) {
  122. // Create key for item
  123. item.key = self.getItemKey(item.title);
  124. // Store to items object
  125. self.items[item.id] = item;
  126. // Store to itemsByTitle object
  127. self.itemsByTitle[item.key] = item;
  128. },
  129. deleteItem: function(id) {
  130. var item = self.items[id];
  131. // Remove from items object
  132. delete self.items[id];
  133. // Remove from itemsByTitle object
  134. var key = (self.options.caseSensitive) ? item.title : item.title.toLowerCase();
  135. delete self.itemsByTitle[key];
  136. },
  137. addItem: function(item) {
  138. // Don't add empty title
  139. if (item==="") return;
  140. var options = self.options;
  141. // If we reached the maximum number of items, skip.
  142. var max = options.max;
  143. if (max!==null && self.item().length>=max) return;
  144. // Filter item
  145. item = self.filterItem(item);
  146. // At this point, if item if not an object, skip.
  147. if (!$.isPlainObject(item)) return;
  148. self.createItem(item);
  149. // Add item on to the list
  150. self.view.item(item)
  151. .attr("data-id", item.id)
  152. .insertBefore(self.textField());
  153. self.trigger("addItem", [item]);
  154. console.log("SDFDS");
  155. return item;
  156. },
  157. removeItem: function(id) {
  158. var item = self.items[id];
  159. // Remove item from the list
  160. self.item("[data-id=" + id + "]")
  161. .remove();
  162. self.deleteItem(id);
  163. self.trigger("removeItem", [item]);
  164. },
  165. getAddedItems: function() {
  166. var items =
  167. self.item().map(function(){
  168. var item = $(this),
  169. id = item.data("id");
  170. return self.items[id];
  171. });
  172. return items;
  173. },
  174. "click": function() {
  175. self.textField().focus();
  176. },
  177. "{itemRemoveButton} click": function(button) {
  178. var item = button.parents(self.item.selector);
  179. self.removeItem(item.data("id"));
  180. },
  181. "{textField} keydown": function(textField, event)
  182. {
  183. var keyCode = event.keyCode;
  184. textField.data("realEnterKey", keyCode==KEYCODE.ENTER);
  185. var textFieldKeydown = self.options.textFieldKeydown;
  186. $.isFunction(textFieldKeydown) && textFieldKeydown.call(self, textField, event);
  187. },
  188. "{textField} keypress": function(textField, event)
  189. {
  190. var keydownIsEnter = textField.data("realEnterKey"),
  191. // When a person enters the IME context menu,
  192. // the keyCode returned during keypress will
  193. // not be the enter keycode.
  194. keypressIsEnter = event.keyCode==KEYCODE.ENTER;
  195. textField.data("realEnterKey", keydownIsEnter && keypressIsEnter);
  196. var item = $.trim(self.textField().val());
  197. // Trigger custom event
  198. var textFieldKeypress = self.options.textFieldKeypress;
  199. if ($.isFunction(textFieldKeypress)) {
  200. item = textFieldKeypress.call(self, textField, event, item);
  201. }
  202. // If item was converted into a null object,
  203. // this means the custom keyup event wants to "preventDefault".
  204. if (item===undefined || item===null) return;
  205. switch (event.keyCode) {
  206. // Add new item
  207. case KEYCODE.ENTER:
  208. if (textField.data("realEnterKey")) {
  209. self.addItem(item);
  210. // and clear text field.
  211. textField.val("");
  212. }
  213. break;
  214. }
  215. },
  216. "{textField} keyup": function(textField, event)
  217. {
  218. var item = $.trim(self.textField().val());
  219. // Trigger custom event if exists
  220. var textFieldKeyup = self.options.textFieldKeyup;
  221. if ($.isFunction(textFieldKeyup)) {
  222. item = textFieldKeyup.call(self, textField, event, item);
  223. }
  224. // If item was converted into a null object,
  225. // this means the custom keyup event wants to "preventDefault".
  226. if (item===undefined || item===null) return;
  227. // Optimization for compiler
  228. var canRemoveItemUsingBackspace = "canRemoveItemUsingBackspace";
  229. switch (event.keyCode) {
  230. // Remove last added item
  231. case KEYCODE.BACKSPACE:
  232. // If the text field is empty
  233. if (item==="") {
  234. // If this is the first time pressing the backspace key
  235. if (!self[canRemoveItemUsingBackspace]) {
  236. // Allow removal of item for subsequent backspace
  237. self[canRemoveItemUsingBackspace] = true;
  238. // If this is the subsequent time pressing the backspace key
  239. } else {
  240. // Look for the item before it
  241. var prevItem = textField.prev(self.item.selector);
  242. // If the item before it exists,
  243. if (prevItem.length > 0) {
  244. // Remove the item.
  245. self.removeItem(prevItem.data("id"));
  246. }
  247. }
  248. }
  249. break;
  250. default:
  251. // Reset backspace removal state
  252. self[canRemoveItemUsingBackspace] = false;
  253. break;
  254. }
  255. }
  256. }}
  257. );
  258. $(document)
  259. .on('click.textboxlist.data-api', '[data-textboxlist]', function(event){
  260. $(this).addController($.Controller.TextboxList).textField().focus();
  261. })
  262. .on('focus.textboxlist.data-api', '[data-textboxlist] [data-textboxlist-textField]', function(event){
  263. $(this).parents("[data-textboxlist]").addController($.Controller.TextboxList);
  264. });
  265. // Textboxlist ends
  266. // Textboxlist.autocomplete start
  267. $.module('textboxlist/autocomplete', function(){
  268. var module = this;
  269. $.template("textboxlist/menu", '<div class="textboxlist-autocomplete" data-textboxlist-autocomplete><div class="textboxlist-autocomplete-inner"><ul class="textboxlist-menu" data-textboxlist-menu></ul></div></div>');
  270. $.template("textboxlist/menuItem", '<li class="textboxlist-menuItem" data-textboxlist-menuItem>[%== html %]</li>');
  271. $.Controller("TextboxList.Autocomplete",
  272. {
  273. defaultOptions: {
  274. view: {
  275. menu: "textboxlist/menu",
  276. menuItem: "textboxlist/menuItem"
  277. },
  278. minLength: 1,
  279. limit: 10,
  280. highlight: true,
  281. caseSensitive: false,
  282. exclusive: false,
  283. // Accepts url, function or array of objects.
  284. // If function, it should return a deferred object.
  285. query: null,
  286. position: {
  287. my: 'left top',
  288. at: 'left bottom',
  289. collision: 'none'
  290. },
  291. filterItem: null,
  292. "{menu}": "[data-textboxlist-menu]",
  293. "{menuItem}": "[data-textboxlist-menuItem]"
  294. }
  295. },
  296. function(self) { return {
  297. init: function() {
  298. // Destroy controller
  299. if (!self.element.data(self.Class.fullName)) {
  300. self.destroy();
  301. // And reimplement on the context menu we created ourselves
  302. self.view.menu()
  303. .appendTo("body")
  304. .data(self.Class.fullName, true)
  305. .addController(self.Class, self.options);
  306. return;
  307. }
  308. // Bind to the keyup event
  309. self.textboxList.update({
  310. textFieldKeypress: self.textFieldKeypress,
  311. textFieldKeyup: self.textFieldKeyup
  312. });
  313. // Set the position to be relative to the textboxList
  314. self.options.position.of = self.textboxList.element;
  315. self.initQuery();
  316. },
  317. initQuery: function() {
  318. // Determine query method
  319. var query = self.options.query || self.textboxList.element.data("query");
  320. // TODO: Wrap up query options and pass to query URL & query function.
  321. // Query URL
  322. if ($.isUrl(query)) {
  323. var url = query;
  324. self.query = function(keyword){
  325. return $.ajax(url + keyword);
  326. }
  327. return;
  328. }
  329. // Query function
  330. if ($.isFunction(query)) {
  331. var func = query;
  332. self.query = function(keyword) {
  333. return func.call(self, keyword);
  334. }
  335. return;
  336. }
  337. // Query dataset
  338. if ($.isArray(query)) {
  339. var dataset = query;
  340. self.query = function(keyword) {
  341. var task = $.Deferred(),
  342. keyword = keyword.toLowerCase();
  343. // Fork this process
  344. // so it won't choke on large dataset.
  345. setTimeout(function(){
  346. var result = $.grep(dataset, function(item){
  347. return item.title.toLowerCase().indexOf(keyword) > -1;
  348. });
  349. task.resolve(result);
  350. }, 0);
  351. return task;
  352. }
  353. return;
  354. }
  355. },
  356. show: function() {
  357. var textboxList = self.textboxList.element;
  358. self.element
  359. .show()
  360. .css({
  361. width: textboxList.outerWidth()
  362. })
  363. .position(self.options.position);
  364. self.hidden = false;
  365. },
  366. hide: function() {
  367. self.element.hide();
  368. self.menuItem().removeClass("active");
  369. self.render.reset();
  370. self.hidden = true;
  371. },
  372. queries: {},
  373. populated: false,
  374. populate: function(keyword) {
  375. self.populated = false;
  376. var options = self.options,
  377. key = (options.caseSensitive) ? keyword : keyword.toLowerCase(),
  378. query = self.queries[key];
  379. var newQuery = !$.isDeferred(query),
  380. runQuery = function(){
  381. // Query the keyword if:
  382. // - The query hasn't been made.
  383. // - The query has been rejected.
  384. if (newQuery || (!newQuery && query.state()=="rejected")) {
  385. query = self.queries[key] = self.query(keyword);
  386. }
  387. // When query is done, render items;
  388. query
  389. .done(
  390. self.render(function(items){
  391. return [items, keyword];
  392. })
  393. )
  394. .fail(function(){
  395. self.hide();
  396. });
  397. }
  398. // If this is a new query
  399. if (newQuery) {
  400. // Don't run until we are sure that the user is finished typing
  401. clearTimeout(self.queryTask);
  402. self.queryTask = setTimeout(runQuery, 250);
  403. // Else run it immediately
  404. } else {
  405. runQuery();
  406. }
  407. },
  408. render: $.Enqueue(function(items, keyword){
  409. if (!$.isArray(items)) return;
  410. // If there are no items, hide menu.
  411. if (items.length < 1) {
  412. self.hide();
  413. return;
  414. }
  415. var menu = self.menu();
  416. if (menu.data("keyword")!==keyword)
  417. {
  418. // Clear out menu items
  419. menu.empty();
  420. $.each(items, function(i, item){
  421. var filterItem = self.options.filterItem;
  422. if ($.isFunction(filterItem)) {
  423. item = filterItem.call(self, item, keyword);
  424. }
  425. // If the item is not an object, stop.
  426. if (!$.isPlainObject(item)) return;
  427. var html = item.menuHtml || item.title;
  428. self.view.menuItem({html: html})
  429. .data("item", item)
  430. .appendTo(menu);
  431. });
  432. menu.data("keyword", keyword);
  433. }
  434. self.show();
  435. }),
  436. getActiveMenuItem: function() {
  437. var activeMenuItem = self.menuItem(".active");
  438. if (activeMenuItem.length < 1) {
  439. activeMenuItem = undefined;
  440. }
  441. return activeMenuItem;
  442. },
  443. textFieldKeypress: function(textField, event, keyword) {
  444. var onlyFromSuggestions = self.options.exclusive;
  445. // If menu is not visible, stop.
  446. if (self.hidden) {
  447. // If we only accept suggested items,
  448. // don't let textboxlist add the keyword
  449. // by returning null.
  450. return (onlyFromSuggestions) ? null : keyword;
  451. }
  452. // Get active menu item
  453. var activeMenuItem = self.getActiveMenuItem();
  454. switch (event.keyCode) {
  455. // If up key is pressed
  456. case KEYCODE.UP:
  457. // Deactivate all menu item
  458. self.menuItem().removeClass("active");
  459. // If no menu items are activated,
  460. if (!activeMenuItem) {
  461. // activate the last one.
  462. self.menuItem(":last").addClass("active");
  463. // Else find the menu item before it,
  464. } else {
  465. // and activate it.
  466. activeMenuItem.prev(self.menuItem.selector)
  467. .addClass("active");
  468. }
  469. break;
  470. // If down key is pressed
  471. case KEYCODE.DOWN:
  472. // Deactivate all menu item
  473. self.menuItem().removeClass("active");
  474. // If no menu items are activated,
  475. if (!activeMenuItem) {
  476. // activate the first one.
  477. self.menuItem(":first").addClass("active");
  478. // Else find the menu item after it,
  479. } else {
  480. // and activate it.
  481. activeMenuItem.next(self.menuItem.selector)
  482. .addClass("active");
  483. }
  484. break;
  485. // If enter is pressed
  486. case KEYCODE.ENTER:
  487. // Get activated item.
  488. var activeMenuItem = self.getActiveMenuItem();
  489. // Hide the menu
  490. self.hide();
  491. // If there is an activated item,
  492. if (activeMenuItem) {
  493. // get the item data,
  494. var item = activeMenuItem.data("item");
  495. // and return the item data to the textboxlist.
  496. return item;
  497. } else if (onlyFromSuggestions) {
  498. return null;
  499. }
  500. break;
  501. // If escape is pressed,
  502. case KEYCODE.ESCAPE:
  503. // hide menu.
  504. self.hide();
  505. break;
  506. }
  507. return (onlyFromSuggestions) ? null : keyword;
  508. },
  509. textFieldKeyup: function(textField, event, keyword) {
  510. var onlyFromSuggestions = self.options.exclusive;
  511. switch (event.keyCode) {
  512. case KEYCODE.UP:
  513. case KEYCODE.DOWN:
  514. case KEYCODE.ENTER:
  515. case KEYCODE.ESCAPE:
  516. // Don't repopulate if these keys were pressed.
  517. break;
  518. default:
  519. // If no keyword given or keyword doesn't meet minimum query length, stop.
  520. if (keyword==="" || (keyword.length < self.options.minLength)) {
  521. self.hide();
  522. // Else populate suggestions.
  523. } else {
  524. self.populate(keyword);
  525. }
  526. break;
  527. }
  528. return (onlyFromSuggestions) ? null : keyword;
  529. },
  530. "{menuItem} click": function(menuItem) {
  531. // Hide context menu
  532. self.hide();
  533. // Add item
  534. var item = menuItem.data("item");
  535. self.textboxList.addItem(item);
  536. // Get text field & clear text field
  537. var textField = self.textboxList.textField().val("");
  538. // Refocus text field
  539. setTimeout(function(){
  540. // Due to event delegation, this needs to be slightly delayed.
  541. textField.focus();
  542. }, 150);
  543. },
  544. "{menuItem} mouseover": function(menuItem) {
  545. self.menuItem().removeClass("active");
  546. menuItem.addClass("active");
  547. },
  548. "{menuItem} mouseout": function(menuItem) {
  549. self.menuItem().removeClass("active");
  550. }
  551. }}
  552. );
  553. module.resolve($.Controller.TextboxList.Autocomplete);
  554. });
  555. // Autocomplete ends
  556. };
  557. exports();
  558. module.resolveWith(exports);
  559. // module body: end
  560. };
  561. // module factory: end
  562. dispatch("textboxlist")
  563. .containing(moduleFactory)
  564. .to("Foundry/2.1 Modules");
  565. }());