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.

718 lines
21 KiB

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