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.

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