state.js 20 KB

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