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.

674 lines
19 KiB

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