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.

725 lines
21 KiB

  1. /*
  2. * # Semantic - State
  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.state = function(parameters) {
  13. var
  14. $allModules = $(this),
  15. settings = $.extend(true, {}, $.fn.state.settings, parameters),
  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. // shortcuts
  23. error = settings.error,
  24. metadata = settings.metadata,
  25. className = settings.className,
  26. namespace = settings.namespace,
  27. states = settings.states,
  28. text = settings.text,
  29. eventNamespace = '.' + namespace,
  30. moduleNamespace = namespace + '-module',
  31. invokedResponse
  32. ;
  33. $allModules
  34. .each(function() {
  35. var
  36. $module = $(this),
  37. element = this,
  38. instance = $module.data(moduleNamespace),
  39. module
  40. ;
  41. module = {
  42. initialize: function() {
  43. module.verbose('Initializing module');
  44. // allow module to guess desired state based on element
  45. if(settings.automatic) {
  46. module.add.defaults();
  47. }
  48. // bind events with delegated events
  49. if(settings.context && moduleSelector !== '') {
  50. if( module.allows('hover') ) {
  51. $(element, settings.context)
  52. .on(moduleSelector, 'mouseenter' + eventNamespace, module.enable.hover)
  53. .on(moduleSelector, 'mouseleave' + eventNamespace, module.disable.hover)
  54. ;
  55. }
  56. if( module.allows('down') ) {
  57. $(element, settings.context)
  58. .on(moduleSelector, 'mousedown' + eventNamespace, module.enable.down)
  59. .on(moduleSelector, 'mouseup' + eventNamespace, module.disable.down)
  60. ;
  61. }
  62. if( module.allows('focus') ) {
  63. $(element, settings.context)
  64. .on(moduleSelector, 'focus' + eventNamespace, module.enable.focus)
  65. .on(moduleSelector, 'blur' + eventNamespace, module.disable.focus)
  66. ;
  67. }
  68. $(settings.context)
  69. .on(moduleSelector, 'mouseenter' + eventNamespace, module.change.text)
  70. .on(moduleSelector, 'mouseleave' + eventNamespace, module.reset.text)
  71. .on(moduleSelector, 'click' + eventNamespace, module.toggle.state)
  72. ;
  73. }
  74. else {
  75. if( module.allows('hover') ) {
  76. $module
  77. .on('mouseenter' + eventNamespace, module.enable.hover)
  78. .on('mouseleave' + eventNamespace, module.disable.hover)
  79. ;
  80. }
  81. if( module.allows('down') ) {
  82. $module
  83. .on('mousedown' + eventNamespace, module.enable.down)
  84. .on('mouseup' + eventNamespace, module.disable.down)
  85. ;
  86. }
  87. if( module.allows('focus') ) {
  88. $module
  89. .on('focus' + eventNamespace, module.enable.focus)
  90. .on('blur' + eventNamespace, module.disable.focus)
  91. ;
  92. }
  93. $module
  94. .on('mouseenter' + eventNamespace, module.change.text)
  95. .on('mouseleave' + eventNamespace, module.reset.text)
  96. .on('click' + eventNamespace, module.toggle.state)
  97. ;
  98. }
  99. module.instantiate();
  100. },
  101. instantiate: function() {
  102. module.verbose('Storing instance of module', module);
  103. instance = module;
  104. $module
  105. .data(moduleNamespace, module)
  106. ;
  107. },
  108. destroy: function() {
  109. module.verbose('Destroying previous module', instance);
  110. $module
  111. .off(eventNamespace)
  112. .removeData(moduleNamespace)
  113. ;
  114. },
  115. refresh: function() {
  116. module.verbose('Refreshing selector cache');
  117. $module = $(element);
  118. },
  119. add: {
  120. defaults: function() {
  121. var
  122. userStates = parameters && $.isPlainObject(parameters.states)
  123. ? parameters.states
  124. : {}
  125. ;
  126. $.each(settings.defaults, function(type, typeStates) {
  127. if( module.is[type] !== undefined && module.is[type]() ) {
  128. module.verbose('Adding default states', type, element);
  129. $.extend(settings.states, typeStates, userStates);
  130. }
  131. });
  132. }
  133. },
  134. is: {
  135. active: function() {
  136. return $module.hasClass(className.active);
  137. },
  138. loading: function() {
  139. return $module.hasClass(className.loading);
  140. },
  141. inactive: function() {
  142. return !( $module.hasClass(className.active) );
  143. },
  144. enabled: function() {
  145. return !( $module.is(settings.filter.active) );
  146. },
  147. disabled: function() {
  148. return ( $module.is(settings.filter.active) );
  149. },
  150. textEnabled: function() {
  151. return !( $module.is(settings.filter.text) );
  152. },
  153. // definitions for automatic type detection
  154. button: function() {
  155. return $module.is('.button:not(a, .submit)');
  156. },
  157. input: function() {
  158. return $module.is('input');
  159. }
  160. },
  161. allow: function(state) {
  162. module.debug('Now allowing state', state);
  163. states[state] = true;
  164. },
  165. disallow: function(state) {
  166. module.debug('No longer allowing', state);
  167. states[state] = false;
  168. },
  169. allows: function(state) {
  170. return states[state] || false;
  171. },
  172. enable: {
  173. state: function(state) {
  174. if(module.allows(state)) {
  175. $module.addClass( className[state] );
  176. }
  177. },
  178. // convenience
  179. focus: function() {
  180. $module.addClass(className.focus);
  181. },
  182. hover: function() {
  183. $module.addClass(className.hover);
  184. },
  185. down: function() {
  186. $module.addClass(className.down);
  187. },
  188. },
  189. disable: {
  190. state: function(state) {
  191. if(module.allows(state)) {
  192. $module.removeClass( className[state] );
  193. }
  194. },
  195. // convenience
  196. focus: function() {
  197. $module.removeClass(className.focus);
  198. },
  199. hover: function() {
  200. $module.removeClass(className.hover);
  201. },
  202. down: function() {
  203. $module.removeClass(className.down);
  204. },
  205. },
  206. toggle: {
  207. state: function() {
  208. var
  209. apiRequest = $module.data(metadata.promise)
  210. ;
  211. if( module.allows('active') && module.is.enabled() ) {
  212. module.refresh();
  213. if(apiRequest !== undefined) {
  214. module.listenTo(apiRequest);
  215. }
  216. else {
  217. module.change.state();
  218. }
  219. }
  220. }
  221. },
  222. listenTo: function(apiRequest) {
  223. module.debug('API request detected, waiting for state signal', apiRequest);
  224. if(apiRequest) {
  225. if(text.loading) {
  226. module.update.text(text.loading);
  227. }
  228. $.when(apiRequest)
  229. .then(function() {
  230. if(apiRequest.state() == 'resolved') {
  231. module.debug('API request succeeded');
  232. settings.activateTest = function(){ return true; };
  233. settings.deactivateTest = function(){ return true; };
  234. }
  235. else {
  236. module.debug('API request failed');
  237. settings.activateTest = function(){ return false; };
  238. settings.deactivateTest = function(){ return false; };
  239. }
  240. module.change.state();
  241. })
  242. ;
  243. }
  244. // xhr exists but set to false, beforeSend killed the xhr
  245. else {
  246. settings.activateTest = function(){ return false; };
  247. settings.deactivateTest = function(){ return false; };
  248. }
  249. },
  250. // checks whether active/inactive state can be given
  251. change: {
  252. state: function() {
  253. module.debug('Determining state change direction');
  254. // inactive to active change
  255. if( module.is.inactive() ) {
  256. module.activate();
  257. }
  258. else {
  259. module.deactivate();
  260. }
  261. if(settings.sync) {
  262. module.sync();
  263. }
  264. $.proxy(settings.onChange, element)();
  265. },
  266. text: function() {
  267. if( module.is.textEnabled() ) {
  268. if( module.is.active() ) {
  269. if(text.hover) {
  270. module.verbose('Changing text to hover text', text.hover);
  271. module.update.text(text.hover);
  272. }
  273. else if(text.disable) {
  274. module.verbose('Changing text to disable text', text.disable);
  275. module.update.text(text.disable);
  276. }
  277. }
  278. else {
  279. if(text.hover) {
  280. module.verbose('Changing text to hover text', text.disable);
  281. module.update.text(text.hover);
  282. }
  283. else if(text.enable){
  284. module.verbose('Changing text to enable text', text.enable);
  285. module.update.text(text.enable);
  286. }
  287. }
  288. }
  289. }
  290. },
  291. activate: function() {
  292. if( $.proxy(settings.activateTest, element)() ) {
  293. module.debug('Setting state to active');
  294. $module
  295. .addClass(className.active)
  296. ;
  297. module.update.text(text.active);
  298. }
  299. $.proxy(settings.onActivate, element)();
  300. },
  301. deactivate: function() {
  302. if($.proxy(settings.deactivateTest, element)() ) {
  303. module.debug('Setting state to inactive');
  304. $module
  305. .removeClass(className.active)
  306. ;
  307. module.update.text(text.inactive);
  308. }
  309. $.proxy(settings.onDeactivate, element)();
  310. },
  311. sync: function() {
  312. module.verbose('Syncing other buttons to current state');
  313. if( module.is.active() ) {
  314. $allModules
  315. .not($module)
  316. .state('activate');
  317. }
  318. else {
  319. $allModules
  320. .not($module)
  321. .state('deactivate')
  322. ;
  323. }
  324. },
  325. get: {
  326. text: function() {
  327. return (settings.selector.text)
  328. ? $module.find(settings.selector.text).text()
  329. : $module.html()
  330. ;
  331. },
  332. textFor: function(state) {
  333. return text[state] || false;
  334. }
  335. },
  336. flash: {
  337. text: function(text, duration) {
  338. var
  339. previousText = module.get.text()
  340. ;
  341. module.debug('Flashing text message', text, duration);
  342. text = text || settings.text.flash;
  343. duration = duration || settings.flashDuration;
  344. module.update.text(text);
  345. setTimeout(function(){
  346. module.update.text(previousText);
  347. }, duration);
  348. }
  349. },
  350. reset: {
  351. // on mouseout sets text to previous value
  352. text: function() {
  353. var
  354. activeText = text.active || $module.data(metadata.storedText),
  355. inactiveText = text.inactive || $module.data(metadata.storedText)
  356. ;
  357. if( module.is.textEnabled() ) {
  358. if( module.is.active() && activeText) {
  359. module.verbose('Resetting active text', activeText);
  360. module.update.text(activeText);
  361. }
  362. else if(inactiveText) {
  363. module.verbose('Resetting inactive text', activeText);
  364. module.update.text(inactiveText);
  365. }
  366. }
  367. }
  368. },
  369. update: {
  370. text: function(text) {
  371. var
  372. currentText = module.get.text()
  373. ;
  374. if(text && text !== currentText) {
  375. module.debug('Updating text', text);
  376. if(settings.selector.text) {
  377. $module
  378. .data(metadata.storedText, text)
  379. .find(settings.selector.text)
  380. .text(text)
  381. ;
  382. }
  383. else {
  384. $module
  385. .data(metadata.storedText, text)
  386. .html(text)
  387. ;
  388. }
  389. }
  390. else {
  391. module.debug('Text is already sane, ignoring update', text);
  392. }
  393. }
  394. },
  395. setting: function(name, value) {
  396. module.debug('Changing setting', name, value);
  397. if(value !== undefined) {
  398. if( $.isPlainObject(name) ) {
  399. $.extend(true, settings, name);
  400. }
  401. else {
  402. settings[name] = value;
  403. }
  404. }
  405. else {
  406. return settings[name];
  407. }
  408. },
  409. internal: function(name, value) {
  410. module.debug('Changing internal', name, value);
  411. if(value !== undefined) {
  412. if( $.isPlainObject(name) ) {
  413. $.extend(true, module, name);
  414. }
  415. else {
  416. module[name] = value;
  417. }
  418. }
  419. else {
  420. return module[name];
  421. }
  422. },
  423. debug: function() {
  424. if(settings.debug) {
  425. if(settings.performance) {
  426. module.performance.log(arguments);
  427. }
  428. else {
  429. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  430. module.debug.apply(console, arguments);
  431. }
  432. }
  433. },
  434. verbose: function() {
  435. if(settings.verbose && settings.debug) {
  436. if(settings.performance) {
  437. module.performance.log(arguments);
  438. }
  439. else {
  440. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  441. module.verbose.apply(console, arguments);
  442. }
  443. }
  444. },
  445. error: function() {
  446. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  447. module.error.apply(console, arguments);
  448. },
  449. performance: {
  450. log: function(message) {
  451. var
  452. currentTime,
  453. executionTime,
  454. previousTime
  455. ;
  456. if(settings.performance) {
  457. currentTime = new Date().getTime();
  458. previousTime = time || currentTime;
  459. executionTime = currentTime - previousTime;
  460. time = currentTime;
  461. performance.push({
  462. 'Element' : element,
  463. 'Name' : message[0],
  464. 'Arguments' : [].slice.call(message, 1) || '',
  465. 'Execution Time' : executionTime
  466. });
  467. }
  468. clearTimeout(module.performance.timer);
  469. module.performance.timer = setTimeout(module.performance.display, 100);
  470. },
  471. display: function() {
  472. var
  473. title = settings.name + ':',
  474. totalTime = 0
  475. ;
  476. time = false;
  477. clearTimeout(module.performance.timer);
  478. $.each(performance, function(index, data) {
  479. totalTime += data['Execution Time'];
  480. });
  481. title += ' ' + totalTime + 'ms';
  482. if(moduleSelector) {
  483. title += ' \'' + moduleSelector + '\'';
  484. }
  485. if($allModules.size() > 1) {
  486. title += ' ' + '(' + $allModules.size() + ')';
  487. }
  488. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  489. console.groupCollapsed(title);
  490. if(console.table) {
  491. console.table(performance);
  492. }
  493. else {
  494. $.each(performance, function(index, data) {
  495. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  496. });
  497. }
  498. console.groupEnd();
  499. }
  500. performance = [];
  501. }
  502. },
  503. invoke: function(query, passedArguments, context) {
  504. var
  505. maxDepth,
  506. found,
  507. response
  508. ;
  509. passedArguments = passedArguments || queryArguments;
  510. context = element || context;
  511. if(typeof query == 'string' && instance !== undefined) {
  512. query = query.split(/[\. ]/);
  513. maxDepth = query.length - 1;
  514. $.each(query, function(depth, value) {
  515. var camelCaseValue = (depth != maxDepth)
  516. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  517. : query
  518. ;
  519. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  520. instance = instance[value];
  521. }
  522. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  523. instance = instance[camelCaseValue];
  524. }
  525. else if( instance[value] !== undefined ) {
  526. found = instance[value];
  527. return false;
  528. }
  529. else if( instance[camelCaseValue] !== undefined ) {
  530. found = instance[camelCaseValue];
  531. return false;
  532. }
  533. else {
  534. module.error(error.method);
  535. return false;
  536. }
  537. });
  538. }
  539. if ( $.isFunction( found ) ) {
  540. response = found.apply(context, passedArguments);
  541. }
  542. else if(found !== undefined) {
  543. response = found;
  544. }
  545. if($.isArray(invokedResponse)) {
  546. invokedResponse.push(response);
  547. }
  548. else if(typeof invokedResponse == 'string') {
  549. invokedResponse = [invokedResponse, response];
  550. }
  551. else if(response !== undefined) {
  552. invokedResponse = response;
  553. }
  554. return found;
  555. }
  556. };
  557. if(methodInvoked) {
  558. if(instance === undefined) {
  559. module.initialize();
  560. }
  561. module.invoke(query);
  562. }
  563. else {
  564. if(instance !== undefined) {
  565. module.destroy();
  566. }
  567. module.initialize();
  568. }
  569. })
  570. ;
  571. return (invokedResponse !== undefined)
  572. ? invokedResponse
  573. : this
  574. ;
  575. };
  576. $.fn.state.settings = {
  577. // module info
  578. name : 'State',
  579. // debug output
  580. debug : true,
  581. // verbose debug output
  582. verbose : true,
  583. // namespace for events
  584. namespace : 'state',
  585. // debug data includes performance
  586. performance: true,
  587. // callback occurs on state change
  588. onActivate : function() {},
  589. onDeactivate : function() {},
  590. onChange : function() {},
  591. // state test functions
  592. activateTest : function() { return true; },
  593. deactivateTest : function() { return true; },
  594. // whether to automatically map default states
  595. automatic : true,
  596. // activate / deactivate changes all elements instantiated at same time
  597. sync : false,
  598. // default flash text duration, used for temporarily changing text of an element
  599. flashDuration : 3000,
  600. // selector filter
  601. filter : {
  602. text : '.loading, .disabled',
  603. active : '.disabled'
  604. },
  605. context : false,
  606. // error
  607. error: {
  608. method : 'The method you called is not defined.'
  609. },
  610. // metadata
  611. metadata: {
  612. promise : 'promise',
  613. storedText : 'stored-text'
  614. },
  615. // change class on state
  616. className: {
  617. focus : 'focus',
  618. hover : 'hover',
  619. down : 'down',
  620. active : 'active',
  621. loading : 'loading'
  622. },
  623. selector: {
  624. // selector for text node
  625. text: false
  626. },
  627. defaults : {
  628. input: {
  629. hover : true,
  630. focus : true,
  631. down : true,
  632. loading : false,
  633. active : false
  634. },
  635. button: {
  636. hover : true,
  637. focus : false,
  638. down : true,
  639. active : true,
  640. loading : true
  641. }
  642. },
  643. states : {
  644. hover : true,
  645. focus : true,
  646. down : true,
  647. loading : false,
  648. active : false
  649. },
  650. text : {
  651. flash : false,
  652. hover : false,
  653. active : false,
  654. inactive : false,
  655. enable : false,
  656. disable : false
  657. }
  658. };
  659. })( jQuery, window , document );