PageRenderTime 59ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/files/sammy/0.7.4/sammy.js

https://gitlab.com/Mirros/jsdelivr
JavaScript | 1347 lines | 1336 code | 3 blank | 8 comment | 4 complexity | a8867928d464a514db977e54c6c4d9d0 MD5 | raw file
  1. // name: sammy
  2. // version: 0.7.4
  3. // Sammy.js / http://sammyjs.org
  4. (function($, window) {
  5. (function(factory){
  6. // Support module loading scenarios
  7. if (typeof define === 'function' && define.amd){
  8. // AMD Anonymous Module
  9. define(['jquery'], factory);
  10. } else {
  11. // No module loader (plain <script> tag) - put directly in global namespace
  12. $.sammy = window.Sammy = factory($);
  13. }
  14. })(function($){
  15. var Sammy,
  16. PATH_REPLACER = "([^\/]+)",
  17. PATH_NAME_MATCHER = /:([\w\d]+)/g,
  18. QUERY_STRING_MATCHER = /\?([^#]*)?$/,
  19. // mainly for making `arguments` an Array
  20. _makeArray = function(nonarray) { return Array.prototype.slice.call(nonarray); },
  21. // borrowed from jQuery
  22. _isFunction = function( obj ) { return Object.prototype.toString.call(obj) === "[object Function]"; },
  23. _isArray = function( obj ) { return Object.prototype.toString.call(obj) === "[object Array]"; },
  24. _isRegExp = function( obj ) { return Object.prototype.toString.call(obj) === "[object RegExp]"; },
  25. _decode = function( str ) { return decodeURIComponent((str || '').replace(/\+/g, ' ')); },
  26. _encode = encodeURIComponent,
  27. _escapeHTML = function(s) {
  28. return String(s).replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  29. },
  30. _routeWrapper = function(verb) {
  31. return function() {
  32. return this.route.apply(this, [verb].concat(Array.prototype.slice.call(arguments)));
  33. };
  34. },
  35. _template_cache = {},
  36. _has_history = !!(window.history && history.pushState),
  37. loggers = [];
  38. // `Sammy` (also aliased as $.sammy) is not only the namespace for a
  39. // number of prototypes, its also a top level method that allows for easy
  40. // creation/management of `Sammy.Application` instances. There are a
  41. // number of different forms for `Sammy()` but each returns an instance
  42. // of `Sammy.Application`. When a new instance is created using
  43. // `Sammy` it is added to an Object called `Sammy.apps`. This
  44. // provides for an easy way to get at existing Sammy applications. Only one
  45. // instance is allowed per `element_selector` so when calling
  46. // `Sammy('selector')` multiple times, the first time will create
  47. // the application and the following times will extend the application
  48. // already added to that selector.
  49. //
  50. // ### Example
  51. //
  52. // // returns the app at #main or a new app
  53. // Sammy('#main')
  54. //
  55. // // equivalent to "new Sammy.Application", except appends to apps
  56. // Sammy();
  57. // Sammy(function() { ... });
  58. //
  59. // // extends the app at '#main' with function.
  60. // Sammy('#main', function() { ... });
  61. //
  62. Sammy = function() {
  63. var args = _makeArray(arguments),
  64. app, selector;
  65. Sammy.apps = Sammy.apps || {};
  66. if (args.length === 0 || args[0] && _isFunction(args[0])) { // Sammy()
  67. return Sammy.apply(Sammy, ['body'].concat(args));
  68. } else if (typeof (selector = args.shift()) == 'string') { // Sammy('#main')
  69. app = Sammy.apps[selector] || new Sammy.Application();
  70. app.element_selector = selector;
  71. if (args.length > 0) {
  72. $.each(args, function(i, plugin) {
  73. app.use(plugin);
  74. });
  75. }
  76. // if the selector changes make sure the reference in Sammy.apps changes
  77. if (app.element_selector != selector) {
  78. delete Sammy.apps[selector];
  79. }
  80. Sammy.apps[app.element_selector] = app;
  81. return app;
  82. }
  83. };
  84. Sammy.VERSION = '0.7.4';
  85. // Add to the global logger pool. Takes a function that accepts an
  86. // unknown number of arguments and should print them or send them somewhere
  87. // The first argument is always a timestamp.
  88. Sammy.addLogger = function(logger) {
  89. loggers.push(logger);
  90. };
  91. // Sends a log message to each logger listed in the global
  92. // loggers pool. Can take any number of arguments.
  93. // Also prefixes the arguments with a timestamp.
  94. Sammy.log = function() {
  95. var args = _makeArray(arguments);
  96. args.unshift("[" + Date() + "]");
  97. $.each(loggers, function(i, logger) {
  98. logger.apply(Sammy, args);
  99. });
  100. };
  101. if (typeof window.console != 'undefined') {
  102. if (_isFunction(window.console.log.apply)) {
  103. Sammy.addLogger(function() {
  104. window.console.log.apply(window.console, arguments);
  105. });
  106. } else {
  107. Sammy.addLogger(function() {
  108. window.console.log(arguments);
  109. });
  110. }
  111. } else if (typeof console != 'undefined') {
  112. Sammy.addLogger(function() {
  113. console.log.apply(console, arguments);
  114. });
  115. }
  116. $.extend(Sammy, {
  117. makeArray: _makeArray,
  118. isFunction: _isFunction,
  119. isArray: _isArray
  120. });
  121. // Sammy.Object is the base for all other Sammy classes. It provides some useful
  122. // functionality, including cloning, iterating, etc.
  123. Sammy.Object = function(obj) { // constructor
  124. return $.extend(this, obj || {});
  125. };
  126. $.extend(Sammy.Object.prototype, {
  127. // Escape HTML in string, use in templates to prevent script injection.
  128. // Also aliased as `h()`
  129. escapeHTML: _escapeHTML,
  130. h: _escapeHTML,
  131. // Returns a copy of the object with Functions removed.
  132. toHash: function() {
  133. var json = {};
  134. $.each(this, function(k,v) {
  135. if (!_isFunction(v)) {
  136. json[k] = v;
  137. }
  138. });
  139. return json;
  140. },
  141. // Renders a simple HTML version of this Objects attributes.
  142. // Does not render functions.
  143. // For example. Given this Sammy.Object:
  144. //
  145. // var s = new Sammy.Object({first_name: 'Sammy', last_name: 'Davis Jr.'});
  146. // s.toHTML()
  147. // //=> '<strong>first_name</strong> Sammy<br /><strong>last_name</strong> Davis Jr.<br />'
  148. //
  149. toHTML: function() {
  150. var display = "";
  151. $.each(this, function(k, v) {
  152. if (!_isFunction(v)) {
  153. display += "<strong>" + k + "</strong> " + v + "<br />";
  154. }
  155. });
  156. return display;
  157. },
  158. // Returns an array of keys for this object. If `attributes_only`
  159. // is true will not return keys that map to a `function()`
  160. keys: function(attributes_only) {
  161. var keys = [];
  162. for (var property in this) {
  163. if (!_isFunction(this[property]) || !attributes_only) {
  164. keys.push(property);
  165. }
  166. }
  167. return keys;
  168. },
  169. // Checks if the object has a value at `key` and that the value is not empty
  170. has: function(key) {
  171. return this[key] && $.trim(this[key].toString()) !== '';
  172. },
  173. // convenience method to join as many arguments as you want
  174. // by the first argument - useful for making paths
  175. join: function() {
  176. var args = _makeArray(arguments);
  177. var delimiter = args.shift();
  178. return args.join(delimiter);
  179. },
  180. // Shortcut to Sammy.log
  181. log: function() {
  182. Sammy.log.apply(Sammy, arguments);
  183. },
  184. // Returns a string representation of this object.
  185. // if `include_functions` is true, it will also toString() the
  186. // methods of this object. By default only prints the attributes.
  187. toString: function(include_functions) {
  188. var s = [];
  189. $.each(this, function(k, v) {
  190. if (!_isFunction(v) || include_functions) {
  191. s.push('"' + k + '": ' + v.toString());
  192. }
  193. });
  194. return "Sammy.Object: {" + s.join(',') + "}";
  195. }
  196. });
  197. // Return whether the event targets this window.
  198. Sammy.targetIsThisWindow = function targetIsThisWindow(event) {
  199. var targetWindow = $(event.target).attr('target');
  200. if ( !targetWindow || targetWindow === window.name || targetWindow === '_self' ) { return true; }
  201. if ( targetWindow === '_blank' ) { return false; }
  202. if ( targetWindow === 'top' && window === window.top ) { return true; }
  203. return false;
  204. };
  205. // The DefaultLocationProxy is the default location proxy for all Sammy applications.
  206. // A location proxy is a prototype that conforms to a simple interface. The purpose
  207. // of a location proxy is to notify the Sammy.Application its bound to when the location
  208. // or 'external state' changes.
  209. //
  210. // The `DefaultLocationProxy` watches for changes to the path of the current window and
  211. // is also able to set the path based on changes in the application. It does this by
  212. // using different methods depending on what is available in the current browser. In
  213. // the latest and greatest browsers it used the HTML5 History API and the `pushState`
  214. // `popState` events/methods. This allows you to use Sammy to serve a site behind normal
  215. // URI paths as opposed to the older default of hash (#) based routing. Because the server
  216. // can interpret the changed path on a refresh or re-entry, though, it requires additional
  217. // support on the server side. If you'd like to force disable HTML5 history support, please
  218. // use the `disable_push_state` setting on `Sammy.Application`. If pushState support
  219. // is enabled, `DefaultLocationProxy` also binds to all links on the page. If a link is clicked
  220. // that matches the current set of routes, the URL is changed using pushState instead of
  221. // fully setting the location and the app is notified of the change.
  222. //
  223. // If the browser does not have support for HTML5 History, `DefaultLocationProxy` automatically
  224. // falls back to the older hash based routing. The newest browsers (IE, Safari > 4, FF >= 3.6)
  225. // support a 'onhashchange' DOM event, thats fired whenever the location.hash changes.
  226. // In this situation the DefaultLocationProxy just binds to this event and delegates it to
  227. // the application. In the case of older browsers a poller is set up to track changes to the
  228. // hash.
  229. Sammy.DefaultLocationProxy = function(app, run_interval_every) {
  230. this.app = app;
  231. // set is native to false and start the poller immediately
  232. this.is_native = false;
  233. this.has_history = _has_history;
  234. this._startPolling(run_interval_every);
  235. };
  236. Sammy.DefaultLocationProxy.fullPath = function(location_obj) {
  237. // Bypass the `window.location.hash` attribute. If a question mark
  238. // appears in the hash IE6 will strip it and all of the following
  239. // characters from `window.location.hash`.
  240. var matches = location_obj.toString().match(/^[^#]*(#.+)$/);
  241. var hash = matches ? matches[1] : '';
  242. return [location_obj.pathname, location_obj.search, hash].join('');
  243. };
  244. $.extend(Sammy.DefaultLocationProxy.prototype , {
  245. // bind the proxy events to the current app.
  246. bind: function() {
  247. var proxy = this, app = this.app, lp = Sammy.DefaultLocationProxy;
  248. $(window).bind('hashchange.' + this.app.eventNamespace(), function(e, non_native) {
  249. // if we receive a native hash change event, set the proxy accordingly
  250. // and stop polling
  251. if (proxy.is_native === false && !non_native) {
  252. proxy.is_native = true;
  253. window.clearInterval(lp._interval);
  254. lp._interval = null;
  255. }
  256. app.trigger('location-changed');
  257. });
  258. if (_has_history && !app.disable_push_state) {
  259. // bind to popstate
  260. $(window).bind('popstate.' + this.app.eventNamespace(), function(e) {
  261. app.trigger('location-changed');
  262. });
  263. // bind to link clicks that have routes
  264. $(document).delegate('a', 'click.history-' + this.app.eventNamespace(), function (e) {
  265. if (e.isDefaultPrevented() || e.metaKey || e.ctrlKey) {
  266. return;
  267. }
  268. var full_path = lp.fullPath(this);
  269. if (this.hostname == window.location.hostname &&
  270. app.lookupRoute('get', full_path) &&
  271. Sammy.targetIsThisWindow(e)) {
  272. e.preventDefault();
  273. proxy.setLocation(full_path);
  274. return false;
  275. }
  276. });
  277. }
  278. if (!lp._bindings) {
  279. lp._bindings = 0;
  280. }
  281. lp._bindings++;
  282. },
  283. // unbind the proxy events from the current app
  284. unbind: function() {
  285. $(window).unbind('hashchange.' + this.app.eventNamespace());
  286. $(window).unbind('popstate.' + this.app.eventNamespace());
  287. $(document).undelegate('a', 'click.history-' + this.app.eventNamespace());
  288. Sammy.DefaultLocationProxy._bindings--;
  289. if (Sammy.DefaultLocationProxy._bindings <= 0) {
  290. window.clearInterval(Sammy.DefaultLocationProxy._interval);
  291. Sammy.DefaultLocationProxy._interval = null;
  292. }
  293. },
  294. // get the current location from the hash.
  295. getLocation: function() {
  296. return Sammy.DefaultLocationProxy.fullPath(window.location);
  297. },
  298. // set the current location to `new_location`
  299. setLocation: function(new_location) {
  300. if (/^([^#\/]|$)/.test(new_location)) { // non-prefixed url
  301. if (_has_history && !this.app.disable_push_state) {
  302. new_location = '/' + new_location;
  303. } else {
  304. new_location = '#!/' + new_location;
  305. }
  306. }
  307. if (new_location != this.getLocation()) {
  308. // HTML5 History exists and new_location is a full path
  309. if (_has_history && !this.app.disable_push_state && /^\//.test(new_location)) {
  310. history.pushState({ path: new_location }, window.title, new_location);
  311. this.app.trigger('location-changed');
  312. } else {
  313. return (window.location = new_location);
  314. }
  315. }
  316. },
  317. _startPolling: function(every) {
  318. // set up interval
  319. var proxy = this;
  320. if (!Sammy.DefaultLocationProxy._interval) {
  321. if (!every) { every = 10; }
  322. var hashCheck = function() {
  323. var current_location = proxy.getLocation();
  324. if (typeof Sammy.DefaultLocationProxy._last_location == 'undefined' ||
  325. current_location != Sammy.DefaultLocationProxy._last_location) {
  326. window.setTimeout(function() {
  327. $(window).trigger('hashchange', [true]);
  328. }, 0);
  329. }
  330. Sammy.DefaultLocationProxy._last_location = current_location;
  331. };
  332. hashCheck();
  333. Sammy.DefaultLocationProxy._interval = window.setInterval(hashCheck, every);
  334. }
  335. }
  336. });
  337. // Sammy.Application is the Base prototype for defining 'applications'.
  338. // An 'application' is a collection of 'routes' and bound events that is
  339. // attached to an element when `run()` is called.
  340. // The only argument an 'app_function' is evaluated within the context of the application.
  341. Sammy.Application = function(app_function) {
  342. var app = this;
  343. this.routes = {};
  344. this.listeners = new Sammy.Object({});
  345. this.arounds = [];
  346. this.befores = [];
  347. // generate a unique namespace
  348. this.namespace = (new Date()).getTime() + '-' + parseInt(Math.random() * 1000, 10);
  349. this.context_prototype = function() { Sammy.EventContext.apply(this, arguments); };
  350. this.context_prototype.prototype = new Sammy.EventContext();
  351. if (_isFunction(app_function)) {
  352. app_function.apply(this, [this]);
  353. }
  354. // set the location proxy if not defined to the default (DefaultLocationProxy)
  355. if (!this._location_proxy) {
  356. this.setLocationProxy(new Sammy.DefaultLocationProxy(this, this.run_interval_every));
  357. }
  358. if (this.debug) {
  359. this.bindToAllEvents(function(e, data) {
  360. app.log(app.toString(), e.cleaned_type, data || {});
  361. });
  362. }
  363. };
  364. Sammy.Application.prototype = $.extend({}, Sammy.Object.prototype, {
  365. // the four route verbs
  366. ROUTE_VERBS: ['get','post','put','delete'],
  367. // An array of the default events triggered by the
  368. // application during its lifecycle
  369. APP_EVENTS: ['run', 'unload', 'lookup-route', 'run-route', 'route-found', 'event-context-before', 'event-context-after', 'changed', 'error', 'check-form-submission', 'redirect', 'location-changed'],
  370. _last_route: null,
  371. _location_proxy: null,
  372. _running: false,
  373. // Defines what element the application is bound to. Provide a selector
  374. // (parseable by `jQuery()`) and this will be used by `$element()`
  375. element_selector: 'body',
  376. // When set to true, logs all of the default events using `log()`
  377. debug: false,
  378. // When set to true, and the error() handler is not overridden, will actually
  379. // raise JS errors in routes (500) and when routes can't be found (404)
  380. raise_errors: false,
  381. // The time in milliseconds that the URL is queried for changes
  382. run_interval_every: 50,
  383. // if using the `DefaultLocationProxy` setting this to true will force the app to use
  384. // traditional hash based routing as opposed to the new HTML5 PushState support
  385. disable_push_state: false,
  386. // The default template engine to use when using `partial()` in an
  387. // `EventContext`. `template_engine` can either be a string that
  388. // corresponds to the name of a method/helper on EventContext or it can be a function
  389. // that takes two arguments, the content of the unrendered partial and an optional
  390. // JS object that contains interpolation data. Template engine is only called/referred
  391. // to if the extension of the partial is null or unknown. See `partial()`
  392. // for more information
  393. template_engine: null,
  394. // //=> Sammy.Application: body
  395. toString: function() {
  396. return 'Sammy.Application:' + this.element_selector;
  397. },
  398. // returns a jQuery object of the Applications bound element.
  399. $element: function(selector) {
  400. return selector ? $(this.element_selector).find(selector) : $(this.element_selector);
  401. },
  402. // `use()` is the entry point for including Sammy plugins.
  403. // The first argument to use should be a function() that is evaluated
  404. // in the context of the current application, just like the `app_function`
  405. // argument to the `Sammy.Application` constructor.
  406. //
  407. // Any additional arguments are passed to the app function sequentially.
  408. //
  409. // For much more detail about plugins, check out:
  410. // [http://sammyjs.org/docs/plugins](http://sammyjs.org/docs/plugins)
  411. //
  412. // ### Example
  413. //
  414. // var MyPlugin = function(app, prepend) {
  415. //
  416. // this.helpers({
  417. // myhelper: function(text) {
  418. // alert(prepend + " " + text);
  419. // }
  420. // });
  421. //
  422. // };
  423. //
  424. // var app = $.sammy(function() {
  425. //
  426. // this.use(MyPlugin, 'This is my plugin');
  427. //
  428. // this.get('#/', function() {
  429. // this.myhelper('and dont you forget it!');
  430. // //=> Alerts: This is my plugin and dont you forget it!
  431. // });
  432. //
  433. // });
  434. //
  435. // If plugin is passed as a string it assumes your are trying to load
  436. // Sammy."Plugin". This is the preferred way of loading core Sammy plugins
  437. // as it allows for better error-messaging.
  438. //
  439. // ### Example
  440. //
  441. // $.sammy(function() {
  442. // this.use('Mustache'); //=> Sammy.Mustache
  443. // this.use('Storage'); //=> Sammy.Storage
  444. // });
  445. //
  446. use: function() {
  447. // flatten the arguments
  448. var args = _makeArray(arguments),
  449. plugin = args.shift(),
  450. plugin_name = plugin || '';
  451. try {
  452. args.unshift(this);
  453. if (typeof plugin == 'string') {
  454. plugin_name = 'Sammy.' + plugin;
  455. plugin = Sammy[plugin];
  456. }
  457. plugin.apply(this, args);
  458. } catch(e) {
  459. if (typeof plugin === 'undefined') {
  460. this.error("Plugin Error: called use() but plugin (" + plugin_name.toString() + ") is not defined", e);
  461. } else if (!_isFunction(plugin)) {
  462. this.error("Plugin Error: called use() but '" + plugin_name.toString() + "' is not a function", e);
  463. } else {
  464. this.error("Plugin Error", e);
  465. }
  466. }
  467. return this;
  468. },
  469. // Sets the location proxy for the current app. By default this is set to
  470. // a new `Sammy.DefaultLocationProxy` on initialization. However, you can set
  471. // the location_proxy inside you're app function to give your app a custom
  472. // location mechanism. See `Sammy.DefaultLocationProxy` and `Sammy.DataLocationProxy`
  473. // for examples.
  474. //
  475. // `setLocationProxy()` takes an initialized location proxy.
  476. //
  477. // ### Example
  478. //
  479. // // to bind to data instead of the default hash;
  480. // var app = $.sammy(function() {
  481. // this.setLocationProxy(new Sammy.DataLocationProxy(this));
  482. // });
  483. //
  484. setLocationProxy: function(new_proxy) {
  485. var original_proxy = this._location_proxy;
  486. this._location_proxy = new_proxy;
  487. if (this.isRunning()) {
  488. if (original_proxy) {
  489. // if there is already a location proxy, unbind it.
  490. original_proxy.unbind();
  491. }
  492. this._location_proxy.bind();
  493. }
  494. },
  495. // provide log() override for inside an app that includes the relevant application element_selector
  496. log: function() {
  497. Sammy.log.apply(Sammy, Array.prototype.concat.apply([this.element_selector],arguments));
  498. },
  499. // `route()` is the main method for defining routes within an application.
  500. // For great detail on routes, check out:
  501. // [http://sammyjs.org/docs/routes](http://sammyjs.org/docs/routes)
  502. //
  503. // This method also has aliases for each of the different verbs (eg. `get()`, `post()`, etc.)
  504. //
  505. // ### Arguments
  506. //
  507. // * `verb` A String in the set of ROUTE_VERBS or 'any'. 'any' will add routes for each
  508. // of the ROUTE_VERBS. If only two arguments are passed,
  509. // the first argument is the path, the second is the callback and the verb
  510. // is assumed to be 'any'.
  511. // * `path` A Regexp or a String representing the path to match to invoke this verb.
  512. // * `callback` A Function that is called/evaluated when the route is run see: `runRoute()`.
  513. // It is also possible to pass a string as the callback, which is looked up as the name
  514. // of a method on the application.
  515. //
  516. route: function(verb, path) {
  517. var app = this, param_names = [], add_route, path_match, callback = Array.prototype.slice.call(arguments,2);
  518. // if the method signature is just (path, callback)
  519. // assume the verb is 'any'
  520. if (callback.length === 0 && _isFunction(path)) {
  521. path = verb;
  522. callback = [path];
  523. verb = 'any';
  524. }
  525. verb = verb.toLowerCase(); // ensure verb is lower case
  526. // if path is a string turn it into a regex
  527. if (path.constructor == String) {
  528. // Needs to be explicitly set because IE will maintain the index unless NULL is returned,
  529. // which means that with two consecutive routes that contain params, the second set of params will not be found and end up in splat instead of params
  530. // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/RegExp/lastIndex
  531. PATH_NAME_MATCHER.lastIndex = 0;
  532. // find the names
  533. while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) {
  534. param_names.push(path_match[1]);
  535. }
  536. // replace with the path replacement
  537. path = new RegExp(path.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$");
  538. }
  539. // lookup callbacks
  540. $.each(callback,function(i,cb){
  541. if (typeof(cb) === 'string') {
  542. callback[i] = app[cb];
  543. }
  544. });
  545. add_route = function(with_verb) {
  546. var r = {verb: with_verb, path: path, callback: callback, param_names: param_names};
  547. // add route to routes array
  548. app.routes[with_verb] = app.routes[with_verb] || [];
  549. // place routes in order of definition
  550. app.routes[with_verb].push(r);
  551. };
  552. if (verb === 'any') {
  553. $.each(this.ROUTE_VERBS, function(i, v) { add_route(v); });
  554. } else {
  555. add_route(verb);
  556. }
  557. // return the app
  558. return this;
  559. },
  560. // Alias for route('get', ...)
  561. get: _routeWrapper('get'),
  562. // Alias for route('post', ...)
  563. post: _routeWrapper('post'),
  564. // Alias for route('put', ...)
  565. put: _routeWrapper('put'),
  566. // Alias for route('delete', ...)
  567. del: _routeWrapper('delete'),
  568. // Alias for route('any', ...)
  569. any: _routeWrapper('any'),
  570. // `mapRoutes` takes an array of arrays, each array being passed to route()
  571. // as arguments, this allows for mass definition of routes. Another benefit is
  572. // this makes it possible/easier to load routes via remote JSON.
  573. //
  574. // ### Example
  575. //
  576. // var app = $.sammy(function() {
  577. //
  578. // this.mapRoutes([
  579. // ['get', '#/', function() { this.log('index'); }],
  580. // // strings in callbacks are looked up as methods on the app
  581. // ['post', '#/create', 'addUser'],
  582. // // No verb assumes 'any' as the verb
  583. // [/dowhatever/, function() { this.log(this.verb, this.path)}];
  584. // ]);
  585. // });
  586. //
  587. mapRoutes: function(route_array) {
  588. var app = this;
  589. $.each(route_array, function(i, route_args) {
  590. app.route.apply(app, route_args);
  591. });
  592. return this;
  593. },
  594. // A unique event namespace defined per application.
  595. // All events bound with `bind()` are automatically bound within this space.
  596. eventNamespace: function() {
  597. return ['sammy-app', this.namespace].join('-');
  598. },
  599. // Works just like `jQuery.fn.bind()` with a couple notable differences.
  600. //
  601. // * It binds all events to the application element
  602. // * All events are bound within the `eventNamespace()`
  603. // * Events are not actually bound until the application is started with `run()`
  604. // * callbacks are evaluated within the context of a Sammy.EventContext
  605. //
  606. bind: function(name, data, callback) {
  607. var app = this;
  608. // build the callback
  609. // if the arity is 2, callback is the second argument
  610. if (typeof callback == 'undefined') { callback = data; }
  611. var listener_callback = function() {
  612. // pull off the context from the arguments to the callback
  613. var e, context, data;
  614. e = arguments[0];
  615. data = arguments[1];
  616. if (data && data.context) {
  617. context = data.context;
  618. delete data.context;
  619. } else {
  620. context = new app.context_prototype(app, 'bind', e.type, data, e.target);
  621. }
  622. e.cleaned_type = e.type.replace(app.eventNamespace(), '');
  623. callback.apply(context, [e, data]);
  624. };
  625. // it could be that the app element doesnt exist yet
  626. // so attach to the listeners array and then run()
  627. // will actually bind the event.
  628. if (!this.listeners[name]) { this.listeners[name] = []; }
  629. this.listeners[name].push(listener_callback);
  630. if (this.isRunning()) {
  631. // if the app is running
  632. // *actually* bind the event to the app element
  633. this._listen(name, listener_callback);
  634. }
  635. return this;
  636. },
  637. // Triggers custom events defined with `bind()`
  638. //
  639. // ### Arguments
  640. //
  641. // * `name` The name of the event. Automatically prefixed with the `eventNamespace()`
  642. // * `data` An optional Object that can be passed to the bound callback.
  643. // * `context` An optional context/Object in which to execute the bound callback.
  644. // If no context is supplied a the context is a new `Sammy.EventContext`
  645. //
  646. trigger: function(name, data) {
  647. this.$element().trigger([name, this.eventNamespace()].join('.'), [data]);
  648. return this;
  649. },
  650. // Reruns the current route
  651. refresh: function() {
  652. this.last_location = null;
  653. this.trigger('location-changed');
  654. return this;
  655. },
  656. // Takes a single callback that is pushed on to a stack.
  657. // Before any route is run, the callbacks are evaluated in order within
  658. // the current `Sammy.EventContext`
  659. //
  660. // If any of the callbacks explicitly return false, execution of any
  661. // further callbacks and the route itself is halted.
  662. //
  663. // You can also provide a set of options that will define when to run this
  664. // before based on the route it proceeds.
  665. //
  666. // ### Example
  667. //
  668. // var app = $.sammy(function() {
  669. //
  670. // // will run at #/route but not at #/
  671. // this.before('#/route', function() {
  672. // //...
  673. // });
  674. //
  675. // // will run at #/ but not at #/route
  676. // this.before({except: {path: '#/route'}}, function() {
  677. // this.log('not before #/route');
  678. // });
  679. //
  680. // this.get('#/', function() {});
  681. //
  682. // this.get('#/route', function() {});
  683. //
  684. // });
  685. //
  686. // See `contextMatchesOptions()` for a full list of supported options
  687. //
  688. before: function(options, callback) {
  689. if (_isFunction(options)) {
  690. callback = options;
  691. options = {};
  692. }
  693. this.befores.push([options, callback]);
  694. return this;
  695. },
  696. // A shortcut for binding a callback to be run after a route is executed.
  697. // After callbacks have no guarunteed order.
  698. after: function(callback) {
  699. return this.bind('event-context-after', callback);
  700. },
  701. // Adds an around filter to the application. around filters are functions
  702. // that take a single argument `callback` which is the entire route
  703. // execution path wrapped up in a closure. This means you can decide whether
  704. // or not to proceed with execution by not invoking `callback` or,
  705. // more usefully wrapping callback inside the result of an asynchronous execution.
  706. //
  707. // ### Example
  708. //
  709. // The most common use case for around() is calling a _possibly_ async function
  710. // and executing the route within the functions callback:
  711. //
  712. // var app = $.sammy(function() {
  713. //
  714. // var current_user = false;
  715. //
  716. // function checkLoggedIn(callback) {
  717. // // /session returns a JSON representation of the logged in user
  718. // // or an empty object
  719. // if (!current_user) {
  720. // $.getJSON('/session', function(json) {
  721. // if (json.login) {
  722. // // show the user as logged in
  723. // current_user = json;
  724. // // execute the route path
  725. // callback();
  726. // } else {
  727. // // show the user as not logged in
  728. // current_user = false;
  729. // // the context of aroundFilters is an EventContext
  730. // this.redirect('#/login');
  731. // }
  732. // });
  733. // } else {
  734. // // execute the route path
  735. // callback();
  736. // }
  737. // };
  738. //
  739. // this.around(checkLoggedIn);
  740. //
  741. // });
  742. //
  743. around: function(callback) {
  744. this.arounds.push(callback);
  745. return this;
  746. },
  747. // Adds a onComplete function to the application. onComplete functions are executed
  748. // at the end of a chain of route callbacks, if they call next(). Unlike after,
  749. // which is called as soon as the route is complete, onComplete is like a final next()
  750. // for all routes, and is thus run asynchronously
  751. //
  752. // ### Example
  753. //
  754. // app.get('/chain',function(context,next){
  755. // console.log('chain1');
  756. // next();
  757. // },function(context,next){
  758. // console.log('chain2');
  759. // next();
  760. // });
  761. // app.get('/link',function(context,next){
  762. // console.log('link1');
  763. // next();
  764. // },function(context,next){
  765. // console.log('link2');
  766. // next();
  767. // });
  768. // app.onComplete(function(){
  769. // console.log("Running finally")
  770. // });
  771. //
  772. // If you go to '/chain', you will get the following messages:
  773. // chain1
  774. // chain2
  775. // Running onComplete
  776. //
  777. //
  778. // If you go to /link, you will get the following messages:
  779. // link1
  780. // link2
  781. // Running onComplete
  782. //
  783. // It really comes to play when doing asynchronous:
  784. // app.get('/chain',function(context,next){
  785. // $.get('/my/url',function(){
  786. // console.log('chain1');
  787. // next();
  788. // })
  789. // },function(context,next){
  790. // console.log('chain2');
  791. // next();
  792. // });
  793. //
  794. onComplete: function(callback) {
  795. this._onComplete = callback;
  796. return this;
  797. },
  798. // Returns `true` if the current application is running.
  799. isRunning: function() {
  800. return this._running;
  801. },
  802. // Helpers extends the EventContext prototype specific to this app.
  803. // This allows you to define app specific helper functions that can be used
  804. // whenever you're inside of an event context (templates, routes, bind).
  805. //
  806. // ### Example
  807. //
  808. // var app = $.sammy(function() {
  809. //
  810. // helpers({
  811. // upcase: function(text) {
  812. // return text.toString().toUpperCase();
  813. // }
  814. // });
  815. //
  816. // get('#/', function() { with(this) {
  817. // // inside of this context I can use the helpers
  818. // $('#main').html(upcase($('#main').text());
  819. // }});
  820. //
  821. // });
  822. //
  823. //
  824. // ### Arguments
  825. //
  826. // * `extensions` An object collection of functions to extend the context.
  827. //
  828. helpers: function(extensions) {
  829. $.extend(this.context_prototype.prototype, extensions);
  830. return this;
  831. },
  832. // Helper extends the event context just like `helpers()` but does it
  833. // a single method at a time. This is especially useful for dynamically named
  834. // helpers
  835. //
  836. // ### Example
  837. //
  838. // // Trivial example that adds 3 helper methods to the context dynamically
  839. // var app = $.sammy(function(app) {
  840. //
  841. // $.each([1,2,3], function(i, num) {
  842. // app.helper('helper' + num, function() {
  843. // this.log("I'm helper number " + num);
  844. // });
  845. // });
  846. //
  847. // this.get('#/', function() {
  848. // this.helper2(); //=> I'm helper number 2
  849. // });
  850. // });
  851. //
  852. // ### Arguments
  853. //
  854. // * `name` The name of the method
  855. // * `method` The function to be added to the prototype at `name`
  856. //
  857. helper: function(name, method) {
  858. this.context_prototype.prototype[name] = method;
  859. return this;
  860. },
  861. // Actually starts the application's lifecycle. `run()` should be invoked
  862. // within a document.ready block to ensure the DOM exists before binding events, etc.
  863. //
  864. // ### Example
  865. //
  866. // var app = $.sammy(function() { ... }); // your application
  867. // $(function() { // document.ready
  868. // app.run();
  869. // });
  870. //
  871. // ### Arguments
  872. //
  873. // * `start_url` Optionally, a String can be passed which the App will redirect to
  874. // after the events/routes have been bound.
  875. run: function(start_url) {
  876. if (this.isRunning()) { return false; }
  877. var app = this;
  878. // actually bind all the listeners
  879. $.each(this.listeners.toHash(), function(name, callbacks) {
  880. $.each(callbacks, function(i, listener_callback) {
  881. app._listen(name, listener_callback);
  882. });
  883. });
  884. this.trigger('run', {start_url: start_url});
  885. this._running = true;
  886. // set last location
  887. this.last_location = null;
  888. if (!(/\#(.+)/.test(this.getLocation())) && typeof start_url != 'undefined') {
  889. this.setLocation(start_url);
  890. }
  891. // check url
  892. this._checkLocation();
  893. this._location_proxy.bind();
  894. this.bind('location-changed', function() {
  895. app._checkLocation();
  896. });
  897. // bind to submit to capture post/put/delete routes
  898. this.bind('submit', function(e) {
  899. if ( !Sammy.targetIsThisWindow(e) ) { return true; }
  900. var returned = app._checkFormSubmission($(e.target).closest('form'));
  901. return (returned === false) ? e.preventDefault() : false;
  902. });
  903. // bind unload to body unload
  904. $(window).bind('unload', function() {
  905. app.unload();
  906. });
  907. // trigger html changed
  908. return this.trigger('changed');
  909. },
  910. // The opposite of `run()`, un-binds all event listeners and intervals
  911. // `run()` Automatically binds a `onunload` event to run this when
  912. // the document is closed.
  913. unload: function() {
  914. if (!this.isRunning()) { return false; }
  915. var app = this;
  916. this.trigger('unload');
  917. // clear interval
  918. this._location_proxy.unbind();
  919. // unbind form submits
  920. this.$element().unbind('submit').removeClass(app.eventNamespace());
  921. // unbind all events
  922. $.each(this.listeners.toHash() , function(name, listeners) {
  923. $.each(listeners, function(i, listener_callback) {
  924. app._unlisten(name, listener_callback);
  925. });
  926. });
  927. this._running = false;
  928. return this;
  929. },
  930. // Not only runs `unbind` but also destroys the app reference.
  931. destroy: function() {
  932. this.unload();
  933. delete Sammy.apps[this.element_selector];
  934. return this;
  935. },
  936. // Will bind a single callback function to every event that is already
  937. // being listened to in the app. This includes all the `APP_EVENTS`
  938. // as well as any custom events defined with `bind()`.
  939. //
  940. // Used internally for debug logging.
  941. bindToAllEvents: function(callback) {
  942. var app = this;
  943. // bind to the APP_EVENTS first
  944. $.each(this.APP_EVENTS, function(i, e) {
  945. app.bind(e, callback);
  946. });
  947. // next, bind to listener names (only if they dont exist in APP_EVENTS)
  948. $.each(this.listeners.keys(true), function(i, name) {
  949. if ($.inArray(name, app.APP_EVENTS) == -1) {
  950. app.bind(name, callback);
  951. }
  952. });
  953. return this;
  954. },
  955. // Returns a copy of the given path with any query string after the hash
  956. // removed.
  957. routablePath: function(path) {
  958. return path.replace(QUERY_STRING_MATCHER, '');
  959. },
  960. // Given a verb and a String path, will return either a route object or false
  961. // if a matching route can be found within the current defined set.
  962. lookupRoute: function(verb, path) {
  963. var app = this, routed = false, i = 0, l, route;
  964. if (typeof this.routes[verb] != 'undefined') {
  965. l = this.routes[verb].length;
  966. for (; i < l; i++) {
  967. route = this.routes[verb][i];
  968. if (app.routablePath(path).match(route.path)) {
  969. routed = route;
  970. break;
  971. }
  972. }
  973. }
  974. return routed;
  975. },
  976. // First, invokes `lookupRoute()` and if a route is found, parses the
  977. // possible URL params and then invokes the route's callback within a new
  978. // `Sammy.EventContext`. If the route can not be found, it calls
  979. // `notFound()`. If `raise_errors` is set to `true` and
  980. // the `error()` has not been overridden, it will throw an actual JS
  981. // error.
  982. //
  983. // You probably will never have to call this directly.
  984. //
  985. // ### Arguments
  986. //
  987. // * `verb` A String for the verb.
  988. // * `path` A String path to lookup.
  989. // * `params` An Object of Params pulled from the URI or passed directly.
  990. //
  991. // ### Returns
  992. //
  993. // Either returns the value returned by the route callback or raises a 404 Not Found error.
  994. //
  995. runRoute: function(verb, path, params, target) {
  996. var app = this,
  997. route = this.lookupRoute(verb, path),
  998. context,
  999. wrapped_route,
  1000. arounds,
  1001. around,
  1002. befores,
  1003. before,
  1004. callback_args,
  1005. path_params,
  1006. final_returned;
  1007. if (this.debug) {
  1008. this.log('runRoute', [verb, path].join(' '));
  1009. }
  1010. this.trigger('run-route', {verb: verb, path: path, params: params});
  1011. if (typeof params == 'undefined') { params = {}; }
  1012. $.extend(params, this._parseQueryString(path));
  1013. if (route) {
  1014. this.trigger('route-found', {route: route});
  1015. // pull out the params from the path
  1016. if ((path_params = route.path.exec(this.routablePath(path))) !== null) {
  1017. // first match is the full path
  1018. path_params.shift();
  1019. // for each of the matches
  1020. $.each(path_params, function(i, param) {
  1021. // if theres a matching param name
  1022. if (route.param_names[i]) {
  1023. // set the name to the match
  1024. params[route.param_names[i]] = _decode(param);
  1025. } else {
  1026. // initialize 'splat'
  1027. if (!params.splat) { params.splat = []; }
  1028. params.splat.push(_decode(param));
  1029. }
  1030. });
  1031. }
  1032. // set event context
  1033. context = new this.context_prototype(this, verb, path, params, target);
  1034. // ensure arrays
  1035. arounds = this.arounds.slice(0);
  1036. befores = this.befores.slice(0);
  1037. // set the callback args to the context + contents of the splat
  1038. callback_args = [context];
  1039. if (params.splat) {
  1040. callback_args = callback_args.concat(params.splat);
  1041. }
  1042. // wrap the route up with the before filters
  1043. wrapped_route = function() {
  1044. var returned, i, nextRoute;
  1045. while (befores.length > 0) {
  1046. before = befores.shift();
  1047. // check the options
  1048. if (app.contextMatchesOptions(context, before[0])) {
  1049. returned = before[1].apply(context, [context]);
  1050. if (returned === false) { return false; }
  1051. }
  1052. }
  1053. app.last_route = route;
  1054. context.trigger('event-context-before', {context: context});
  1055. // run multiple callbacks
  1056. if (typeof(route.callback) === "function") {
  1057. route.callback = [route.callback];
  1058. }
  1059. if (route.callback && route.callback.length) {
  1060. i = -1;
  1061. nextRoute = function() {
  1062. i++;
  1063. if (route.callback[i]) {
  1064. returned = route.callback[i].apply(context,callback_args);
  1065. } else if (app._onComplete && typeof(app._onComplete === "function")) {
  1066. app._onComplete(context);
  1067. }
  1068. };
  1069. callback_args.push(nextRoute);
  1070. nextRoute();
  1071. }
  1072. context.trigger('event-context-after', {context: context});
  1073. return returned;
  1074. };
  1075. $.each(arounds.reverse(), function(i, around) {
  1076. var last_wrapped_route = wrapped_route;
  1077. wrapped_route = function() { return around.apply(context, [last_wrapped_route]); };
  1078. });
  1079. try {
  1080. final_returned = wrapped_route();
  1081. } catch(e) {
  1082. this.error(['500 Error', verb, path].join(' '), e);
  1083. }
  1084. return final_returned;
  1085. } else {
  1086. return this.notFound(verb, path);
  1087. }
  1088. },
  1089. // Matches an object of options against an `EventContext` like object that
  1090. // contains `path` and `verb` attributes. Internally Sammy uses this
  1091. // for matching `before()` filters against specific options. You can set the
  1092. // object to _only_ match certain paths or verbs, or match all paths or verbs _except_
  1093. // those that match the options.
  1094. //
  1095. // ### Example
  1096. //
  1097. // var app = $.sammy(),
  1098. // context = {verb: 'get', path: '#/mypath'};
  1099. //
  1100. // // match against a path string
  1101. // app.contextMatchesOptions(context, '#/mypath'); //=> true
  1102. // app.contextMatchesOptions(context, '#/otherpath'); //=> false
  1103. // // equivalent to
  1104. // app.contextMatchesOptions(context, {only: {path:'#/mypath'}}); //=> true
  1105. // app.contextMatchesOptions(context, {only: {path:'#/otherpath'}}); //=> false
  1106. // // match against a path regexp
  1107. // app.contextMatchesOptions(context, /path/); //=> true
  1108. // app.contextMatchesOptions(context, /^path/); //=> false
  1109. // // match only a verb
  1110. // app.contextMatchesOptions(context, {only: {verb:'get'}}); //=> true
  1111. // app.contextMatchesOptions(context, {only: {verb:'post'}}); //=> false
  1112. // // match all except a verb
  1113. // app.contextMatchesOptions(context, {except: {verb:'post'}}); //=> true
  1114. // app.contextMatchesOptions(context, {except: {verb:'get'}}); //=> false
  1115. // // match all except a path
  1116. // app.contextMatchesOptions(context, {except: {path:'#/otherpath'}}); //=> true
  1117. // app.contextMatchesOptions(context, {except: {path:'#/mypath'}}); //=> false
  1118. // // match multiple paths
  1119. // app.contextMatchesOptions(context, {path: ['#/mypath', '#/otherpath']}); //=> true
  1120. // app.contextMatchesOptions(context, {path: ['#/otherpath', '#/thirdpath']}); //=> false
  1121. // // equivalent to
  1122. // app.contextMatchesOptions(context, {only: {path: ['#/mypath', '#/otherpath']}}); //=> true
  1123. // app.contextMatchesOptions(context, {only: {path: ['#/otherpath', '#/thirdpath']}}); //=> false
  1124. // // match all except multiple paths
  1125. // app.contextMatchesOptions(context, {except: {path: ['#/mypath', '#/otherpath']}}); //=> false
  1126. // app.contextMatchesOptions(context, {except: {path: ['#/otherpath', '#/thirdpath']}}); //=> true
  1127. //
  1128. contextMatchesOptions: function(context, match_options, positive) {
  1129. var options = match_options;
  1130. // normalize options
  1131. if (typeof options === 'string' || _isRegExp(options)) {
  1132. options = {path: options};
  1133. }
  1134. if (typeof positive === 'undefined') {
  1135. positive = true;
  1136. }
  1137. // empty options always match
  1138. if ($.isEmptyObject(options)) {
  1139. return true;
  1140. }
  1141. // Do we have to match against multiple paths?
  1142. if (_isArray(options.path)){
  1143. var results, numopt, opts, len;
  1144. results = [];
  1145. for (numopt = 0, len = options.path.length; numopt < len; numopt += 1) {
  1146. opts = $.extend({}, options, {path: options.path[numopt]});
  1147. results.push(this.contextMatchesOptions(context, opts));
  1148. }
  1149. var matched = $.inArray(true, results) > -1 ? true : false;
  1150. return positive ? matched : !matched;
  1151. }
  1152. if (options.only) {
  1153. return this.contextMatchesOptions(context, options.only, true);
  1154. } else if (options.except) {
  1155. return this.contextMatchesOptions(context, options.except, false);
  1156. }
  1157. var path_matched = true, verb_matched = true;
  1158. if (options.path) {
  1159. if (!_isRegExp(options.path)) {
  1160. options.path = new RegExp(options.path.toString() + '$');
  1161. }
  1162. path_matched = options.path.test(context.path);
  1163. }
  1164. if (options.verb) {
  1165. if(typeof options.verb === 'string') {
  1166. verb_matched = options.verb === context.verb;
  1167. } else {
  1168. verb_matched = options.verb.indexOf(context.verb) > -1;
  1169. }
  1170. }
  1171. return positive ? (verb_matched && path_matched) : !(verb_matched && path_matched);
  1172. },
  1173. // Delegates to the `location_proxy` to get the current location.
  1174. // See `Sammy.DefaultLocationProxy` for more info on location proxies.
  1175. getLocation: function() {
  1176. return this._location_proxy.getLocation();
  1177. },
  1178. // Delegates to the `location_proxy` to set the current location.
  1179. // See `Sammy.DefaultLocationProxy` for more info on location proxies.
  1180. //
  1181. // ### Arguments
  1182. //
  1183. // * `new_location` A new location string (e.g. '#/')
  1184. //
  1185. setLocation: function(new_location) {
  1186. return this._location_proxy.setLocation(new_location);
  1187. },
  1188. // Swaps the content of `$element()` with `content`
  1189. // You can override this method to provide an alternate swap behavior
  1190. // for `EventContext.partial()`.
  1191. //
  1192. // ### Example
  1193. //
  1194. // var app = $.sammy(function() {
  1195. //
  1196. // // implements a 'fade out'/'fade in'
  1197. // this.swap = function(content, callback) {
  1198. // var context = this;
  1199. // context.$element().fadeOut('slow', function() {
  1200. // context.$element().html(content);
  1201. // context.$element().fadeIn('slow', function() {
  1202. // if (callback) {
  1203. // callback.apply();
  1204. // }
  1205. // });
  1206. // });
  1207. // };
  1208. //
  1209. // });
  1210. //
  1211. swap: function(content, callback) {
  1212. var $el = this.$element().html(content);
  1213. if (_isFunction(callback)) { callback(content); }
  1214. return $el;
  1215. },
  1216. // a simple global cache for templates. Uses the same semantics as
  1217. // `Sammy.Cache` and `Sammy.Storage` so can easily be replaced with
  1218. // a persistent storage that lasts beyond the current request.
  1219. templateCache: function(key, value) {
  1220. if (typeof value != 'undefined') {
  1221. return _template_cache[key] = value;
  1222. } else {
  1223. return _template_cache[key];
  1224. }
  1225. },
  1226. // clear the templateCache
  1227. clearTemplateCache: function() {
  1228. return (_template_cache = {});
  1229. },
  1230. // This throws a '404 Not Found' error by invoking `error()`.
  1231. // Override this method or `error()` to provide custom
  1232. // 404 behavior (i.e redirecting to / or showing a warning)
  1233. notFound: function(verb, path) {
  1234. var ret = this.error(['404 Not Found', verb, path].join(' '));
  1235. return (verb === 'get') ? ret : true;
  1236. },
  1237. // The base error handler takes a string `message` and an `Error`
  1238. // object. If `raise_errors` is set to `true` on the app level,
  1239. // this will re-throw the error to the browser. Otherwise it will send the error
  1240. // to `log()`. Override this method to provide custom error handling
  1241. // e.g logging to a server side component or displaying some feedback to the
  1242. // user.
  1243. error: function(message, original_error) {
  1244. if (!original_error) { original_error = new Error(); }
  1245. original_error.message = [message, original_er