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.

709 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. 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( $.isFunction( module[settings.action] ) ) {
  166. module.verbose('Triggering preset action', settings.action);
  167. module[ settings.action ](text, value);
  168. }
  169. else if( $.isFunction(settings.action) ) {
  170. module.verbose('Triggering user action', settings.action);
  171. settings.action(text, value);
  172. }
  173. else {
  174. module.error(error.action);
  175. }
  176. },
  177. intent: function(event, callback) {
  178. module.debug('Determining whether event occurred in dropdown', event.target);
  179. callback = callback || function(){};
  180. if( $(event.target).closest($menu).size() === 0 ) {
  181. module.verbose('Triggering event', callback);
  182. callback();
  183. }
  184. else {
  185. module.verbose('Event occurred in dropdown, canceling callback');
  186. }
  187. }
  188. },
  189. bind: {
  190. intent: function() {
  191. module.verbose('Binding hide intent event to document');
  192. $document
  193. .on(module.get.selectEvent(), module.event.test.hide)
  194. ;
  195. }
  196. },
  197. unbind: {
  198. intent: function() {
  199. module.verbose('Removing hide intent event from document');
  200. $document
  201. .off(module.get.selectEvent())
  202. ;
  203. }
  204. },
  205. nothing: function() {},
  206. changeText: function(text, value) {
  207. module.set.text(text);
  208. module.hide();
  209. },
  210. updateForm: function(text, value) {
  211. module.set.text(text);
  212. module.set.value(value);
  213. module.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. module.debug('Changing text', text, $text);
  246. $text.removeClass(className.placeholder);
  247. $text.text(text);
  248. },
  249. value: function(value) {
  250. module.debug('Adding selected value to hidden input', value, $input);
  251. $input.val(value);
  252. },
  253. active: function() {
  254. $module.addClass(className.active);
  255. },
  256. visible: function() {
  257. $module.addClass(className.visible);
  258. },
  259. selected: function(value) {
  260. var
  261. $selectedItem = module.get.item(value),
  262. selectedText
  263. ;
  264. if($selectedItem) {
  265. module.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. module.set.text(selectedText);
  274. }
  275. }
  276. },
  277. remove: {
  278. active: function() {
  279. $module.removeClass(className.active);
  280. },
  281. visible: function() {
  282. $module.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 !$module.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( module.is.hidden($currentMenu) ) {
  314. module.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', module.event.resetStyle)
  337. .end()
  338. .slideDown(100, 'easeOutQuad', function() {
  339. $.proxy(module.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(module.event.resetStyle, this)();
  350. callback();
  351. })
  352. ;
  353. }
  354. else {
  355. module.error(error.transition);
  356. }
  357. }
  358. },
  359. hide: function(callback, $subMenu) {
  360. var
  361. $currentMenu = $subMenu || $menu
  362. ;
  363. callback = callback || function(){};
  364. if(module.is.visible($currentMenu) ) {
  365. module.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', module.event.resetStyle)
  387. .end()
  388. .delay(50)
  389. .slideUp(100, 'easeOutQuad', function() {
  390. $.proxy(module.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(module.event.resetStyle, this)();
  401. callback();
  402. })
  403. ;
  404. }
  405. else {
  406. module.error(error.transition);
  407. }
  408. }
  409. }
  410. },
  411. show: function() {
  412. module.debug('Checking if dropdown can show');
  413. if( module.is.hidden() ) {
  414. module.hideOthers();
  415. module.set.active();
  416. module.animate.show(module.set.visible);
  417. if( module.can.click() ) {
  418. module.bind.intent();
  419. }
  420. $.proxy(settings.onShow, element)();
  421. }
  422. },
  423. hide: function() {
  424. if( module.is.visible() ) {
  425. module.debug('Hiding dropdown');
  426. if( module.can.click() ) {
  427. module.unbind.intent();
  428. }
  429. module.remove.active();
  430. module.animate.hide(module.remove.visible);
  431. $.proxy(settings.onHide, element)();
  432. }
  433. },
  434. delay: {
  435. show: function() {
  436. module.verbose('Delaying show event to ensure user intent');
  437. clearTimeout(module.timer);
  438. module.timer = setTimeout(module.show, settings.delay.show);
  439. },
  440. hide: function() {
  441. module.verbose('Delaying hide event to ensure user intent');
  442. clearTimeout(module.timer);
  443. module.timer = setTimeout(module.hide, settings.delay.hide);
  444. }
  445. },
  446. hideOthers: function() {
  447. module.verbose('Finding other dropdowns to hide');
  448. $allModules
  449. .not($module)
  450. .has(selector.menu + ':visible')
  451. .dropdown('hide')
  452. ;
  453. },
  454. toggle: function() {
  455. module.verbose('Toggling menu visibility');
  456. if( module.is.hidden() ) {
  457. module.show();
  458. }
  459. else {
  460. module.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, module, name);
  480. }
  481. else {
  482. module[name] = value;
  483. }
  484. }
  485. else {
  486. return module[name];
  487. }
  488. },
  489. debug: function() {
  490. if(settings.debug) {
  491. if(settings.performance) {
  492. module.performance.log(arguments);
  493. }
  494. else {
  495. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  496. module.debug.apply(console, arguments);
  497. }
  498. }
  499. },
  500. verbose: function() {
  501. if(settings.verbose && settings.debug) {
  502. if(settings.performance) {
  503. module.performance.log(arguments);
  504. }
  505. else {
  506. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  507. module.verbose.apply(console, arguments);
  508. }
  509. }
  510. },
  511. error: function() {
  512. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  513. module.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(module.performance.timer);
  535. module.performance.timer = setTimeout(module.performance.display, 100);
  536. },
  537. display: function() {
  538. var
  539. title = settings.name + ':',
  540. totalTime = 0
  541. ;
  542. time = false;
  543. clearTimeout(module.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. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  552. console.groupCollapsed(title);
  553. if(console.table) {
  554. console.table(performance);
  555. }
  556. else {
  557. $.each(performance, function(index, data) {
  558. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  559. });
  560. }
  561. console.groupEnd();
  562. }
  563. performance = [];
  564. }
  565. },
  566. invoke: function(query, passedArguments, context) {
  567. var
  568. maxDepth,
  569. found
  570. ;
  571. passedArguments = passedArguments || queryArguments;
  572. context = element || context;
  573. if(typeof query == 'string' && instance !== undefined) {
  574. query = query.split(/[\. ]/);
  575. maxDepth = query.length - 1;
  576. $.each(query, function(depth, value) {
  577. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  578. instance = instance[value];
  579. }
  580. else if( instance[value] !== undefined ) {
  581. found = instance[value];
  582. }
  583. else {
  584. module.error(error.method);
  585. }
  586. });
  587. }
  588. if ( $.isFunction( found ) ) {
  589. return found.apply(context, passedArguments);
  590. }
  591. return found || false;
  592. }
  593. };
  594. if(methodInvoked) {
  595. if(instance === undefined) {
  596. module.initialize();
  597. }
  598. module.invoke(query);
  599. }
  600. else {
  601. if(instance !== undefined) {
  602. module.destroy();
  603. }
  604. module.initialize();
  605. }
  606. })
  607. ;
  608. return (invokedResponse)
  609. ? invokedResponse
  610. : this
  611. ;
  612. };
  613. $.fn.dropdown.settings = {
  614. name : 'Dropdown',
  615. namespace : 'dropdown',
  616. verbose : true,
  617. debug : true,
  618. performance : true,
  619. on : 'click',
  620. action : 'hide',
  621. delay: {
  622. show: 200,
  623. hide: 300
  624. },
  625. transition : 'slide down',
  626. duration : 250,
  627. onChange : function(){},
  628. onShow : function(){},
  629. onHide : function(){},
  630. error : {
  631. action : 'You called a dropdown action that was not defined',
  632. method : 'The method you called is not defined.',
  633. transition : 'The requested transition was not found'
  634. },
  635. metadata: {
  636. text : 'text',
  637. value : 'value'
  638. },
  639. selector : {
  640. menu : '.menu',
  641. item : '.menu > .item',
  642. text : '> .text',
  643. input : '> input[type="hidden"]'
  644. },
  645. className : {
  646. active : 'active',
  647. placeholder : 'default',
  648. disabled : 'disabled',
  649. visible : 'visible'
  650. }
  651. };
  652. })( jQuery, window , document );