You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

618 lines
17 KiB

  1. /* ******************************
  2. Semantic Module: Checkbox
  3. Author: Jack Lukic
  4. Notes: First Commit MAy 25, 2013
  5. Simple plug-in which maintains the state for ui dropdown
  6. ****************************** */
  7. ;(function ( $, window, document, undefined ) {
  8. $.fn.dropdown = function(parameters) {
  9. var
  10. $allModules = $(this),
  11. $document = $(document),
  12. settings = ( $.isPlainObject(parameters) )
  13. ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
  14. : $.fn.dropdown.settings,
  15. eventNamespace = '.' + settings.namespace,
  16. moduleNamespace = 'module-' + settings.namespace,
  17. moduleSelector = $allModules.selector || '',
  18. time = new Date().getTime(),
  19. performance = [],
  20. query = arguments[0],
  21. methodInvoked = (typeof query == 'string'),
  22. queryArguments = [].slice.call(arguments, 1),
  23. invokedResponse,
  24. allModules
  25. ;
  26. $allModules
  27. .each(function() {
  28. var
  29. $module = $(this),
  30. $menu = $(this).find(settings.selector.menu),
  31. $item = $(this).find(settings.selector.item),
  32. $text = $(this).find(settings.selector.text),
  33. $input = $(this).find(settings.selector.input),
  34. isTouchDevice = ('ontouchstart' in document.documentElement),
  35. selector = $module.selector || '',
  36. element = this,
  37. instance = $module.data('module-' + settings.namespace),
  38. className = settings.className,
  39. metadata = settings.metadata,
  40. namespace = settings.namespace,
  41. animation = settings.animation,
  42. errors = settings.errors,
  43. module
  44. ;
  45. module = {
  46. initialize: function() {
  47. module.verbose('Initializing dropdown with bound events', $module);
  48. if(isTouchDevice) {
  49. $module
  50. .on('touchstart' + eventNamespace, module.event.test.toggle)
  51. ;
  52. }
  53. else if(settings.on == 'click') {
  54. $module
  55. .on('click' + eventNamespace, module.event.test.toggle)
  56. ;
  57. }
  58. else if(settings.on == 'hover') {
  59. $module
  60. .on('mouseenter' + eventNamespace, module.delay.show)
  61. .on('mouseleave' + eventNamespace, module.delay.hide)
  62. ;
  63. }
  64. else {
  65. $module
  66. .on(settings.on + eventNamespace, module.toggle)
  67. ;
  68. }
  69. if(settings.action == 'form') {
  70. module.set.selected();
  71. }
  72. $item
  73. .on(module.get.selectEvent() + eventNamespace, module.event.item.click)
  74. ;
  75. $module
  76. .data(moduleNamespace, module)
  77. ;
  78. },
  79. destroy: function() {
  80. module.verbose('Destroying previous module for', $module);
  81. $module
  82. .off(namespace)
  83. ;
  84. },
  85. event: {
  86. stopPropagation: function(event) {
  87. event.stopPropagation();
  88. },
  89. test: {
  90. toggle: function(event) {
  91. module.intent.test(event, module.toggle);
  92. event.stopPropagation();
  93. },
  94. hide: function(event) {
  95. module.intent.test(event, module.hide);
  96. event.stopPropagation();
  97. }
  98. },
  99. item: {
  100. click: function (event) {
  101. var
  102. $choice = $(this),
  103. text = $choice.data(metadata.text) || $choice.text(),
  104. value = $choice.data(metadata.value) || text
  105. ;
  106. module.verbose('Adding active state to selected item');
  107. $item
  108. .removeClass(className.active)
  109. ;
  110. $choice
  111. .addClass(className.active)
  112. ;
  113. module.action.determine(text, value);
  114. $.proxy(settings.onChange, $module.get())(value, text);
  115. event.stopPropagation();
  116. }
  117. },
  118. resetStyle: function() {
  119. $(this).removeAttr('style');
  120. }
  121. },
  122. intent: {
  123. test: function(event, callback) {
  124. module.debug('Determining whether event occurred in dropdown', event.target);
  125. callback = callback || function(){};
  126. if( $(event.target).closest($menu).size() === 0 ) {
  127. module.verbose('Triggering event', callback);
  128. callback();
  129. }
  130. else {
  131. module.verbose('Event occurred in dropdown, canceling callback');
  132. }
  133. },
  134. bind: function() {
  135. module.verbose('Binding hide intent event to document');
  136. $document
  137. .on(module.get.selectEvent(), module.event.test.hide)
  138. ;
  139. },
  140. unbind: function() {
  141. module.verbose('Removing hide intent event from document');
  142. $document
  143. .off(module.get.selectEvent())
  144. ;
  145. }
  146. },
  147. action: {
  148. determine: function(text, value) {
  149. if( $.isFunction( module.action[settings.action] ) ) {
  150. module.verbose('Triggering preset action', settings.action);
  151. module.action[ settings.action ](text, value);
  152. }
  153. else if( $.isFunction(settings.action) ) {
  154. module.verbose('Triggering user action', settings.action);
  155. settings.action(text, value);
  156. }
  157. else {
  158. module.error(errors.action);
  159. }
  160. },
  161. none: function() {},
  162. hide: function() {
  163. module.hide();
  164. },
  165. changeText: function(text, value) {
  166. module.set.text(text);
  167. module.hide();
  168. },
  169. form: function(text, value) {
  170. module.set.text(text);
  171. module.set.value(value);
  172. module.hide();
  173. }
  174. },
  175. get: {
  176. selectEvent: function() {
  177. return (isTouchDevice)
  178. ? 'touchstart'
  179. : 'click'
  180. ;
  181. },
  182. text: function() {
  183. return $text.text();
  184. },
  185. value: function() {
  186. return $input.val();
  187. },
  188. item: function(value) {
  189. var
  190. $selectedItem
  191. ;
  192. value = value || $input.val();
  193. $item
  194. .each(function() {
  195. if( $(this).data(metadata.value) == value ) {
  196. $selectedItem = $(this);
  197. }
  198. })
  199. ;
  200. return $selectedItem || false;
  201. }
  202. },
  203. set: {
  204. text: function(text) {
  205. module.debug('Changing text', text, $text);
  206. $text.removeClass(className.placeholder);
  207. $text.text(text);
  208. },
  209. value: function(value) {
  210. module.debug('Adding selected value to hidden input', value, $input);
  211. $input.val(value);
  212. },
  213. selected: function(value) {
  214. var
  215. selectedValue = value || $input.val(),
  216. $selectedItem = module.get.item(value),
  217. selectedText
  218. ;
  219. if($selectedItem) {
  220. module.debug('Setting selected menu item to', $selectedItem);
  221. selectedText = $selectedItem.data(metadata.text) || $selectedItem.text();
  222. $item
  223. .removeClass(className.active)
  224. ;
  225. $selectedItem
  226. .addClass(className.active)
  227. ;
  228. module.set.text(selectedText);
  229. }
  230. }
  231. },
  232. is: {
  233. visible: function() {
  234. return $menu.is(':visible');
  235. },
  236. hidden: function() {
  237. return $menu.is(':not(:visible)');
  238. }
  239. },
  240. can: {
  241. click: function() {
  242. return (isTouchDevice || settings.on == 'click');
  243. },
  244. show: function() {
  245. return !$module.hasClass(className.disabled);
  246. }
  247. },
  248. animate: {
  249. show: function() {
  250. module.verbose('Doing menu showing animation');
  251. if(animation.show == 'none') {
  252. $menu
  253. .show()
  254. ;
  255. }
  256. else if(animation.show == 'fade') {
  257. $menu
  258. .clearQueue()
  259. .fadeIn(150)
  260. ;
  261. }
  262. else if(animation.show == 'slide') {
  263. $menu
  264. .hide()
  265. .clearQueue()
  266. .children()
  267. .clearQueue()
  268. .css('opacity', 0)
  269. .delay(50)
  270. .animate({
  271. opacity : 1
  272. }, 200, 'easeOutQuad', module.event.resetStyle)
  273. .end()
  274. .slideDown(100, 'easeOutQuad', module.event.resetStyle)
  275. ;
  276. }
  277. else {
  278. module.error(errors.animation);
  279. }
  280. },
  281. hide: function() {
  282. module.verbose('Doing menu hiding animation');
  283. if(animation.hide == 'none') {
  284. $menu
  285. .hide()
  286. ;
  287. }
  288. else if(animation.hide == 'fade') {
  289. $menu
  290. .clearQueue()
  291. .fadeOut(150)
  292. ;
  293. }
  294. else if(animation.hide == 'slide') {
  295. $menu
  296. .show()
  297. .clearQueue()
  298. .children()
  299. .clearQueue()
  300. .css('opacity', 1)
  301. .animate({
  302. opacity : 0
  303. }, 100, 'easeOutQuad', module.event.resetStyle)
  304. .end()
  305. .delay(50)
  306. .slideUp(100, 'easeOutQuad', module.event.resetStyle)
  307. ;
  308. }
  309. else {
  310. module.error(errors.animation);
  311. }
  312. }
  313. },
  314. show: function() {
  315. module.debug('Checking if dropdown can show');
  316. if( !module.is.visible() ) {
  317. module.hideOthers();
  318. $module
  319. .addClass(className.visible)
  320. ;
  321. module.animate.show();
  322. if( module.can.click() ) {
  323. module.intent.bind();
  324. }
  325. $.proxy(settings.onShow, $module.get() )();
  326. }
  327. },
  328. hide: function() {
  329. if( !module.is.hidden() ) {
  330. module.debug('Hiding dropdown');
  331. $module
  332. .removeClass(className.visible)
  333. ;
  334. if( module.can.click() ) {
  335. module.intent.unbind();
  336. }
  337. module.animate.hide();
  338. $.proxy(settings.onHide, $module.get() )();
  339. }
  340. },
  341. delay: {
  342. show: function() {
  343. module.verbose('Delaying show event to ensure user intent');
  344. clearTimeout(module.graceTimer);
  345. module.graceTimer = setTimeout(module.show, settings.delay.show);
  346. },
  347. hide: function() {
  348. module.verbose('Delaying hide event to ensure user intent');
  349. clearTimeout(module.graceTimer);
  350. module.graceTimer = setTimeout(module.hide, settings.delay.hide);
  351. }
  352. },
  353. hideOthers: function() {
  354. module.verbose('Finding other dropdowns to hide');
  355. $allModules
  356. .not($module)
  357. .has(settings.selector.menu + ':visible')
  358. .dropdown('hide')
  359. ;
  360. },
  361. toggle: function() {
  362. module.verbose('Toggling menu visibility');
  363. if( module.is.hidden() ) {
  364. module.show();
  365. }
  366. else {
  367. module.hide();
  368. }
  369. },
  370. setting: function(name, value) {
  371. if(value !== undefined) {
  372. if( $.isPlainObject(name) ) {
  373. $.extend(true, settings, name);
  374. }
  375. else {
  376. settings[name] = value;
  377. }
  378. }
  379. else {
  380. return settings[name];
  381. }
  382. },
  383. internal: function(name, value) {
  384. if(value !== undefined) {
  385. if( $.isPlainObject(name) ) {
  386. $.extend(true, module, name);
  387. }
  388. else {
  389. module[name] = value;
  390. }
  391. }
  392. else {
  393. return module[name];
  394. }
  395. },
  396. debug: function() {
  397. if(settings.debug) {
  398. if(settings.performance) {
  399. module.performance.log(arguments);
  400. }
  401. else {
  402. module.debug = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  403. }
  404. }
  405. },
  406. verbose: function() {
  407. if(settings.verbose && settings.debug) {
  408. if(settings.performance) {
  409. module.performance.log(arguments);
  410. }
  411. else {
  412. module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  413. }
  414. }
  415. },
  416. error: function() {
  417. module.error = Function.prototype.bind.call(console.log, console, settings.moduleName + ':');
  418. },
  419. performance: {
  420. log: function(message) {
  421. var
  422. currentTime,
  423. executionTime,
  424. previousTime
  425. ;
  426. if(settings.performance) {
  427. currentTime = new Date().getTime();
  428. previousTime = time || currentTime,
  429. executionTime = currentTime - previousTime;
  430. time = currentTime;
  431. performance.push({
  432. 'Element' : element,
  433. 'Name' : message[0],
  434. 'Arguments' : message[1] || '',
  435. 'Execution Time' : executionTime
  436. });
  437. }
  438. clearTimeout(module.performance.timer);
  439. module.performance.timer = setTimeout(module.performance.display, 100);
  440. },
  441. display: function() {
  442. var
  443. title = settings.moduleName + ':',
  444. totalTime = 0
  445. ;
  446. time = false;
  447. $.each(performance, function(index, data) {
  448. totalTime += data['Execution Time'];
  449. });
  450. title += ' ' + totalTime + 'ms';
  451. if(moduleSelector) {
  452. title += ' \'' + moduleSelector + '\'';
  453. }
  454. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  455. console.groupCollapsed(title);
  456. if(console.table) {
  457. console.table(performance);
  458. }
  459. else {
  460. $.each(performance, function(index, data) {
  461. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  462. });
  463. }
  464. console.groupEnd();
  465. }
  466. performance = [];
  467. }
  468. },
  469. invoke: function(query, passedArguments, context) {
  470. var
  471. maxDepth,
  472. found
  473. ;
  474. passedArguments = passedArguments || queryArguments;
  475. context = element || context;
  476. if(typeof query == 'string' && instance !== undefined) {
  477. query = query.split('.');
  478. maxDepth = query.length - 1;
  479. $.each(query, function(depth, value) {
  480. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  481. instance = instance[value];
  482. }
  483. else if( instance[value] !== undefined ) {
  484. found = instance[value];
  485. }
  486. else {
  487. module.error(errors.method);
  488. }
  489. });
  490. }
  491. if ( $.isFunction( found ) ) {
  492. instance.verbose('Executing invoked function', found);
  493. return found.apply(context, passedArguments);
  494. }
  495. return found || false;
  496. }
  497. };
  498. if(methodInvoked) {
  499. if(instance === undefined) {
  500. module.initialize();
  501. }
  502. invokedResponse = module.invoke(query);
  503. }
  504. else {
  505. if(instance !== undefined) {
  506. module.destroy();
  507. }
  508. module.initialize();
  509. }
  510. })
  511. ;
  512. return (invokedResponse)
  513. ? invokedResponse
  514. : this
  515. ;
  516. };
  517. $.fn.dropdown.settings = {
  518. moduleName : 'Dropdown',
  519. namespace : 'dropdown',
  520. verbose : true,
  521. debug : true,
  522. performance : true,
  523. on : 'click',
  524. action : 'hide',
  525. delay: {
  526. show: 50,
  527. hide: 300
  528. },
  529. animation : {
  530. show: 'slide',
  531. hide: 'slide'
  532. },
  533. onChange : function(){},
  534. onShow : function(){},
  535. onHide : function(){},
  536. errors : {
  537. action : 'You called a dropdown action that was not defined',
  538. method : 'The method you called is not defined.',
  539. animation : 'The requested animation was not found'
  540. },
  541. metadata: {
  542. text : 'text',
  543. value : 'value'
  544. },
  545. selector : {
  546. menu : '.menu',
  547. item : '.menu > .item',
  548. text : '> .text',
  549. input : '> input[type="hidden"]'
  550. },
  551. className : {
  552. active : 'active',
  553. placeholder : 'default',
  554. disabled : 'disabled',
  555. visible : 'visible'
  556. }
  557. };
  558. })( jQuery, window , document );