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.

695 lines
20 KiB

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