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.

696 lines
20 KiB

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