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.

752 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.name + ':');
  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.name + ':');
  461. module.verbose.apply(console, arguments);
  462. }
  463. }
  464. },
  465. error: function() {
  466. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  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.name + ':',
  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($allModules.size() > 1) {
  506. title += ' ' + '(' + $allModules.size() + ')';
  507. }
  508. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  509. console.groupCollapsed(title);
  510. if(console.table) {
  511. console.table(performance);
  512. }
  513. else {
  514. $.each(performance, function(index, data) {
  515. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  516. });
  517. }
  518. console.groupEnd();
  519. }
  520. performance = [];
  521. }
  522. },
  523. invoke: function(query, passedArguments, context) {
  524. var
  525. maxDepth,
  526. found,
  527. response
  528. ;
  529. passedArguments = passedArguments || queryArguments;
  530. context = element || context;
  531. if(typeof query == 'string' && instance !== undefined) {
  532. query = query.split(/[\. ]/);
  533. maxDepth = query.length - 1;
  534. $.each(query, function(depth, value) {
  535. var camelCaseValue = (depth != maxDepth)
  536. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  537. : query
  538. ;
  539. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  540. instance = instance[value];
  541. }
  542. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  543. instance = instance[camelCaseValue];
  544. }
  545. else if( instance[value] !== undefined ) {
  546. found = instance[value];
  547. return false;
  548. }
  549. else if( instance[camelCaseValue] !== undefined ) {
  550. found = instance[camelCaseValue];
  551. return false;
  552. }
  553. else {
  554. module.error(error.method);
  555. return false;
  556. }
  557. });
  558. }
  559. if ( $.isFunction( found ) ) {
  560. response = found.apply(context, passedArguments);
  561. }
  562. else if(found !== undefined) {
  563. response = found;
  564. }
  565. if($.isArray(invokedResponse)) {
  566. invokedResponse.push(response);
  567. }
  568. else if(typeof invokedResponse == 'string') {
  569. invokedResponse = [invokedResponse, response];
  570. }
  571. else if(response !== undefined) {
  572. invokedResponse = response;
  573. }
  574. return found;
  575. }
  576. };
  577. if(methodInvoked) {
  578. if(instance === undefined) {
  579. module.initialize();
  580. }
  581. module.invoke(query);
  582. }
  583. else {
  584. if(instance !== undefined) {
  585. module.destroy();
  586. }
  587. module.initialize();
  588. }
  589. })
  590. ;
  591. return (invokedResponse !== undefined)
  592. ? invokedResponse
  593. : this
  594. ;
  595. };
  596. $.fn.state.settings = {
  597. // module info
  598. name : 'State',
  599. // debug output
  600. debug : true,
  601. // verbose debug output
  602. verbose : true,
  603. // namespace for events
  604. namespace : 'state',
  605. // debug data includes performance
  606. performance: true,
  607. // callback occurs on state change
  608. onActivate : function() {},
  609. onDeactivate : function() {},
  610. onChange : function() {},
  611. // state test functions
  612. activateTest : function() { return true; },
  613. deactivateTest : function() { return true; },
  614. // whether to automatically map default states
  615. automatic : true,
  616. // activate / deactivate changes all elements instantiated at same time
  617. sync : false,
  618. // default flash text duration, used for temporarily changing text of an element
  619. flashDuration : 3000,
  620. // selector filter
  621. filter : {
  622. text : '.loading, .disabled',
  623. active : '.disabled'
  624. },
  625. context : false,
  626. // error
  627. error: {
  628. method : 'The method you called is not defined.'
  629. },
  630. // metadata
  631. metadata: {
  632. promise : 'promise',
  633. storedText : 'stored-text'
  634. },
  635. // change class on state
  636. className: {
  637. focus : 'focus',
  638. hover : 'hover',
  639. down : 'down',
  640. active : 'active',
  641. loading : 'loading'
  642. },
  643. selector: {
  644. // selector for text node
  645. text: false
  646. },
  647. defaults : {
  648. input: {
  649. hover : true,
  650. focus : true,
  651. down : true,
  652. loading : false,
  653. active : false
  654. },
  655. button: {
  656. hover : true,
  657. focus : false,
  658. down : true,
  659. active : true,
  660. loading : true
  661. }
  662. },
  663. states : {
  664. hover : true,
  665. focus : true,
  666. down : true,
  667. loading : false,
  668. active : false
  669. },
  670. text : {
  671. flash : false,
  672. hover : false,
  673. active : false,
  674. inactive : false,
  675. enable : false,
  676. disable : false
  677. }
  678. };
  679. })( jQuery, window , document );