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.

693 lines
20 KiB

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