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.

663 lines
19 KiB

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