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.

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