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.

723 lines
20 KiB

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