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.

594 lines
17 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 arguments available
  36. query = arguments[0],
  37. passedArguments = [].slice.call(arguments, 1),
  38. invokedResponse
  39. ;
  40. $allModules
  41. .each(function() {
  42. var
  43. $module = $(this),
  44. settings = $.extend(true, {}, $.fn.state.settings, parameters),
  45. selector = $module.selector || '',
  46. element = this,
  47. instance = $module.data('module-' + settings.namespace),
  48. methodInvoked = (typeof query == 'string'),
  49. // shortcuts
  50. namespace = settings.namespace,
  51. metadata = settings.metadata,
  52. className = settings.className,
  53. states = settings.states,
  54. text = settings.text,
  55. module
  56. ;
  57. module = {
  58. initialize: function() {
  59. module.verbose('Initializing module for', element);
  60. // allow module to guess desired state based on element
  61. if(settings.automatic) {
  62. module.add.defaults();
  63. }
  64. // bind events with delegated events
  65. if(settings.context && selector !== '') {
  66. if( module.allows('hover') ) {
  67. $(element, settings.context)
  68. .on(selector, 'mouseenter.' + namespace, module.hover.enable)
  69. .on(selector, 'mouseleave.' + namespace, module.hover.disable)
  70. ;
  71. }
  72. if( module.allows('pressed') ) {
  73. $(element, settings.context)
  74. .on(selector, 'mousedown.' + namespace, module.pressed.enable)
  75. .on(selector, 'mouseup.' + namespace, module.pressed.disable)
  76. ;
  77. }
  78. if( module.allows('focus') ) {
  79. $(element, settings.context)
  80. .on(selector, 'focus.' + namespace, module.focus.enable)
  81. .on(selector, 'blur.' + namespace, module.focus.disable)
  82. ;
  83. }
  84. $(settings.context)
  85. .on(selector, 'mouseover.' + namespace, module.text.change)
  86. .on(selector, 'mouseout.' + namespace, module.text.reset)
  87. .on(selector, 'click.' + namespace, module.toggle)
  88. ;
  89. }
  90. else {
  91. if( module.allows('hover') ) {
  92. $module
  93. .on('mouseenter.' + namespace, module.hover.enable)
  94. .on('mouseleave.' + namespace, module.hover.disable)
  95. ;
  96. }
  97. if( module.allows('pressed') ) {
  98. $module
  99. .on('mousedown.' + namespace, module.pressed.enable)
  100. .on('mouseup.' + namespace, module.pressed.disable)
  101. ;
  102. }
  103. if( module.allows('focus') ) {
  104. $module
  105. .on('focus.' + namespace, module.focus.enable)
  106. .on('blur.' + namespace, module.focus.disable)
  107. ;
  108. }
  109. $module
  110. .on('mouseover.' + namespace, module.text.change)
  111. .on('mouseout.' + namespace, module.text.reset)
  112. .on('click.' + namespace, module.toggle)
  113. ;
  114. }
  115. $module
  116. .data('module-' + namespace, module)
  117. ;
  118. },
  119. destroy: function() {
  120. module.verbose('Destroying previous module for', element);
  121. $module
  122. .off('.' + namespace)
  123. ;
  124. },
  125. refresh: function() {
  126. module.verbose('Refreshing selector cache for', element);
  127. $module = $(element);
  128. },
  129. add: {
  130. defaults: function() {
  131. var
  132. userStates = parameters && $.isPlainObject(parameters.states)
  133. ? parameters.states
  134. : {}
  135. ;
  136. $.each(settings.defaults, function(type, typeStates) {
  137. if( module.is[type] !== undefined && module.is[type]() ) {
  138. module.verbose('Adding default states for detected type:', type, element);
  139. $.extend(settings.states, typeStates, userStates);
  140. }
  141. });
  142. }
  143. },
  144. is: {
  145. active: function() {
  146. return $module.hasClass(className.active);
  147. },
  148. loading: function() {
  149. return $module.hasClass(className.loading);
  150. },
  151. inactive: function() {
  152. return !( $module.hasClass(className.active) );
  153. },
  154. enabled: function() {
  155. return !( $module.is(settings.filter.active) );
  156. },
  157. disabled: function() {
  158. return ( $module.is(settings.filter.active) );
  159. },
  160. textEnabled: function() {
  161. return !( $module.is(settings.filter.text) );
  162. },
  163. // definitions for automatic type detection
  164. button: function() {
  165. return $module.is('.button:not(a, .submit)');
  166. },
  167. input: function() {
  168. return $module.is('input');
  169. }
  170. },
  171. allows: function(state) {
  172. return states[state] || false;
  173. },
  174. enable: function(state) {
  175. if(module.allows(state)) {
  176. $module.addClass( className[state] );
  177. }
  178. },
  179. disable: function(state) {
  180. if(module.allows(state)) {
  181. $module.removeClass( className[state] );
  182. }
  183. },
  184. textFor: function(state) {
  185. return text[state] || false;
  186. },
  187. focus : {
  188. enable: function() {
  189. $module.addClass(className.focus);
  190. },
  191. disable: function() {
  192. $module.removeClass(className.focus);
  193. }
  194. },
  195. hover : {
  196. enable: function() {
  197. $module.addClass(className.hover);
  198. },
  199. disable: function() {
  200. $module.removeClass(className.hover);
  201. }
  202. },
  203. pressed : {
  204. enable: function() {
  205. $module
  206. .addClass(className.pressed)
  207. .one('mouseleave', module.pressed.disable)
  208. ;
  209. },
  210. disable: function() {
  211. $module.removeClass(className.pressed);
  212. }
  213. },
  214. // determines method for state activation
  215. toggle: function() {
  216. var
  217. apiRequest = $module.data(metadata.promise)
  218. ;
  219. if( module.allows('active') && module.is.enabled() ) {
  220. module.refresh();
  221. if(apiRequest !== undefined) {
  222. module.listenTo(apiRequest);
  223. }
  224. else {
  225. module.change();
  226. }
  227. }
  228. },
  229. listenTo: function(apiRequest) {
  230. module.debug('API request detected, waiting for state signal');
  231. if(apiRequest) {
  232. if(text.loading) {
  233. module.text.update(text.loading);
  234. }
  235. $.when(apiRequest)
  236. .then(function() {
  237. if(apiRequest.state() == 'resolved') {
  238. module.debug('API request succeeded');
  239. settings.activateTest = function(){ return true; };
  240. settings.deactivateTest = function(){ return true; };
  241. }
  242. else {
  243. module.debug('API request failed');
  244. settings.activateTest = function(){ return false; };
  245. settings.deactivateTest = function(){ return false; };
  246. }
  247. module.change();
  248. })
  249. ;
  250. }
  251. // xhr exists but set to false, beforeSend killed the xhr
  252. else {
  253. settings.activateTest = function(){ return false; };
  254. settings.deactivateTest = function(){ return false; };
  255. }
  256. },
  257. // checks whether active/inactive state can be given
  258. change: function() {
  259. module.debug('Determining state change direction');
  260. // inactive to active change
  261. if( module.is.inactive() ) {
  262. module.activate();
  263. }
  264. else {
  265. module.deactivate();
  266. }
  267. if(settings.sync) {
  268. module.sync();
  269. }
  270. settings.onChange();
  271. },
  272. activate: function() {
  273. if( $.proxy(settings.activateTest, element)() ) {
  274. module.debug('Setting state to active');
  275. $module
  276. .addClass(className.active)
  277. ;
  278. module.text.update(text.active);
  279. }
  280. },
  281. deactivate: function() {
  282. if($.proxy(settings.deactivateTest, element)() ) {
  283. module.debug('Setting state to inactive');
  284. $module
  285. .removeClass(className.active)
  286. ;
  287. module.text.update(text.inactive);
  288. }
  289. },
  290. sync: function() {
  291. module.verbose('Syncing other buttons to current state');
  292. if( module.is.active() ) {
  293. $allModules
  294. .not($module)
  295. .state('activate');
  296. }
  297. else {
  298. $allModules
  299. .not($module)
  300. .state('deactivate')
  301. ;
  302. }
  303. },
  304. text: {
  305. // finds text node to update
  306. get: function() {
  307. return (settings.textFilter)
  308. ? $module.find(settings.textFilter).text()
  309. : $module.html()
  310. ;
  311. },
  312. change: function() {
  313. if( module.is.textEnabled() ) {
  314. if( module.is.active() ) {
  315. if(text.hover) {
  316. module.verbose('Changing text to hover text', text.hover);
  317. module.text.update(text.hover);
  318. }
  319. else if(text.disable) {
  320. module.verbose('Changing text to disable text', text.disable);
  321. module.text.update(text.disable);
  322. }
  323. }
  324. else {
  325. if(text.hover) {
  326. module.verbose('Changing text to hover text', text.disable);
  327. module.text.update(text.hover);
  328. }
  329. else if(text.enable){
  330. module.verbose('Changing text to enable text', text.disable);
  331. module.text.update(text.enable);
  332. }
  333. }
  334. }
  335. },
  336. // on mouseout sets text to previous value
  337. reset : function() {
  338. var
  339. activeText = text.active || $module.data(metadata.storedText),
  340. inactiveText = text.inactive || $module.data(metadata.storedText)
  341. ;
  342. if( module.is.textEnabled() ) {
  343. if( module.is.active() && activeText) {
  344. module.verbose('Resetting active text', activeText);
  345. module.text.update(activeText);
  346. }
  347. else if(inactiveText) {
  348. module.verbose('Resetting inactive text', activeText);
  349. module.text.update(inactiveText);
  350. }
  351. }
  352. },
  353. update: function(text) {
  354. var
  355. currentText = module.text.get()
  356. ;
  357. if(text && text !== currentText) {
  358. module.debug('Updating text to', text);
  359. if(settings.textFilter) {
  360. $module
  361. .data(metadata.storedText, text)
  362. .find(settings.textFilter)
  363. .text(text)
  364. ;
  365. }
  366. else {
  367. $module
  368. .data(metadata.storedText, text)
  369. .html(text)
  370. ;
  371. }
  372. }
  373. }
  374. },
  375. /* standard module */
  376. setting: function(name, value) {
  377. if(value === undefined) {
  378. return settings[name];
  379. }
  380. settings[name] = value;
  381. },
  382. verbose: function() {
  383. if(settings.verbose) {
  384. module.debug.apply(this, arguments);
  385. }
  386. },
  387. debug: function() {
  388. var
  389. output = [],
  390. message = settings.moduleName + ': ' + arguments[0],
  391. variables = [].slice.call( arguments, 1 ),
  392. log = console.info || console.log || function(){}
  393. ;
  394. log = Function.prototype.bind.call(log, console);
  395. if(settings.debug) {
  396. output.push(message);
  397. log.apply(console, output.concat(variables) );
  398. }
  399. },
  400. error: function() {
  401. var
  402. output = [],
  403. errorMessage = settings.moduleName + ': ' + arguments[0],
  404. variables = [].slice.call( arguments, 1 ),
  405. log = console.warn || console.log || function(){}
  406. ;
  407. log = Function.prototype.bind.call(log, console);
  408. if(settings.debug) {
  409. output.push(errorMessage);
  410. output.concat(variables);
  411. log.apply(console, output.concat(variables) );
  412. }
  413. },
  414. invoke: function(query, context, passedArguments) {
  415. var
  416. maxDepth,
  417. found
  418. ;
  419. passedArguments = passedArguments || [].slice.call( arguments, 2 );
  420. if(typeof query == 'string' && instance !== undefined) {
  421. query = query.split('.');
  422. maxDepth = query.length - 1;
  423. $.each(query, function(depth, value) {
  424. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  425. instance = instance[value];
  426. return true;
  427. }
  428. else if( instance[value] !== undefined ) {
  429. found = instance[value];
  430. return true;
  431. }
  432. module.error(settings.errors.method);
  433. return false;
  434. });
  435. }
  436. if ( $.isFunction( found ) ) {
  437. return found.apply(context, passedArguments);
  438. }
  439. // return retrieved variable or chain
  440. return found;
  441. }
  442. };
  443. // check for invoking internal method
  444. if(methodInvoked) {
  445. invokedResponse = module.invoke(query, this, passedArguments);
  446. }
  447. // otherwise initialize
  448. else {
  449. if(instance !== undefined) {
  450. module.destroy();
  451. }
  452. module.initialize();
  453. }
  454. })
  455. ;
  456. // chain or return queried method
  457. return (invokedResponse !== undefined)
  458. ? invokedResponse
  459. : this
  460. ;
  461. };
  462. $.fn.state.settings = {
  463. // module info
  464. moduleName : 'State Module',
  465. // debug output
  466. debug : true,
  467. // verbose debug output
  468. verbose : false,
  469. namespace : 'state',
  470. // callback occurs on state change
  471. onChange: function() {},
  472. // state test functions
  473. activateTest : function() { return true; },
  474. deactivateTest : function() { return true; },
  475. // whether to automatically map default states
  476. automatic: true,
  477. // activate / deactivate changes all elements instantiated at same time
  478. sync: false,
  479. // selector filter
  480. filter : {
  481. text : '.loading, .disabled',
  482. active : '.disabled'
  483. },
  484. textFilter : false,
  485. context : false,
  486. // errors
  487. errors: {
  488. method : 'The method you called is not defined.'
  489. },
  490. // metadata
  491. metadata: {
  492. promise : 'promise',
  493. storedText : 'stored-text'
  494. },
  495. // change class on state
  496. className: {
  497. focus : 'focus',
  498. hover : 'hover',
  499. pressed : 'down',
  500. active : 'active',
  501. loading : 'loading'
  502. },
  503. defaults : {
  504. input: {
  505. hover : true,
  506. focus : true,
  507. pressed : true,
  508. loading : false,
  509. active : false
  510. },
  511. button: {
  512. hover : true,
  513. focus : false,
  514. pressed : true,
  515. active : false,
  516. loading : true
  517. }
  518. },
  519. states : {
  520. hover : true,
  521. focus : true,
  522. pressed : true,
  523. loading : false,
  524. active : false
  525. },
  526. text : {
  527. hover : false,
  528. active : false,
  529. inactive : false,
  530. enable : false,
  531. disable : false
  532. }
  533. };
  534. })( jQuery, window , document );