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.

675 lines
19 KiB

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