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.

801 lines
24 KiB

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