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.

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