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.

690 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. ;
  168. if( module.allows('active') && module.is.enabled() ) {
  169. module.refresh();
  170. if($.fn.api !== undefined) {
  171. apiRequest = $module.api('get request');
  172. if(apiRequest) {
  173. module.listenTo(apiRequest);
  174. return;
  175. }
  176. }
  177. module.change.state();
  178. }
  179. }
  180. },
  181. listenTo: function(apiRequest) {
  182. module.debug('API request detected, waiting for state signal', apiRequest);
  183. if(apiRequest) {
  184. if(text.loading) {
  185. module.update.text(text.loading);
  186. }
  187. $.when(apiRequest)
  188. .then(function() {
  189. if(apiRequest.state() == 'resolved') {
  190. module.debug('API request succeeded');
  191. settings.activateTest = function(){ return true; };
  192. settings.deactivateTest = function(){ return true; };
  193. }
  194. else {
  195. module.debug('API request failed');
  196. settings.activateTest = function(){ return false; };
  197. settings.deactivateTest = function(){ return false; };
  198. }
  199. module.change.state();
  200. })
  201. ;
  202. }
  203. // xhr exists but set to false, beforeSend killed the xhr
  204. else {
  205. settings.activateTest = function(){ return false; };
  206. settings.deactivateTest = function(){ return false; };
  207. }
  208. },
  209. // checks whether active/inactive state can be given
  210. change: {
  211. state: function() {
  212. module.debug('Determining state change direction');
  213. // inactive to active change
  214. if( module.is.inactive() ) {
  215. module.activate();
  216. }
  217. else {
  218. module.deactivate();
  219. }
  220. if(settings.sync) {
  221. module.sync();
  222. }
  223. $.proxy(settings.onChange, element)();
  224. },
  225. text: function() {
  226. if( module.is.textEnabled() ) {
  227. if(module.is.disabled() ) {
  228. module.verbose('Changing text to disabled text', text.hover);
  229. module.update.text(text.disabled);
  230. }
  231. else if( module.is.active() ) {
  232. if(text.hover) {
  233. module.verbose('Changing text to hover text', text.hover);
  234. module.update.text(text.hover);
  235. }
  236. else if(text.deactivate) {
  237. module.verbose('Changing text to deactivating text', text.deactivate);
  238. module.update.text(text.deactivate);
  239. }
  240. }
  241. else {
  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.activate){
  247. module.verbose('Changing text to activating text', text.activate);
  248. module.update.text(text.activate);
  249. }
  250. }
  251. }
  252. }
  253. },
  254. activate: function() {
  255. if( $.proxy(settings.activateTest, element)() ) {
  256. module.debug('Setting state to active');
  257. $module
  258. .addClass(className.active)
  259. ;
  260. module.update.text(text.active);
  261. $.proxy(settings.onActivate, element)();
  262. }
  263. },
  264. deactivate: function() {
  265. if($.proxy(settings.deactivateTest, element)() ) {
  266. module.debug('Setting state to inactive');
  267. $module
  268. .removeClass(className.active)
  269. ;
  270. module.update.text(text.inactive);
  271. $.proxy(settings.onDeactivate, element)();
  272. }
  273. },
  274. sync: function() {
  275. module.verbose('Syncing other buttons to current state');
  276. if( module.is.active() ) {
  277. $allModules
  278. .not($module)
  279. .state('activate');
  280. }
  281. else {
  282. $allModules
  283. .not($module)
  284. .state('deactivate')
  285. ;
  286. }
  287. },
  288. get: {
  289. text: function() {
  290. return (settings.selector.text)
  291. ? $module.find(settings.selector.text).text()
  292. : $module.html()
  293. ;
  294. },
  295. textFor: function(state) {
  296. return text[state] || false;
  297. }
  298. },
  299. flash: {
  300. text: function(text, duration, callback) {
  301. var
  302. previousText = module.get.text()
  303. ;
  304. module.debug('Flashing text message', text, duration);
  305. text = text || settings.text.flash;
  306. duration = duration || settings.flashDuration;
  307. callback = callback || function() {};
  308. module.update.text(text);
  309. setTimeout(function(){
  310. module.update.text(previousText);
  311. $.proxy(callback, element)();
  312. }, duration);
  313. }
  314. },
  315. reset: {
  316. // on mouseout sets text to previous value
  317. text: function() {
  318. var
  319. activeText = text.active || $module.data(metadata.storedText),
  320. inactiveText = text.inactive || $module.data(metadata.storedText)
  321. ;
  322. if( module.is.textEnabled() ) {
  323. if( module.is.active() && activeText) {
  324. module.verbose('Resetting active text', activeText);
  325. module.update.text(activeText);
  326. }
  327. else if(inactiveText) {
  328. module.verbose('Resetting inactive text', activeText);
  329. module.update.text(inactiveText);
  330. }
  331. }
  332. }
  333. },
  334. update: {
  335. text: function(text) {
  336. var
  337. currentText = module.get.text()
  338. ;
  339. if(text && text !== currentText) {
  340. module.debug('Updating text', text);
  341. if(settings.selector.text) {
  342. $module
  343. .data(metadata.storedText, text)
  344. .find(settings.selector.text)
  345. .text(text)
  346. ;
  347. }
  348. else {
  349. $module
  350. .data(metadata.storedText, text)
  351. .html(text)
  352. ;
  353. }
  354. }
  355. else {
  356. module.debug('Text is already sane, ignoring update', text);
  357. }
  358. }
  359. },
  360. setting: function(name, value) {
  361. module.debug('Changing setting', name, value);
  362. if( $.isPlainObject(name) ) {
  363. $.extend(true, settings, name);
  364. }
  365. else if(value !== undefined) {
  366. settings[name] = value;
  367. }
  368. else {
  369. return settings[name];
  370. }
  371. },
  372. internal: function(name, value) {
  373. if( $.isPlainObject(name) ) {
  374. $.extend(true, module, name);
  375. }
  376. else if(value !== undefined) {
  377. module[name] = value;
  378. }
  379. else {
  380. return module[name];
  381. }
  382. },
  383. debug: function() {
  384. if(settings.debug) {
  385. if(settings.performance) {
  386. module.performance.log(arguments);
  387. }
  388. else {
  389. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  390. module.debug.apply(console, arguments);
  391. }
  392. }
  393. },
  394. verbose: function() {
  395. if(settings.verbose && settings.debug) {
  396. if(settings.performance) {
  397. module.performance.log(arguments);
  398. }
  399. else {
  400. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  401. module.verbose.apply(console, arguments);
  402. }
  403. }
  404. },
  405. error: function() {
  406. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  407. module.error.apply(console, arguments);
  408. },
  409. performance: {
  410. log: function(message) {
  411. var
  412. currentTime,
  413. executionTime,
  414. previousTime
  415. ;
  416. if(settings.performance) {
  417. currentTime = new Date().getTime();
  418. previousTime = time || currentTime;
  419. executionTime = currentTime - previousTime;
  420. time = currentTime;
  421. performance.push({
  422. 'Name' : message[0],
  423. 'Arguments' : [].slice.call(message, 1) || '',
  424. 'Element' : element,
  425. 'Execution Time' : executionTime
  426. });
  427. }
  428. clearTimeout(module.performance.timer);
  429. module.performance.timer = setTimeout(module.performance.display, 100);
  430. },
  431. display: function() {
  432. var
  433. title = settings.name + ':',
  434. totalTime = 0
  435. ;
  436. time = false;
  437. clearTimeout(module.performance.timer);
  438. $.each(performance, function(index, data) {
  439. totalTime += data['Execution Time'];
  440. });
  441. title += ' ' + totalTime + 'ms';
  442. if(moduleSelector) {
  443. title += ' \'' + moduleSelector + '\'';
  444. }
  445. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  446. console.groupCollapsed(title);
  447. if(console.table) {
  448. console.table(performance);
  449. }
  450. else {
  451. $.each(performance, function(index, data) {
  452. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  453. });
  454. }
  455. console.groupEnd();
  456. }
  457. performance = [];
  458. }
  459. },
  460. invoke: function(query, passedArguments, context) {
  461. var
  462. object = instance,
  463. maxDepth,
  464. found,
  465. response
  466. ;
  467. passedArguments = passedArguments || queryArguments;
  468. context = element || context;
  469. if(typeof query == 'string' && object !== undefined) {
  470. query = query.split(/[\. ]/);
  471. maxDepth = query.length - 1;
  472. $.each(query, function(depth, value) {
  473. var camelCaseValue = (depth != maxDepth)
  474. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  475. : query
  476. ;
  477. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  478. object = object[camelCaseValue];
  479. }
  480. else if( object[camelCaseValue] !== undefined ) {
  481. found = object[camelCaseValue];
  482. return false;
  483. }
  484. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  485. object = object[value];
  486. }
  487. else if( object[value] !== undefined ) {
  488. found = object[value];
  489. return false;
  490. }
  491. else {
  492. module.error(error.method, query);
  493. return false;
  494. }
  495. });
  496. }
  497. if ( $.isFunction( found ) ) {
  498. response = found.apply(context, passedArguments);
  499. }
  500. else if(found !== undefined) {
  501. response = found;
  502. }
  503. if($.isArray(returnedValue)) {
  504. returnedValue.push(response);
  505. }
  506. else if(returnedValue !== undefined) {
  507. returnedValue = [returnedValue, response];
  508. }
  509. else if(response !== undefined) {
  510. returnedValue = response;
  511. }
  512. return found;
  513. }
  514. };
  515. if(methodInvoked) {
  516. if(instance === undefined) {
  517. module.initialize();
  518. }
  519. module.invoke(query);
  520. }
  521. else {
  522. if(instance !== undefined) {
  523. module.destroy();
  524. }
  525. module.initialize();
  526. }
  527. })
  528. ;
  529. return (returnedValue !== undefined)
  530. ? returnedValue
  531. : this
  532. ;
  533. };
  534. $.fn.state.settings = {
  535. // module info
  536. name : 'State',
  537. // debug output
  538. debug : false,
  539. // verbose debug output
  540. verbose : true,
  541. // namespace for events
  542. namespace : 'state',
  543. // debug data includes performance
  544. performance: true,
  545. // callback occurs on state change
  546. onActivate : function() {},
  547. onDeactivate : function() {},
  548. onChange : function() {},
  549. // state test functions
  550. activateTest : function() { return true; },
  551. deactivateTest : function() { return true; },
  552. // whether to automatically map default states
  553. automatic : true,
  554. // activate / deactivate changes all elements instantiated at same time
  555. sync : false,
  556. // default flash text duration, used for temporarily changing text of an element
  557. flashDuration : 1000,
  558. // selector filter
  559. filter : {
  560. text : '.loading, .disabled',
  561. active : '.disabled'
  562. },
  563. context : false,
  564. // error
  565. error: {
  566. method : 'The method you called is not defined.'
  567. },
  568. // metadata
  569. metadata: {
  570. promise : 'promise',
  571. storedText : 'stored-text'
  572. },
  573. // change class on state
  574. className: {
  575. active : 'active',
  576. disabled : 'disabled',
  577. error : 'error',
  578. loading : 'loading',
  579. success : 'success',
  580. warning : 'warning'
  581. },
  582. selector: {
  583. // selector for text node
  584. text: false
  585. },
  586. defaults : {
  587. input: {
  588. disabled : true,
  589. loading : true,
  590. active : true
  591. },
  592. button: {
  593. disabled : true,
  594. loading : true,
  595. active : true,
  596. },
  597. progress: {
  598. active : true,
  599. success : true,
  600. warning : true,
  601. error : true
  602. }
  603. },
  604. states : {
  605. active : true,
  606. disabled : true,
  607. error : true,
  608. loading : true,
  609. success : true,
  610. warning : true
  611. },
  612. text : {
  613. disabled : false,
  614. flash : false,
  615. hover : false,
  616. active : false,
  617. inactive : false,
  618. activate : false,
  619. deactivate : false
  620. }
  621. };
  622. })( jQuery, window , document );