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.

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