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.

749 lines
22 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. error = settings.error,
  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. listenTo: function(apiRequest) {
  243. module.debug('API request detected, waiting for state signal', apiRequest);
  244. if(apiRequest) {
  245. if(text.loading) {
  246. module.update.text(text.loading);
  247. }
  248. $.when(apiRequest)
  249. .then(function() {
  250. if(apiRequest.state() == 'resolved') {
  251. module.debug('API request succeeded');
  252. settings.activateTest = function(){ return true; };
  253. settings.deactivateTest = function(){ return true; };
  254. }
  255. else {
  256. module.debug('API request failed');
  257. settings.activateTest = function(){ return false; };
  258. settings.deactivateTest = function(){ return false; };
  259. }
  260. module.change.state();
  261. })
  262. ;
  263. }
  264. // xhr exists but set to false, beforeSend killed the xhr
  265. else {
  266. settings.activateTest = function(){ return false; };
  267. settings.deactivateTest = function(){ return false; };
  268. }
  269. },
  270. // checks whether active/inactive state can be given
  271. change: {
  272. state: function() {
  273. module.debug('Determining state change direction');
  274. // inactive to active change
  275. if( module.is.inactive() ) {
  276. module.activate();
  277. }
  278. else {
  279. module.deactivate();
  280. }
  281. if(settings.sync) {
  282. module.sync();
  283. }
  284. $.proxy(settings.onChange, element)();
  285. },
  286. text: function() {
  287. if( module.is.textEnabled() ) {
  288. if( module.is.active() ) {
  289. if(text.hover) {
  290. module.verbose('Changing text to hover text', text.hover);
  291. module.update.text(text.hover);
  292. }
  293. else if(text.disable) {
  294. module.verbose('Changing text to disable text', text.disable);
  295. module.update.text(text.disable);
  296. }
  297. }
  298. else {
  299. if(text.hover) {
  300. module.verbose('Changing text to hover text', text.disable);
  301. module.update.text(text.hover);
  302. }
  303. else if(text.enable){
  304. module.verbose('Changing text to enable text', text.enable);
  305. module.update.text(text.enable);
  306. }
  307. }
  308. }
  309. }
  310. },
  311. activate: function() {
  312. if( $.proxy(settings.activateTest, element)() ) {
  313. module.debug('Setting state to active');
  314. $module
  315. .addClass(className.active)
  316. ;
  317. module.update.text(text.active);
  318. }
  319. $.proxy(settings.onActivate, element)();
  320. },
  321. deactivate: function() {
  322. if($.proxy(settings.deactivateTest, element)() ) {
  323. module.debug('Setting state to inactive');
  324. $module
  325. .removeClass(className.active)
  326. ;
  327. module.update.text(text.inactive);
  328. }
  329. $.proxy(settings.onDeactivate, element)();
  330. },
  331. sync: function() {
  332. module.verbose('Syncing other buttons to current state');
  333. if( module.is.active() ) {
  334. $allModules
  335. .not($module)
  336. .state('activate');
  337. }
  338. else {
  339. $allModules
  340. .not($module)
  341. .state('deactivate')
  342. ;
  343. }
  344. },
  345. get: {
  346. text: function() {
  347. return (settings.selector.text)
  348. ? $module.find(settings.selector.text).text()
  349. : $module.html()
  350. ;
  351. },
  352. textFor: function(state) {
  353. return text[state] || false;
  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. module.debug.apply(console, arguments);
  451. }
  452. }
  453. },
  454. verbose: function() {
  455. if(settings.verbose && settings.debug) {
  456. if(settings.performance) {
  457. module.performance.log(arguments);
  458. }
  459. else {
  460. module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  461. module.verbose.apply(console, arguments);
  462. }
  463. }
  464. },
  465. error: function() {
  466. module.error = Function.prototype.bind.call(console.error, console, settings.moduleName + ':');
  467. module.error.apply(console, arguments);
  468. },
  469. performance: {
  470. log: function(message) {
  471. var
  472. currentTime,
  473. executionTime,
  474. previousTime
  475. ;
  476. if(settings.performance) {
  477. currentTime = new Date().getTime();
  478. previousTime = time || currentTime;
  479. executionTime = currentTime - previousTime;
  480. time = currentTime;
  481. performance.push({
  482. 'Element' : element,
  483. 'Name' : message[0],
  484. 'Arguments' : [].slice.call(message, 1) || '',
  485. 'Execution Time' : executionTime
  486. });
  487. }
  488. clearTimeout(module.performance.timer);
  489. module.performance.timer = setTimeout(module.performance.display, 100);
  490. },
  491. display: function() {
  492. var
  493. title = settings.moduleName + ':',
  494. totalTime = 0
  495. ;
  496. time = false;
  497. clearTimeout(module.performance.timer);
  498. $.each(performance, function(index, data) {
  499. totalTime += data['Execution Time'];
  500. });
  501. title += ' ' + totalTime + 'ms';
  502. if(moduleSelector) {
  503. title += ' \'' + moduleSelector + '\'';
  504. }
  505. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  506. console.groupCollapsed(title);
  507. if(console.table) {
  508. console.table(performance);
  509. }
  510. else {
  511. $.each(performance, function(index, data) {
  512. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  513. });
  514. }
  515. console.groupEnd();
  516. }
  517. performance = [];
  518. }
  519. },
  520. invoke: function(query, passedArguments, context) {
  521. var
  522. maxDepth,
  523. found,
  524. response
  525. ;
  526. passedArguments = passedArguments || queryArguments;
  527. context = element || context;
  528. if(typeof query == 'string' && instance !== undefined) {
  529. query = query.split(/[\. ]/);
  530. maxDepth = query.length - 1;
  531. $.each(query, function(depth, value) {
  532. var camelCaseValue = (depth != maxDepth)
  533. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  534. : query
  535. ;
  536. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  537. instance = instance[value];
  538. }
  539. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  540. instance = instance[camelCaseValue];
  541. }
  542. else if( instance[value] !== undefined ) {
  543. found = instance[value];
  544. return false;
  545. }
  546. else if( instance[camelCaseValue] !== undefined ) {
  547. found = instance[camelCaseValue];
  548. return false;
  549. }
  550. else {
  551. module.error(error.method);
  552. return false;
  553. }
  554. });
  555. }
  556. if ( $.isFunction( found ) ) {
  557. response = found.apply(context, passedArguments);
  558. }
  559. else if(found !== undefined) {
  560. response = found;
  561. }
  562. if($.isArray(invokedResponse)) {
  563. invokedResponse.push(response);
  564. }
  565. else if(typeof invokedResponse == 'string') {
  566. invokedResponse = [invokedResponse, response];
  567. }
  568. else if(response !== undefined) {
  569. invokedResponse = response;
  570. }
  571. return found;
  572. }
  573. };
  574. if(methodInvoked) {
  575. if(instance === undefined) {
  576. module.initialize();
  577. }
  578. module.invoke(query);
  579. }
  580. else {
  581. if(instance !== undefined) {
  582. module.destroy();
  583. }
  584. module.initialize();
  585. }
  586. })
  587. ;
  588. return (invokedResponse !== undefined)
  589. ? invokedResponse
  590. : this
  591. ;
  592. };
  593. $.fn.state.settings = {
  594. // module info
  595. moduleName : 'State',
  596. // debug output
  597. debug : true,
  598. // verbose debug output
  599. verbose : true,
  600. // namespace for events
  601. namespace : 'state',
  602. // debug data includes performance
  603. performance: true,
  604. // callback occurs on state change
  605. onActivate : function() {},
  606. onDeactivate : function() {},
  607. onChange : function() {},
  608. // state test functions
  609. activateTest : function() { return true; },
  610. deactivateTest : function() { return true; },
  611. // whether to automatically map default states
  612. automatic : true,
  613. // activate / deactivate changes all elements instantiated at same time
  614. sync : false,
  615. // default flash text duration, used for temporarily changing text of an element
  616. flashDuration : 3000,
  617. // selector filter
  618. filter : {
  619. text : '.loading, .disabled',
  620. active : '.disabled'
  621. },
  622. context : false,
  623. // error
  624. error: {
  625. method : 'The method you called is not defined.'
  626. },
  627. // metadata
  628. metadata: {
  629. promise : 'promise',
  630. storedText : 'stored-text'
  631. },
  632. // change class on state
  633. className: {
  634. focus : 'focus',
  635. hover : 'hover',
  636. down : 'down',
  637. active : 'active',
  638. loading : 'loading'
  639. },
  640. selector: {
  641. // selector for text node
  642. text: false
  643. },
  644. defaults : {
  645. input: {
  646. hover : true,
  647. focus : true,
  648. down : true,
  649. loading : false,
  650. active : false
  651. },
  652. button: {
  653. hover : true,
  654. focus : false,
  655. down : true,
  656. active : true,
  657. loading : true
  658. }
  659. },
  660. states : {
  661. hover : true,
  662. focus : true,
  663. down : true,
  664. loading : false,
  665. active : false
  666. },
  667. text : {
  668. flash : false,
  669. hover : false,
  670. active : false,
  671. inactive : false,
  672. enable : false,
  673. disable : false
  674. }
  675. };
  676. })( jQuery, window , document );