├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .nvmrc ├── Gruntfile.js ├── MIT-LICENSE.txt ├── Pilot.d.ts ├── Pilot.js ├── README.md ├── index.html ├── mixin ├── cito.js ├── fest.js ├── mithril.js ├── react.js ├── view.js └── xtpl.js ├── package-lock.json ├── package.json ├── src ├── action-queue.js ├── loader.js ├── match.js ├── pilot.js ├── querystring.js ├── request.js ├── route.js ├── status.js └── url.js ├── statics ├── body.png ├── body__glow.png ├── docs.json ├── logo.png ├── main.css └── spinner.gif ├── tests ├── alias.tests.js ├── bench.html ├── index.html ├── loader.tests.js ├── pilot.tests.js ├── polyfills │ └── url.js ├── querystring.tests.js ├── url.tests.js └── urls.js └── vendors ├── Emitter.js ├── Promise.js ├── performance.js └── require.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-amd-to-commonjs", 4 | [ 5 | "transform-string-literal-replace", { 6 | "patterns": { 7 | "Emitter": "../vendors/Emitter", 8 | "Promise": "../vendors/Promise" 9 | } 10 | } 11 | ] 12 | ], 13 | "sourceMaps": "both" 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 5 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Pilot.min.js 3 | node_modules 4 | report 5 | .idea 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.14.0 2 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | 8 | jshint: { 9 | all: ['src/*.js', 'tests/*.tests.js'] 10 | }, 11 | 12 | version: { 13 | src: 'Pilot.js' 14 | }, 15 | 16 | qunit: { 17 | all: ['tests/index.html'], 18 | options: { 19 | '--web-security': 'no', 20 | coverage: { 21 | src: ['<%=jshint.all%>'], 22 | instrumentedFiles: '/tmp/', 23 | htmlReport: 'report/coverage', 24 | coberturaReport: 'report/', 25 | linesThresholdPct: 80, 26 | functionsThresholdPct: 80, 27 | branchesThresholdPct: 70, 28 | statementsThresholdPct: 80 29 | } 30 | } 31 | }, 32 | 33 | uglify: { 34 | options: { 35 | banner: '/*! Pilot <%= pkg.version %> - <%= pkg.license %> | <%= pkg.repository.url %> */\n' 36 | }, 37 | dist: { 38 | files: { 39 | 'Pilot.min.js': ['Pilot.js'] 40 | } 41 | } 42 | }, 43 | 44 | requirejs: { 45 | compile: { 46 | options: { 47 | findNestedDependencies: true, 48 | baseUrl: './', 49 | include: 'src/pilot.js', 50 | paths: { 51 | 'Emitter': 'empty:' 52 | }, 53 | out: 'Pilot.js', 54 | optimize: 'none', 55 | wrap: { 56 | start: `(function (define, factory) { 57 | define(['Emitter'], function (Emitter) { 58 | var defined = {Emitter: Emitter}; 59 | var syncDefine = function (name, deps, callback) { 60 | var i = deps.length, depName; 61 | 62 | while (i--) { 63 | depName = name.split('/').slice(0, -1).join('/'); 64 | deps[i] = defined[deps[i].replace('./', depName ? depName + '/' : '')]; 65 | } 66 | 67 | defined[name] = callback.apply(null, deps); 68 | }; 69 | syncDefine.amd = true; 70 | factory(syncDefine); 71 | return defined['src/pilot.js']; 72 | }); 73 | })(typeof define === 'function' && define.amd ? define : function (deps, callback) { 74 | window.Pilot = callback(window.Emitter); 75 | }, function (define) {`, 76 | 77 | end: `});` 78 | } 79 | } 80 | } 81 | } 82 | }); 83 | 84 | 85 | grunt.loadNpmTasks('grunt-version'); 86 | grunt.loadNpmTasks('grunt-qunit-istanbul'); 87 | grunt.loadNpmTasks('grunt-contrib-uglify'); 88 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 89 | 90 | 91 | // Тестирование 92 | grunt.registerTask('test', ['qunit']); 93 | 94 | // Сборка 95 | grunt.registerTask('build', ['requirejs']); 96 | 97 | // Минификация 98 | grunt.registerTask('min', ['uglify']); 99 | 100 | // Default task. 101 | grunt.registerTask('default', ['version', 'test', 'build', 'min']); 102 | }; 103 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013-2016 Lebedev Konstantin 2 | http://rubaxa.github.com/Pilot/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pilot.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module 'pilotjs/vendors/Emitter' { 3 | // ==== ./vendors/Emitter.js ==== 4 | 5 | type EventListener = (...args: any[]) => any; 6 | 7 | namespace Emitter { 8 | export class Event { 9 | target: T; 10 | details: D; 11 | result?: R; 12 | 13 | constructor(type: string | Object | Event); 14 | 15 | isDefaultPrevented(): boolean; 16 | 17 | preventDefault(); 18 | 19 | stopPropagation(); 20 | 21 | isPropagationStopped(): boolean; 22 | } 23 | } 24 | 25 | class Emitter { 26 | constructor(); 27 | 28 | on(events: string, fn: EventListener): this; 29 | 30 | off(events: string, fn: EventListener): this; 31 | 32 | one(events: string, fn: EventListener): this; 33 | 34 | emit(type: string, args: any[]): any; 35 | 36 | trigger(type: string, args: any[], details?: any): this; 37 | 38 | static readonly version: string; 39 | 40 | static apply(target: T): T & Emitter; 41 | 42 | static getListeners(target: Object, name: string): EventListener[]; 43 | } 44 | 45 | export = Emitter; 46 | } 47 | 48 | declare module 'pilotjs' { 49 | 50 | // ==== ./src/action-queue.js ==== 51 | 52 | import Emitter = require("pilotjs/vendors/Emitter"); 53 | 54 | namespace Pilot { 55 | export type ActionQueuePriority = number; 56 | export type ActionId = number; 57 | 58 | export interface Action { 59 | type: string; 60 | uid?: ActionId; 61 | priority?: ActionQueuePriority; 62 | } 63 | 64 | class ActionQueue extends Emitter { 65 | constructor(); 66 | 67 | push(request: Request, action: Action): ActionId; 68 | 69 | static readonly PRIORITY_HIGH: ActionQueuePriority; 70 | static readonly PRIORITY_LOW: ActionQueuePriority; 71 | } 72 | 73 | // ==== ./src/loader.js ==== 74 | 75 | export type LoaderProcessor = () => void; 76 | 77 | export interface LoaderOptions { 78 | persist?: boolean; 79 | processing?: LoaderProcessor; 80 | } 81 | 82 | export type LoaderModelFetcher = () => void; 83 | 84 | export interface LoaderModelObject { 85 | name: string; 86 | fetch: LoaderModelFetcher; 87 | match: Match; 88 | } 89 | 90 | export type LoaderModel = LoaderModelObject | LoaderModelFetcher; 91 | 92 | class Loader { 93 | readonly models: Record; 94 | readonly names: string[]; 95 | 96 | constructor(models: Record | Loader, options: LoaderOptions); 97 | 98 | defaults(): Record; 99 | 100 | fetch(): Promise; 101 | 102 | dispatch(): Promise; 103 | 104 | extend(models: Record): Loader; 105 | 106 | getLastReq(): Request | undefined; 107 | 108 | extract(model: LoaderModel): Object; 109 | 110 | bind(route: Route, model: LoaderModel); 111 | 112 | setDebug(debug: boolean); 113 | 114 | static readonly ACTION_NAVIGATE: string; 115 | static readonly ACTION_NONE: string; 116 | static readonly PRIORITY_LOW: typeof ActionQueue.PRIORITY_LOW; 117 | static readonly PRIORITY_HIGH: typeof ActionQueue.PRIORITY_HIGH; 118 | } 119 | 120 | // ==== ./src/match.js ==== 121 | 122 | export type MatchFn = (key: string) => boolean; 123 | export type MatchArray = string[]; 124 | export type Match = MatchArray | MatchFn; 125 | 126 | // ==== ./src/querystring.js ==== 127 | 128 | export type QueryItem = string | number | symbol; 129 | export type Query = Record; 130 | 131 | interface QueryString { 132 | parse(search: string): Query; 133 | 134 | stringify(query: Query): string; 135 | } 136 | 137 | // ==== ./src/request.js ==== 138 | 139 | class Request { 140 | href: string; 141 | protocol: string; 142 | host: string; 143 | hostname: string; 144 | port: string; 145 | path: string; 146 | pathname: string; 147 | search: string; 148 | query: Query; 149 | params: Record; 150 | hash: string; 151 | route: Route; 152 | router: Pilot; 153 | referrer?: string; 154 | redirectHref?: string; 155 | alias?: string; 156 | 157 | constructor(url: string | URL, referrer?: string, router?: string); 158 | 159 | clone(): Request; 160 | 161 | is(id: string): boolean; 162 | 163 | redirectTo(href: string, interrupt?: boolean); 164 | 165 | toString(): string; 166 | 167 | snapshot(): Readonly; 168 | } 169 | 170 | // ==== ./src/route.js ==== 171 | 172 | export interface RouteUrlParamsConfig { 173 | default?: any; 174 | decode: (value: string) => any; 175 | } 176 | 177 | export type UrlBuilder = (params: Record, query: Query) => string; 178 | 179 | export interface RouteUrlObject { 180 | pattern: string; 181 | params?: Record; 182 | toUrl?: (params: Record, query: Query, builder: UrlBuilder) => string; 183 | } 184 | 185 | export type RouteUrl = string | RouteUrlObject; 186 | 187 | export interface RouteOptions { 188 | model?: Loader; 189 | aliases?: Record; 190 | } 191 | 192 | export type Model = Record; 193 | 194 | class Route extends Emitter { 195 | id: string; 196 | active?: boolean; 197 | regions: string[]; 198 | router: Pilot; 199 | model: Model; 200 | parentId?: string; 201 | parentRoute?: Route; 202 | readonly request?: Request; 203 | readonly params: Record; 204 | 205 | constructor(options: RouteOptions, router: Pilot); 206 | 207 | protected init(); 208 | 209 | protected _initOptions(options: RouteOptions); 210 | 211 | protected _initMixins(); 212 | 213 | handling(url: URL, req: Request, currentRoute: Route, model: Record); 214 | 215 | match(URL: URL, req: Request): boolean; 216 | 217 | fetch(req: Request): Promise; 218 | 219 | getUrl(params: Record, query: Query | 'inherit'): string; 220 | 221 | is(id: string): boolean; 222 | 223 | snapshot(): Readonly; 224 | 225 | on(event: 'before-init', fn: (event: Emitter.Event) => any): this; 226 | on(event: 'init', fn: (event: Emitter.Event) => any): this; 227 | on(event: 'model', fn: (event: Emitter.Event, model: Model, req: Request) => any): this; 228 | on(event: 'route-start', fn: (event: Emitter.Event, req: Request) => any): this; 229 | on(event: 'route-change', fn: (event: Emitter.Event, req: Request) => any): this; 230 | on(event: 'route', fn: (event: Emitter.Event, req: Request) => any): this; 231 | on(event: 'route-end', fn: (event: Emitter.Event, req: Request) => any): this; 232 | on(events: string, fn: EventListener): this; 233 | 234 | one(event: 'before-init', fn: (event: Emitter.Event) => any): this; 235 | one(event: 'init', fn: (event: Emitter.Event) => any): this; 236 | one(event: 'model', fn: (event: Emitter.Event, model: Model, req: Request) => any): this; 237 | one(event: 'route-start', fn: (event: Emitter.Event, req: Request) => any): this; 238 | one(event: 'route-change', fn: (event: Emitter.Event, req: Request) => any): this; 239 | one(event: 'route', fn: (event: Emitter.Event, req: Request) => any): this; 240 | one(event: 'route-end', fn: (event: Emitter.Event, req: Request) => any): this; 241 | one(events: string, fn: EventListener): this; 242 | 243 | static readonly Region: typeof Region; 244 | } 245 | 246 | class Region extends Route { 247 | constructor(name: string, options: RouteOptions, route: Route); 248 | } 249 | 250 | // ==== ./src/status.js ==== 251 | 252 | class Status { 253 | code: number; 254 | details?: any; 255 | 256 | constructor(code: number, details?: any); 257 | 258 | toJSON(): Object; 259 | 260 | static from(value: any): Status; 261 | } 262 | 263 | // ==== ./src/url.js ==== 264 | 265 | class URL { 266 | protocol: string; 267 | protocolSeparator: string; 268 | credhost: string; 269 | cred: string; 270 | username: string; 271 | password: string; 272 | host: string; 273 | hostname: string; 274 | port: string; 275 | origin: string; 276 | path: string; 277 | pathname: string; 278 | segment1: string; 279 | segment2: string; 280 | search: string; 281 | query: Query; 282 | params: Record; 283 | hash: string; 284 | 285 | constructor(url: string, base?: string | URL | Location); 286 | 287 | setQuery(query: string | Query, remove?: boolean | string[]): URL; 288 | 289 | addToQuery(query: Query): URL; 290 | 291 | removeFromQuery(query: string | string[]): URL; 292 | 293 | update(): URL; 294 | 295 | toString(): string; 296 | 297 | static parse(url: string): URL; 298 | 299 | static readonly parseQueryString: QueryString["parse"]; 300 | static readonly stringifyQueryString: QueryString["stringify"]; 301 | 302 | static toMatcher(pattern: string | RegExp): RegExp; 303 | 304 | static match(pattern: string | RegExp, url: string | URL): Record; 305 | } 306 | 307 | // ==== ./src/pilot.js ==== 308 | 309 | export type AccessFunction = (route: Route) => Promise; 310 | 311 | export interface PilotRouteOption { 312 | url?: RouteUrlObject; 313 | } 314 | 315 | export interface PilotRouteOptions { 316 | url?: string; 317 | access?: AccessFunction; 318 | } 319 | 320 | export type PilotRouteMap = PilotRouteOptions | Record; 321 | 322 | export interface PilotNavDetails { 323 | initiator?: string; 324 | replaceState?: boolean; 325 | force?: boolean; 326 | } 327 | 328 | export interface PilotCompatibleLogger { 329 | add(key: string, details?: any); 330 | 331 | call(key: string, details: any, wrappedContent: () => void); 332 | } 333 | 334 | export type PilotListenFilter = (url: string) => boolean; 335 | 336 | export interface PilotListenOptions { 337 | logger?: PilotCompatibleLogger; 338 | autoStart?: boolean; 339 | filter?: PilotListenFilter; 340 | replaceState?: boolean; 341 | } 342 | } 343 | 344 | class Pilot extends Emitter { 345 | model: Object; 346 | request: Pilot.Request; 347 | route?: Pilot.Route; 348 | activeRoute?: Pilot.Route; 349 | activeUrl: Pilot.URL; 350 | url?: Pilot.URL; 351 | activeRequest?: Pilot.Request; 352 | routes: Pilot.Route[]; 353 | previousRoute?: Readonly; 354 | 355 | constructor(map: Pilot.PilotRouteMap); 356 | 357 | getUrl(id: string, params?: Record, query?: Pilot.Query | 'inherit'): string; 358 | 359 | go(id: string, params?: Record, query?: Pilot.Query | 'inherit', details?: Object): Promise; 360 | 361 | nav(href: string | Pilot.URL | Request, details?: Pilot.PilotNavDetails): Promise; 362 | 363 | listenFrom(target: HTMLElement, options: Pilot.PilotListenOptions); 364 | 365 | reload(); 366 | 367 | on(event: 'before-route', fn: (event: Emitter.Event, req: Request) => any): this; 368 | on(event: 'error', fn: (event: Emitter.Event, req: Request, error: unknown) => any): this; 369 | on(event: 'route-fail', fn: (event: Emitter.Event, req: Request, currentRoute: Pilot.Route, error: unknown) => any): this; 370 | on(event: 'route', fn: (event: Emitter.Event, req: Request, currentRoute: Pilot.Route) => any): this; 371 | on(event: 'route-end', fn: (event: Emitter.Event, req: Request, currentRoute: Pilot.Route) => any): this; 372 | on(event: 'beforereload', fn: (event: Emitter.Event) => any): this; 373 | on(event: 'reload', fn: (event: Emitter.Event) => any): this; 374 | on(event: 'reloadend', fn: (event: Emitter.Event) => any): this; 375 | on(events: string, fn: EventListener): this; 376 | 377 | one(event: 'before-route', fn: (event: Emitter.Event, req: Request) => any): this; 378 | one(event: 'error', fn: (event: Emitter.Event, req: Request, error: unknown) => any): this; 379 | one(event: 'route-fail', fn: (event: Emitter.Event, req: Request, currentRoute: Pilot.Route, error: unknown) => any): this; 380 | one(event: 'route', fn: (event: Emitter.Event, req: Request, currentRoute: Pilot.Route) => any): this; 381 | one(event: 'route-end', fn: (event: Emitter.Event, req: Request, currentRoute: Pilot.Route) => any): this; 382 | one(event: 'beforereload', fn: (event: Emitter.Event) => any): this; 383 | one(event: 'reload', fn: (event: Emitter.Event) => any): this; 384 | one(event: 'reloadend', fn: (event: Emitter.Event) => any): this; 385 | one(events: string, fn: EventListener): this; 386 | 387 | static create(map: Pilot.PilotRouteMap): Pilot; 388 | 389 | static readonly queryString: Pilot.QueryString; 390 | static readonly version: string; 391 | } 392 | 393 | export default Pilot; 394 | 395 | } 396 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pilot v2 2 | -------- 3 | Multifunctional JavaScript router solves the problem of routing your application, providing full control over the route. 4 | 5 | 6 | ### Get started 7 | 8 | ```js 9 | const router = Pilot.create({ 10 | '#route-id': { 11 | url: '/:type(/:detail)?', // route pattern 12 | model: { 13 | user: (req) => fetch(`/api/${req.params.type}`).then(r => r.json()), 14 | }, 15 | onroute(/**Pilot.Request*/req) { 16 | console.log(this.model.user); 17 | } 18 | } 19 | }); 20 | 21 | // Запускаем перехват ссылок и history api 22 | router.listenFrom(document, {autoStart: true}); 23 | 24 | // Где-то в коде 25 | router.go('#route-id').then(() => ...); 26 | router.getUrl('#route-id', {type: 'user'}); // '/user'; 27 | router.route.getUrl({type: 'user'}); // '/user'; 28 | router.route.getUrl({type: 'user', detail: 123}); // '/user/123'; 29 | ``` 30 | 31 | --- 32 | 33 | ### API 34 | 35 | - **create**(stitemap: `Object`): `Pilot` 36 | - **URL**([url: `string`[, base: `string`]]) — see [Native URL](https://developer.mozilla.org/ru/docs/Web/API/URL) and 37 | - **parse**(url: `string`) 38 | - **toMatcher**(pattern: `string|RegExp`) 39 | - `#properties` 40 | - **protocol**: `string` 41 | - **protocolSeparator**: `string` 42 | - **credhost**: `string` 43 | - **cred**: `string` 44 | - **username**: `string` 45 | - **password**: `string` 46 | - **host**: `string` 47 | - **hostname**: `string` 48 | - **port**: `string` 49 | - **origin**: `string` 50 | - **path**: `string` or **pathname** 51 | - **segment1**: `string` 52 | - **segment2**: `string` 53 | - **search**: `string` 54 | - **query**: `object` 55 | - **params**: `object` 56 | - **hash**: `string` 57 | - `#methods` 58 | - **addToQuery**(add: `object|string|null`) 59 | - **removeFromQuery**(remove: `string[]`) 60 | - **setQuery**(add: `object|string|null`[, remove: `string[]`) 61 | - **queryString** 62 | - **parse**(value: `string`): `object` 63 | - **stringify**(query: `object`): `string` 64 | 65 | --- 66 | 67 | 68 | ### `Pilot` lifecycle 69 | 70 | #### beforeroute 71 | 72 | - **req**:`Pilot.Request` 73 | 74 | --- 75 | 76 | #### route 77 | 78 | - **req**:`Pilot.Request` 79 | - **route**:`Pilot.Route` 80 | 81 | --- 82 | 83 | #### routefail 84 | 85 | - **req**:`Pilot.Request` 86 | - **route**:`Pilot.Route` 87 | - **error**:`Error` 88 | 89 | --- 90 | 91 | #### routeend 92 | 93 | - **req**:`Pilot.Request` 94 | - **route**:`Pilot.Route` 95 | 96 | --- 97 | 98 | ### `Pilot` methods and properties 99 | 100 | #### model:`Object` 101 | List of all models 102 | 103 | --- 104 | 105 | #### request:`Pilot.Request` 106 | Current Request. 107 | 108 | --- 109 | 110 | #### activeUrl:`URL` 111 | Active/Current URL. 112 | 113 | --- 114 | 115 | #### route:`Pilot.Route` 116 | Current route. 117 | 118 | --- 119 | 120 | #### getUrl(id[, params[, query]]):`string` 121 | 122 | - **id**:`string` — route id 123 | - **params**:`object` — route parametrs (optional) 124 | - **query**:`object|inherit` — route GET-query parametrs (optional) 125 | 126 | --- 127 | 128 | #### go(id[, params[, query[, details]]]):`Promise` 129 | 130 | - **id**:`string` — route id 131 | - **params**:`object` — route parameters (optional) 132 | - **query**:`object|inherit` — route GET-query parameters (optional) 133 | - **details**:`object` - route navigation details (options) 134 | 135 | --- 136 | 137 | #### nav(url[, details]):`Promise` 138 | 139 | - **url**:`string` 140 | - **details**:`object` - route navigation details (options) 141 | 142 | 143 | #### reload():`Promise` 144 | 145 | Emits `beforereload` and `reload` events. if a handler to `beforereload` returns `false`, does not 146 | perform actual reload and returns a resolved promise instead. 147 | 148 | --- 149 | 150 | ### `Pilot.Route` methods and properties 151 | 152 | #### model:`Object` 153 | Local models (inherit global models). 154 | 155 | --- 156 | 157 | #### init() 158 | Protected method. 159 | 160 | --- 161 | 162 | #### getUrl([params, [query]]):`string` 163 | 164 | - **params**:`Object` (optional) 165 | - **query**:`object|inherit` — route GET-query parametrs (optional) 166 | 167 | --- 168 | 169 | #### is(id):`boolean` 170 | 171 | **id**:string — route id or space-separated list 172 | 173 | 174 | --- 175 | 176 | ### `Pilot.Loader` 177 | 178 | ```js 179 | const modelLoader = new Pilot.Loader({ 180 | user: ({params:{id}}) => fetch(`/api/user/${id}`).then(r => r.json()), 181 | status: () => fetch(`/api/status`).then(r => r.json()), 182 | }, { 183 | // неважно сколько раз вызвать `fetch`, 184 | // если уже есть запрос на сервер, новый не последует 185 | persist: true, 186 | 187 | // Обработку данных загруженной модели 188 | processingModel(modelName, modelData, req, models) { 189 | return {...modelData, pathed: true}; // or Promise 190 | }, 191 | 192 | // Обработка ошибки при загрузки модели 193 | processingModelError(modelName, error, req, models) { 194 | return Promise.resolve({defaultData: 123}); // или undefined для reject 195 | }, 196 | 197 | // Финальная обработка полученных данных 198 | processing(req, models) { 199 | return {...models, patched: true}; 200 | }, 201 | }); 202 | 203 | // Используем `modelLoader` 204 | const router = Pilot.create({ 205 | model: modelLoader, 206 | }); 207 | 208 | // Где-то в коде 209 | modelLoader.fetch().then(model => { 210 | console.log(model.user); 211 | console.log(model.status); 212 | }); 213 | ``` 214 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Pilot — multifunction JavaScript router 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 170 | 171 | 172 |
173 | 174 |
175 |
176 |
Example
177 |
178 |
179 | 180 | 181 | 182 |
183 |
184 |
185 | source: app.js 186 |
187 |
188 |
189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 260 | 261 | 262 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /mixin/cito.js: -------------------------------------------------------------------------------- 1 | define(['cito', 'pilot/mixin/view'], function (/** cito */cito, view) { 2 | var _timers = {}; 3 | 4 | // Export 5 | return Object.keys(view).reduce(function (target, name) { 6 | target[name] = target[name] || view[name]; 7 | return target; 8 | }, { 9 | // cito mixin 10 | components: {}, 11 | 12 | apply: function () { 13 | var _this = this; 14 | var components = _this.components; 15 | 16 | _this.components = Object.keys(components).map(function (name) { 17 | var component = components[name](_this, name); 18 | _this[name] = component; 19 | return component; 20 | }); 21 | 22 | view.apply.apply(_this, arguments); 23 | 24 | _this.router.one('route-end', function () { 25 | if (_this.el) { 26 | _this.$apply = function () { 27 | _timers = {}; 28 | _this.render(); 29 | }; 30 | 31 | _this.$apply(); 32 | 33 | _this.router.$apply = _this.$apply; 34 | _this.router.on('route-end', _this.$apply); 35 | } 36 | }); 37 | }, 38 | 39 | renderComponents: function () { 40 | return this.components.map(function (component) { 41 | var time = performance.now(); 42 | var result = component.render(); 43 | 44 | _timers[component.displayName] = performance.now() - time; 45 | 46 | return result; 47 | }); 48 | }, 49 | 50 | render: function () { 51 | var start = performance.now(); 52 | var fragment = {tag:'div', key: 'root', children: this.renderComponents()}; 53 | 54 | _timers['renderComponents'] = performance.now() - start; 55 | 56 | var time = performance.now(); 57 | 58 | if (this._vdom) { 59 | cito.vdom.update(this._vdom, fragment); 60 | } 61 | else { 62 | this._vdom = cito.vdom.append(this.el, fragment); 63 | } 64 | 65 | _timers['cito.vdom'] = performance.now() - time; 66 | _timers['TOTAL'] = performance.now() - start; 67 | 68 | console.table(Object.keys(_timers).reduce(function (table, name) { 69 | table[name] = { 70 | 'time, ms': _timers[name].toFixed(3) * 1 71 | }; 72 | 73 | return table; 74 | }, {})); 75 | } 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /mixin/fest.js: -------------------------------------------------------------------------------- 1 | define(['pilot/mixin/view'], function (view) { 2 | // Export 3 | return Object.keys(view).reduce(function (target, name) { 4 | target[name] = target[name] || view[name]; 5 | return target; 6 | }, { 7 | // Fest mixin 8 | 9 | components: {}, 10 | 11 | apply: function () { 12 | var pid, 13 | _this = this; 14 | 15 | _this.dom = '
'; 16 | 17 | // Родительская DOM-примись 18 | view.apply.apply(this, arguments); 19 | 20 | // todo: нужно переделывать 21 | document.body.appendChild(_this.el); 22 | 23 | // Биндинг компонентов к DOM 24 | Object.keys(_this.components).forEach(function (name) { 25 | var el = document.createElement('span'); 26 | 27 | _this[name] = _this.components[name]; 28 | 29 | _this[name].route = _this; 30 | _this[name].bind(el); 31 | 32 | _this.el.appendChild(el); 33 | }); 34 | 35 | _this.on('route', function () { 36 | clearTimeout(pid); 37 | pid = setTimeout(function () { 38 | Object.keys(_this.components).forEach(function (name) { 39 | _this[name].$apply(); 40 | }); 41 | }, 1); 42 | }); 43 | }, 44 | 45 | 46 | $: function (selector) { 47 | return this.components[selector] || view.$.call(this, selector); 48 | } 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /mixin/mithril.js: -------------------------------------------------------------------------------- 1 | define(['mithril', 'pilot/mixin/view'], function (/** Mithril */m, view) { 2 | var _timers = {}; 3 | 4 | // Export 5 | return Object.keys(view).reduce(function (target, name) { 6 | target[name] = target[name] || view[name]; 7 | return target; 8 | }, { 9 | // React mixin 10 | components: {}, 11 | 12 | apply: function () { 13 | var _this = this; 14 | var components = _this.components; 15 | 16 | _this.components = Object.keys(components).map(function (name) { 17 | var component = components[name](_this, name); 18 | _this[name] = component; 19 | return component; 20 | }); 21 | 22 | view.apply.apply(_this, arguments); 23 | 24 | _this.router.one('route-end', function () { 25 | if (_this.el) { 26 | _this.$apply = function () { 27 | _timers = {}; 28 | _this.render(); 29 | }; 30 | 31 | _this.$apply(); 32 | 33 | _this.router.$apply = _this.$apply; 34 | _this.router.on('route-end', _this.$apply); 35 | } 36 | }); 37 | }, 38 | 39 | renderComponents: function () { 40 | return this.components.map(function (component) { 41 | var time = performance.now(); 42 | var result = component.render(); 43 | 44 | _timers[component.displayName] = performance.now() - time; 45 | 46 | return result; 47 | }); 48 | }, 49 | 50 | render: function () { 51 | var start = performance.now(); 52 | var fragment = this.renderComponents(); 53 | 54 | _timers['renderComponents'] = performance.now() - start; 55 | 56 | var time = performance.now(); 57 | m.render(this.el, fragment); 58 | 59 | _timers['mithril.render'] = performance.now() - time; 60 | _timers['TOTAL'] = performance.now() - start; 61 | 62 | console.table(Object.keys(_timers).reduce(function (table, name) { 63 | table[name] = { 64 | 'time, ms': _timers[name].toFixed(3)*1 65 | }; 66 | 67 | return table; 68 | }, {})); 69 | } 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /mixin/react.js: -------------------------------------------------------------------------------- 1 | define(['react', 'pilot/mixin/view'], function (/** React */React, view) { 2 | // Export 3 | return Object.keys(view).reduce(function (target, name) { 4 | target[name] = target[name] || view[name]; 5 | return target; 6 | }, { 7 | // React mixin 8 | components: {}, 9 | 10 | apply: function () { 11 | var _this = this; 12 | var components = _this.components; 13 | 14 | Object.keys(components).map(function (name) { 15 | components[name] = components[name](_this, name); 16 | }); 17 | 18 | view.apply.apply(_this, arguments); 19 | 20 | if (_this.el) { 21 | _this.one('before-init', function () { 22 | Object.keys(components).map(function (name) { 23 | React.render(components[name], _this.el); 24 | }); 25 | }); 26 | } 27 | }, 28 | 29 | renderComponents: function () { 30 | return React.addons.createFragment(this.components); 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /mixin/view.js: -------------------------------------------------------------------------------- 1 | define(['jquery'], function ($) { 2 | /** @type {window} */ 3 | var win = window; 4 | 5 | /** @type {HTMLDocument} */ 6 | var document = win.document; 7 | 8 | 9 | /** @type {HTMLElement} */ 10 | var helper = document.createElement('div'); 11 | 12 | 13 | // Export 14 | return { 15 | /** @type {string} */ 16 | el: '', 17 | 18 | /** @type {jQuery} */ 19 | $el: null, 20 | 21 | /** @type {string} */ 22 | dom: '', 23 | 24 | /** @type {HTMLElement} */ 25 | parentEl: null, 26 | 27 | // Применение примиси 28 | apply: function () { 29 | if (this.dom) { // Строим DOM из строки 30 | helper.innerHTML = this.dom; 31 | this.el = helper.firstElementChild || helper.firstChild; 32 | 33 | helper.removeChild(this.el); 34 | } 35 | else if (typeof this.el == 'string') { 36 | this.el = document.getElementById(this.el.replace(/^#/, '')); 37 | } 38 | 39 | this.$el = $(this.el); 40 | 41 | // А теперь добавляем в DOM, если это не так 42 | if (this.el && this.el.nodeType > 0) { 43 | this._toggleView(false); 44 | 45 | this.one('before-init', function () { 46 | // Поиск родителя 47 | if (this.parentEl === null) { 48 | var parentRoute = this.parentRoute; 49 | 50 | if (parentRoute) { 51 | do { 52 | this.parentEl = ( 53 | parentRoute.el && 54 | parentRoute.$el.find('[data-view-id="' + (this.name || this.id).replace(/^#/, '') + '"]')[0] || 55 | 0 && parentRoute.el 56 | ); 57 | } 58 | while (!this.parentEl && (parentRoute = parentRoute.parentRoute)); 59 | } else { 60 | this.parentEl = document.body; 61 | } 62 | } 63 | 64 | //if (this.parentEl && this.parentEl !== this.el) { 65 | // this.parentEl.appendChild(this.el); 66 | //} 67 | }.bind(this)); 68 | } 69 | 70 | this.on('route-start route-end', function (evt) { 71 | this._toggleView(evt.type == 'routestart'); 72 | }.bind(this)); 73 | }, 74 | 75 | _toggleView: function (state) { 76 | if (this.router) { 77 | this.el && this.el.style && (this.el.style.display = state ? '' : 'none'); 78 | } 79 | }, 80 | 81 | $: function (selector) { 82 | return this.$el.find(selector); 83 | } 84 | }; 85 | }); 86 | -------------------------------------------------------------------------------- /mixin/xtpl.js: -------------------------------------------------------------------------------- 1 | define(['xtpl', 'pilot/mixin/view'], function (/** xtpl */xtpl, view) { 2 | // Export 3 | return Object.keys(view).reduce(function (target, name) { 4 | target[name] = target[name] || view[name]; 5 | return target; 6 | }, { 7 | // xtpl mixin 8 | template: null, 9 | components: {}, 10 | 11 | apply: function () { 12 | var _this = this; 13 | 14 | // todo: Нужно что-то с этим сделать 15 | xtpl.mod('routeUrl', function (id, params) { 16 | return _this.router.getUrl(id, params) 17 | }); 18 | 19 | 20 | view.apply.apply(_this, arguments); 21 | 22 | _this.one('before-init', function () { 23 | var template = _this.template && _this.template.replace(/\sx-route:\s*["'](.*?)['"]/g, function (_, id) { 24 | var view, 25 | ctxPath; 26 | 27 | id = id.split(':'); 28 | 29 | if (id[0] === '#') { 30 | id[0] = '#__root__'; 31 | } 32 | 33 | view = _this.router[id[0]]; 34 | ctxPath = 'router["' + id[0] + '"]'; 35 | 36 | if (id[1]) { 37 | view = view.regions[id[1]]; 38 | ctxPath += '.regions["' + id[1] + '"]'; 39 | } 40 | 41 | return view && view.toXBlock ? view.toXBlock(ctxPath) : ''; 42 | }); 43 | 44 | _this.router.one('route-end', function () { 45 | if (_this.el) { 46 | //console.log(template) 47 | xtpl.bind(_this.el, template, _this); 48 | 49 | _this.router.$apply = _this.$apply; 50 | _this.router.on('route-end', _this.$apply); 51 | } 52 | }); 53 | }); 54 | }, 55 | 56 | 57 | toXBlock: function (ctxPath) { 58 | var _this = this; 59 | var blocks = Object.keys(this.components).map(function (name) { 60 | var block = _this.components[name]; 61 | 62 | _this[name] = block; 63 | block.$apply = function () { 64 | _this.router.$apply(); 65 | }; 66 | 67 | return 'scope ctx["' + name + '"] { `ctx.init();` ' + block.template + ' }'; 68 | }); 69 | 70 | return 'scope ctx.' + ctxPath + ' { ' + blocks.join('\n') + ' }'; 71 | } 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pilotjs", 3 | "version": "2.18.0", 4 | "devDependencies": { 5 | "@types/jest": "23.3.13", 6 | "amdclean": "^2.7.0", 7 | "babel-jest": "^23.6.0", 8 | "babel-plugin-transform-amd-to-commonjs": "1.2.0", 9 | "babel-plugin-transform-string-literal-replace": "^1.0.2", 10 | "eslint": "^5.16.0", 11 | "grunt": "^1.0.1", 12 | "grunt-contrib-jshint": "^1.0.0", 13 | "grunt-contrib-requirejs": "^1.0.0", 14 | "grunt-contrib-uglify": "^1.0.0", 15 | "grunt-qunit-istanbul": "^1.0.0", 16 | "grunt-version": "*", 17 | "jest": "^23.6.0", 18 | "request": "^2.65.0" 19 | }, 20 | "description": "Multifunction JavaScript router.", 21 | "main": "Pilot.js", 22 | "types": "Pilot.d.ts", 23 | "scripts": { 24 | "test": "jest --coverage && npm run lint", 25 | "lint": "eslint src", 26 | "build": "grunt build", 27 | "prepublish": "npm run build" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git://github.com/rubaxa/Pilot.git" 32 | }, 33 | "keywords": [ 34 | "Pilot", 35 | "router", 36 | "routing", 37 | "route" 38 | ], 39 | "author": "Konstantin Lebedev ", 40 | "license": "MIT", 41 | "include": [ 42 | "Pilot.js" 43 | ], 44 | "jest": { 45 | "testRegex": "\\.tests\\.js$", 46 | "transform": { 47 | "\\.js$": "babel-jest" 48 | }, 49 | "transformIgnorePatterns": [], 50 | "moduleFileExtensions": [ 51 | "js", 52 | "json" 53 | ], 54 | "coverageReporters": [ 55 | "html" 56 | ], 57 | "coverageDirectory": "report", 58 | "collectCoverageFrom": [ 59 | "src/*.js" 60 | ] 61 | }, 62 | "dependencies": {} 63 | } 64 | -------------------------------------------------------------------------------- /src/action-queue.js: -------------------------------------------------------------------------------- 1 | define(['Emitter'], function(Emitter) { 2 | 3 | function ActionQueue() { 4 | Emitter.apply(this); 5 | 6 | this._queue = []; 7 | this._activeIds = {}; 8 | this._id = 0; 9 | this._lastQueueItem = void 0; 10 | this._endedCount = -1; 11 | } 12 | 13 | ActionQueue.PRIORITY_HIGH = 1; 14 | ActionQueue.PRIORITY_LOW = 0; 15 | 16 | ActionQueue.prototype = { 17 | constructor: ActionQueue, 18 | 19 | push: function(request, action) { 20 | // TODO: arg types check 21 | 22 | // Проставляем по умолчанию наивысший приоритет 23 | if (action.priority == null) { 24 | action.priority = ActionQueue.PRIORITY_HIGH; 25 | } 26 | 27 | var queueItem = { 28 | request: request, 29 | action: action, 30 | timestamp: Date.now(), 31 | id: this._id++ 32 | }; 33 | 34 | // Добавляем в очередь 35 | this._queue.push(queueItem); 36 | // Возвращаем уникальный id 37 | return queueItem.id; 38 | }, 39 | 40 | remove: function(id) { 41 | // Если query был в _activeIds 42 | if (this._activeIds[id]) { 43 | // Сбросим _lastQueueItem 44 | if (this._lastQueueItem === this._activeIds[id]) { 45 | this._lastQueueItem = void 0; 46 | } 47 | 48 | // Сообщим, что прекратили выполнять этот экшн 49 | this.notifyEnd(id, void 0); 50 | return; 51 | } 52 | 53 | var nextQueue = []; 54 | 55 | // Формируем новую очередь без экшна с указанным id 56 | for (var i = 0; i < this._queue.length; i++) { 57 | if (this._queue[i].id !== id) { 58 | nextQueue.push(this._queue[i]); 59 | } 60 | } 61 | 62 | // Сохраним новую очередь 63 | this._queue = nextQueue; 64 | // Сообщим, что прекратили выполнять этот экшн 65 | this.notifyEnd(id, void 0); 66 | }, 67 | 68 | canPoll: function() { 69 | var nextItem = this._queue[0]; 70 | var lastActiveItem = this._lastQueueItem; 71 | 72 | // Не можем поллить, так как очередь пуста 73 | if (!nextItem) { 74 | return false; 75 | } 76 | 77 | // Можем поллить, так как ничего не запущено 78 | if (!lastActiveItem) { 79 | return true; 80 | } 81 | 82 | // Можем поллить, если приоритет последнего запущенного экшна равен приоритету следующего экшна в очереди 83 | return lastActiveItem.action.priority === nextItem.action.priority; 84 | }, 85 | 86 | poll: function() { 87 | var queueItem = this._queue.shift(); 88 | 89 | this._activeIds[queueItem.id] = queueItem; 90 | this._lastQueueItem = queueItem; 91 | 92 | return queueItem; 93 | }, 94 | 95 | notifyEnd: function(id, result, error, throws) { 96 | // Сбрасываем lastQueueItem, если закончили именно его 97 | if (this._lastQueueItem === this._activeIds[id]) { 98 | this._lastQueueItem = void 0; 99 | } 100 | 101 | // Удаляем из активных в любом случае 102 | delete this._activeIds[id]; 103 | 104 | // Сообщаем Loader 105 | if (throws) { 106 | this.emit(id + ':error', error); 107 | } else { 108 | this.emit(id + ':end', result); 109 | } 110 | 111 | // Увеличиваем счётчик завершённых экшнов 112 | this._endedCount++; 113 | }, 114 | 115 | awaitEnd: function(id) { 116 | // Если экшн уже давно выполнился 117 | if (id <= this._endedCount) { 118 | return Promise.resolve(); 119 | } 120 | 121 | // Ожидаем выполнения экшна 122 | return new Promise(function(resolve, reject) { 123 | this.one(id + ':end', resolve); 124 | this.one(id + ':error', reject); 125 | }.bind(this)); 126 | }, 127 | }; 128 | 129 | return ActionQueue; 130 | 131 | }); 132 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | define(['./match', './action-queue'], function (match, ActionQueue) { 2 | 'use strict'; 3 | 4 | var _cast = function (name, model) { 5 | if (typeof model === 'function') { 6 | model = { fetch: model }; 7 | } 8 | 9 | model.name = name; 10 | model.match = match.cast(model.match); 11 | 12 | return model; 13 | }; 14 | 15 | // На коленке полифиллим setImmediate 16 | var setImmediate = window.setImmediate || function setImmediateDummyPolyfillFromPilotJS(callback) { 17 | setTimeout(callback, 0); 18 | }; 19 | 20 | /** 21 | * @typedef {object} LoaderOptions 22 | * @property {boolean} persist 23 | * @property {Function} processing 24 | */ 25 | 26 | 27 | /** 28 | * @class Pilot.Loader 29 | * @extends Emitter 30 | * @constructs Pilot.Loader 31 | * @param {Object} models 32 | * @param {LoaderOptions} [options] 33 | * @constructor 34 | */ 35 | var Loader = function (models, options) { 36 | if (models instanceof Loader) { 37 | return models; 38 | } 39 | 40 | this.models = models = (models || {}); 41 | this.names = Object.keys(models); 42 | 43 | this._index = {}; 44 | this._options = options || {}; 45 | 46 | this._lastReq = null; 47 | this._fetchPromises = {}; 48 | 49 | // Инкрементивный ID запросов нужен для performance 50 | this._lastReqId = 0; 51 | // Дебаг-режим, выводит в performance все экшны 52 | this._debug = false; 53 | // Очередь экшнов 54 | this._actionQueue = new ActionQueue(); 55 | 56 | this.names.forEach(function (name) { 57 | this._index[name] = _cast(name, models[name]); 58 | }, this); 59 | }; 60 | 61 | 62 | Loader.prototype = /** @lends Pilot.Loader# */{ 63 | constructor: Loader, 64 | 65 | 66 | defaults: function () { 67 | var defaults = {}; 68 | 69 | this.names.forEach(function (name) { 70 | defaults[name] = this._index[name].defaults; 71 | }, this); 72 | 73 | return defaults; 74 | }, 75 | 76 | fetch: function (req) { 77 | return this._executeAction(req, { 78 | type: Loader.ACTION_NAVIGATE, 79 | priority: Loader.PRIORITY_LOW 80 | }); 81 | }, 82 | 83 | dispatch: function (action) { 84 | return this._executeAction(null, action); 85 | }, 86 | 87 | makeWaitFor: function (models, index, req, action, options, promises) { 88 | var _this = this; 89 | 90 | var waitFor = function (name) { 91 | var idx = models[name]; 92 | var model = index[name]; 93 | 94 | if (idx === void 0) { 95 | idx = new Promise(function (resolve) { 96 | if (model.fetch && model.match(req.route.id, req)) { 97 | resolve(model.fetch(req, waitFor, action, _this._lastModels)); 98 | } else { 99 | resolve(model.defaults); 100 | } 101 | }) 102 | .then(function (data) { 103 | if (options.processingModel) { 104 | data = options.processingModel(name, data, req, models, action); 105 | } 106 | return data; 107 | }) 108 | .catch(function (err) { 109 | if (options.processingModelError) { 110 | var p = options.processingModelError(name, err, req, models, action); 111 | if (p !== null) { 112 | return p; 113 | } 114 | } 115 | return Promise.reject(err); 116 | }); 117 | 118 | idx = promises.push(idx) - 1; 119 | models[name] = idx; 120 | } 121 | 122 | return promises[idx]; 123 | }; 124 | 125 | return waitFor; 126 | }, 127 | 128 | _loadSources: function (req, action) { 129 | var _this = this; 130 | 131 | // Нужно для отметок в performance 132 | var requestId = _this._lastReqId++; 133 | 134 | // Используем предыдущий запрос, если не передали 135 | if (req == null) { 136 | req = _this._lastReq; 137 | } 138 | 139 | // Запомним этот запрос как последний, чьё выполнение мы начали 140 | _this._lastReq = req; 141 | 142 | var _index = _this._index; 143 | var _options = _this._options; 144 | var _persistKey = req.toString() + action.type + action.uid; 145 | var _fetchPromises = _this._fetchPromises; 146 | 147 | if (_options.persist && _fetchPromises[_persistKey]) { 148 | return _fetchPromises[_persistKey]; 149 | } 150 | 151 | var priorityName = action.priority === Loader.PRIORITY_LOW ? 'LOW' : 'HIGH'; 152 | var measureName = 'PilotJS [' + priorityName + '] ' + action.type + ' ' + requestId; 153 | 154 | if (_this._debug && window.performance) { 155 | window.performance.mark('start:' + measureName); 156 | } 157 | 158 | // Имена источников 159 | var names = _this.names; 160 | // Будущие данные источников 161 | var models = {}; 162 | // Промисы источников 163 | var promises = []; 164 | 165 | // Делаем функцию waitFor для текущего запроса 166 | var waitFor = this.makeWaitFor(models, _index, req, action, _options, promises); 167 | 168 | // Загружаем все модели 169 | names.forEach(waitFor); 170 | 171 | var _promise = Promise 172 | .all(promises) 173 | .then(function (results) { 174 | _this._measurePerformance(measureName); 175 | 176 | // Формируем новое состояние 177 | names.forEach(function (name) { 178 | models[name] = results[models[name]]; 179 | }); 180 | 181 | // Вызываем коллбек processing 182 | _options.processing && (models = _options.processing(req, action, models)); 183 | 184 | if (_this._bindedRoute) { 185 | _this._bindedRoute.model = _this.extract(models); 186 | } 187 | 188 | // Запоминаем загруженные модели 189 | _this._lastModels = models; 190 | 191 | return models; 192 | }) 193 | .catch(function (error) { 194 | _this._measurePerformance(measureName); 195 | throw error; 196 | }); 197 | 198 | if (_options.persist) { 199 | _fetchPromises[_persistKey] = _promise; 200 | 201 | // После выполнения текущего запроса нужно удалить промис из _fetchPromises 202 | _fetchPromises[_persistKey].then(function () { 203 | delete _fetchPromises[_persistKey]; 204 | }, function () { 205 | delete _fetchPromises[_persistKey]; 206 | }); 207 | } 208 | 209 | // Запоминаем промис запроса 210 | _this._lastPromise = _promise; 211 | 212 | return _promise; 213 | }, 214 | 215 | _executeAction: function (req, action) { 216 | var _this = this; 217 | var _req = req; 218 | 219 | // Action по умолчанию 220 | action = action && typeof action === 'object' ? action : { 221 | type: Loader.ACTION_NONE, 222 | priority: Loader.PRIORITY_HIGH 223 | }; 224 | 225 | // Используем предыдущий запрос, если не передали 226 | if (_req == null) { 227 | _req = _this._lastReq; 228 | } 229 | 230 | // Если у нас стоит persist: true, то сначала проверим, что такой запрос уже есть 231 | // См. тест 'dispatch with low priority and persist fires only once' 232 | var _persistKey = _req.toString() + action.type + action.uid; 233 | var _fetchPromises = _this._fetchPromises; 234 | 235 | if (_this._options.persist && _fetchPromises[_persistKey]) { 236 | return _fetchPromises[_persistKey]; 237 | } 238 | 239 | // Добавляем экшн в очередь 240 | var actionId = _this._actionQueue.push(_req, action); 241 | // Пробуем выполнить следующий экшн из очереди 242 | this._tryProcessQueue(); 243 | // Возвращаем промис, который выполнится, когда выполнится этот экшн 244 | return _this._actionQueue.awaitEnd(actionId); 245 | }, 246 | 247 | _tryProcessQueue: function() { 248 | while (this._actionQueue.canPoll()) { 249 | var queueItem = this._actionQueue.poll(); 250 | 251 | // Отправляем экшн выполняться 252 | var actionPromise = this._loadSources(queueItem.request, queueItem.action); 253 | 254 | actionPromise 255 | .then(function (queueItem, result) { 256 | // Сообщаем, что экшн прекратили выполнять 257 | this._actionQueue.notifyEnd(queueItem.id, result); 258 | // Пробуем выполнить следующий экшн 259 | this._tryProcessQueue(); 260 | }.bind(this, queueItem)) 261 | .catch(function (queueItem, error) { 262 | // Сообщаем, что экшн прекратили выполнять 263 | this._actionQueue.notifyEnd(queueItem.id, null, error, true); 264 | // Пробуем выполнить следующий экшн 265 | this._tryProcessQueue(); 266 | 267 | throw error; 268 | }.bind(this, queueItem)) 269 | } 270 | }, 271 | 272 | 273 | _measurePerformance: function (measureName) { 274 | if (this._debug && window.performance) { 275 | window.performance.mark('end:' + measureName); 276 | window.performance.measure(measureName, 'start:' + measureName, 'end:' + measureName); 277 | 278 | window.performance.clearMarks('start:' + measureName); 279 | window.performance.clearMarks('end:' + measureName); 280 | } 281 | }, 282 | 283 | 284 | extend: function (models) { 285 | models = models || {}; 286 | 287 | this.names.forEach(function (name) { 288 | models[name] = models[name] || this.models[name]; 289 | }, this); 290 | 291 | return new Loader(models); 292 | }, 293 | 294 | getLastReq: function () { 295 | return this._lastReq; 296 | }, 297 | 298 | /** 299 | * Достаем только принадлежание лоудеру свойства 300 | * @param {Object} model 301 | * @returns {Object} 302 | */ 303 | extract: function (model) { 304 | var data = {}; 305 | 306 | this.names.forEach(function (name) { 307 | data[name] = model[name]; 308 | }); 309 | 310 | return data; 311 | }, 312 | 313 | bind: function (route, model) { 314 | route.model = this.extract(model); 315 | this._bindedRoute = route; 316 | }, 317 | 318 | /** 319 | * Включаем / выключаем дебаг-режим 320 | * @param {boolean} debug 321 | */ 322 | setDebug: function (debug) { 323 | this._debug = !!debug; 324 | } 325 | }; 326 | 327 | Loader.ACTION_NAVIGATE = 'NAVIGATE'; 328 | Loader.ACTION_NONE = 'NONE'; 329 | Loader.PRIORITY_LOW = ActionQueue.PRIORITY_LOW; 330 | Loader.PRIORITY_HIGH = ActionQueue.PRIORITY_HIGH; 331 | 332 | // Export 333 | return Loader; 334 | }); 335 | -------------------------------------------------------------------------------- /src/match.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | // Export 3 | return { 4 | cast: function (list) { 5 | var matches = {}, 6 | match = list; 7 | 8 | if (typeof list !== 'function') { 9 | if (list === true || list === void 0) { 10 | match = function () { 11 | return true; 12 | }; 13 | } 14 | else { 15 | list.forEach(function (key) { 16 | matches[key] = true; 17 | }); 18 | 19 | match = function (key) { 20 | return matches[key]; 21 | }; 22 | } 23 | } 24 | 25 | return match; 26 | } 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /src/pilot.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'Emitter', 3 | './url', 4 | './match', 5 | './loader', 6 | './request', 7 | './route', 8 | './status', 9 | './querystring' 10 | ], function ( 11 | /** Emitter */Emitter, 12 | /** URL */URL, 13 | /** Object */match, 14 | /** Pilot.Loader */Loader, 15 | /** Pilot.Request */Request, 16 | /** Pilot.Route */Route, 17 | /** Pilot.Status */Status, 18 | /** Pilot.queryString */queryString 19 | ) { 20 | 'use strict'; 21 | 22 | var aboutBlankUrl = new URL('about:blank'); 23 | var resolvedPromise = Promise.resolve(); 24 | 25 | var MOUSE_BUTTON_AUXILIARY = 1; // Средняя кнопка мыши 26 | var MOUSE_BUTTON_SECONDARY = 2; // Правая кнопка мыши 27 | 28 | function _normalizeRouteUrl(url, relative) { 29 | relative = relative || {}; 30 | 31 | if (!url) { 32 | url = relative.pattern; 33 | } 34 | 35 | if (typeof url === 'string') { 36 | url = { pattern: url }; 37 | } 38 | 39 | if (url.pattern.charAt(0) !== '/') { 40 | url.pattern = (relative.pattern + '/' + url.pattern.replace(/(^\.\/|^\.$)/, '')); 41 | } 42 | 43 | url.pattern = url.pattern.replace(/\/+/g, '/'); 44 | url.params = url.params || relative.params || {}; 45 | url.query = url.query || relative.query || {}; 46 | url.toUrl = url.toUrl || relative.toUrl; 47 | 48 | return url; 49 | } 50 | 51 | 52 | 53 | /** 54 | * @class Pilot 55 | * @param {Object} map крата маршрутов 56 | */ 57 | var Pilot = function (map) { 58 | var routes = []; 59 | 60 | map.url = map.url || '/'; 61 | map.access = map.access || function () { 62 | return resolvedPromise; 63 | }; 64 | 65 | // Подготавливаем данные 66 | (function _prepareRoute(map) { 67 | map.__group__ = false; 68 | 69 | Object.keys(map).forEach(function (key) { 70 | var options = map[key]; 71 | 72 | if (key.charAt(0) === '#') { 73 | // Это маршрут, следовательно `map` — группа 74 | map.__group__ = true; 75 | delete map[key]; 76 | 77 | options.id = key; 78 | options.parentId = map.id; 79 | 80 | options.url = _normalizeRouteUrl(options.url, map.url); 81 | options.model = options.model ? map.model.extend(options.model) : map.model; 82 | options.access = options.access || map.access; 83 | 84 | routes.push(options); 85 | _prepareRoute(options); 86 | } 87 | }); 88 | })({ 89 | '#__root__': map, 90 | model: new Loader(map.model) 91 | }); 92 | 93 | 94 | this.model = map.model.defaults(); 95 | this.__model__ = map.model; 96 | 97 | 98 | /** 99 | * Текущий реквест 100 | * @type {Pilot.Request} 101 | */ 102 | this.request = new Request('about:blank', '', this); 103 | 104 | 105 | /** 106 | * Активный URL 107 | * @type {URL} 108 | */ 109 | this.activeUrl = aboutBlankUrl; 110 | 111 | 112 | /** 113 | * Массив маршрутов 114 | * @type {Pilot.Route[]} 115 | */ 116 | this.routes = routes.map(function (route) { 117 | route = new Route(route, this); 118 | 119 | this[route.id] = route; 120 | 121 | return route; 122 | }, this); 123 | }; 124 | 125 | 126 | Pilot.prototype = /** @lends Pilot# */{ 127 | constructor: Pilot, 128 | 129 | /** 130 | * Получить URL по id 131 | * @param {string} id 132 | * @param {Object} [params] 133 | * @param {Object|'inherit'} [query] 134 | */ 135 | getUrl: function (id, params, query) { 136 | return this[id].getUrl(params, query); 137 | }, 138 | 139 | 140 | /** 141 | * Перейти по id 142 | * @param {string} id 143 | * @param {Object} [params] 144 | * @param {Object|'inherit'} [query] 145 | * @param {Object} [details] 146 | * @return {Promise} 147 | */ 148 | go: function (id, params, query, details) { 149 | return this.nav(this[id].getUrl(params, query), details); 150 | }, 151 | 152 | 153 | /** 154 | * Навигация по маршруту 155 | * @param {string|URL|Pilot.Request} href 156 | * @param {{initiator: string, replaceState: boolean, force: boolean}} [details] 157 | * @returns {Promise} 158 | */ 159 | nav: function (href, details) { 160 | var req, 161 | url = new URL(href.toString(), location), 162 | _this = this, 163 | routes = _this.routes, 164 | _promise = _this._promise, 165 | currentRoute; 166 | 167 | details = details || {}; 168 | 169 | // URL должен отличаться от активного, либо если передали флаг force 170 | if (_this.activeUrl.href !== url.href || details.force) { 171 | // Создаем объект реквеста и дальше с ним работаем 172 | req = new Request(url, _this.request.href, _this); 173 | 174 | // Находим нужный нам маршрут 175 | currentRoute = routes.find(function (/** Pilot.Route */item) { 176 | // Пытаемся сматчить этот маршрут по алиасу 177 | var matchedAlias = item.aliases.find(function (alias) { 178 | var matcher = alias.matcher; 179 | return matcher && matcher(url, req); 180 | }); 181 | 182 | // Получилось? 183 | if (matchedAlias) { 184 | req.alias = matchedAlias.name; 185 | return true; 186 | } 187 | 188 | // Матчим по основной регулярке 189 | return !item.__group__ && item.match(url, req); 190 | }); 191 | 192 | _this.activeUrl = url; 193 | _this.activeRequest = req; 194 | _this.activeRoute = currentRoute; 195 | _this.previousRoute = _this.route && _this.route.snapshot(); 196 | 197 | if (!_this.route) { 198 | _this.route = currentRoute; 199 | } 200 | 201 | _this.trigger('before-route', [req], details); 202 | 203 | if (!_this._promise) { 204 | _this._promise = _promise = new Promise(function (resolve, reject) { 205 | // Только в целях оптимизации стека 206 | _this._resolve = resolve; 207 | _this._reject = reject; 208 | }); 209 | 210 | _promise['catch'](function (err) { 211 | if (currentRoute) { 212 | // todo: Найти ближайшую 404 213 | currentRoute.trigger(err.code + '', [req, err], details); 214 | currentRoute.trigger('error', [req, err], details); 215 | } 216 | 217 | _this.trigger('route-fail', [req, currentRoute, err], details); 218 | _this.trigger('route-end', [req, currentRoute], details); 219 | }); 220 | } 221 | 222 | 223 | if (!currentRoute) { 224 | // Если маршрут не найден, кидаем ошибку 225 | _this._reject(new Status(404)); 226 | } 227 | else { 228 | req.route = currentRoute; 229 | 230 | // Запрашиваем доступ к маршруту 231 | currentRoute.access(req).then(function () { 232 | // Доступ есть, теперь собираем данные для маршрута 233 | return currentRoute.fetch(req).then(function (/** Object */model) { 234 | if (_this.activeUrl === url && model !== null) { 235 | _this.url = url; 236 | _this.referrer = _this.request.href; 237 | _this.route = currentRoute; 238 | _this.request = req; 239 | 240 | _this.__model__.bind(_this, model); 241 | 242 | // Обходим всем маршруты и тегерим события 243 | routes.forEach(function (/** Route */route) { 244 | route.handling(url, req.clone(), currentRoute, model); 245 | }); 246 | 247 | if (!req.redirectHref) { 248 | _this.trigger('route', [req, currentRoute], details); 249 | 250 | if (!req.redirectHref) { 251 | _this.trigger('route-end', [req, currentRoute], details); 252 | 253 | if (!req.redirectHref) { 254 | _this._promise = null; 255 | _this._resolve(); 256 | 257 | return; // exit 258 | } 259 | } 260 | } 261 | 262 | _this.useHistory && history.replaceState(null, null, req.redirectHref); 263 | _this.nav(req.redirectHref); 264 | } 265 | }); 266 | })['catch'](function (err) { 267 | console.warn(err); 268 | 269 | // Обработка ошибки 270 | if (_this.activeUrl === url) { 271 | if (err instanceof Request) { 272 | _this.useHistory && history.replaceState(null, null, err.href); 273 | _this.nav(err.href); 274 | return; 275 | } 276 | 277 | _this._promise = null; 278 | _this.activeUrl = aboutBlankUrl; 279 | _this._reject(Status.from(err)); 280 | 281 | return Promise.reject(err); 282 | } 283 | }); 284 | } 285 | } 286 | 287 | return _promise || resolvedPromise; 288 | }, 289 | 290 | /** 291 | * Слушать события 292 | * @param {HTMLElement} target 293 | * @param {{logger:object, autoStart: boolean, filter: Function, replaceState: boolean}} options 294 | */ 295 | listenFrom: function (target, options) { 296 | var _this = this; 297 | var logger = options.logger; 298 | var filter = options.filter; 299 | var popStateNav = function () { 300 | _this.nav(location.href, {initiator: 'popstate'}); 301 | }; 302 | 303 | _this.useHistory = true; 304 | 305 | // Корректировка url, если location не совпадает 306 | _this.on('routeend', function (evt, req) { 307 | var href = req.toString(); 308 | var replaceState = evt.details && evt.details.replaceState; 309 | 310 | if (location.toString() !== href) { 311 | logger && logger.add('router.pushState', {href: href}); 312 | history[replaceState ? 'replaceState' : 'pushState'](null, null, href); 313 | } 314 | }); 315 | 316 | // Слушаем `back` 317 | window.addEventListener('popstate', function () { 318 | var url = location.href; 319 | var isFilterPassed = !filter || filter(url); 320 | if (!isFilterPassed) { 321 | return; 322 | } 323 | 324 | if (logger) { 325 | logger.call('router.nav.popstate', {href: location.href}, popStateNav); 326 | } else { 327 | popStateNav(); 328 | } 329 | }, false); 330 | 331 | // Перехватываем действия пользователя 332 | target.addEventListener('click', function pilotClickListener(evt) { 333 | var el = evt.target; 334 | var level = 0; 335 | var MAX_LEVEL = 10; 336 | var hostnameRegExp = new RegExp('^' + location.protocol + '//' + location.hostname); 337 | 338 | do { 339 | var url = el.href; 340 | 341 | if ( 342 | url && 343 | hostnameRegExp.test(url) && 344 | !evt.defaultPrevented && 345 | !( 346 | evt.metaKey || 347 | evt.ctrlKey || 348 | evt.button === MOUSE_BUTTON_SECONDARY || 349 | evt.button === MOUSE_BUTTON_AUXILIARY 350 | ) && 351 | (!filter || filter(url)) 352 | ) { 353 | evt.preventDefault(); 354 | 355 | var details = { 356 | initiator: 'click', 357 | replaceState: el.getAttribute('data-history-replace-state') === 'y', 358 | }; 359 | 360 | var clickNav = function () { 361 | _this.nav(url, details); 362 | history[details.replaceState ? 'replaceState' : 'pushState'](null, null, url); 363 | }; 364 | 365 | if (logger) { 366 | logger.call('router.nav.click', {href: url}, clickNav); 367 | } else { 368 | clickNav(); 369 | } 370 | break; 371 | } 372 | } while ((el = el.parentNode) && (++level < MAX_LEVEL)); 373 | 374 | el = null; 375 | }); 376 | 377 | if (options.autoStart) { 378 | if (logger) { 379 | logger.call('router.nav.initial', {href: location.href}, function () { 380 | _this.nav(location.href, { 381 | initiator: 'initial', 382 | replaceState: options.replaceState, 383 | }); 384 | }); 385 | } else { 386 | _this.nav(location.href, { 387 | initiator: 'initial', 388 | replaceState: options.replaceState, 389 | }); 390 | } 391 | } 392 | }, 393 | 394 | /** 395 | * Метод вызывает событие перезагрузки приложения. 396 | */ 397 | reload: function () { 398 | var _this = this; 399 | 400 | var evt = new Emitter.Event('beforereload'); 401 | _this.trigger(evt); 402 | 403 | // Отменили ли перезагрузку? 404 | var reloadCancelled = evt.result === false; 405 | 406 | if (!reloadCancelled) { 407 | _this.trigger('reload'); 408 | } 409 | 410 | _this.trigger('reloadend', null, {cancelled: reloadCancelled}); 411 | 412 | if (reloadCancelled) { 413 | return Promise.resolve(); 414 | } 415 | 416 | return _this.nav(_this.activeUrl.href, {force: true, replaceState: true}) 417 | } 418 | }; 419 | 420 | 421 | /** 422 | * Создать роутер 423 | * @param {Object} sitemap 424 | * @return {Pilot} 425 | */ 426 | Pilot.create = function (sitemap) { 427 | return new Pilot(sitemap); 428 | }; 429 | 430 | Emitter.apply(Pilot.prototype); 431 | 432 | // Export 433 | Pilot.URL = URL; 434 | Pilot.Loader = Loader; 435 | Pilot.Status = Status; 436 | Pilot.Request = Request; 437 | Pilot.Route = Route; 438 | Pilot.queryString = queryString; 439 | Pilot.version = '2.0.0'; 440 | 441 | return Pilot; 442 | }); 443 | -------------------------------------------------------------------------------- /src/querystring.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | 'use strict'; 3 | 4 | var encodeURIComponent = window.encodeURIComponent; 5 | var decodeURIComponent = window.decodeURIComponent; 6 | 7 | 8 | function _stringifyParam(key, val, indexes) { 9 | /* jshint eqnull:true */ 10 | if (val == null || val === '' || typeof val !== 'object') { 11 | return encodeURIComponent(key) + 12 | (indexes ? '[' + indexes.join('][') + ']' : '') + 13 | (val == null || val === '' ? '' : '=' + encodeURIComponent(val)); 14 | } 15 | else { 16 | var pairs = []; 17 | 18 | for (var i in val) { 19 | if (val.hasOwnProperty(i)) { 20 | pairs.push(_stringifyParam(key, val[i], (indexes || []).concat(i >= 0 ? '' : encodeURIComponent(i)))); 21 | } 22 | } 23 | 24 | return pairs.join('&'); 25 | } 26 | } 27 | 28 | /** 29 | * @module Pilot.queryString 30 | */ 31 | var queryStringNew = /** @lends queryString */{ 32 | /** 33 | * Parse query string 34 | * @param {string} search 35 | * @returns {Object} 36 | */ 37 | parse: function (search) { 38 | if (/^[?#]/.test(search)) { 39 | search = search.substr(1); 40 | } 41 | 42 | search = search.trim(); 43 | 44 | var entries = Array.from(new URLSearchParams(search).entries()).map(function (entry) { 45 | var k = entry[0]; 46 | var v = entry[1]; 47 | 48 | return [k.replace('[]', ''), v]; 49 | }); 50 | 51 | var aggr = {}; 52 | 53 | for (var i = 0; i < entries.length; i++) { 54 | var entry = entries[i]; 55 | var k = entry[0]; 56 | var v = entry[1]; 57 | 58 | if (!aggr[k]) { 59 | aggr[k] = v; 60 | continue; 61 | } 62 | 63 | if (Array.isArray(aggr[k])) { 64 | aggr[k].push(v); 65 | continue; 66 | } 67 | 68 | aggr[k] = [aggr[k], v]; 69 | } 70 | 71 | return aggr; 72 | }, 73 | 74 | 75 | /** 76 | * Stringify query object 77 | * @param {Object} query 78 | * @returns {string} 79 | */ 80 | stringify: function (query) { 81 | if (!query || !(query instanceof Object)) { 82 | return ''; 83 | } 84 | 85 | var objectParams = []; 86 | var params = new URLSearchParams(); 87 | Object.entries(query).forEach(function (entry) { 88 | var k = entry[0]; 89 | var v = entry[1]; 90 | 91 | if (typeof v !== 'string') { 92 | objectParams.push(_stringifyParam(k, v)); 93 | } else { 94 | params.append(k, v); 95 | } 96 | }); 97 | 98 | if (objectParams.length === 0) { 99 | return params.toString(); 100 | } 101 | 102 | return [params.toString(), objectParams.join('&')].filter(Boolean).join('&'); 103 | } 104 | }; 105 | 106 | /** 107 | * @module Pilot.queryString 108 | */ 109 | var queryStringOld = /** @lends queryString */{ 110 | /** 111 | * Parse query string 112 | * @param {string} search 113 | * @returns {Object} 114 | */ 115 | parse: function (search) { 116 | var query = {}; 117 | 118 | if (typeof search === 'string') { 119 | if (/^[?#]/.test(search)) { 120 | search = search.substr(1); 121 | } 122 | 123 | var pairs = search.trim().split('&'), 124 | i = 0, 125 | n = pairs.length, 126 | pair, 127 | name, 128 | val; 129 | 130 | for (; i < n; i++) { 131 | pair = pairs[i].split('='); 132 | name = pair.shift().replace('[]', ''); 133 | val = pair.join('='); 134 | 135 | if (val === void 0) { 136 | val = ''; 137 | } 138 | else { 139 | try { 140 | val = decodeURIComponent(val); 141 | } 142 | catch (err) { 143 | val = unescape(val); 144 | } 145 | } 146 | 147 | if (name) { 148 | if (query[name] === void 0) { 149 | query[name] = val; 150 | } 151 | else if (query[name] instanceof Array) { 152 | query[name].push(val); 153 | } 154 | else { 155 | query[name] = [query[name], val]; 156 | } 157 | } 158 | } 159 | } 160 | 161 | return query; 162 | }, 163 | 164 | 165 | /** 166 | * Stringify query object 167 | * @param {Object} query 168 | * @returns {string} 169 | */ 170 | stringify: function (query) { 171 | var str = [], key, val; 172 | 173 | if (query && query instanceof Object) { 174 | for (key in query) { 175 | if (query.hasOwnProperty(key)) { 176 | str.push(_stringifyParam(key, query[key])); 177 | } 178 | } 179 | } 180 | 181 | return str.join('&'); 182 | } 183 | }; 184 | 185 | var hasUrlSearchParams = !!window.URLSearchParams; 186 | 187 | var queryString = hasUrlSearchParams ? queryStringNew : queryStringOld; 188 | 189 | // Export 190 | return Object.assign(queryString, {'new': queryStringNew, 'old': queryStringOld}); 191 | }); 192 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | define(['./url', './querystring'], function (/** URL */URL, /** queryString */queryString) { 2 | 3 | 4 | /** 5 | * @class Pilot.Request 6 | * @constructs Pilot.Request 7 | * @param {string} url 8 | * @param {string} [referrer] 9 | * @param {Pilot} [router] 10 | */ 11 | var Request = function (url, referrer, router) { 12 | url = new URL(url); 13 | 14 | this.href = url.href; 15 | this.protocol = url.protocol; 16 | this.host = url.host; 17 | this.hostname = url.hostname; 18 | this.port = url.port; 19 | 20 | this.path = 21 | this.pathname = url.pathname; 22 | 23 | this.search = url.search; 24 | this.query = queryString.parse(url.search); 25 | this.params = {}; 26 | 27 | this.hash = url.hash; 28 | 29 | this.route = router && (router.route || router.activeRoute) || {}; 30 | this.router = router; 31 | this.referrer = referrer; 32 | this.redirectHref = null; 33 | 34 | // Алиас, который сматчился 35 | this.alias = void 0; 36 | }; 37 | 38 | 39 | Request.prototype = /** @lends Request# */{ 40 | constructor: Request, 41 | 42 | clone: function () { 43 | var req = new Request(this.href, this.referrer, this.router); 44 | 45 | req.query = this.query; 46 | req.params = this.params; 47 | 48 | return req; 49 | }, 50 | 51 | is: function (id) { 52 | return !!(this.route && (this.route.id == id)); 53 | }, 54 | 55 | redirectTo: function (href, interrupt) { 56 | this.redirectHref = href; 57 | 58 | if (interrupt) { 59 | throw new Request(href, this.href, this.router); 60 | } 61 | }, 62 | 63 | toString: function () { 64 | return this.href; 65 | }, 66 | 67 | snapshot: function () { 68 | return Object.create(this, { 69 | query: { 70 | value: Object.assign({}, this.query) 71 | }, 72 | params: { 73 | value: Object.assign({}, this.params) 74 | } 75 | }); 76 | } 77 | }; 78 | 79 | 80 | // Export 81 | return Request; 82 | }); 83 | -------------------------------------------------------------------------------- /src/route.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'Emitter', 3 | './match', 4 | './url', 5 | './querystring' 6 | ], function ( 7 | /** Emitter */Emitter, 8 | /** Object */match, 9 | /** URL */Url, 10 | /** queryString */queryString 11 | ) { 12 | 'use strict'; 13 | 14 | var R_SPACE = /\s+/; 15 | 16 | /** 17 | * Обработка параметров url согласно правилам 18 | * @param {Object} rules правила 19 | * @param {Object} target объект обработки 20 | * @param {Pilot.Request} req оригинальный запрос 21 | * @returns {boolean} 22 | * @private 23 | */ 24 | var _urlProcessing = function (rules, target, req) { 25 | return Object.keys(rules).every(function (name) { 26 | var rule = rules[name], 27 | value = target[name]; 28 | 29 | if (value === void 0) { 30 | if (rule['default'] != null) { 31 | target[name] = rule['default']; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | target[name] = value = (rule.decode ? rule.decode(value, req) : value); 38 | 39 | return !rule.validate || rule.validate(value, req); 40 | }); 41 | }; 42 | 43 | var _cleanUrl = function (url) { 44 | return url.replace(/\/+$/, '/'); 45 | }; 46 | 47 | /** 48 | * Кладёт в target правила из source, если их там не было 49 | */ 50 | var _mergeRules = function (source, target) { 51 | if (!source) { 52 | source = {}; 53 | } 54 | 55 | if (!target) { 56 | target = {}; 57 | } 58 | 59 | Object.keys(source).forEach(function (sourceRule) { 60 | if (!target[sourceRule]) { 61 | target[sourceRule] = source[sourceRule]; 62 | } 63 | }); 64 | 65 | return target; 66 | }; 67 | 68 | 69 | /** 70 | * Преобразование образца маршрута в функцию генерации URL 71 | * @param {string} pattern 72 | * @return {Function} 73 | * @private 74 | */ 75 | var _toUrlBuilder = function (pattern) { 76 | var code = 'var url = "', 77 | i = 0, 78 | chr, 79 | expr; 80 | 81 | // Чистим образец 82 | pattern = pattern.replace(/([?*]|(\[.*?))/g, ''); 83 | 84 | function parseGroupStatement(prefix) { 85 | var str = 'url += "'; 86 | 87 | while (chr = pattern[i++]) { 88 | if (chr === ':') { // Переменная 89 | expr = pattern.substr(i).match(/[a-z0-9_-]+/)[0]; 90 | str += '" + (params ? params["' + expr + '"] : "") + "'; 91 | i += expr.length; 92 | } 93 | else if (chr === ')' || chr === '|') { // Группа или её закрытие 94 | code += prefix + 'if (params["' + expr + '"]) {' + str + '";}\n'; 95 | (chr === '|') && parseGroupStatement('else '); 96 | break; 97 | } 98 | else { 99 | str += chr; 100 | } 101 | } 102 | } 103 | 104 | // Main loop 105 | while (chr = pattern[i++]) { 106 | if (chr === ':') { // Переменная 107 | expr = pattern.substr(i).match(/[a-z0-9_-]+/)[0]; 108 | code += '" + (params ? params["' + expr + '"] : "") + "'; 109 | i += expr.length 110 | } 111 | else if (chr === '(') { // Открытие группы 112 | code += '";\n'; 113 | parseGroupStatement(''); 114 | code += 'url += "'; 115 | } else { 116 | code += chr; 117 | } 118 | } 119 | 120 | /* jshint evil:true */ 121 | return new Function( 122 | 'cleanUrl, stringify', 123 | 'return function urlBuilder(params, query) {\n' + code + '/";' + 124 | ' return cleanUrl(url) + (query ? "?" + stringify(query) : "");' + 125 | '}' 126 | )( 127 | _cleanUrl, 128 | queryString.stringify 129 | ); 130 | }; 131 | 132 | 133 | /** 134 | * @class Route 135 | * @memberof Pilot 136 | * @extends Emitter 137 | * @constructs Pilot.Route 138 | * @param {Object} options 139 | * @param {Pilot} router 140 | */ 141 | var Route = function (options, router) { 142 | /** 143 | * Описание URL 144 | * @type {Object} 145 | * @private 146 | */ 147 | this.url = {}; 148 | 149 | /** 150 | * Параметры маршрута 151 | * @type {Object} 152 | * @public 153 | * @readonly 154 | */ 155 | this.params = {}; 156 | 157 | /** 158 | * Регионы 159 | * @type {Array} 160 | * @public 161 | */ 162 | this.regions = []; 163 | 164 | /** 165 | * Ссылка на роутер 166 | * @type {Pilot} 167 | */ 168 | this.router = router; 169 | 170 | // Инит опций и свойств по ним 171 | this._initOptions(options); 172 | 173 | /** 174 | * Зазгрузчик моделей 175 | * @type {Pilot.Loader} 176 | * @private 177 | */ 178 | this.__model__ = options.model; 179 | 180 | /** 181 | * Модели 182 | * @type {Object} 183 | * @public 184 | */ 185 | this.model = options.model.defaults(); 186 | 187 | // Основной URL маршрута 188 | this.url.regexp = Url.toMatcher(this.url.pattern + (options.__group__ ? '/:any([a-z0-9\\/-]*)' : '')); 189 | this._urlBuilder = _toUrlBuilder(this.url.pattern); 190 | 191 | // Алиасы 192 | options.aliases = options.aliases || {}; 193 | 194 | this.aliases = Object.keys(options.aliases).map(function (name) { 195 | var url = options.aliases[name]; 196 | 197 | if (typeof url === 'string') { 198 | url = {pattern: url}; 199 | } 200 | 201 | // Какой-то неправильный URL передали 202 | if (!url || typeof url !== 'object') { 203 | return function () { 204 | return false 205 | }; 206 | } 207 | 208 | // Собираем всё, что нужно для построения URL по алиасу и наоборот 209 | url.regexp = Url.toMatcher(url.pattern); 210 | 211 | url.params = _mergeRules(this.url.params, url.params); 212 | url.query = _mergeRules(this.url.query, url.query); 213 | 214 | return { 215 | // Делаем функцию, которая будет смотреть совпадения этого урла с любым другим 216 | matcher: this._matchWithAnyUrl.bind(this, url), 217 | name: name 218 | }; 219 | }.bind(this)); 220 | 221 | // Родительский маршрут (группа) 222 | this.parentRoute = this.router[this.parentId]; 223 | 224 | this._initMixins() 225 | }; 226 | 227 | 228 | Route.prototype = /** @lends Route# */{ 229 | constructor: Route, 230 | 231 | /** 232 | * Внутряняя инициализация маршрута 233 | * @private 234 | */ 235 | __init: function () { 236 | this.inited = true; 237 | 238 | this.trigger('before-init', this); 239 | this.init(); 240 | this.trigger('init', this); 241 | }, 242 | 243 | /** 244 | * Пользовательская инициализация маршрута 245 | * @protected 246 | */ 247 | init: function () { 248 | }, 249 | 250 | /** 251 | * @param {Object} options 252 | * @protected 253 | */ 254 | _initOptions: function (options) { 255 | var _this = this; 256 | 257 | _this.options = options; 258 | 259 | Object.keys(options).forEach(function (key) { 260 | var value = options[key], 261 | act = key.match(/^(one?)[:-](\w+)/); 262 | 263 | if (key === '*') { // Регионы 264 | Object.keys(value).map(function (name) { 265 | var region = new Route.Region(name, value[name], _this); 266 | 267 | _this.regions.push(region); 268 | _this.regions[name] = region; 269 | 270 | _this.on('model', function () { 271 | region.model = _this.model; 272 | }); 273 | }); 274 | } 275 | else if (act) { 276 | if (act[1]) { // Биндинг событий 277 | _this[act[1]](act[2].replace(/-/g, ''), function (evt, req) { 278 | // Передаем только `req` 279 | _this[key](req); 280 | }); 281 | } 282 | } 283 | 284 | _this[key] = value; 285 | }); 286 | }, 287 | 288 | 289 | /** 290 | * Подмешиваем 291 | * @protected 292 | */ 293 | _initMixins: function () { 294 | Array.isArray(this.mixins) && this.mixins.forEach(function (mix) { 295 | Object.keys(mix).forEach(function (name) { 296 | if (name != 'apply') { 297 | this[name] = this[name] || mix[name]; 298 | } 299 | }, this); 300 | 301 | mix.apply && mix.apply.call(this, this); 302 | }, this); 303 | }, 304 | 305 | 306 | /** 307 | * Обработка маршрута 308 | * @param {URL} url 309 | * @param {Request} req 310 | * @param {Route} currentRoute 311 | * @param {Object} model 312 | */ 313 | handling: function (url, req, currentRoute, model) { 314 | // Либо это «мы», либо группа (только так, никаких множественных маршрутов) 315 | if ((this === currentRoute) || (this.__group__ && this.match(url, req))) { 316 | this.model = this.__model__.extract(model); 317 | this.params = req.params; 318 | this.request = req; 319 | 320 | this.trigger('model', [this.model, req]); 321 | 322 | if (!this.active) { 323 | this.active = true; 324 | 325 | // Внутренняя инициализация 326 | !this.inited && this.__init(); 327 | 328 | this.trigger('route-start', req); 329 | } 330 | else { 331 | this.trigger('route-change', req); 332 | } 333 | 334 | this.trigger('route', req); 335 | 336 | // Обработка регионов 337 | this.regions.forEach(function (/** Route.Region */region) { 338 | if (this.active && region.match(currentRoute.id)) { 339 | if (!region.active) { 340 | region.active = true; 341 | 342 | !region.inited && region.__init(); 343 | 344 | region.trigger('route-start', req); 345 | } 346 | 347 | region.trigger('route', req); 348 | } 349 | else if (region.active) { 350 | region.active = false; 351 | region.trigger('route-end', req); 352 | } 353 | }, this); 354 | } 355 | else if (this.active) { 356 | this.active = false; 357 | this.model = this.__model__.defaults(); 358 | 359 | // Это не копипаст! 360 | this.regions.forEach(function (/** Route.Region */region) { 361 | if (region.active) { 362 | region.active = false; 363 | region.trigger('route-end', req); 364 | } 365 | }); 366 | 367 | this.trigger('route-end', req); 368 | } 369 | }, 370 | 371 | 372 | /** 373 | * Проверка маршрута 374 | * @param {URL} url 375 | * @param {Pilot.Request} req 376 | * @returns {boolean} 377 | */ 378 | match: function (url, req) { 379 | return this._matchWithAnyUrl(this.url, url, req); 380 | }, 381 | 382 | /** 383 | * Проверка маршрута с любым URL 384 | * @param {URL} matcherUrl - url, который проверяем 385 | * @param {URL} matchingUrl - url, с которым проверяем 386 | * @param {Pilot.Request} req 387 | * @returns {boolean} 388 | * @private 389 | */ 390 | _matchWithAnyUrl: function (matcherUrl, matchingUrl, req) { 391 | var params = Url.match(matcherUrl.regexp, matchingUrl.pathname), 392 | query = matchingUrl.query, 393 | _paramsRules = matcherUrl.params, 394 | _queryRules = matcherUrl.query; 395 | 396 | return ( 397 | params && 398 | (!_paramsRules || _urlProcessing(_paramsRules, req.params = params, req)) && 399 | (!_queryRules || _urlProcessing(_queryRules, req.query = query, req)) 400 | ); 401 | }, 402 | 403 | /** 404 | * Получить данные 405 | * @param {Pilot.Request} req 406 | * @returns {Promise} 407 | */ 408 | fetch: function (req) { 409 | return this.__model__.fetch(req); 410 | }, 411 | 412 | /** 413 | * Получить URL 414 | * @param {Object} [params] 415 | * @param {Object|'inherit'} [query] 416 | * @return {string} 417 | */ 418 | getUrl: function (params, query) { 419 | if (query === 'inherit') { 420 | query = this.router.request.query; 421 | } 422 | 423 | return this.url.toUrl ? this.url.toUrl(params, query, this._urlBuilder) : this._urlBuilder(params, query); 424 | }, 425 | 426 | /** 427 | * @param {string} id 428 | * @return {boolean} 429 | */ 430 | is: function (id) { 431 | if (id.indexOf(' ') > -1) { 432 | var list = id.split(R_SPACE); 433 | var idx = list.length; 434 | 435 | while (idx--) { 436 | if (list[idx] === this.id) { 437 | return true; 438 | } 439 | } 440 | } 441 | 442 | return this.id === id; 443 | }, 444 | 445 | snapshot: function () { 446 | var snapshot = Object.create(this, { 447 | params: { 448 | value: Object.assign({}, this.params) 449 | }, 450 | request: { 451 | value: this.request && this.request.snapshot() 452 | }, 453 | url: { 454 | value: Object.assign({}, this.url) 455 | }, 456 | parentRoute: { 457 | value: this.parentRoute && this.parentRoute.snapshot() 458 | }, 459 | aliases: { 460 | value: this.aliases.map(function (alias) { 461 | return Object.assign({}, alias) 462 | }) 463 | } 464 | }); 465 | if (snapshot.request) { 466 | snapshot.request.route = snapshot; 467 | } 468 | 469 | return snapshot; 470 | } 471 | }; 472 | 473 | 474 | Emitter.apply(Route.prototype); 475 | 476 | 477 | /** 478 | * Регион маршрута 479 | * @class Route.Region 480 | * @extends Route 481 | * @memberof Pilot 482 | * @constructs Pilot.Route.Region 483 | */ 484 | Route.Region = function (name, options, route) { 485 | this.name = name; 486 | this.router = route.router; 487 | this.parentRoute = route; 488 | 489 | this._initOptions(options); 490 | this._initMixins(); 491 | 492 | this.match = match.cast(options.match); 493 | }; 494 | 495 | 496 | // Наследуем `Route` 497 | Route.Region.prototype = Object.create(Route.prototype); 498 | Route.Region.prototype.constructor = Route.Region; 499 | Route.Region.prototype.getUrl = function (params) { 500 | return this.parentRoute.getUrl(params); 501 | }; 502 | 503 | 504 | // Export 505 | return Route; 506 | }); 507 | -------------------------------------------------------------------------------- /src/status.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | /** 3 | * @class Pilot.Status 4 | * @constructs Pilot.Status 5 | * @param {number} code 6 | * @param {*} details 7 | */ 8 | var Status = function (code, details) { 9 | this.code = code; 10 | this.details = details; 11 | }; 12 | 13 | 14 | Status.prototype = /** @lends Pilot.Status */{ 15 | constructor: Status, 16 | 17 | toJSON: function () { 18 | return {code: this.code, details: this.details}; 19 | } 20 | }; 21 | 22 | 23 | /** 24 | * Преобразовать в статус 25 | * @methodOf Pilot.Status 26 | * @param {*} value 27 | * @return {Pilot.Status} 28 | */ 29 | Status.from = function (value) { 30 | if (value && value.status) { 31 | value = new Status(value.status, value); 32 | } 33 | else if (!value || !value.code) { 34 | value = new Status(500, value); 35 | } 36 | 37 | return value; 38 | }; 39 | 40 | 41 | // Export 42 | return Status; 43 | }); 44 | -------------------------------------------------------------------------------- /src/url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL module 3 | * Base on http://jsperf.com/url-parsing/26 4 | */ 5 | 6 | define(['./querystring'], function (/** queryString */queryString) { 7 | 'use strict'; 8 | 9 | var parseQueryString = queryString.parse; 10 | var stringifyQueryString = queryString.stringify; 11 | var encodeURIComponent = window.encodeURIComponent; 12 | 13 | 14 | /** 15 | * URL Parser 16 | * @type {RegExp} 17 | * @const 18 | */ 19 | var R_URL_PARSER = /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/; 20 | 21 | 22 | /** 23 | * Protocol checker 24 | * @type {RegExp} 25 | * @const 26 | */ 27 | var R_PROTOCOL = /^[a-z]+:/; 28 | 29 | 30 | /** 31 | * Protocol separator 32 | * @type {string} 33 | * @const 34 | */ 35 | var DOUBLE_SLASH = '//'; 36 | 37 | 38 | /** 39 | * @class Url 40 | * @constructs Url 41 | * @param {string} url 42 | * @param {string|Url|location} [base] 43 | */ 44 | function Url(url, base) { 45 | if (base === void 0) { 46 | base = location; 47 | } else if (typeof base === 'string') { 48 | base = new Url(base); 49 | } 50 | 51 | 52 | /* jshint eqnull:true */ 53 | if (url == null) { 54 | url = base.toString(); 55 | } 56 | else if (!R_PROTOCOL.test(url)) { 57 | var protocol = base.protocol, 58 | host = base.host, 59 | pathname = base.pathname; 60 | 61 | if (url.charAt(0) === '#') { 62 | url = base.toString().split('#')[0] + url; 63 | } 64 | else if (url.substr(0, 2) === DOUBLE_SLASH) { 65 | // without protocol 66 | url = protocol + url; 67 | } 68 | else if (url.charAt(0) === '/') { 69 | // absolute path 70 | url = protocol + DOUBLE_SLASH + host + url; 71 | } 72 | else { 73 | // relative path 74 | url = protocol + DOUBLE_SLASH + host + pathname.substr(0, pathname.lastIndexOf('/') + 1) + url; 75 | } 76 | } 77 | 78 | // todo: support punycode 79 | var matches = R_URL_PARSER.exec(url); 80 | 81 | this.protocol = matches[4] || ''; 82 | this.protocolSeparator = matches[5] || ''; 83 | 84 | this.credhost = matches[6] || ''; 85 | this.cred = matches[7] || ''; 86 | 87 | this.username = matches[8] || ''; 88 | this.password = matches[9] || ''; 89 | 90 | this.host = matches[10] || ''; 91 | this.hostname = matches[11] || ''; 92 | this.port = matches[12] || ''; 93 | this.origin = this.protocol + this.protocolSeparator + this.hostname; 94 | 95 | this.path = 96 | this.pathname = matches[13] || '/'; 97 | 98 | this.segment1 = matches[14] || ''; 99 | this.segment2 = matches[15] || ''; 100 | 101 | this.search = matches[16] || ''; 102 | this.query = parseQueryString(this.search); 103 | this.params = {}; 104 | 105 | this.hash = matches[17] || ''; 106 | 107 | this.update(); 108 | } 109 | 110 | 111 | Url.fn = Url.prototype = /** @lends Url# */{ 112 | constructor: Url, 113 | 114 | /** 115 | * Set query params 116 | * @param {object|string} query 117 | * @param {array|true} [remove] if `true`, clear the current `query` and set new 118 | * @returns {Url} 119 | */ 120 | setQuery: function (query, remove) { 121 | var currentQuery = this.query; 122 | 123 | if (typeof query === 'string'){ 124 | query = parseQueryString(query); 125 | } 126 | 127 | if (remove === true) { 128 | this.query = query; 129 | } 130 | else { 131 | if (query != null) { 132 | for (var key in query) { 133 | if (query.hasOwnProperty(key)) { 134 | if (query[key] == null) { 135 | delete currentQuery[key]; 136 | } else { 137 | currentQuery[key] = query[key]; 138 | } 139 | } 140 | } 141 | } 142 | 143 | if (remove) { 144 | if (!(remove instanceof Array)) { 145 | remove = [remove]; 146 | } 147 | 148 | remove.forEach(function (name) { 149 | delete currentQuery[name]; 150 | }); 151 | } 152 | } 153 | 154 | return this.update(); 155 | }, 156 | 157 | 158 | /** 159 | * Add query params 160 | * @param {object} query 161 | * @returns {Url} 162 | */ 163 | addToQuery: function (query) { 164 | return this.setQuery(query); 165 | }, 166 | 167 | 168 | /** 169 | * Remove query params 170 | * @param {string|array} query 171 | * @returns {Url} 172 | */ 173 | removeFromQuery: function (query) { 174 | return this.setQuery(void 0, query); 175 | }, 176 | 177 | 178 | /** @returns {Url} */ 179 | update: function () { 180 | var search = []; 181 | 182 | for (var key in this.query) { 183 | var value = this.query[key]; 184 | search.push(encodeURI(key) + (value != '' ? '=' + encodeURIComponent(value) : '')); 185 | } 186 | 187 | this.search = search.length ? '?' + search.join('&') : ''; 188 | 189 | this.url = this.href = ( 190 | this.protocol + this.protocolSeparator + 191 | (this.username ? encodeURIComponent(this.username) + (this.password ? ':' + encodeURIComponent(this.password) : '') + '@' : '') + 192 | this.host + this.pathname + this.search + this.hash 193 | ); 194 | 195 | return this; 196 | }, 197 | 198 | 199 | toString: function () { 200 | return this.url; 201 | } 202 | }; 203 | 204 | 205 | /** 206 | * Parse URL 207 | * @static 208 | * @param {string} url 209 | * @returns {Url} 210 | */ 211 | Url.parse = function (url) { 212 | return new Url(url); 213 | }; 214 | 215 | 216 | /** 217 | * Parse query string 218 | * @method Url.parseQueryString 219 | * @param {string} str 220 | * @returns {Object} 221 | */ 222 | Url.parseQueryString = queryString.parse; 223 | 224 | 225 | /** 226 | * Stringify query object 227 | * @method Url.parseQueryString 228 | * @param {Object} query 229 | * @returns {string} 230 | */ 231 | Url.stringifyQueryString = stringifyQueryString; 232 | 233 | 234 | /** 235 | * Конвертация описания пути в регулярное выражение 236 | * @param {string|RegExp} pattern 237 | * @return {RegExp} 238 | */ 239 | Url.toMatcher = function (pattern) { 240 | // https://github.com/visionmedia/express/blob/master/lib/utils.js#L248 241 | if (pattern instanceof RegExp) { 242 | return pattern; 243 | } 244 | 245 | if (Array.isArray(pattern)) { 246 | pattern = '(' + pattern.join('|') + ')'; 247 | } 248 | 249 | var keys = []; 250 | 251 | pattern = pattern 252 | .concat('/*') 253 | .replace(/\/+/g, '/') 254 | //.replace(/(\/\(|\(\/)/g, '(?:/') 255 | .replace(/\(([^\?])/g, '(?:$1') 256 | .replace(/(\/)?(\.)?:(\w+)(?:(\([^)]+\)))?(\?)?(\*)?/g, function(_, slash, format, key, capture, optional, star){ 257 | keys.push({ 258 | name: key, 259 | optional: !!optional 260 | }); 261 | 262 | slash = slash || ''; 263 | 264 | return '' + 265 | (optional ? '' : slash) + 266 | '(?:' + 267 | (optional ? slash : '') + 268 | (format || '') + (capture || (format && '([^/.]+)' || '([^/]+)')).replace('(?:', '(') + ')' + 269 | (optional || '') + 270 | (star ? '(/*)?' : '') 271 | ; 272 | }) 273 | .replace(/([\/.])/g, '\\$1') 274 | ; 275 | 276 | pattern = new RegExp('^' + pattern + '$', 'i'); 277 | pattern.keys = keys; 278 | 279 | return pattern; 280 | }; 281 | 282 | 283 | /** 284 | * Вытащить параметры из url 285 | * @param {string} pattern 286 | * @param {string|Url} [url] 287 | * @returns {Object|null} 288 | */ 289 | Url.match = function (pattern, url) { 290 | var i, n, 291 | value, 292 | params = {}, 293 | matches; 294 | 295 | url = Url.parse(url); 296 | pattern = Url.toMatcher(pattern); 297 | matches = pattern.exec(url.path); 298 | 299 | if (matches) { 300 | for (i = 1, n = matches.length; i < n; i++) { 301 | value = matches[i]; 302 | 303 | if (value !== void 0) { 304 | params[pattern.keys[i - 1].name] = value; 305 | } 306 | } 307 | 308 | return params; 309 | } 310 | 311 | return null; 312 | }; 313 | 314 | 315 | // Export 316 | return Url; 317 | }); 318 | -------------------------------------------------------------------------------- /statics/body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/Pilot/580aecf0adc88cf81a14f1c78b3a10e4261c8cf6/statics/body.png -------------------------------------------------------------------------------- /statics/body__glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/Pilot/580aecf0adc88cf81a14f1c78b3a10e4261c8cf6/statics/body__glow.png -------------------------------------------------------------------------------- /statics/docs.json: -------------------------------------------------------------------------------- 1 | {"Installation":{"label":"install","class":"Installation","descr":{"en":"`npm install pilotjs`
\n`cd pilotjs`
\n`npm install`
\n`grunt`","ru":"`npm install pilotjs`
\n`cd pilotjs`
\n`npm install`
\n`grunt`"},"props":{},"fn":{}},"Pilot":{"label":"install","class":"Pilot","descr":{"en":"Multifunctional JavaScript router solves the problem of routing your application,\nproviding full control over the route. It can work either by itself or as a part\nof other framework, such as Backbone, in which it could be an excellent substitute\nfor the standard `Backbone.Router` and even `Backbone.View`. Just try it.","ru":"Многофункциональный JavaScript router, решает проблему маршрутизации вашего приложения,\nобеспечивая полный контроль над маршрутом. Может работать как сам по себе, так и другими framework'ами,\nнапример Backbone, где он станет прекрасной заменой стандартным `Backbone.Router` и даже `Backbone.View`,\nпросто попробуйте."},"props":{"history":{"name":"history","type":"Array","label":"Pilot.history","descr":{"en":"Browsing history, the behavior is similar to `window.history`.","ru":"История навигации, поведение аналогично `window.history`."},"code":{"type":"js","source":{"en":"var Ace = new Pilot;\n\nAce.nav('/airport/');\nAce.nav('/airport/depot/');\n// etc\n\nconsole.log(Ace.history);\n// [\"http://site.com/airport/\", \"http://site.com/airport/depot/\", ..]","ru":"var Ace = new Pilot;\n\nAce.nav('/airport/');\nAce.nav('/airport/depot/');\n// etc\n\nconsole.log(Ace.history);\n// [\"http://site.com/airport/\", \"http://site.com/airport/depot/\", ..]"}}}},"fn":{"constructor":{"name":"constructor","label":"Pilot.constructor","args":{"options":{"en":"parameter object, see an Example","ru":"объект параметров, см. пример"}},"variants":[{"args":[{"name":"options","type":"Object","optional":true}],"descr":{"en":"","ru":""}}],"returns":"void","code":{"type":"js","source":{"en":"var Ivan = new Pilot({\n el: null // container, which is used to intercept clicks\n , production: false // this one is needed for switching off logging and error display\n});","ru":"var Ivan = new Pilot({\n el: null // контейнер, с которого нужно перехватывать клики\n , production: false // нужен для отключения логирования и вывода ошибок\n});"}}},"getUrl":{"name":"getUrl","label":"Pilot.getUrl","args":{"id":{"en":"unique route identifier","ru":"уникальный идентификатор маршрута"},"params":{"en":"parameters used to construct url","ru":"параметры, которые будут использованы при построении url"},"extra":{"en":"additional parameters","ru":"дополнительные параметры"}},"variants":[{"args":[{"name":"id","type":"String","optional":false},{"name":"params","type":"Object","optional":false},{"name":"extra","type":"Object","optional":true}],"descr":{"en":"Get url by route id. You can use this method to construct url by route id and parameters without keeping links inside.","ru":"Получить url, по id маршрута. Чтобы не зашивать ссылки внутри, вы можете использовать данный метода,\nкоторый по id маршрута и параметрам, собирает готовый url."}}],"returns":"String","code":{"type":"js","source":{"en":"var Ivan = new Pilot;\n\n// Add route\nIvan.route('address', '/:city/(street/:id/)', function (evt, req){ });\n\nIvan.getUrl('address'); // return \"/\"\nIvan.getUrl('address', { city: 'berlin' }); // \"/berlin/\"\nIvan.getUrl('address', { city: 'berlin', id: 123 }); // \"/berlin/street/10/\"","ru":"var Ivan = new Pilot;\n\n// Add route\nIvan.route('address', '/:city/(street/:id/)', function (evt, req){ });\n\nIvan.getUrl('address'); // return \"/\"\nIvan.getUrl('address', { city: 'berlin' }); // \"/berlin/\"\nIvan.getUrl('address', { city: 'berlin', id: 123 }); // \"/berlin/street/10/\""}}},"go":{"name":"go","label":"Pilot.go","args":{"id":{"en":"unique route identifier","ru":"уникальный идентификатор маршрута"},"params":{"en":"parameters used to construct url","ru":"параметры, которые будут использоваться для построения url"}},"variants":[{"args":[{"name":"id","type":"String","optional":false},{"name":"params","type":"Object","optional":true}],"descr":{"en":"Navigation by the route id. It is often necessary not only to understand how user moved,\nbut also to move user to the correct url, depending on his actions. This method allows you\nto change the route, operating with its id and parameters only.","ru":"Навигация по id маршрута. Часто нужно не только понять, как переместился пользователь,\nно и в зависимости от его действий перейти на нужный url, этот метод позволяет сменить маршрут,\nоперируя только его id и параметрами."}}],"returns":"jQuery.Deffered","code":{"type":"js","source":{"en":"var Ivan = new Pilot;\n\nIvan.route('coordinates', '/coordinates/:lat/:long', function (evt, req){ });\n\nIvan.go('coordinates', { lat: 16 }); // location: \"/coordinates/16/\"\nIvan.go('coordinates', { lat: 16, long: 178 }); // location: \"/coordinates/16/178/\"","ru":"var Ivan = new Pilot;\n\nIvan.route('coordinates', '/coordinates/:lat/:long', function (evt, req){ });\n\nIvan.go('coordinates', { lat: 16 }); // location: \"/coordinates/16/\"\nIvan.go('coordinates', { lat: 16, long: 178 }); // location: \"/coordinates/16/178/\""}}},"nav":{"name":"nav","label":"Pilot.nav","args":{"url":{"en":"relative or absolute url","ru":"относительный или абсолютный url"},"force":{"en":"force the user to move to the url even if he's already on it","ru":"все равно перейти на нужный урл, даже если мы уже там"}},"variants":[{"args":[{"name":"url","type":"String","optional":false},{"name":"force","type":"Boolean","optional":true}],"descr":{"en":"Navigation by url. With the call of this method, router finds all the handlers for this route and goes to it.\nIf url has not been changed, the transition will not be executed, but you can get round it using `force` parameter.","ru":"Навигация по url. Вызывая этот метод, роутер найдет все обработчики, для данного\nмаршрута и перейдет на него. Если url не изменился, то переход выполнен не будет,\nно это можно обойти, используй параметр `force`."}}],"returns":"jQuery.Deffered","code":{"type":"js","source":{"en":"var Ivan = new Pilot\n .on('beforeroute', function (req){ })\n .on('route', function (req){ })\n .on('404', function (req){ console.log('Route not found'); })\n .route('/base/moscow/', function (evt, req){\n console.log('Greetings from Moscow');\n })\n;\n\nIvan.nav('/base/moscow/'); // \"Greetings from Moscow\"\nIvan.nav('/base/moon/'); // \"Route not found\"","ru":"var Ivan = new Pilot\n .on('beforeroute', function (req){ })\n .on('route', function (req){ })\n .on('404', function (req){ console.log('Route not found'); })\n .route('/base/moscow/', function (evt, req){\n console.log('Greetings from Moscow');\n })\n;\n\nIvan.nav('/base/moscow/'); // \"Greetings from Moscow\"\nIvan.nav('/base/moon/'); // \"Route not found\""}}},"start":{"name":"start","label":"Pilot.start","args":{"url":{"en":"initial routing point","ru":"начальная \"точка\" маршрутизации"}},"variants":[{"args":[{"name":"url","type":"String","optional":true}],"descr":{"en":"Start router. When called with no parameters, the initial url is obtained through `Pilot.getLocation`.","ru":"Запустить роутер. Если вызывать без параметров, начальный url будет получен через `Pilot.getLocation`."}}],"returns":"void"},"route":{"name":"route","label":"Pilot.route:simple","args":{"pattern":{"en":"used for matching request while navigation","ru":"будет использован, для сопоставления с запросом при навигации"},"handler":{"en":"called with `event` and `request` parameters, if `pattern` matched with url","ru":"будет вызван с двумя параметрами event и request, если pattern подошел к url"},"withEndEvent":{"en":"calls `handler` in the end of route","ru":"вызывать `handler` при завершении маршрута"},"id":{"en":"unique route identifier","ru":"уникальный идентификатор маршрута"},"ctrl":{"en":"controller methods or successor `Pilot.Route`","ru":"методы котроллера или наследник `Pilot.Route`"},"options":{"en":"initialization options","ru":"будут переданным при инстанцировании контроллера"}},"variants":[{"args":[{"name":"pattern","type":"String","optional":false},{"name":"handler","type":"Function","optional":false},{"name":"withEndEvent","type":"Boolean","optional":true}],"descr":{"en":"A simple way to add a route.","ru":"Простой способ добавления маршрута."}},{"args":[{"name":"id","type":"String","optional":false},{"name":"pattern","type":"String","optional":false},{"name":"handler","type":"Function","optional":false},{"name":"withEndEvent","type":"Boolean","optional":true}],"descr":{"en":"A simple way to add a named route.","ru":"Простой способ добавления именного маршрута."}},{"args":[{"name":"pattern","type":"String","optional":false},{"name":"ctrl","type":"Object|Pilot.Route","optional":false}],"descr":{"en":"Add a route controller. Route controller is a powerful tool. With its help one you can tell the\nrouter (passing `Deffered` parameter), that it ought to wait for the receipt of data and only\nthen make the move to the route. This approach works well in combination with multiple controllers\non one route, where each one performs its small task, e.g., the first one gets a banner, the second\none get list of posts, and the third one gets user profile.","ru":"Добавить контроллер маршрута. Контроллер маршрута — это уже серьезно, с помощью его можно\nсообщить роутеру (передав Deffered), что сначала нужно дождаться получения данных и только потом, осуществить\nпереход на маршрут. Такой подход хорош в сочетании с множественными контроллерами на один маршрут,\nгде каждый выполняет свою маленькую задачу, например один получает баннер, другой список постов, а третий\nпрофиль юзера."}},{"args":[{"name":"id","type":"String","optional":false},{"name":"pattern","type":"String","optional":false},{"name":"ctrl","type":"Object|Pilot.Route","optional":false}],"descr":{"en":"Add a named route controller.","ru":"Добавить именованный контроллер маршрута."}}],"returns":"Pilot","code":{"type":"js","source":{"en":"var airport = Pilot.Route.extend({\n onRoute: function (){\n console.log('base:', this.getData().name);\n }\n});\n\nIvan\n .route('/base/1', airport, { data: { name: 'Moscow' } })\n .route('/base/2', airport, { data: { name: 'Yaroslavl' } })\n;\n\nIvan.nav('/base/1'); // \"base: Moscow\"\nIvan.nav('/base/2'); // \"base: Yaroslavl\"","ru":"var airport = Pilot.Route.extend({\n onRoute: function (){\n console.log('base:', this.getData().name);\n }\n});\n\nIvan\n .route('/base/1', airport, { data: { name: 'Moscow' } })\n .route('/base/2', airport, { data: { name: 'Yaroslavl' } })\n;\n\nIvan.nav('/base/1'); // \"base: Moscow\"\nIvan.nav('/base/2'); // \"base: Yaroslavl\""}}},"createGroup":{"name":"createGroup","label":"Pilot.createGroup","args":{"pattern":{"en":"base pattern","ru":"базавый паттерн"},"id":{"en":"unique route identifier","ru":"уникальный идентификатор маршрута"}},"variants":[{"args":[{"name":"pattern","type":"String","optional":false}],"descr":{"en":"Create a group and assign routes relative to it.","ru":"Создайте группу и назначайте маршруты относительно её."}},{"args":[{"name":"id","type":"String","optional":false},{"name":"pattern","type":"String","optional":false}],"descr":{"en":"Create a named group","ru":"Создание именованной группы"}}],"returns":"Pilot","code":{"type":"js","source":{"en":"var Ivan = new Pilot;\n .createGroup('/base/')\n .route('.', function (evt, req){ console.log('def'); })\n .route(':id', function (evt, req){\n console.log('base: '+.req.params.id);\n })\n .closeGroup()\n;\n\nIvan.nav('/base/'); // \"def\"\nIvan.nav('/base/123/'); // \"base: 123\"","ru":"var Ivan = new Pilot;\n .createGroup('/base/')\n .route('.', function (evt, req){ console.log('def'); })\n .route(':id', function (evt, req){\n console.log('base: '+.req.params.id);\n })\n .closeGroup()\n;\n\nIvan.nav('/base/'); // \"def\"\nIvan.nav('/base/123/'); // \"base: 123\""}}},"closeGroup":{"name":"closeGroup","label":"Pilot.closeGroup","args":{},"variants":[{"args":0,"descr":{"en":"Close the group and return the last one or router.","ru":"Закрыть группу и вернуть предыдущую, либо сам роутер"}}],"returns":"Pilot"},"on":{"name":"on","label":"Pilot.on","args":{"events":{"en":"one or more events, `namespace` can be used","ru":"одно или несколько событий, можно использовать namespace"},"fn":{"en":"handler function","ru":"функция обработчик"}},"variants":[{"args":[{"name":"events","type":"String","optional":false},{"name":"fn","type":"Function","optional":false}],"descr":{"en":"Add a handler for one or more events. Pilot has four events: `beforeroute`, `route`, `404` и `error`","ru":"Добавить обработчик одного или нескольких событий.\nУ Pilot есть четыре события: `beforeroute`, `route`, `404` и `error`"}}],"returns":"Pilot","code":{"type":"js","source":{"en":"new Pilot\n .on('beforeroute', function (evt/**$.Event*/, req/**Pilot.Request*/){ })\n .on('route', function (evt/**$.Event*/, req/**Pilot.Request*/){ })\n .on('404', function (evt/**$.Event*/, req/**Pilot.Request*/){ })\n .on('error', function (evt/**$.Event*/, err/**Error*/){ })\n;","ru":"new Pilot\n .on('beforeroute', function (evt/**$.Event*/, req/**Pilot.Request*/){ })\n .on('route', function (evt/**$.Event*/, req/**Pilot.Request*/){ })\n .on('404', function (evt/**$.Event*/, req/**Pilot.Request*/){ })\n .on('error', function (evt/**$.Event*/, err/**Error*/){ })\n;"}}},"off":{"name":"off","label":"Pilot.off","args":{"events":{"en":"one or more events, `namespace` can be used","ru":"одно или несколько событий, можно использовать namespace"},"fn":{"en":"handler function","ru":"функция обработчик"}},"variants":[{"args":[{"name":"events","type":"String","optional":false},{"name":"fn","type":"Function","optional":false}],"descr":{"en":"Switch off event handler.","ru":"Снять обработчик события."}}],"returns":"Pilot","code":{"type":"js","source":{"en":"new Pilot\n // Subscribe\n .on('route.one', function (evt/**$.Event*/, req/**Pilot.Request*/){\n // Unsubscribe using namespace\n this.off('.one');\n })\n;","ru":"new Pilot\n // Подписываемся\n .on('route.one', function (evt/**$.Event*/, req/**Pilot.Request*/){\n // Отписываемся используя namespace\n this.off('.one');\n })\n;"}}},"emit":{"name":"emit","label":"Pilot.emit","args":{"event":{"en":"event name","ru":"название события"},"args":{"en":"extra arguments","ru":"дополнительные аргументы"}},"variants":[{"args":[{"name":"event","type":"String","optional":false},{"name":"args","type":"Array","optional":true}],"descr":{"en":"Emit event.","ru":"Испустить событие."}}],"returns":"Pilot","code":{"type":"js","source":{"en":"var Ace = new Pilot\n .on('custom', function (evt/**$.Event*/, foo/**String*/){ })\n;\n\n\n// Emit event\nAce.emit('custom', [\"foo\"]);","ru":"var Ace = new Pilot\n .on('custom', function (evt/**$.Event*/, foo/**String*/){ })\n;\n\n\n// Испускаем событие\nAce.emit('custom', [\"foo\"]);"}}},"hasBack":{"name":"hasBack","label":"Pilot.hasBack","args":{},"variants":[{"args":0,"descr":{"en":"Verify that you can go back through `history`","ru":"Проверка возможности перехода назад по `history`"}}],"returns":"Boolean"},"hasForward":{"name":"hasForward","label":"Pilot.hasForward","args":{},"variants":[{"args":0,"descr":{"en":"Verify that you can go forward through `history`","ru":"Проверка возможности перехода вперед по `history`"}}],"returns":"Boolean"},"back":{"name":"back","label":"Pilot.back","args":{},"variants":[{"args":0,"descr":{"en":"Go to the previous url in `history`","ru":"Перейти на предыдущий url в `history`"}}],"returns":"jQuery.Deffered"},"forward":{"name":"forward","label":"Pilot.forward","args":{},"variants":[{"args":0,"descr":{"en":"Go to the next url relative to the current position in `history`","ru":"Перейти на следующий url, относительно текущий позиции в `history`"}}],"returns":"jQuery.Deffered"}}},"Pilot.Route":{"label":"Pilot.Route","class":"Pilot.Route","descr":{"en":"This class of the route controller allows not only to control events of starting,\nchanging or ending of the route, but also to inform the router that before going to\nthe correct url it has to wait the collection of data necessary to this controller.","ru":"Класс контроллера маршрута, позволяет не только, контролировать события начала,\nизменения и конца маршрута, но и сообщать роутеру, что перед переходом, на нужный url\nнужно дождаться сбора данных, нужных этому контроллеру."},"props":{"@events":{"name":"@events","type":-1,"label":"Pilot.Route.@events","descr":{"en":"Available events: `routestart`, `routechange` and `routeend`.\nThere is also `route` event, which is similar to `routestart` and `routechange`.","ru":"Доступные события: `routestart`, `routechange` и `routeend`.\nТакже есть событие `route`, соответсвует `routestart` и `routechange`."},"code":{"type":"js","source":{"en":"var airbase = Pilot.Route.extend({\n init: function (){\n this.on('routestart routeend', function (evt/**$.Event*/, req/**Pilot.Request*/){\n // ...\n });\n },\n\n onRoute: function (evt/**$.Event*/, req/**Pilot.Request*/){\n // You can also define a method with the name of the event\n }\n});","ru":"var airbase = Pilot.Route.extend({\n init: function (){\n this.on('routestart routeend', function (evt/**$.Event*/, req/**Pilot.Request*/){\n // ...\n });\n },\n\n onRoute: function (evt/**$.Event*/, req/**Pilot.Request*/){\n // Также можно определить метод с названием события\n }\n});"}}},"paramsRules":{"name":"paramsRules","type":"Object","label":"Pilot.Route.paramsRules","descr":{"en":"Additional rules test parameters","ru":"Дополнительные правила проверки для параметров."},"code":{"type":"js","source":{"en":"Ivan.route(\"/just/:name/\", {\n paramsRules: {\n name: function (value/**String*/, req/**Pilot.Request*/){\n return value == \"moscow\" || value == \"kiev\";\n }\n }\n});","ru":"Ivan.route(\"/just/:name/\", {\n paramsRules: {\n name: function (value/**String*/, req/**Pilot.Request*/){\n return value == \"moscow\" || value == \"kiev\";\n }\n }\n});"}}},"accessPermission":{"name":"accessPermission","type":"String","label":"Pilot.Route.accessPermission","descr":{"en":"","ru":"Установить доступ к маршруту."},"code":{"type":"js","source":{"en":"Pilot.access['denied'] = function (req/**Pilot.Request*/){\n return false;\n};\n\nvar Spy = new Pilot;\n\nSpy.route('/public/', function (){ console.log(\"Public!\"); })\n\nSpy.route('/private/', {\n accessPermission: 'denied', // permission\n accessDeniedRedirectTo: '/public/'\n});\n\nSpy.route('/public/closed/', {\n accessPermission: 'denied', // permission\n accessDeniedRedirectTo: '..'\n});\n\n\nSpy.nav('/private/'); // \"Public!\"\nSpy.nav('/public/closed/'); // \"Public!\"","ru":"Pilot.access['denied'] = function (req/**Pilot.Request*/){\n return false;\n};\n\nvar Spy = new Pilot;\n\nSpy.route('/public/', function (){ console.log(\"Public!\"); })\n\nSpy.route('/private/', {\n accessPermission: 'denied', // permission\n accessDeniedRedirectTo: '/public/'\n});\n\nSpy.route('/public/closed/', {\n accessPermission: 'denied', // permission\n accessDeniedRedirectTo: '..'\n});\n\n\nSpy.nav('/private/'); // \"Public!\"\nSpy.nav('/public/closed/'); // \"Public!\""}}},"accessDeniedRedirectTo":{"name":"accessDeniedRedirectTo","type":"String","label":"Pilot.Route.accessDeniedRedirectTo","descr":{"en":"Adopt such values ​​as: `url`, `route id`, `function` or `..` to rise to a up level.","ru":"Редирект, в случае отказа в доступе.\nПринимает такие значения как: `url`, `route id`, `function` или `..` чтобы подняться на уровень вверх."},"code":{"type":"js","source":{"en":"var ClosedBase = Pilot.Route.extend({\n accessPermission: false,\n accessDeniedRedirectTo: function (req/**Pilot.Request*/){\n return this.router.getUrl('home');\n }\n});","ru":"var ClosedBase = Pilot.Route.extend({\n accessPermission: false,\n accessDeniedRedirectTo: function (req/**Pilot.Request*/){\n return this.router.getUrl('home');\n }\n});"}}},"inited":{"name":"inited","type":"Boolean","label":"Pilot.Route.inited","descr":{"en":"Route initialization flag.","ru":"Флаг инициализации маршрута."},"code":{"type":"js","source":{"en":"var airbase = Pilot.Route.extend({\n loadData: function (){\n if( !this.inited ){\n this.setData({ name: 'Ramstein' });\n }\n }\n});","ru":"var airbase = Pilot.Route.extend({\n loadData: function (){\n if( !this.inited ){\n this.setData({ name: 'Ramstein' });\n }\n }\n});"}}},"router":{"name":"router","type":"Pilot","label":"Pilot.Route.router","descr":{"en":"Link to the router.","ru":"Ссылка на роутер."}},"boundAll":{"name":"boundAll","type":"Array","label":"Pilot.Route.boundAll","descr":{"en":"List of methods that will be executed in the context of the object.\nIt's very convenient for using with functions which will be used as event handlers.","ru":"Список методов, которые будут выполняться в контексте этого объекта.\nОчень удобно для функций, которые будут использоваться в качестве обработчиков событий."},"code":{"type":"js","source":{"en":"var City = Pilot.Route.extend({\n name: 'Moscow',\n boundAll: ['matryoshka', 'vodka', 'balalaika'],\n init: function (){\n $('#take').click(this.matryoshka);\n $('#drink').click(this.vodka);\n $('#play').click(this.balalaika);\n },\n matryoshka: function (evt){ console.log(this.city+': take ', evt) },\n vodka: function (evt){ console.log(this.city+': drink ', evt) },\n balalaika: function (evt){ console.log(this.city+': play ', evt) },\n});","ru":"var City = Pilot.Route.extend({\n name: 'Moscow',\n boundAll: ['matryoshka', 'vodka', 'balalaika'],\n init: function (){\n $('#take').click(this.matryoshka);\n $('#drink').click(this.vodka);\n $('#play').click(this.balalaika);\n },\n matryoshka: function (evt){ console.log(this.city+': take ', evt) },\n vodka: function (evt){ console.log(this.city+': drink ', evt) },\n balalaika: function (evt){ console.log(this.city+': play ', evt) },\n});"}}}},"fn":{"bound":{"name":"bound","label":"Pilot.Route.bound","args":{"fn":{"en":"function or its name in the controller","ru":"функция, либо её название в контроллере"}},"variants":[{"args":[{"name":"fn","type":"String|Function","optional":false}],"descr":{"en":"Bound the method with the context of the controller.","ru":"Связать метод с контекстом контроллера."}}],"returns":"Function","code":{"type":"js","source":{"en":"var airport = Pilot.View.extend({\n el: '#airport',\n init: function (){\n // Bound function\n this.$el.on('mouseenter', this.bound(function (evt){\n this._onHover(evt);\n }));\n \n // Bound by method name\n this.$el.on('mouseleave', this.bound('_onHover'));\n },\n _onHover: function (evt){\n this.$el.toggleClass('hovered', evt.type == 'mouseenter');\n }\n});","ru":"var airport = Pilot.View.extend({\n el: '#airport',\n init: function (){\n // Bound function\n this.$el.on('mouseenter', this.bound(function (evt){\n this._onHover(evt);\n }));\n \n // Bound by method name\n this.$el.on('mouseleave', this.bound('_onHover'));\n },\n _onHover: function (evt){\n this.$el.toggleClass('hovered', evt.type == 'mouseenter');\n }\n});"}}},"init":{"name":"init","label":"Pilot.Route.init","args":{},"variants":[{"args":0,"descr":{"en":"This method is intended to redefine and should be called once at the time of initialization of the controller.\nRemember that the initialization is not involved in creating the instance, that occurs in the first\ncall of the controller after `loadData`, but before the `routestart` event.","ru":"Это метод рассчитан на переопределение и будет вызван один раз в момент инициализации контроллера.\nПомните, что инициализация не связана созданием инстанса, она происходит при первом вызове контроллера,\nпосле `loadData`, но до события `routestart`."}}],"returns":"void","code":{"type":"js","source":{"en":"var airport = Pilot.Route.extend({\n init: function (){\n this.$el = $('#airport');\n }\n});","ru":"var airport = Pilot.Route.extend({\n init: function (){\n this.$el = $('#airport');\n }\n});"}}},"loadData":{"name":"loadData","label":"Pilot.Route.loadData","args":{"req":{"en":"request object","ru":"объект запроса"}},"variants":[{"args":[{"name":"req","type":"Pilot.Request","optional":false}],"descr":{"en":"This method should be called before `routestart`, `routechange`.\nIf `$.Deffered` returns, router will wait for the end of the controller data collection\nand then execute the navigation.","ru":"Метод будет вызван перед событием `routestart`, `routechange`. Если вренуть $.Deffered,\nто роутер дождётся окончания сбора данных контроллера и только потом осуществит навигацию."}}],"returns":"jQuery.Deffered|Null","code":{"type":"js","source":{"en":"var airport = Pilot.Route.extend({\n loadData: function (req){\n return $.ajax('/load/data/', req.query, this.bound(function (data){\n this.setData( data );\n }));\n },\n onRoute: function (){\n var data = this.getData();\n }\n});","ru":"var airport = Pilot.Route.View.extend({\n loadData: function (req){\n return $.ajax('/load/data/', req.query, this.bound(function (data){\n this.setData( data );\n }));\n },\n onRoute: function (){\n var data = this.getData();\n }\n});"}}},"getUrl":{"name":"getUrl","label":"Pilot.Route.getUrl","args":{"id":{"en":"unique route identifier","ru":"уникальный идентификатор маршрута"},"params":{"en":"parameters used to construct url","ru":"параметры, которые будут использованы при построении url"},"extra":{"en":"additional parameters","ru":"дополнительные параметры"}},"variants":[{"args":[{"name":"id","type":"String","optional":false},{"name":"params","type":"Object","optional":false},{"name":"extra","type":"Object","optional":true}],"descr":{"en":"Get url by route id. You can use this method to construct url by route id and parameters without keeping links inside.","ru":"Получить url, по id маршрута. Чтобы не зашивать ссылки внутри, вы можете использовать данный метода,\nкоторый по id маршрута и параметрам, собирает готовый url."}}],"returns":"String"},"getData":{"name":"getData","label":"Pilot.Route.getData","args":{},"variants":[{"args":0,"descr":{"en":"A simple method to get controller data.","ru":"Простой метод, для получения данных хранимых контроллером."}}],"returns":"Object","code":{"type":"js","source":{"en":"var airport = Pilot.Route.extend({\n data: { name: 'default' }\n});\n\n(new airport).getData().name; // \"default\"\n(new airport({ data: { name: 'NY' } })).getData().name; // \"NY\"","ru":"var airport = Pilot.Route.extend({\n data: { name: 'default' }\n});\n\n(new airport).getData().name; // \"default\"\n(new airport({ data: { name: 'NY' } })).getData().name; // \"NY\""}}},"setData":{"name":"setData","label":"Pilot.Route.setData","args":{"data":{"en":"new data","ru":"новые данные"},"merge":{"en":"merge with the current ones","ru":"слить с уже установленными данными"}},"variants":[{"args":[{"name":"data","type":"Object","optional":false},{"name":"merge","type":"Boolean","optional":true}],"descr":{"en":"Set new controller data or merge with the current ones.","ru":"Установит данные контроллера, или слить с текущими."}}],"returns":"Pilot.Route","code":{"type":"js","source":{"en":"var airport = Pilot.Route.extend({\n data: { name: 'default', city: 'unknown' }\n});\n\n(new airport).setData({ name: 'Foo' }).getData();\n// { name: 'Foo' }\n\n(new airport).setData({ name: 'Foo' }, true).getData();\n// { name: 'Foo', city: 'unknown' }\n\n(new airport).setData({ name: 'Foo', city: 'Bar' }).getData();\n// { name: 'Foo', city: 'Bar' }","ru":"var airport = Pilot.Route.extend({\n data: { name: 'default', city: 'unknown' }\n});\n\n(new airport).setData({ name: 'Foo' }).getData();\n// { name: 'Foo' }\n\n(new airport).setData({ name: 'Foo' }, true).getData();\n// { name: 'Foo', city: 'unknown' }\n\n(new airport).setData({ name: 'Foo', city: 'Bar' }).getData();\n// { name: 'Foo', city: 'Bar' }"}}}}},"Pilot.View":{"label":"Pilot.View","class":"Pilot.View","descr":{"en":"`Pilot.Route` successor implements methods for working with DOM elements, events and patterning.\nBy default, `Pilot.View` is subscribed to `routestart` and `routeend` events and controls the visibility\nof a DOM element associated with it, setting it to `display: none` or removing it.","ru":"Наследник Pilot.Route, имплементирует в себе методы для работы с DOM элементами, событиями и шаблонизацией.\nПо умолчанию, `Pilot.View` подписан события `routestart` и `routeend` контролируя видимость\nDOM элемента, связанного с ним, выставляя ему `display: none` или убирая его."},"props":{"el":{"name":"el","type":"HTMLElement","label":"Pilot.View.el","descr":{"en":"Link to the DOM element, with which `View` is working.","ru":"Ссылка на DOM элемент, за которые отвечает вид."},"code":{"type":"js","source":{"en":"var airport = Pilot.View.extend({\n el: '#airport-default'\n});\n\n(new airport).el; // HTMLElement:
..
\n(new airport({ el: '#moscow' })).el; // HTMLElement:
..
","ru":"var airport = Pilot.View.extend({\n el: '#airport-default'\n});\n\n(new airport).el; // HTMLElement:
..
\n(new airport({ el: '#moscow' })).el; // HTMLElement:
..
"}}},"$el":{"name":"$el","type":"jQuery","label":"Pilot.View.$el","descr":{"en":"jQuery collection, for more convenient work.","ru":"jQuery коллекция, для более удобной работы."},"code":{"type":"js","source":{"en":"var base = Pilot.View.extend({\n el: '#moscow'\n});\n\n(new base).el; // jQuery[
..
]\n(new base({ el: '#moon' })).el; // jQuery[
..
]","ru":"var base = Pilot.View.extend({\n el: '#moscow'\n});\n\n(new base).el; // jQuery[
..
]\n(new base({ el: '#moon' })).el; // jQuery[
..
]"}}},"tagName":{"name":"tagName","type":"String","label":"Pilot.View.tagName","descr":{"en":"If you specify this option, this tag will be created while the initialization.","ru":"Если указать этот параметр, то при инициализации будет создан этот тег."},"code":{"type":"js","source":{"en":"var base = Pilot.View.extend({\n tagName: 'span'\n});\n\n(new base).el; // HTMLElement: ..\n(new base).$el.appendTo('body'); // jQuery[..]","ru":"var base = Pilot.View.extend({\n tagName: 'span'\n});\n\n(new base).el; // HTMLElement: ..\n(new base).$el.appendTo('body'); // jQuery[..]"}}},"tag":{"name":"tag","type":"String","label":"Pilot.View.tag","descr":{"en":"Create a tag and put it in the container.","ru":"Создать тег и вставить его в нужный контейнер."},"code":{"type":"js","source":{"en":"var base = Pilot.View.extend({\n tag: '#box span.base.base_military'\n});\n\n(new base).el; // HTMLElement: ..","ru":"var base = Pilot.View.extend({\n tag: '#box span.base.base_military'\n});\n\n(new base).el; // HTMLElement: .."}}},"singleton":{"name":"singleton","type":"Boolean","label":"Pilot.View.singleton","descr":{"en":"","ru":""},"code":{"type":"js","source":{"en":"var airbase = Pilot.View.extend({\n el: '#aribase',\n sigleton: true,\n onRouteStart: function (evt, req){\n console.log('start:', req.path);\n },\n onRouteChange: function (evt, req){\n console.log('change:', req.path);\n },\n onRouteStart: function (evt, req){\n console.log('end:', req.path);\n }\n});\n\nvar Ivan = new Pilot\n .route('/sky/foo/', airbase)\n .route('/sky/bar/', airbase)\n .route('/sky/baz/', function (evt, req){\n console.log('Sky base Baz');\n })\n .route('/sky/qux/', airbase)\n;\n\nIvan.nav('/sky/foo/'); // \"start: /sky/foo/\"\nIvan.nav('/sky/bar/'); // \"change: /sky/bar/\"\nIvan.nav('/sky/qux/'); // \"change: /sky/qux/\"\nIvan.nav('/sky/baz/'); // \"Sky base Baz\"\n // \"end: /sky/baz/\"","ru":"var airbase = Pilot.View.extend({\n el: '#aribase',\n sigleton: true,\n onRouteStart: function (evt, req){\n console.log('start:', req.path);\n },\n onRouteChange: function (evt, req){\n console.log('change:', req.path);\n },\n onRouteStart: function (evt, req){\n console.log('end:', req.path);\n }\n});\n\nvar Ivan = new Pilot\n .route('/sky/foo/', airbase)\n .route('/sky/bar/', airbase)\n .route('/sky/baz/', function (evt, req){\n console.log('Sky base Baz');\n })\n .route('/sky/qux/', airbase)\n;\n\nIvan.nav('/sky/foo/'); // \"start: /sky/foo/\"\nIvan.nav('/sky/bar/'); // \"change: /sky/bar/\"\nIvan.nav('/sky/qux/'); // \"change: /sky/qux/\"\nIvan.nav('/sky/baz/'); // \"Sky base Baz\"\n // \"end: /sky/baz/\""}}},"template":{"name":"template","type":"Fucntion","label":"Pilot.View.template","descr":{"en":"Here can be any patterning function.","ru":"Тут может быть любая функция шаблонизации."},"code":{"type":"js","source":{"en":"var region = Pilot.View.extend({\n template: function (data/**Object*/){\n /* \"data\" is equal this.getData() */\n // Use any template engine\n return xtpl.fetch('templates/region.xtpl', data);\n }\n});","ru":"var region = Pilot.View.extend({\n template: function (data/**Object*/){\n /* \"data\" is equal this.getData() */\n // Use any template engine\n return xtpl.fetch('templates/region.xtpl', data);\n }\n});"}}}},"fn":{"toggleView":{"name":"toggleView","label":"Pilot.View.toggleView","args":{"state":{"en":"`true`: route start, `false`: route end","ru":"true начало маршрута, false - конец"}},"variants":[{"args":[{"name":"state","type":"Boolean","optional":false}],"descr":{"en":"This method is called at the start and in the end of the route.\nIts redefining can help you change the way elements are displayed, e.g., to add the animation.","ru":"Это метод вызывается в начале маршрута и конце, переопределив его вы можете изменить способ, которым отображать\nсвязанные элемент, например добавив анимацию."}}],"returns":"void","code":{"type":"js","source":{"en":"var region = Pilot.View.extend({\n toggleView: function (state/**Boolean*/){\n this.$el.animate({ opacity: +state }, 'fast');\n }\n});","ru":"var region = Pilot.View.extend({\n toggleView: function (state/**Boolean*/){\n this.$el.animate({ opacity: +state }, 'fast');\n }\n});"}}},"setElement":{"name":"setElement","label":"Pilot.View.setElement","args":{"selector":{"en":"string containing jQuery selector or HTMLElement, [detail](http://api.jquery.com/jQuery/)","ru":"строка содержащая jQuery selector или HTMLElement, [detail](http://api.jquery.com/jQuery/)"}},"variants":[{"args":[{"name":"selector","type":"jQuerySelector","optional":false}],"descr":{"en":"Set the element with which 'View' is working (automatically changes `this.el` and `this.$el` properties).","ru":"Установить элемент, с которым работает вид, автоматически меняет свойства `this.el` и `this.$el`."}}],"returns":"Pilot.View"},"$":{"name":"$","label":"Pilot.View.$","args":{"selector":{"en":"string containing jQuery selector or HTMLElement, [detail](http://api.jquery.com/jQuery/)","ru":"строка содержащая jQuery selector или HTMLElement, [detail](http://api.jquery.com/jQuery/)"}},"variants":[{"args":[{"name":"selector","type":"jQuerySelector","optional":false}],"descr":{"en":"Select elements inside the 'View' (equal to `this.$el.find`, but more easy).","ru":"Выбрать элементы внутри вида, равносильно `this.$el.find`, но более удобно."}}],"returns":"jQuery"},"getHtml":{"name":"getHtml","label":"Pilot.View.getHtml","args":{"data":{"en":"data for patterning","ru":"данные для шаблонизации"}},"variants":[{"args":[{"name":"data","type":"Object","optional":true}],"descr":{"en":"Get HTML based on `this.template` and sent data or 'View' data.","ru":"Получить HTML на основе `this.template` и переданных данных, либо данных вида."}}],"returns":"String"},"render":{"name":"render","label":"Pilot.View.render","args":{},"variants":[{"args":0,"descr":{"en":"Refresh HTML `this.el` by `this.getHtml()`","ru":"Обновляет HTML `this.el`, при помощи `this.getHtml()`"}}],"returns":"void","code":{"type":"js","source":{"en":"var city = Pilot.View.extend({\n templateFile: 'city/default.xtpl',\n template: function (obj){\n return xtpl.fetch(this.templateFile, obj);\n },\n onRoute: function (){\n this.render();\n }\n});","ru":"var city = Pilot.View.extend({\n templateFile: 'city/default.xtpl',\n template: function (obj){\n return xtpl.fetch(this.templateFile, obj);\n },\n onRoute: function (){\n this.render();\n }\n});"}}}}},"Pattern-syntax route":{"label":"Pilot.View.render","class":"Pattern-syntax route","descr":{"en":"
    \n\t
  • `/search/` — strict match
  • \n\t
  • `/gallery/:tag/` — parameterized
  • \n\t
  • `/search/result/:page?` — parameterized (optional)
  • \n\t
  • `/user/:id(\\\\d+)` — parameter validation
  • \n\t
  • `/search/(result/:page/)?` — grouping
  • \n
","ru":"
    \n\t
  • `/search/` — строгое соответсвие
  • \n\t
  • `/gallery/:tag/` — параметризованный
  • \n\t
  • `/search/result/:page?` — параметризованный (необязательный)
  • \n\t
  • `/user/:id(\\\\d+)` — валидация параметров
  • \n\t
  • `/search/(result/:page/)?` — группировка
  • \n
"},"props":{},"fn":{}},"Pilot.Request":{"label":"Pilot.Request","class":"Pilot.Request","descr":{"en":"route: `/gallery/:tag/:perPage?(/page/:page)?`
\nrequest: `/gallery/cubism/20/page/123?search=text`","ru":"route: `/gallery/:tag/:perPage?(/page/:page)?`
\nrequest: `/gallery/cubism/20/page/123?search=text`"},"props":{"@extend":{"name":"@extend","type":-1,"label":"Pilot.Request.@extend","descr":{"en":"Add and use its methods, eg:","ru":"Расширение объекта собственными методами."},"code":{"type":"js","source":{"en":"Pilot.Request.fn.getPage = function (){\n return parseInt(this.params.page || this.query.page, 10) || 1;\n};\n\n(new Pilot)\n .route('/news/page/:page', function (evt, req/**Pilot.Request*/){\n var page = req.getPage();\n console.log('news.page:', page);\n })\n .route('/search/', function (evt, req/**Pilot.Request*/){\n var page = req.getPage();\n console.log('search.page:', page);\n })\n .nav('/news/page/') // news.page: 1\n .nav('/news/page/2/') // news.page: 2\n .nav('/search/?page=123') // search.page: 123\n;","ru":"Pilot.Request.fn.getPage = function (){\n return parseInt(this.params.page || this.query.page, 10) || 1;\n};\n\n(new Pilot)\n .route('/news/page/:page', function (evt, req/**Pilot.Request*/){\n var page = req.getPage();\n console.log('news.page:', page);\n })\n .route('/search/', function (evt, req/**Pilot.Request*/){\n var page = req.getPage();\n console.log('search.page:', page);\n })\n .nav('/news/page/') // news.page: 1\n .nav('/news/page/2/') // news.page: 2\n .nav('/search/?page=123') // search.page: 123\n;"}}},"url":{"name":"url","type":"String","label":"Pilot.Request.@extend","descr":{"en":"Absolute url: `http://domain.com/gallery/cubism/20/page/3?search=text`","ru":"Абсолютный url: `http://domain.com/gallery/cubism/20/page/3?search=text`"}},"path":{"name":"path","type":"String","label":"Pilot.Request.@extend","descr":{"en":"The path relative to the web-site root: `/gallery/cubism/20/page/3`","ru":"Путь, относительно корня сайта: `/gallery/cubism/20/page/3`"}},"search":{"name":"search","type":"String","label":"Pilot.Request.@extend","descr":{"en":"GET parameters string: `?search=text`","ru":"Строка GET-параметров: `?search=text`"}},"query":{"name":"query","type":"Object","label":"Pilot.Request.@extend","descr":{"en":"GET parameters object: `{ search: \"text\" }`","ru":"Объект GET-параметров: `{ search: \"text\" }`"}},"params":{"name":"params","type":"Object","label":"Pilot.Request.@extend","descr":{"en":"Route parameters: `{ tag: \"cubism\", perPage: 20, page: 123 }`","ru":"Параметры маршрута: `{ tag: \"cubism\", perPage: 20, page: 123 }`"}},"referrer":{"name":"referrer","type":"String","label":"Pilot.Request.@extend","descr":{"en":"Contains url of previous request: `http://domain.com/gallery/cubism/20/page/12`","ru":"Содержит url предыдущего запроса: `http://domain.com/gallery/cubism/20/page/12`"}}},"fn":{"clone":{"name":"clone","label":"Pilot.Request.@extend","args":{},"variants":[{"args":0,"descr":{"en":"Clone method."}}],"returns":"Pilot.Request"}}},"History API":{"label":"HistoryAPI","class":"History API","descr":{"en":"By default, the library doesn't contain any polyfills and rely only on native support.","ru":"По умолчанию, библиотека не содержит никаких полифилов и рассчитывает только на нативную поддержку."},"props":{"Pilot.pushState":{"name":"Pilot.pushState","type":"Boolean","label":"Pilot.pushState","descr":{"en":"Use the full History API, otherwise `location.hash`.","ru":"Использовать полноценное History API, иначе `location.hash`."},"code":{"type":"js","source":{"en":"Pilot.pushState = true;","ru":"Pilot.pushState = true;"}}}},"fn":{"Pilot.getLocation":{"name":"Pilot.getLocation","label":"Pilot.getLocation","args":{},"variants":[{"args":0,"descr":{"en":"Get current location.","ru":"Получить текущее положение."}}],"returns":"String"},"Pilot.setLocation":{"name":"Pilot.setLocation","label":"Pilot.setLocation","args":{"req":{"en":"request object","ru":"объект запроса"}},"variants":[{"args":[{"name":"req","type":"Object","optional":false}],"descr":{"en":"Set a new location.","ru":"Установить новое положение."}}],"returns":"void"}}},"Changelog":{"label":"changelog","class":"Changelog","descr":{"en":"","ru":""},"props":{"1.3":{"name":"1.3","type":-1,"label":"changelog","descr":{"en":"
    \n\t
  • + `paramsRules` route option
  • \n\t
  • + `accessPermission` route option
  • \n\t
  • + `accessDeniedRedirectTo` route option
  • \n
"}},"1.2.1":{"name":"1.2.1","type":-1,"label":"changelog","descr":{"en":"
    \n\t
  • + Support Zepto, Ender or $
  • \n\t
  • Fixed set request params
  • \n\t
  • Fixed Pilot options
  • \n
"}},"1.2.0":{"name":"1.2.0","type":-1,"label":"changelog","descr":{"en":"
    \n\t
  • [#4](https://github.com/RubaXa/Pilot/pull/4): Added Pilot.Request.
  • \n\t
  • + Pilot.utils.each
  • \n\t
  • + Pilot.utils.extend
  • \n\t
  • + Pilot.utils.qs.parse(queryString)/**Object*/
  • \n\t
  • + Pilot.utils.qs.stringify(queryObject)/**String*/
  • \n
"}},"1.1.0":{"name":"1.1.0","type":-1,"label":"changelog","descr":{"en":"
    \n\t
  • [#3](https://github.com/RubaXa/Pilot/pull/3): Allow customize selector for links.
  • \n
"}},"1.0.0":{"name":"1.0.0","type":-1,"label":"changelog","descr":{"en":"First release","ru":"First release"}}},"fn":{}}} -------------------------------------------------------------------------------- /statics/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/Pilot/580aecf0adc88cf81a14f1c78b3a10e4261c8cf6/statics/logo.png -------------------------------------------------------------------------------- /statics/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | font-size: 14px; 5 | font-family: "Arial", Helvetica, Garuda, sans-serif; 6 | } 7 | 8 | html, body { 9 | min-height: 100%; 10 | } 11 | 12 | .sidebar { 13 | top: 0; 14 | left: 0; 15 | height: 100%; 16 | position: fixed; 17 | overflow: auto; 18 | overflow-x: hidden; 19 | width: 250px; 20 | background-color: rgba(255,255,255,.8); 21 | *background-color: #fff; 22 | box-shadow: 0 0 20px rgba(0,0,0,.8) 23 | } 24 | 25 | .body { 26 | position: relative; 27 | line-height: 140%; 28 | background-image: url('body.png'); 29 | } 30 | 31 | .body__glow { 32 | top: 0; 33 | left: 0; 34 | width: 100%; 35 | height: 200px; 36 | opacity: .12; 37 | position: absolute; 38 | background: url('body__glow.png') top center no-repeat; 39 | z-index: -1; 40 | *display: none; 41 | } 42 | 43 | a { 44 | color: #333; 45 | text-decoration: none; 46 | } 47 | a:hover { 48 | color: #f60; 49 | border-bottom: 1px dotted #f60; 50 | } 51 | 52 | ul { 53 | list-style: none; 54 | padding: 0; 55 | margin: 5px 0 5px 15px; 56 | } 57 | 58 | .logo { 59 | width: 324px; 60 | height: 233px; 61 | margin: 40px auto 0; 62 | background: url('logo.png') no-repeat; 63 | } 64 | 65 | .menu { 66 | margin: 20px 15px; 67 | font-size: 16px; 68 | line-height: 130%; 69 | } 70 | .menu__name { 71 | border-bottom: 1px dotted #333; 72 | } 73 | 74 | .content { 75 | margin: 50px 40px 0; 76 | padding-bottom: 100px; 77 | max-width: 700px; 78 | text-shadow: 0 0 1px #fff; 79 | } 80 | 81 | 82 | .descr { 83 | margin-top: 5px; 84 | margin-bottom: 10px; 85 | } 86 | 87 | code { 88 | font-family: monospace; 89 | border-radius: 5px; 90 | } 91 | .code { 92 | font-size: 90%; 93 | font-family: monospace; 94 | border: 1px solid #ccc; 95 | border-radius: 3px; 96 | background-color: rgba(255,255,255,.3); 97 | *background-color: #f3f3f3; 98 | padding: 0 3px; 99 | } 100 | 101 | .fn { 102 | margin: 10px 0 0; 103 | font-size: 16px; 104 | } 105 | 106 | .type { 107 | color: #060; 108 | font-weight: normal; 109 | } 110 | .fn__args { 111 | font-weight: normal; 112 | } 113 | 114 | .sticky_bottom { 115 | top: auto; 116 | bottom: 0; 117 | position: fixed; 118 | } 119 | 120 | 121 | /* 122 | 123 | XCode style (c) Angel Garcia 124 | 125 | */ 126 | 127 | pre code { 128 | display: block; padding: 0.5em; 129 | background: rgba(255,255,255,.6); color: black; 130 | } 131 | 132 | pre .comment, 133 | pre .template_comment, 134 | pre .javadoc, 135 | pre .comment * { 136 | color: rgb(0,106,0); 137 | } 138 | 139 | pre .keyword, 140 | pre .literal, 141 | pre .nginx .title { 142 | color: rgb(170,13,145); 143 | } 144 | pre .method, 145 | pre .list .title, 146 | pre .tag .title, 147 | pre .setting .value, 148 | pre .winutils, 149 | pre .tex .command, 150 | pre .http .title, 151 | pre .request, 152 | pre .status { 153 | color: #008; 154 | } 155 | 156 | pre .envvar, 157 | pre .tex .special { 158 | color: #660; 159 | } 160 | 161 | pre .string { 162 | color: rgb(196,26,22); 163 | } 164 | pre .tag .value, 165 | pre .cdata, 166 | pre .filter .argument, 167 | pre .attr_selector, 168 | pre .apache .cbracket, 169 | pre .date, 170 | pre .regexp { 171 | color: #080; 172 | } 173 | 174 | pre .sub .identifier, 175 | pre .pi, 176 | pre .tag, 177 | pre .tag .keyword, 178 | pre .decorator, 179 | pre .ini .title, 180 | pre .shebang, 181 | pre .prompt, 182 | pre .hexcolor, 183 | pre .rules .value, 184 | pre .css .value .number, 185 | pre .symbol, 186 | pre .symbol .string, 187 | pre .number, 188 | pre .css .function, 189 | pre .clojure .title, 190 | pre .clojure .built_in { 191 | color: rgb(28,0,207); 192 | } 193 | 194 | pre .class .title, 195 | pre .haskell .type, 196 | pre .smalltalk .class, 197 | pre .javadoctag, 198 | pre .yardoctag, 199 | pre .phpdoc, 200 | pre .typename, 201 | pre .tag .attribute, 202 | pre .doctype, 203 | pre .class .id, 204 | pre .built_in, 205 | pre .setting, 206 | pre .params, 207 | pre .clojure .attribute { 208 | color: rgb(92,38,153); 209 | } 210 | 211 | pre .variable { 212 | color: rgb(63,110,116); 213 | } 214 | pre .css .tag, 215 | pre .rules .property, 216 | pre .pseudo, 217 | pre .subst { 218 | color: #000; 219 | } 220 | 221 | pre .css .class, pre .css .id { 222 | color: #9B703F; 223 | } 224 | 225 | pre .value .important { 226 | color: #ff7700; 227 | font-weight: bold; 228 | } 229 | 230 | pre .rules .keyword { 231 | color: #C5AF75; 232 | } 233 | 234 | pre .annotation, 235 | pre .apache .sqbracket, 236 | pre .nginx .built_in { 237 | color: #9B859D; 238 | } 239 | 240 | pre .preprocessor, 241 | pre .preprocessor * { 242 | color: rgb(100,56,32); 243 | } 244 | 245 | pre .tex .formula { 246 | background-color: #EEE; 247 | font-style: italic; 248 | } 249 | 250 | pre .diff .header, 251 | pre .chunk { 252 | color: #808080; 253 | font-weight: bold; 254 | } 255 | 256 | pre .diff .change { 257 | background-color: #BCCFF9; 258 | } 259 | 260 | pre .addition { 261 | background-color: #BAEEBA; 262 | } 263 | 264 | pre .deletion { 265 | background-color: #FFC8BD; 266 | } 267 | 268 | pre .comment .yardoctag { 269 | font-weight: bold; 270 | } 271 | 272 | pre .method .id { 273 | color: #000; 274 | } 275 | -------------------------------------------------------------------------------- /statics/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/Pilot/580aecf0adc88cf81a14f1c78b3a10e4261c8cf6/statics/spinner.gif -------------------------------------------------------------------------------- /tests/alias.tests.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, test, expect */ 2 | 3 | const Pilot = require('../src/pilot'); 4 | 5 | describe('Pilot:alias', () => { 6 | 7 | test('alias without parameter decoders', async () => { 8 | const app = Pilot.create({ 9 | '#index': { 10 | url: '/:folder?', 11 | aliases: { 12 | 'compose': '/compose/:folder?/:id?' 13 | } 14 | }, 15 | }); 16 | 17 | await app.nav('/'); 18 | 19 | // Обычный переход, алиасов нет 20 | expect(app.route.is('#index')).toBeTruthy(); 21 | expect(app.route.params).toEqual({}); 22 | expect(app.request.alias).toEqual(void 0); 23 | 24 | await app.nav('/inbox'); 25 | 26 | // Обычный переход, алиасов нет 27 | expect(app.route.is('#index')).toBeTruthy(); 28 | expect(app.route.params).toEqual({folder: 'inbox'}); 29 | expect(app.request.alias).toEqual(void 0); 30 | 31 | await app.nav('/compose'); 32 | 33 | // Переход с алиасом без параметров 34 | expect(app.route.is('#index')).toBeTruthy(); 35 | expect(app.route.params).toEqual({}); 36 | expect(app.request.alias).toEqual('compose'); 37 | 38 | await app.nav('/compose/inbox'); 39 | 40 | // Переход с алиасом и параметром из основного роута 41 | expect(app.route.is('#index')).toBeTruthy(); 42 | expect(app.route.params).toEqual({folder: 'inbox'}); 43 | expect(app.request.alias).toEqual('compose'); 44 | 45 | await app.nav('/compose/inbox/id'); 46 | 47 | // Переход с алиасом и всеми параметрами 48 | expect(app.route.is('#index')).toBeTruthy(); 49 | expect(app.route.params).toEqual({folder: 'inbox', id: 'id'}); 50 | expect(app.request.alias).toEqual('compose'); 51 | }); 52 | 53 | test('alias with parameter decoders', async () => { 54 | const app = Pilot.create({ 55 | '#index': { 56 | url: { 57 | pattern: '/:type?/:id?', 58 | params: { 59 | type: { 60 | decode: function (value, req) { 61 | return value; 62 | } 63 | }, 64 | id: { 65 | validate: function (value) { 66 | return value >= 0; 67 | }, 68 | decode: function (value, req) { 69 | return parseInt(value, 10); 70 | } 71 | } 72 | } 73 | }, 74 | aliases: { 75 | 'compose': { 76 | pattern: '/compose/:message?/:id?', 77 | params: { 78 | message: { 79 | decode: function (value, req) { 80 | return 'msg_' + parseInt(value, 10); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | }, 87 | }); 88 | 89 | await app.nav('/'); 90 | 91 | // Обычный переход, алиасов нет 92 | expect(app.route.is('#index')).toBeTruthy(); 93 | expect(app.route.params).toEqual({}); 94 | expect(app.request.alias).toEqual(void 0); 95 | 96 | await app.nav('/inbox'); 97 | 98 | // Обычный переход, алиасов нет 99 | expect(app.route.is('#index')).toBeTruthy(); 100 | expect(app.route.params).toEqual({type: 'inbox'}); 101 | expect(app.request.alias).toEqual(void 0); 102 | 103 | await app.nav('/inbox/3'); 104 | 105 | // Обычный переход, алиасов нет 106 | expect(app.route.is('#index')).toBeTruthy(); 107 | expect(app.route.params).toEqual({type: 'inbox', id: 3}); 108 | expect(app.request.alias).toEqual(void 0); 109 | 110 | await app.nav('/compose'); 111 | 112 | // Переход с алиасом без параметров 113 | expect(app.route.is('#index')).toBeTruthy(); 114 | expect(app.route.params).toEqual({}); 115 | expect(app.request.alias).toEqual('compose'); 116 | 117 | await app.nav('/compose/3002'); 118 | 119 | // Переход с алиасом и параметром 120 | expect(app.route.is('#index')).toBeTruthy(); 121 | expect(app.route.params).toEqual({message: 'msg_3002'}); 122 | expect(app.request.alias).toEqual('compose'); 123 | 124 | await app.nav('/compose/3002/3'); 125 | 126 | // Переход с алиасом и всеми параметрами 127 | expect(app.route.is('#index')).toBeTruthy(); 128 | expect(app.route.params).toEqual({message: 'msg_3002', id: 3}); 129 | expect(app.request.alias).toEqual('compose'); 130 | }); 131 | 132 | test('alias cleanup', async () => { 133 | const app = Pilot.create({ 134 | '#index': { 135 | url: '/', 136 | aliases: { 137 | 'compose': '/compose/' 138 | } 139 | }, 140 | }); 141 | 142 | await app.nav('/'); 143 | 144 | // Обычный переход, алиасов нет 145 | expect(app.route.is('#index')).toBeTruthy(); 146 | expect(app.route.params).toEqual({}); 147 | expect(app.request.alias).toEqual(void 0); 148 | 149 | await app.nav('/compose'); 150 | 151 | // Переход с алиасом без параметров 152 | expect(app.route.is('#index')).toBeTruthy(); 153 | expect(app.route.params).toEqual({}); 154 | expect(app.request.alias).toEqual('compose'); 155 | 156 | await app.nav('/'); 157 | 158 | // Обычный переход, алиасов нет 159 | expect(app.route.is('#index')).toBeTruthy(); 160 | expect(app.route.params).toEqual({}); 161 | expect(app.request.alias).toEqual(void 0); 162 | }); 163 | 164 | }); 165 | -------------------------------------------------------------------------------- /tests/bench.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 18 | 19 | 20 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pilot :: tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /tests/loader.tests.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, test, expect */ 2 | 3 | const Loader = require('../src/loader'); 4 | 5 | const reqX = { route: { id: '#X' } }; 6 | const reqY = { route: { id: '#Y' } }; 7 | 8 | function sleep(milliseconds) { 9 | return new Promise(resolve => setTimeout(resolve, milliseconds)); 10 | } 11 | 12 | // Этот загрузчик любит спать и писать об этом в лог 13 | async function createSleepyLoggingLoader(log, {persist} = {persist: false}) { 14 | const loader = new Loader({ 15 | // Этот "переход по маршруту" будет просто ждать нужное кол-во милилсекунд 16 | // Ещё он может зависнуть, т.е. вернуть промис, который не разрезолвится никогда 17 | data(request, waitFor, action) { 18 | if (action.hang) { 19 | return new Promise(_ => {}); 20 | } 21 | 22 | return sleep(action.timeout) 23 | .then(() => `timeout ${action.timeout}`); 24 | } 25 | }, { 26 | persist, 27 | processing(request, action, models) { 28 | log.push(models.data); 29 | } 30 | }); 31 | 32 | await loader.fetch(reqX); 33 | log.length = 0; 34 | 35 | return loader; 36 | } 37 | 38 | describe('Loader', () => { 39 | 40 | test('defaults', function () { 41 | var loader = new Loader({ 42 | foo: function () {}, 43 | bar: { 44 | defaults: 123 45 | } 46 | }); 47 | 48 | expect(loader.defaults()).toEqual({foo: void 0, bar: 123}); 49 | }); 50 | 51 | 52 | test('fetch', async () => { 53 | var loader = new Loader({ 54 | foo: function () { 55 | return 1 56 | }, 57 | bar: { 58 | match: [], 59 | defaults: 2 60 | }, 61 | baz: { 62 | match: ['#X'], 63 | defaults: -3, 64 | fetch: function () { 65 | return Promise.resolve(3); 66 | } 67 | }, 68 | qux: function (req, waitFor) { 69 | return waitFor('baz').then(function (value) { 70 | return value * 2; 71 | }); 72 | } 73 | }); 74 | 75 | await loader.fetch(reqX).then(function (models) { 76 | expect(models).toEqual({foo: 1, bar: 2, baz: 3, qux: 6}); 77 | 78 | return loader.fetch(reqY).then(function (models) { 79 | expect(models).toEqual({foo: 1, bar: 2, baz: -3, qux: -6}); 80 | }); 81 | }) 82 | }); 83 | 84 | 85 | test('dispatch with high priority and no persist', async () => { 86 | const log = []; 87 | const loader = await createSleepyLoggingLoader(log); 88 | 89 | loader.dispatch({timeout: 20}); 90 | loader.dispatch({timeout: 10}); 91 | 92 | await sleep(30); 93 | 94 | expect(log).toEqual(['timeout 10', 'timeout 20']); 95 | }); 96 | 97 | 98 | test('dispatch with low priority and no persist', async () => { 99 | const log = []; 100 | const loader = await createSleepyLoggingLoader(log); 101 | 102 | loader.dispatch({timeout: 20, priority: Loader.PRIORITY_LOW}); 103 | loader.dispatch({timeout: 10, priority: Loader.PRIORITY_LOW}); 104 | 105 | await sleep(100); 106 | 107 | expect(log).toEqual(['timeout 10', 'timeout 20']); 108 | }); 109 | 110 | 111 | test('dispatch low after high priority and no persist', async () => { 112 | const log = []; 113 | const loader = await createSleepyLoggingLoader(log); 114 | 115 | loader.dispatch({timeout: 20, priority: Loader.PRIORITY_HIGH}); 116 | loader.dispatch({timeout: 10, priority: Loader.PRIORITY_LOW}); 117 | 118 | await sleep(100); 119 | 120 | expect(log).toEqual(['timeout 20', 'timeout 10']); 121 | }); 122 | 123 | 124 | test('dispatch high after low priority and no persist', async () => { 125 | const log = []; 126 | const loader = await createSleepyLoggingLoader(log); 127 | 128 | loader.dispatch({timeout: 20, priority: Loader.PRIORITY_LOW}); 129 | loader.dispatch({timeout: 10, priority: Loader.PRIORITY_HIGH}); 130 | 131 | await sleep(100); 132 | 133 | expect(log).toEqual(['timeout 20', 'timeout 10']); 134 | }); 135 | 136 | 137 | test('dispatch with high priority and persist', async () => { 138 | const log = []; 139 | const loader = await createSleepyLoggingLoader(log, {persist: true}); 140 | 141 | loader.dispatch({timeout: 20}); 142 | loader.dispatch({timeout: 10}); 143 | 144 | await sleep(50); 145 | 146 | expect(log).toEqual(['timeout 20']); 147 | }); 148 | 149 | 150 | test('dispatch with low priority and persist', async () => { 151 | const log = []; 152 | const loader = await createSleepyLoggingLoader(log, {persist: true}); 153 | 154 | loader.dispatch({timeout: 20, priority: Loader.PRIORITY_LOW}); 155 | loader.dispatch({timeout: 10, priority: Loader.PRIORITY_LOW}); 156 | 157 | await sleep(100); 158 | 159 | expect(log).toEqual(['timeout 20']); 160 | }); 161 | 162 | 163 | test('dispatch with low priority and persist fires only once', async () => { 164 | const log = []; 165 | const loader = await createSleepyLoggingLoader(log, {persist: true}); 166 | 167 | loader.dispatch({timeout: 20, priority: Loader.PRIORITY_LOW}); 168 | await sleep(12); 169 | 170 | loader.dispatch({timeout: 10, priority: Loader.PRIORITY_LOW}); 171 | await sleep(100); 172 | 173 | expect(log).toEqual(['timeout 20']); 174 | }); 175 | 176 | 177 | test('dispatch high, high, low, high and no persist', async () => { 178 | const log = []; 179 | const loader = await createSleepyLoggingLoader(log); 180 | 181 | loader.dispatch({timeout: 50, priority: Loader.PRIORITY_HIGH}); 182 | await loader.dispatch({timeout: 40, priority: Loader.PRIORITY_HIGH}); 183 | 184 | loader.dispatch({timeout: 30, priority: Loader.PRIORITY_LOW}); 185 | await sleep(15); 186 | 187 | loader.dispatch({timeout: 10, priority: Loader.PRIORITY_HIGH}); 188 | await sleep(60); 189 | 190 | expect(log).toEqual(['timeout 40', 'timeout 50', 'timeout 30', 'timeout 10']); 191 | }); 192 | 193 | 194 | test('extend', async () => { 195 | var loader = new Loader({ 196 | foo: { 197 | value: 0, 198 | fetch: function () { 199 | return ++this.value; 200 | } 201 | }, 202 | bar: { 203 | defaults: 123 204 | } 205 | }); 206 | 207 | var extLoader = loader.extend({ 208 | bar: function () { 209 | return 321; 210 | } 211 | }); 212 | 213 | await loader.fetch(reqX).then(function (models) { 214 | expect(models).toEqual({foo: 1, bar: 123}); 215 | 216 | return extLoader.fetch(reqY).then(function (models) { 217 | expect(models).toEqual({foo: 2, bar: 321}); 218 | 219 | return loader.fetch(reqY).then(function (models) { 220 | expect(models).toEqual({foo: 3, bar: 123}); 221 | }); 222 | }); 223 | }); 224 | }); 225 | 226 | 227 | test('fetch:error', async () => { 228 | var loader = new Loader({ 229 | foo: function () { 230 | throw "error"; 231 | } 232 | }); 233 | 234 | expect(loader.fetch(reqX)).rejects; 235 | }); 236 | 237 | 238 | // Тест проходит индивидуально, но падает в общем прогоне 239 | // Я пока не знаю, что с ним не так 240 | xtest('infinite loop', async () => { 241 | const log = []; 242 | const loader = await createSleepyLoggingLoader(log); 243 | 244 | loader.dispatch({priority: Loader.PRIORITY_HIGH, hang: true}); 245 | await sleep(50); 246 | await loader.dispatch({timeout: 40, priority: Loader.PRIORITY_HIGH}); 247 | 248 | expect(log).toEqual(['timeout 40']); 249 | }) 250 | }); 251 | -------------------------------------------------------------------------------- /tests/pilot.tests.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, test, expect */ 2 | 3 | const Pilot = require('../src/pilot'); 4 | 5 | describe('Pilot', () => { 6 | var TYPES = { 7 | 'inbox': 1, 8 | 'spam': 2 9 | }; 10 | 11 | function createMockApp() { 12 | return Pilot.create({ 13 | '#idx': { 14 | url: '/', 15 | model: { 16 | indexes: function () { 17 | return 123; 18 | } 19 | }, 20 | 'on:route': function () { 21 | this.ok = true; 22 | } 23 | }, 24 | 25 | '#foo': { 26 | url: '/:foo', 27 | 28 | '*': { 29 | 'region': { 30 | match: ['#baz'], 31 | 'on:routestart': function () { 32 | this.foo = 'start'; 33 | }, 34 | 'on:routeend': function () { 35 | this.foo = 'end'; 36 | } 37 | } 38 | }, 39 | 40 | model: { 41 | data: function (req) { 42 | return req.params.foo; 43 | } 44 | }, 45 | 46 | '#bar': { 47 | url: 'bar' 48 | }, 49 | 50 | '#baz': { 51 | url: './baz', 52 | model: { 53 | subdata: function () { 54 | return 'baz'; 55 | } 56 | } 57 | } 58 | }, 59 | 60 | '#fail': { 61 | url: './fail/:id', 62 | model: { 63 | failData: function (req) { 64 | return req.params.id == 1 ? Promise.reject() : Promise.resolve('OK'); 65 | } 66 | } 67 | }, 68 | 69 | '#folder': { 70 | url: '/:folder?' 71 | }, 72 | 73 | '#letters': { 74 | url: { 75 | pattern: '/messages/(:type|folder/:id)?', 76 | params: { 77 | type: { 78 | decode: function (value, req) { 79 | req.params.id = TYPES[value] || 0; 80 | return value; 81 | } 82 | }, 83 | id: { 84 | validate: function (value) { 85 | return value >= 0; 86 | }, 87 | decode: function (value, req) { 88 | // req.params.type = ID_TYPES[value]; 89 | return parseInt(value, 10); 90 | } 91 | } 92 | }, 93 | toUrl: function (params, query, builder) { 94 | if (!(params instanceof Object)) { 95 | params = params >= 0 ? {id: params} : {type: params}; 96 | } 97 | 98 | if (params.id === 0) { 99 | params.type = 'inbox'; 100 | } 101 | 102 | return builder(params, query); 103 | } 104 | } 105 | } 106 | }); 107 | } 108 | 109 | 110 | test('routes', () => { 111 | const app = createMockApp(); 112 | 113 | expect(app.routes.map(function (route) { 114 | return {id: route.id, url: route.url.pattern, group: route.__group__}; 115 | })).toEqual([ 116 | {"id": "#__root__", "url": "/", "group": true}, 117 | {"id": "#idx", "url": "/", "group": false}, 118 | {"id": "#foo", "url": "/:foo", "group": true}, 119 | {"id": "#bar", "url": "/:foo/bar", "group": false}, 120 | {"id": "#baz", "url": "/:foo/baz", "group": false}, 121 | {"id": "#fail", "url": "/fail/:id", "group": false}, 122 | {"id": "#folder", "url": "/:folder?", "group": false}, 123 | {"id": "#letters", "url": "/messages/(:type|folder/:id)?", "group": false} 124 | ]); 125 | }); 126 | 127 | test('nav', async () => { 128 | const app = createMockApp(); 129 | 130 | expect(app['#foo'].regions.length).toBe(1); 131 | expect(app.model).toEqual({}); 132 | expect(app['#idx'].model).toEqual({indexes: void 0}); 133 | 134 | var replaceState = false; 135 | 136 | app.one('routeend', (evt) => { 137 | replaceState = evt.details.replaceState; 138 | }); 139 | 140 | // ------------------------------------------ // 141 | await app.nav('/', {replaceState: true}); 142 | 143 | expect(replaceState).toBeTruthy(); 144 | expect(app.route.id).toBe('#idx'); 145 | expect(app.url.href).toBe(app.request.href); 146 | expect(app.request.path).toBe('/'); 147 | 148 | expect(app['#idx'].active).toBeTruthy(); 149 | expect(app['#idx'].ok).toBeTruthy(); 150 | expect(!app['#foo'].active).toBeTruthy(); 151 | 152 | expect(app.model).toEqual({}); 153 | expect(app.route.model).toEqual({indexes: 123}); 154 | 155 | // ------------------------------------------ // 156 | await app.nav('/xxx/bar'); 157 | 158 | expect(app.route.id).toBe('#bar'); 159 | expect(app.request.path).toBe('/xxx/bar'); 160 | 161 | // ------------------------------------------ // 162 | await app.nav('/yyy/baz/'); 163 | 164 | expect(app['#foo'].active).toBeTruthy(); 165 | expect(app.route.id).toBe('#baz'); 166 | expect(app['#foo'].regions[0].foo).toBe('start'); 167 | 168 | expect(app['#idx'].model).toEqual({indexes: void 0}); 169 | expect(app['#foo'].model).toEqual({data: 'yyy'}); 170 | expect(app.route.model).toEqual({data: 'yyy', subdata: 'baz'}); 171 | 172 | // ------------------------------------------ // 173 | await app.nav('/zzz/bar/'); 174 | 175 | expect(app['#foo'].regions[0].foo).toBe('end'); 176 | }); 177 | 178 | test('getUrl', () => { 179 | const app = createMockApp(); 180 | 181 | expect(app.getUrl('#folder')).toBe('/'); 182 | expect(app.getUrl('#folder', {folder: 0})).toBe('/0/'); 183 | 184 | expect(app.getUrl('#letters')).toBe('/messages/'); 185 | expect(app.getUrl('#letters', {type: 'inbox'})).toBe('/messages/inbox/'); 186 | expect(app.getUrl('#letters', {id: 2})).toBe('/messages/folder/2/'); 187 | expect(app.getUrl('#letters', 'inbox')).toBe('/messages/inbox/'); 188 | expect(app.getUrl('#letters', 2)).toBe('/messages/folder/2/'); 189 | expect(app.getUrl('#letters', 0)).toBe('/messages/inbox/'); 190 | expect(app.getUrl('#letters', {id: 0})).toBe('/messages/inbox/'); 191 | expect(app.getUrl('#letters', {id: 0}, {foo: 'bar'})).toBe('/messages/inbox/?foo=bar'); 192 | }); 193 | 194 | test('getUrl: inherit query', async () => { 195 | const app = createMockApp(); 196 | 197 | await app.nav('/?foo&bar=Y'); 198 | expect(app.getUrl('#letters', {}, 'inherit')).toBe('/messages/?foo&bar=Y'); 199 | }); 200 | 201 | test('letters', async () => { 202 | const app = createMockApp(); 203 | 204 | await app.go('#letters', {type: 'inbox'}); 205 | expect(app.route.params).toEqual({id: 1, type: 'inbox'}); 206 | 207 | await app.go('#letters', {id: 2}); 208 | expect(app.route.params).toEqual({id: 2}); 209 | 210 | let error; 211 | 212 | try { 213 | await app.go('#letters', {id: 'str'}); 214 | } catch (_) { 215 | error = _; 216 | } 217 | 218 | expect(error.code).toBe(404); 219 | expect(app.route.params).toEqual({id: 2}); 220 | }); 221 | 222 | test('search/query', async () => { 223 | var query; 224 | var app = Pilot.create({ 225 | model: { 226 | results: function (req) { 227 | query = req.query; 228 | } 229 | }, 230 | 231 | '#index': { 232 | url: { 233 | pattern: '/:folder', 234 | params: {folder: {validate: function (value) { return value !== 'search'; }}} 235 | } 236 | }, 237 | 238 | '#search': { 239 | url: '/search/' 240 | } 241 | }); 242 | 243 | await app.nav('/search/?find=foo'); 244 | expect(query).toEqual({find: 'foo'}); 245 | }); 246 | 247 | xtest('race condition', async () => { 248 | var log = []; 249 | var loader = new Pilot.Loader({ 250 | value: function (req) { 251 | return sleep(req.params.time); 252 | } 253 | }, { 254 | persist: true, 255 | processing: function (req, action, model) { 256 | log.push(model.value); 257 | return model; 258 | }, 259 | }); 260 | 261 | var race = Pilot.create({ 262 | model: loader, 263 | '#index': {url: '/:time'} 264 | }); 265 | 266 | race.nav('/100'); 267 | race.nav('/50'); 268 | race.nav('/80'); 269 | race.nav('/30'); 270 | race.nav('/80'); 271 | 272 | setTimeout(function () { 273 | loader.fetch(); 274 | }, 60); 275 | 276 | await sleep(110); 277 | expect(log).toEqual(['50']); 278 | }); 279 | 280 | test('force', async () => { 281 | const app = createMockApp(); 282 | 283 | var navigated = 0; 284 | 285 | var handleRoute = function () { 286 | navigated++; 287 | }; 288 | 289 | await app.go('#letters', {type: 'inbox'}); 290 | app.on('route', handleRoute); 291 | 292 | // Сейчас не перейдёт 293 | await app.go('#letters', {type: 'inbox'}); 294 | 295 | expect(navigated).toBe(0); 296 | 297 | // А сейчас перейдёт 298 | await app.go('#letters', {type: 'inbox'}, null, {force: true}); 299 | 300 | expect(navigated).toBe(1); 301 | app.off('route', handleRoute); 302 | }); 303 | 304 | test('model/fail', async () => { 305 | const app = createMockApp(); 306 | 307 | var log = []; 308 | var rnd = Math.random(); 309 | app.on('beforeroute route-fail route-end', function (evt) { 310 | log.push(evt.type) 311 | }); 312 | 313 | return app.go('#fail', {id: 1}).then().catch(function () { 314 | return app.go('#fail', {id: 1}).then(console.log).catch(function () { 315 | return rnd; 316 | }); 317 | }).then(function (x) { 318 | expect(x).toBe(rnd); 319 | expect(log).toEqual([ 320 | 'beforeroute', 'routefail', 'routeend', 321 | 'beforeroute', 'routefail', 'routeend', 322 | ]); 323 | }); 324 | }); 325 | 326 | test('view reload event', () => { 327 | const app = createMockApp(); 328 | const log = []; 329 | 330 | app.on('beforereload reload reloadend', ({type, details}) => { 331 | const evt = {type}; 332 | if (details) { 333 | evt.details = details; 334 | } 335 | 336 | log.push(evt); 337 | }); 338 | 339 | app.activeUrl = new Pilot.URL(app['#letters'].getUrl({type: 'inbox'}), location); 340 | app.reload(); 341 | 342 | expect(log).toEqual([ 343 | {type: 'beforereload'}, 344 | {type: 'reload'}, 345 | {type: "reloadend", details: {cancelled: false}} 346 | ]); 347 | }); 348 | 349 | test('view reload event cancels', () => { 350 | const app = createMockApp(); 351 | const log = []; 352 | 353 | app.on('beforereload reload reloadend', ({type, details}) => { 354 | const evt = {type}; 355 | if (details) { 356 | evt.details = details; 357 | } 358 | 359 | log.push(evt); 360 | return false; 361 | }); 362 | 363 | app.activeUrl = new Pilot.URL(app['#letters'].getUrl({type: 'inbox'}), location); 364 | app.reload(); 365 | 366 | expect(log).toEqual([{type: 'beforereload'}, {type: 'reloadend', details: {cancelled: true}}]); 367 | }); 368 | 369 | test('previous route', async () => { 370 | const app = createMockApp(); 371 | const previousRouteAt = {}; 372 | const {attach, detach} = setupHandlersFor(app, (event) => () => { 373 | previousRouteAt[event] = app.previousRoute 374 | }, ['before-route', 'route', 'route-end']); 375 | 376 | attach(); 377 | 378 | await app.go('#idx'); 379 | 380 | expect(previousRouteAt['before-route']).toBeUndefined(); 381 | expect(previousRouteAt['route']).toBeUndefined(); 382 | expect(previousRouteAt['route-end']).toBeUndefined(); 383 | 384 | await app.nav('/xxx/bar'); 385 | 386 | expect(previousRouteAt['before-route']).toBeDefined(); 387 | expect(previousRouteAt['before-route'].id).toEqual('#idx'); 388 | expect(previousRouteAt['before-route'].is('#idx')).toBeTruthy(); 389 | 390 | expect(previousRouteAt['route']).toBeDefined(); 391 | expect(previousRouteAt['route'].id).toEqual('#idx'); 392 | expect(previousRouteAt['route'].is('#idx')).toBeTruthy(); 393 | 394 | expect(previousRouteAt['route-end']).toBeDefined(); 395 | expect(previousRouteAt['route-end'].id).toEqual('#idx'); 396 | expect(previousRouteAt['route-end'].is('#idx')).toBeTruthy(); 397 | 398 | await app.nav('/yyy/baz'); 399 | 400 | expect(previousRouteAt['before-route']).toBeDefined(); 401 | expect(previousRouteAt['before-route'].id).toEqual('#bar'); 402 | expect(previousRouteAt['before-route'].is('#bar')).toBeTruthy(); 403 | 404 | expect(previousRouteAt['route']).toBeDefined(); 405 | expect(previousRouteAt['route'].id).toEqual('#bar'); 406 | expect(previousRouteAt['route'].is('#bar')).toBeTruthy(); 407 | 408 | expect(previousRouteAt['route-end']).toBeDefined(); 409 | expect(previousRouteAt['route-end'].id).toEqual('#bar'); 410 | expect(previousRouteAt['route-end'].is('#bar')).toBeTruthy(); 411 | 412 | detach(); 413 | }); 414 | 415 | test('previous route: same route different params', async () => { 416 | const app = createMockApp(); 417 | const previousRoutes = []; 418 | const {attach, detach} = setupHandlersFor(app, () => () => { 419 | previousRoutes.push(app.previousRoute); 420 | }, ['route']); 421 | 422 | await app.go('#letters', {type: 'inbox'}); 423 | attach(); 424 | await app.go('#letters', {type: 'spam'}); 425 | await app.go('#idx'); 426 | detach(); 427 | 428 | const [first, second] = previousRoutes; 429 | 430 | expect(first).toBeDefined(); 431 | expect(second).toBeDefined(); 432 | 433 | expect(first.is('#letters')).toBeTruthy(); 434 | expect(second.is('#letters')).toBeTruthy(); 435 | 436 | expect(first.params.type).toEqual('inbox'); 437 | expect(second.params.type).toEqual('spam'); 438 | }); 439 | 440 | test('previous route: query', async () => { 441 | const app = createMockApp(); 442 | let previousRoute; 443 | const {attach, detach} = setupHandlersFor(app, () => () => { 444 | previousRoute = app.previousRoute; 445 | }, ['route']); 446 | 447 | await app.nav('/messages/inbox?key=value'); 448 | attach(); 449 | await app.go('#letters', {type: 'spam'}); 450 | detach(); 451 | 452 | expect(previousRoute).toBeDefined(); 453 | expect(previousRoute.is('#letters')).toBeTruthy(); 454 | expect(previousRoute.params.type).toEqual('inbox'); 455 | expect(previousRoute.request.query.key).toEqual('value'); 456 | 457 | await app.go('#idx'); 458 | 459 | expect(previousRoute).toBeDefined(); 460 | expect(previousRoute.is('#letters')).toBeTruthy(); 461 | expect(previousRoute.params.type).toEqual('inbox'); 462 | expect(previousRoute.request.query.key).toEqual('value'); 463 | }); 464 | 465 | test('previous route: getUrl', async () => { 466 | const app = createMockApp(); 467 | let previousRoute; 468 | const {attach, detach} = setupHandlersFor(app, () => () => { 469 | previousRoute = app.previousRoute; 470 | }, ['route']); 471 | 472 | await app.go('#letters', {type: 'inbox'}); 473 | attach(); 474 | await app.nav('/'); 475 | detach(); 476 | 477 | const {params} = previousRoute; 478 | 479 | expect(previousRoute).toBeDefined(); 480 | expect(previousRoute.is('#letters')).toBeTruthy(); 481 | expect(previousRoute.getUrl(params)).toEqual('/messages/inbox/'); 482 | 483 | await app.nav('/?key=value'); 484 | 485 | const {request: {query}} = app; 486 | 487 | expect(previousRoute.getUrl(params, query)).toEqual('/messages/inbox/?key=value'); 488 | }); 489 | 490 | test.skip('listenFrom', async () => { // Проходит сам по себе, но ловит асинхронную ошибку от model/fail, пока скипаем 491 | let navigated = 0; 492 | const handleRoute = function () { 493 | navigated++; 494 | }; 495 | 496 | const app = createMockApp(); 497 | app.on('route', handleRoute); 498 | 499 | const link = document.createElement('a'); 500 | link.href = '/messages/inbox/'; 501 | 502 | link.click(); 503 | expect(navigated).toBe(0); 504 | 505 | app.listenFrom(link, {}); 506 | 507 | link.click(); 508 | await sleep(100); 509 | 510 | expect(app.activeUrl.pathname).toBe('/messages/inbox/'); 511 | expect(navigated).toBe(1); 512 | 513 | app.off('route', handleRoute); 514 | }); 515 | 516 | test.skip('listenFrom middle/right click', async () => { // Проходит сам по себе, но ловит асинхронную ошибку от model/fail, пока скипаем 517 | let navigated = 0; 518 | const handleRoute = function () { 519 | navigated++; 520 | }; 521 | 522 | const leftClick = new MouseEvent('click', { button: 0, which: 0 }); 523 | const middleClick = new MouseEvent('click', { button: 1, which: 1 }); 524 | const rightClick = new MouseEvent('click', { button: 2, which: 2 }); 525 | 526 | const app = createMockApp(); 527 | app.on('route', handleRoute); 528 | 529 | const link = document.createElement('a'); 530 | link.href = '/messages/inbox/'; 531 | 532 | link.dispatchEvent(leftClick); 533 | link.dispatchEvent(middleClick); 534 | link.dispatchEvent(rightClick); 535 | expect(navigated).toBe(0); 536 | 537 | app.listenFrom(link, {}); 538 | 539 | link.dispatchEvent(leftClick); 540 | await sleep(100); 541 | 542 | expect(app.activeUrl.pathname).toBe('/messages/inbox/'); 543 | expect(navigated).toBe(1); 544 | 545 | await app.nav('/'); 546 | expect(app.activeUrl.pathname).toBe('/'); 547 | expect(navigated).toBe(2); 548 | 549 | link.dispatchEvent(middleClick); 550 | link.dispatchEvent(rightClick); 551 | await sleep(100); 552 | 553 | expect(app.activeUrl.pathname).toBe('/'); 554 | expect(navigated).toBe(2); 555 | 556 | app.off('route', handleRoute); 557 | }); 558 | 559 | function sleep(time) { 560 | return new Promise(function (resolve) { 561 | setTimeout(function () { 562 | resolve(time); 563 | }, time) 564 | }) 565 | } 566 | 567 | function setupHandlersFor(app, callBackFor, events) { 568 | const handlers = {}; 569 | 570 | return { 571 | attach: () => events.forEach((event) => { 572 | app.on(event, handlers[event] = callBackFor(event)); 573 | }), 574 | detach: () => events.forEach((event) => { 575 | if (handlers[event]) { 576 | app.off(event, handlers[event]); 577 | delete handlers[event]; 578 | } 579 | }), 580 | }; 581 | } 582 | }); 583 | -------------------------------------------------------------------------------- /tests/polyfills/url.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // URL Polyfill 4 | // Draft specification: http://url.spec.whatwg.org 5 | 6 | // Notes: 7 | // - Primarily useful for parsing URLs and modifying query parameters 8 | // - Should work in IE8+ and everything more modern 9 | 10 | (function (global) { 11 | "use strict"; 12 | 13 | var origURL = global.URL; 14 | 15 | function URLUtils(url) { 16 | var anchor = document.createElement("a"); 17 | 18 | anchor.href = url; 19 | 20 | return anchor; 21 | } 22 | 23 | global.URL = function URL(url, base) { 24 | if (!(this instanceof global.URL)) { 25 | throw new TypeError("Failed to construct 'URL': Please use the 'new' operator."); 26 | } 27 | 28 | if (base) { 29 | url = (function () { 30 | var doc; 31 | 32 | // Use another document/base tag/anchor for relative URL resolution, if possible 33 | if (document.implementation && document.implementation.createHTMLDocument) { 34 | doc = document.implementation.createHTMLDocument(""); 35 | } else if (document.implementation && document.implementation.createDocument) { 36 | doc = document.implementation.createElement("http://www.w3.org/1999/xhtml", "html", null); 37 | doc.documentElement.appendChild(doc.createElement("head")); 38 | doc.documentElement.appendChild(doc.createElement("body")); 39 | } else if (window.ActiveXObject) { 40 | doc = new window.ActiveXObject("htmlfile"); 41 | doc.write(""); 42 | doc.close(); 43 | } 44 | 45 | if (!doc) { 46 | throw Error("base not supported"); 47 | } 48 | 49 | var baseTag = doc.createElement("base"); 50 | baseTag.href = base; 51 | 52 | doc.getElementsByTagName("head")[0].appendChild(baseTag); 53 | 54 | var anchor = doc.createElement("a"); 55 | anchor.href = url; 56 | 57 | return anchor.href; 58 | })(); 59 | } 60 | 61 | // An inner object implementing URLUtils (either a native URL 62 | // object or an HTMLAnchorElement instance) is used to perform the 63 | // URL algorithms. With full ES5 getter/setter support, return a 64 | // regular object For IE8's limited getter/setter support, a 65 | // different HTMLAnchorElement is returned with properties 66 | // overridden 67 | 68 | var instance = URLUtils(url || ""); 69 | 70 | // Detect for ES5 getter/setter support 71 | var ES5_GET_SET = Object.defineProperties && (function () { 72 | var o = {}; 73 | 74 | Object.defineProperties(o, { 75 | p: { 76 | get: function () { 77 | return true; 78 | } 79 | } 80 | }); 81 | 82 | return o.p; 83 | })(); 84 | 85 | var self = ES5_GET_SET ? this : document.createElement("a"); 86 | 87 | // NOTE: Doesn't do the encoding/decoding dance 88 | function parse(input, isindex) { 89 | var sequences = input.split("&"); 90 | 91 | if (isindex && sequences[0].indexOf("=") === -1) { 92 | sequences[0] = "=" + sequences[0]; 93 | } 94 | 95 | var pairs = []; 96 | 97 | sequences.forEach(function (bytes) { 98 | if (bytes.length === 0) { 99 | return; 100 | } 101 | 102 | var index = bytes.indexOf("="); 103 | 104 | if (index !== -1) { 105 | var name = bytes.substring(0, index); 106 | var value = bytes.substring(index + 1); 107 | } else { 108 | name = bytes; 109 | value = ""; 110 | } 111 | 112 | name = name.replace(/\+/g, " "); 113 | value = value.replace(/\+/g, " "); 114 | pairs.push({ name: name, value: value }); 115 | }); 116 | 117 | var output = []; 118 | 119 | pairs.forEach(function (pair) { 120 | output.push({ 121 | name: decodeURIComponent(pair.name), 122 | value: decodeURIComponent(pair.value) 123 | }); 124 | }); 125 | 126 | return output; 127 | } 128 | 129 | function URLSearchParams(url_object, init) { 130 | var pairs = []; 131 | 132 | if (init) { 133 | pairs = parse(init); 134 | } 135 | 136 | this._setPairs = function (list) { 137 | pairs = list; 138 | }; 139 | 140 | this._updateSteps = function () { 141 | updateSteps(); 142 | }; 143 | 144 | var updating = false; 145 | 146 | function updateSteps() { 147 | if (updating) { 148 | return; 149 | }updating = true; 150 | 151 | // TODO: For all associated url objects 152 | url_object.search = serialize(pairs); 153 | 154 | updating = false; 155 | } 156 | 157 | // NOTE: Doesn't do the encoding/decoding dance 158 | function serialize(pairs) { 159 | var output = "", 160 | first = true; 161 | 162 | pairs.forEach(function (pair) { 163 | var name = encodeURIComponent(pair.name); 164 | var value = encodeURIComponent(pair.value); 165 | 166 | if (!first) { 167 | output += "&"; 168 | } 169 | 170 | output += name + "=" + value; 171 | 172 | first = false; 173 | }); 174 | 175 | return output.replace(/%20/g, "+"); 176 | } 177 | 178 | Object.defineProperties(this, { 179 | append: { 180 | value: (function (_value) { 181 | var _valueWrapper = function value(_x, _x2) { 182 | return _value.apply(this, arguments); 183 | }; 184 | 185 | _valueWrapper.toString = function () { 186 | return _value.toString(); 187 | }; 188 | 189 | return _valueWrapper; 190 | })(function (name, value) { 191 | pairs.push({ 192 | name: name, 193 | value: value 194 | }); 195 | 196 | updateSteps(); 197 | }) 198 | }, 199 | 200 | "delete": { 201 | value: function value(name) { 202 | for (var i = 0; i < pairs.length;) { 203 | if (pairs[i].name === name) { 204 | pairs.splice(i, 1); 205 | } else { 206 | ++i; 207 | } 208 | } 209 | 210 | updateSteps(); 211 | } 212 | }, 213 | 214 | get: { 215 | value: function value(name) { 216 | for (var i = 0; i < pairs.length; ++i) { 217 | if (pairs[i].name === name) { 218 | return pairs[i].value; 219 | } 220 | } 221 | 222 | return null; 223 | } 224 | }, 225 | 226 | getAll: { 227 | value: function value(name) { 228 | var result = []; 229 | 230 | for (var i = 0; i < pairs.length; ++i) { 231 | if (pairs[i].name === name) { 232 | result.push(pairs[i].value); 233 | } 234 | } 235 | 236 | return result; 237 | } 238 | }, 239 | 240 | has: { 241 | value: function value(name) { 242 | for (var i = 0; i < pairs.length; ++i) { 243 | if (pairs[i].name === name) { 244 | return true; 245 | } 246 | } 247 | 248 | return false; 249 | } 250 | }, 251 | 252 | set: { 253 | value: (function (_value) { 254 | var _valueWrapper = function value(_x, _x2) { 255 | return _value.apply(this, arguments); 256 | }; 257 | 258 | _valueWrapper.toString = function () { 259 | return _value.toString(); 260 | }; 261 | 262 | return _valueWrapper; 263 | })(function (name, value) { 264 | var found = false; 265 | 266 | for (var i = 0; i < pairs.length;) { 267 | if (pairs[i].name === name) { 268 | if (!found) { 269 | pairs[i].value = value; 270 | found = true; 271 | ++i; 272 | } else { 273 | pairs.splice(i, 1); 274 | } 275 | } else { 276 | ++i; 277 | } 278 | } 279 | 280 | if (!found) { 281 | pairs.push({ name: name, value: value }); 282 | } 283 | 284 | updateSteps(); 285 | }) 286 | }, 287 | 288 | toString: { 289 | value: function value() { 290 | return serialize(pairs); 291 | } 292 | } 293 | }); 294 | } 295 | 296 | var queryObject = new URLSearchParams(self, instance.search ? instance.search.substring(1) : null); 297 | 298 | Object.defineProperties(self, { 299 | href: { 300 | get: function get() { 301 | return instance.href; 302 | }, 303 | 304 | set: function set(v) { 305 | instance.href = v; 306 | tidy_instance(); 307 | update_steps(); 308 | } 309 | }, 310 | 311 | origin: { 312 | get: function get() { 313 | if ("origin" in instance) { 314 | return instance.origin; 315 | } 316 | 317 | return this.protocol + "//" + this.host; 318 | } 319 | }, 320 | 321 | protocol: { 322 | get: function get() { 323 | return instance.protocol; 324 | }, 325 | 326 | set: function set(v) { 327 | instance.protocol = v; 328 | } 329 | }, 330 | 331 | username: { 332 | get: function get() { 333 | return instance.username; 334 | }, 335 | 336 | set: function set(v) { 337 | instance.username = v; 338 | } 339 | }, 340 | 341 | password: { 342 | get: function get() { 343 | return instance.password; 344 | }, 345 | 346 | set: function set(v) { 347 | instance.password = v; 348 | } 349 | }, 350 | 351 | host: { 352 | get: function get() { 353 | // IE returns default port in |host| 354 | var re = ({ 355 | "http:": /:80$/, 356 | "https:": /:443$/, 357 | "ftp:": /:21$/ 358 | })[instance.protocol]; 359 | 360 | return re ? instance.host.replace(re, "") : instance.host; 361 | }, 362 | 363 | set: function set(v) { 364 | instance.host = v; 365 | } 366 | }, 367 | 368 | hostname: { 369 | get: function get() { 370 | return instance.hostname; 371 | }, 372 | 373 | set: function set(v) { 374 | instance.hostname = v; 375 | } 376 | }, 377 | 378 | port: { 379 | get: function get() { 380 | return instance.port; 381 | }, 382 | 383 | set: function set(v) { 384 | instance.port = v; 385 | } 386 | }, 387 | 388 | pathname: { 389 | get: function get() { 390 | // IE does not include leading '/' in |pathname| 391 | if (instance.pathname.charAt(0) !== "/") { 392 | return "/" + instance.pathname; 393 | } 394 | 395 | return instance.pathname; 396 | }, 397 | 398 | set: function set(v) { 399 | instance.pathname = v; 400 | } 401 | }, 402 | 403 | search: { 404 | get: function get() { 405 | return instance.search; 406 | }, 407 | 408 | set: function set(v) { 409 | if (instance.search !== v) { 410 | instance.search = v; 411 | tidy_instance(); 412 | update_steps(); 413 | } 414 | } 415 | }, 416 | searchParams: { 417 | get: function get() { 418 | return queryObject; 419 | } 420 | // TODO: implement setter 421 | }, 422 | hash: { 423 | get: function get() { 424 | return instance.hash; 425 | }, 426 | 427 | set: function set(v) { 428 | instance.hash = v; 429 | tidy_instance(); 430 | } 431 | }, 432 | 433 | toString: { 434 | value: function value() { 435 | return instance.toString(); 436 | } 437 | }, 438 | 439 | valueOf: { 440 | value: function value() { 441 | return instance.valueOf(); 442 | } 443 | } 444 | }); 445 | 446 | function tidy_instance() { 447 | var href = instance.href.replace(/#$|\?$|\?(?=#)/g, ""); 448 | 449 | if (instance.href !== href) { 450 | instance.href = href; 451 | } 452 | } 453 | 454 | function update_steps() { 455 | queryObject._setPairs(instance.search ? parse(instance.search.substring(1)) : []); 456 | 457 | queryObject._updateSteps(); 458 | } 459 | 460 | return self; 461 | }; 462 | 463 | if (origURL) { 464 | for (var i in origURL) { 465 | if (origURL.hasOwnProperty(i)) { 466 | global.URL[i] = origURL[i]; 467 | } 468 | } 469 | } 470 | })(window); 471 | -------------------------------------------------------------------------------- /tests/querystring.tests.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, test, expect */ 2 | 3 | ['new', 'old'].forEach(type => { 4 | 5 | describe(`queryString: ${type}`, () => { 6 | const queryString = require('../src/querystring')[type]; 7 | 8 | // Parser 9 | const casesParse = { 10 | '?foo=bar': {foo: 'bar'}, 11 | '#foo=bar': {foo: 'bar'}, 12 | 'foo=bar': {foo: 'bar'}, 13 | 'foo=bar&key=val': {foo: 'bar', key: 'val'}, 14 | 'foo': {foo: ''}, 15 | 'foo&key': {foo: '', key: ''}, 16 | 'foo=bar&key': {foo: 'bar', key: ''}, 17 | '?': {}, 18 | '#': {}, 19 | ' ': {}, 20 | 'foo=bar&foo=baz': {foo: ['bar', 'baz']}, 21 | 'foo[]=bar&foo[]=baz': {foo: ['bar', 'baz']}, 22 | // 'test=1+2': {test: '1 2'} 23 | // 'foo[bar]=1&foo[baz]=1': {foo: {bar:1, baz: 1}}, 24 | //'foo=bar=baz': { foo: 'bar=baz' } 25 | }; 26 | 27 | 28 | Object.keys(casesParse).forEach(function (search) { 29 | test(`parse ${search}`, () => { 30 | expect(queryString.parse(search)).toEqual(casesParse[search]); 31 | }); 32 | }); 33 | 34 | 35 | // Stringify 36 | const casesStringify = { 37 | 'null': {'null':null}, 38 | 'undefined': {'undefined':void 0}, 39 | 'empty': {empty: ''}, 40 | 'foo=bar': {foo: 'bar'}, 41 | 'foo=bar&key=val': {foo: 'bar', key: 'val'}, 42 | 'foo&key': {foo: '', key: ''}, 43 | 'foo=bar&key': {foo: 'bar', key: ''}, 44 | 'foo=bar%3Dbaz': {foo: 'bar=baz'}, 45 | 'id[]=1&id[]=2': {id: [1, 2]}, 46 | 'id[foo]=1&id[bar]=2': {id: {foo:1, bar:2}}, 47 | 'id[foo][bar]=2': {id: {foo: {bar:2}}}, 48 | 'id[][bar][]=1': {id: [{bar:[1]}]} 49 | }; 50 | 51 | // Полностью эквивалентно по спеке URL, теряется при сериализации 52 | const optionalReplacements = { 53 | 'foo=bar&key': 'foo=bar&key=', 54 | 'foo&key': 'foo=&key=', 55 | 'empty': 'empty=' 56 | } 57 | 58 | 59 | Object.keys(casesStringify).forEach(function (query) { 60 | const data = casesStringify[query]; 61 | 62 | test(`stringify ${JSON.stringify(data)}`, () => { 63 | expect([query, optionalReplacements[query]].filter(a => a !== undefined)).toContain(queryString.stringify(data)); 64 | }); 65 | }); 66 | 67 | const identityTestCases = [ 68 | '', 69 | 'test=1+2' 70 | ]; 71 | 72 | if (type === 'old') { 73 | return; 74 | } 75 | 76 | identityTestCases.forEach(function (query) { 77 | test(`identity of query ${query}`, () => { 78 | expect(queryString.stringify(queryString.parse(query))).toEqual(query); 79 | }); 80 | }); 81 | }); 82 | }) 83 | 84 | -------------------------------------------------------------------------------- /tests/url.tests.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, test, expect */ 2 | 3 | const Url = require('../src/url'); 4 | const urls = require('./urls'); 5 | 6 | describe('Url', () => { 7 | 8 | // https://developer.mozilla.org/en-US/docs/Web/API/URLUtils 9 | var URLUtils = window.URL; 10 | 11 | 12 | function DOMUrl(href) { 13 | var a = document.createElement('a'); 14 | a.href = href || location.toString(); 15 | return a; 16 | } 17 | 18 | var cases = [ 19 | //null, 20 | 'about:blank', 21 | 'http://rubaxa.org/', 22 | 'ftp://foo:bar@rubaxa.org/', 23 | 'http://rubaxa.org/', 24 | 'https://rubaxa.org/', 25 | 'http://rubaxa.org/foo', 26 | 'http://rubaxa.org/foo/bar?query', 27 | 'http://rubaxa.org/foo/bar?foo?bar', 28 | 'http://rubaxa.org/foo/bar#foo?foo=1', 29 | 'http://rubaxa.org///foo/////bar#foo?foo=1', 30 | // FIXME '#hash' 31 | ]; 32 | 33 | // URL vs. URLUtils 34 | test.each(cases)('URLUtils: %s', x => { 35 | var actual = new Url(x); 36 | var expected = new URLUtils(x); 37 | 38 | 'href protocol host hostname port pathname search hash username password'.split(' ').forEach(function (attr) { 39 | expect(actual[attr]).toBe(expected[attr]); // "/" так надо для `about:blank` 40 | }); 41 | }); 42 | 43 | // URL vs. URLUtils(url, relative) 44 | test.each([ 45 | 'foo', 46 | '/foo', 47 | '?query', 48 | '#hash' 49 | ])('URLUtils(url, relative): %s', x => { 50 | var actual = new Url(x, 'http://mail.ru/'); 51 | var expected = new URLUtils(x || '', 'http://mail.ru/'); 52 | 53 | 'href protocol host hostname port pathname search hash username password'.split(' ').forEach(function (attr) { 54 | expect(actual[attr]).toBe(expected[attr]); 55 | }); 56 | }); 57 | 58 | // URL vs. 59 | test.each(cases)(': %s', x => { 60 | var actual = new Url(x); 61 | var expected = DOMUrl(x); 62 | 63 | 'href protocol host hostname port pathname search hash username password'.split(' ').forEach(function (attr) { 64 | expect(actual[attr]).toBe(expected[attr]); 65 | }); 66 | }); 67 | 68 | test('without protocol', () => { 69 | var href = '//mail.ru/'; 70 | 71 | // FIXME URL vs. URLUtils 72 | // expect(new Url(href).href).toBe(new URLUtils(href).href); 73 | // URL vs. 74 | expect(new Url(href).href).toBe(DOMUrl(href).href); 75 | }); 76 | 77 | 78 | // Check urls 79 | test.each(urls)('Check url: %s', x => { 80 | // URL vs. URLUtils 81 | expect(new Url(x).href).toBe(new URLUtils(x).href); 82 | // URL vs. 83 | expect(new Url(x, location).href).toBe(DOMUrl(x).href); 84 | }); 85 | 86 | 87 | test('/messages/(:type|folder/:id)', () => { 88 | var pattern = '/messages/(:type|folder/:id)'; 89 | var matcher = Url.toMatcher(pattern); 90 | 91 | expect(matcher.source).toBe('^\\/messages\\/(?:(?:([^\\/]+))|folder\\/(?:([^\\/]+)))\\/*$'); 92 | expect(matcher.keys).toEqual([ 93 | {"name": "type", "optional": false}, 94 | {"name": "id", "optional": false} 95 | ]); 96 | 97 | // Тестируем `type` 98 | expect(Url.match(pattern)).toBe(null); 99 | expect(Url.match(pattern, '/messages/')).toEqual(null); 100 | expect(Url.match(pattern, '/messages/inbox')).toEqual({type: 'inbox'}); 101 | expect(Url.match(pattern, '/messages/inbox/')).toEqual({type: 'inbox'}); 102 | expect(Url.match(pattern, '/messages/inbox//')).toEqual({type: 'inbox'}); 103 | expect(Url.match(pattern, '/messages/inbox/////')).toEqual({type: 'inbox'}); 104 | 105 | // Тестируем `id` 106 | expect(Url.match(pattern, '/messages/folder/123')).toEqual({id: '123'}); 107 | expect(Url.match(pattern, '/messages/folder/123/')).toEqual({id: '123'}); 108 | expect(Url.match(pattern, '/messages/folder/123////')).toEqual({id: '123'}); 109 | }); 110 | 111 | 112 | test('/messages/(:type?|folder/:id)', () => { 113 | var pattern = '/messages/(:type?|folder/:id)'; 114 | var matcher = Url.toMatcher(pattern); 115 | 116 | expect(matcher.source).toBe('^\\/messages\\/(?:(?:([^\\/]+))?|folder\\/(?:([^\\/]+)))\\/*$'); 117 | expect(matcher.keys).toEqual([ 118 | {"name": "type", "optional": true}, 119 | {"name": "id", "optional": false} 120 | ]); 121 | expect(Url.match(pattern, '/messages/')).toEqual({}); 122 | expect(Url.match(pattern, '/messages/spam')).toEqual({type: 'spam'}); 123 | expect(Url.match(pattern, '/messages/folder/123')).toEqual({id: '123'}); 124 | }); 125 | 126 | 127 | test('/:mode(foo|bar)', () => { 128 | var pattern = '/:mode(foo|bar)'; 129 | var matcher = Url.toMatcher(pattern); 130 | 131 | expect(matcher.source).toBe('^\\/(?:(foo|bar))\\/*$'); 132 | expect(matcher.keys).toEqual([{"name": "mode", "optional": false}]); 133 | expect(Url.match(pattern, '/')).toEqual(null); 134 | expect(Url.match(pattern, '/foo')).toEqual({mode: 'foo'}); 135 | expect(Url.match(pattern, '/bar/')).toEqual({mode: 'bar'}); 136 | expect(Url.match(pattern, '/baz/')).toEqual(null); 137 | }); 138 | 139 | 140 | test('/:page/details/:id?', () => { 141 | var pattern = '/:page/details/:id?'; 142 | var matcher = Url.toMatcher(pattern); 143 | 144 | expect(matcher.source).toBe('^\\/(?:([^\\/]+))\\/details(?:\\/([^\\/]+))?\\/*$'); 145 | expect(matcher.keys).toEqual([{"name": "page", "optional": false}, {"name": "id", "optional": true}]); 146 | expect(Url.match(pattern, '/')).toEqual(null); 147 | expect(Url.match(pattern, '/foo/details/')).toEqual({page: 'foo'}); 148 | expect(Url.match(pattern, '/foo/details/123')).toEqual({page: 'foo', id: '123'}); 149 | expect(Url.match(pattern, '/details/123')).toEqual(null); 150 | }); 151 | 152 | 153 | test('/:folder(foo|\\d+)?', () => { 154 | var pattern = '/:folder(foo|\\d+)?'; 155 | var matcher = Url.toMatcher(pattern); 156 | 157 | expect(matcher.source).toBe('^(?:\\/(foo|\\d+))?\\/*$'); 158 | }); 159 | 160 | test('query methods', () => { 161 | var req = new Url('https://mail.ru/?foo&bar=baz'); 162 | 163 | expect(req.query.foo).toBe(''); 164 | expect(req.query.bar).toBe('baz'); 165 | 166 | req.addToQuery({qux: 'Y'}); 167 | expect(req.query.qux).toBe('Y'); 168 | 169 | req.setQuery('qux=N&zzz'); 170 | expect(req.query.qux).toBe('N'); 171 | expect(req.query.zzz).toBe(''); 172 | 173 | req.removeFromQuery('zzz'); 174 | expect(req.query.zzz).toBe(void 0); 175 | 176 | req.removeFromQuery(['foo', 'bar']); 177 | expect(req.query.zzz).toBe(void 0); 178 | 179 | req.addToQuery({qux: null, 's"t"r': '<%^&>'}); 180 | expect(req + '').toBe('https://mail.ru/?s%22t%22r=%3C%25%5E%26%3E'); 181 | 182 | req.setQuery({}, true); 183 | expect(req + '').toBe('https://mail.ru/'); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /tests/urls.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | return [ 3 | "http://foo.com/blah_blah", 4 | "http://foo.com/blah_blah/", 5 | "http://foo.com/blah_blah_(wikipedia)", 6 | "http://foo.com/blah_blah_(wikipedia)_(again)", 7 | "http://www.example.com/wpstyle/?p=364", 8 | "https://www.example.com/foo/?bar=baz&inga=42&quux", 9 | //"http://✪df.ws/123", 10 | "http://userid:password@example.com:8080", 11 | "http://userid:password@example.com:8080/", 12 | "http://userid@example.com", 13 | "http://userid@example.com/", 14 | "http://userid@example.com:8080", 15 | "http://userid@example.com:8080/", 16 | "http://userid:password@example.com", 17 | "http://userid:password@example.com/", 18 | "http://142.42.1.1/", 19 | "http://142.42.1.1:8080/", 20 | //"http://➡.ws/䨹", 21 | //"http://⌘.ws", 22 | //"http://⌘.ws/", 23 | "http://foo.com/blah_(wikipedia)#cite-1", 24 | "http://foo.com/blah_(wikipedia)_blah#cite-1", 25 | //"http://foo.com/unicode_(✪)_in_parens", 26 | "http://foo.com/(something)?after=parens", 27 | //"http://☺.damowmow.com/", 28 | "http://code.google.com/events/#&product=browser", 29 | "http://j.mp", 30 | "ftp://foo.bar/baz", 31 | "http://foo.bar/?q=Test%20URL-encoded%20stuff", 32 | //"http://مثال.إختبر", 33 | //"http://例子.测试", 34 | //"http://उदाहरण.परीक्षा", 35 | //"http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", 36 | "http://1337.net", 37 | "http://a.b-c.de", 38 | "http://223.255.255.254" 39 | ]; 40 | }); 41 | -------------------------------------------------------------------------------- /vendors/Emitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author RubaXa 3 | * @license MIT 4 | */ 5 | (function () { 6 | "use strict"; 7 | 8 | var RDASH = /-/g, 9 | RSPACE = /\s+/, 10 | 11 | r_camelCase = /-(.)/g, 12 | camelCase = function (_, chr) { 13 | return chr.toUpperCase(); 14 | }, 15 | 16 | hasOwn = ({}).hasOwnProperty, 17 | emptyArray = [] 18 | ; 19 | 20 | 21 | 22 | /** 23 | * Получить список слушателей 24 | * @param {Object} target 25 | * @param {string} name 26 | * @returns {Array} 27 | * @memberOf Emitter 28 | */ 29 | function getListeners(target, name) { 30 | var list = target.__emList; 31 | 32 | name = name.toLowerCase().replace(RDASH, ''); 33 | 34 | if (list === void 0) { 35 | list = target.__emList = {}; 36 | list[name] = []; 37 | } 38 | else if (list[name] === void 0) { 39 | list[name] = []; 40 | } 41 | 42 | return list[name]; 43 | } 44 | 45 | 46 | 47 | /** 48 | * Излучатель событий 49 | * @class Emitter 50 | * @constructs Emitter 51 | */ 52 | var Emitter = function () { 53 | }; 54 | Emitter.fn = Emitter.prototype = /** @lends Emitter# */ { 55 | constructor: Emitter, 56 | 57 | 58 | /** 59 | * Прикрепить обработчик для одного или нескольких событий, поддерживается `handleEvent` 60 | * @param {string} events одно или несколько событий, разделенных пробелом 61 | * @param {Function} fn функция обработчик 62 | * @returns {Emitter} 63 | */ 64 | on: function (events, fn) { 65 | events = events.split(RSPACE); 66 | 67 | var n = events.length, list; 68 | 69 | while (n--) { 70 | list = getListeners(this, events[n]); 71 | list.push(fn); 72 | } 73 | 74 | return this; 75 | }, 76 | 77 | 78 | /** 79 | * Удалить обработчик для одного или нескольких событий 80 | * @param {string} [events] одно или несколько событий, разделенных пробелом 81 | * @param {Function} [fn] функция обработчик, если не передать, будут отвязаны все обработчики 82 | * @returns {Emitter} 83 | */ 84 | off: function (events, fn) { 85 | if (events === void 0) { 86 | this.__emList = events; 87 | } 88 | else { 89 | events = events.split(RSPACE); 90 | 91 | var n = events.length; 92 | 93 | while (n--) { 94 | var list = getListeners(this, events[n]), i = list.length, idx = -1; 95 | 96 | if (arguments.length === 1) { 97 | list.splice(0, 1e5); // dirty hack 98 | } else { 99 | if (list.indexOf) { 100 | idx = list.indexOf(fn); 101 | } else { // old browsers 102 | while (i--) { 103 | /* istanbul ignore else */ 104 | if (list[i] === fn) { 105 | idx = i; 106 | break; 107 | } 108 | } 109 | } 110 | 111 | if (idx !== -1) { 112 | list.splice(idx, 1); 113 | } 114 | } 115 | } 116 | } 117 | 118 | return this; 119 | }, 120 | 121 | 122 | /** 123 | * Прикрепить обработчик события, который выполняется единожды 124 | * @param {string} events событие или список 125 | * @param {Function} fn функция обработчик 126 | * @returns {Emitter} 127 | */ 128 | one: function (events, fn) { 129 | var proxy = function () { 130 | this.off(events, proxy); 131 | return fn.apply(this, arguments); 132 | }; 133 | 134 | return this.on(events, proxy); 135 | }, 136 | 137 | 138 | /** 139 | * Распространить событие 140 | * @param {string} type тип события 141 | * @param {Array} [args] аргумент или массив аргументов 142 | * @returns {*} 143 | */ 144 | emit: function (type, args) { 145 | var list = getListeners(this, type), 146 | i = list.length, 147 | fn, 148 | ctx, 149 | tmp, 150 | retVal, 151 | argsLength 152 | ; 153 | 154 | type = 'on' + type.charAt(0).toUpperCase() + type.substr(1); 155 | 156 | if (type.indexOf('-') > -1) { 157 | type = type.replace(r_camelCase, camelCase); 158 | } 159 | 160 | if (typeof this[type] === 'function') { 161 | retVal = this[type].apply(this, [].concat(args)); 162 | } 163 | 164 | if (i > 0) { 165 | args = args === void 0 ? emptyArray : [].concat(args); 166 | argsLength = args.length; 167 | 168 | while (i--) { 169 | fn = list[i]; 170 | ctx = this; 171 | 172 | /* istanbul ignore else */ 173 | if (fn !== void 0) { 174 | if (fn.handleEvent !== void 0) { 175 | ctx = fn; 176 | fn = fn.handleEvent; 177 | } 178 | 179 | if (argsLength === 0) { 180 | tmp = fn.call(ctx); 181 | } 182 | else if (argsLength === 1) { 183 | tmp = fn.call(ctx, args[0]); 184 | } 185 | else if (argsLength === 2) { 186 | tmp = fn.call(ctx, args[0], args[1]); 187 | } 188 | else { 189 | tmp = fn.apply(ctx, args); 190 | } 191 | 192 | if (tmp !== void 0) { 193 | retVal = tmp; 194 | } 195 | } 196 | } 197 | } 198 | 199 | return retVal; 200 | }, 201 | 202 | 203 | /** 204 | * Распространить `Emitter.Event` 205 | * @param {string} type тип события 206 | * @param {Array} [args] аргумент или массив аргументов 207 | * @param {*} [details] детали объекта события 208 | * @returns {Emitter} 209 | */ 210 | trigger: function (type, args, details) { 211 | var evt = new Event(type); 212 | 213 | evt.target = evt.target || this; 214 | evt.details = details; 215 | evt.result = this.emit(type.type || type, [evt].concat(args)); 216 | 217 | return this; 218 | } 219 | }; 220 | 221 | 222 | 223 | /** 224 | * Событие 225 | * @class Emitter.Event 226 | * @constructs Emitter.Event 227 | * @param {string|Object|Event} type тип события 228 | * @returns {Emitter.Event} 229 | */ 230 | function Event(type) { 231 | if (type instanceof Event) { 232 | return type; 233 | } 234 | 235 | if (type.type) { 236 | for (var key in type) { 237 | /* istanbul ignore else */ 238 | if (hasOwn.call(type, key)) { 239 | this[key] = type[key]; 240 | } 241 | } 242 | 243 | type = type.type; 244 | } 245 | 246 | this.type = type.toLowerCase().replace(RDASH, ''); 247 | } 248 | 249 | Event.fn = Event.prototype = /** @lends Emitter.Event# */ { 250 | constructor: Event, 251 | 252 | 253 | /** @type {boolean} */ 254 | defaultPrevented: false, 255 | 256 | 257 | /** @type {boolean} */ 258 | propagationStopped: false, 259 | 260 | 261 | /** 262 | * Позволяет определить, было ли отменено действие по умолчанию 263 | * @returns {boolean} 264 | */ 265 | isDefaultPrevented: function () { 266 | return this.defaultPrevented; 267 | }, 268 | 269 | 270 | /** 271 | * Отменить действие по умолчанию 272 | */ 273 | preventDefault: function () { 274 | this.defaultPrevented = true; 275 | }, 276 | 277 | 278 | /** 279 | * Остановить продвижение события 280 | */ 281 | stopPropagation: function () { 282 | this.propagationStopped = true; 283 | }, 284 | 285 | 286 | /** 287 | * Позволяет определить, было ли отменено продвижение события 288 | * @return {boolean} 289 | */ 290 | isPropagationStopped: function () { 291 | return this.propagationStopped; 292 | } 293 | }; 294 | 295 | 296 | /** 297 | * Подмешать методы к объекту 298 | * @static 299 | * @memberof Emitter 300 | * @param {Object} target цель 301 | * @returns {Object} 302 | */ 303 | Emitter.apply = function (target) { 304 | target.on = Emitter.fn.on; 305 | target.off = Emitter.fn.off; 306 | target.one = Emitter.fn.one; 307 | target.emit = Emitter.fn.emit; 308 | target.trigger = Emitter.fn.trigger; 309 | return target; 310 | }; 311 | 312 | 313 | // Версия модуля 314 | Emitter.version = "0.3.0"; 315 | 316 | 317 | // exports 318 | Emitter.Event = Event; 319 | Emitter.getListeners = getListeners; 320 | 321 | 322 | if (typeof define === "function" && (define.amd || /* istanbul ignore next */ define.ajs)) { 323 | define('Emitter', [], function () { 324 | return Emitter; 325 | }); 326 | } else if (typeof module != "undefined" && module.exports) { 327 | module.exports = Emitter; 328 | } else { 329 | window.Emitter = Emitter; 330 | } 331 | })(); 332 | -------------------------------------------------------------------------------- /vendors/Promise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author RubaXa 3 | * @license MIT 4 | */ 5 | (function () { 6 | "use strict"; 7 | 8 | 9 | function _then(promise, method, callback) { 10 | return function () { 11 | var args = arguments, retVal; 12 | 13 | /* istanbul ignore else */ 14 | if (typeof callback === 'function') { 15 | try { 16 | retVal = callback.apply(promise, args); 17 | } catch (err) { 18 | promise.reject(err); 19 | return; 20 | } 21 | 22 | if (retVal && typeof retVal.then === 'function') { 23 | if (retVal.done && retVal.fail) { 24 | retVal.__noLog = true; 25 | retVal.done(promise.resolve).fail(promise.reject); 26 | retVal.__noLog = false; 27 | } 28 | else { 29 | retVal.then(promise.resolve, promise.reject); 30 | } 31 | return; 32 | } else { 33 | args = [retVal]; 34 | method = 'resolve'; 35 | } 36 | } 37 | 38 | promise[method].apply(promise, args); 39 | }; 40 | } 41 | 42 | 43 | /** 44 | * «Обещания» поддерживают как [нативный](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 45 | * интерфейс, так и [$.Deferred](http://api.jquery.com/category/deferred-object/). 46 | * 47 | * @class Promise 48 | * @constructs Promise 49 | * @param {Function} [executor] 50 | */ 51 | var Promise = function (executor) { 52 | var _completed = false; 53 | 54 | function _finish(state, result) { 55 | dfd.done = 56 | dfd.fail = function () { 57 | return dfd; 58 | }; 59 | 60 | dfd[state ? 'done' : 'fail'] = function (fn) { 61 | /* istanbul ignore else */ 62 | if (typeof fn === 'function') { 63 | fn(result); 64 | } 65 | return dfd; 66 | }; 67 | 68 | var fn, 69 | fns = state ? _doneFn : _failFn, 70 | i = 0, 71 | n = fns.length 72 | ; 73 | 74 | for (; i < n; i++) { 75 | fn = fns[i]; 76 | /* istanbul ignore else */ 77 | if (typeof fn === 'function') { 78 | fn(result); 79 | } 80 | } 81 | 82 | fns = _doneFn = _failFn = null; 83 | } 84 | 85 | 86 | function _setState(state) { 87 | return function (result) { 88 | if (_completed) { 89 | return dfd; 90 | } 91 | 92 | _completed = true; 93 | 94 | dfd.resolve = 95 | dfd.reject = function () { 96 | return dfd; 97 | }; 98 | 99 | if (state && result && result.then && result.pending !== false) { 100 | // Опачки! 101 | result.then( 102 | function (result) { _finish(true, result); }, 103 | function (result) { _finish(false, result); } 104 | ); 105 | } 106 | else { 107 | _finish(state, result); 108 | } 109 | 110 | return dfd; 111 | }; 112 | } 113 | 114 | var 115 | _doneFn = [], 116 | _failFn = [], 117 | 118 | dfd = { 119 | /** 120 | * Добавляет обработчик, который будет вызван, когда «обещание» будет «разрешено» 121 | * @param {Function} fn функция обработчик 122 | * @returns {Promise} 123 | * @memberOf Promise# 124 | */ 125 | done: function done(fn) { 126 | _doneFn.push(fn); 127 | return dfd; 128 | }, 129 | 130 | /** 131 | * Добавляет обработчик, который будет вызван, когда «обещание» будет «отменено» 132 | * @param {Function} fn функция обработчик 133 | * @returns {Promise} 134 | * @memberOf Promise# 135 | */ 136 | fail: function fail(fn) { 137 | _failFn.push(fn); 138 | return dfd; 139 | }, 140 | 141 | /** 142 | * Добавляет сразу два обработчика 143 | * @param {Function} [doneFn] будет выполнено, когда «обещание» будет «разрешено» 144 | * @param {Function} [failFn] или когда «обещание» будет «отменено» 145 | * @returns {Promise} 146 | * @memberOf Promise# 147 | */ 148 | then: function then(doneFn, failFn) { 149 | var promise = Promise(); 150 | 151 | dfd.__noLog = true; // для логгера 152 | 153 | dfd 154 | .done(_then(promise, 'resolve', doneFn)) 155 | .fail(_then(promise, 'reject', failFn)) 156 | ; 157 | 158 | dfd.__noLog = false; 159 | 160 | return promise; 161 | }, 162 | 163 | notify: function () { // jQuery support 164 | return dfd; 165 | }, 166 | 167 | progress: function () { // jQuery support 168 | return dfd; 169 | }, 170 | 171 | promise: function () { // jQuery support 172 | // jQuery support 173 | return dfd; 174 | }, 175 | 176 | /** 177 | * Добавить обработчик «обещаний» в независимости от выполнения 178 | * @param {Function} fn функция обработчик 179 | * @returns {Promise} 180 | * @memberOf Promise# 181 | */ 182 | always: function always(fn) { 183 | dfd.done(fn).fail(fn); 184 | return dfd; 185 | }, 186 | 187 | 188 | /** 189 | * «Разрешить» «обещание» 190 | * @param {*} result 191 | * @returns {Promise} 192 | * @method 193 | * @memberOf Promise# 194 | */ 195 | resolve: _setState(true), 196 | 197 | 198 | /** 199 | * «Отменить» «обещание» 200 | * @param {*} result 201 | * @returns {Promise} 202 | * @method 203 | * @memberOf Promise# 204 | */ 205 | reject: _setState(false) 206 | } 207 | ; 208 | 209 | 210 | /** 211 | * @name Promise#catch 212 | * @alias fail 213 | * @method 214 | */ 215 | dfd['catch'] = function (fn) { 216 | return dfd.then(null, fn); 217 | }; 218 | 219 | 220 | dfd.constructor = Promise; 221 | 222 | 223 | // Работеам как native Promises 224 | /* istanbul ignore else */ 225 | if (typeof executor === 'function') { 226 | try { 227 | executor(dfd.resolve, dfd.reject); 228 | } catch (err) { 229 | dfd.reject(err); 230 | } 231 | } 232 | 233 | return dfd; 234 | }; 235 | 236 | 237 | /** 238 | * Дождаться «разрешения» всех обещаний 239 | * @static 240 | * @memberOf Promise 241 | * @param {Array} iterable массив значений/обещаний 242 | * @returns {Promise} 243 | */ 244 | Promise.all = function (iterable) { 245 | var dfd = Promise(), 246 | d, 247 | i = 0, 248 | n = iterable.length, 249 | remain = n, 250 | values = [], 251 | _fn, 252 | _doneFn = function (i, val) { 253 | (i >= 0) && (values[i] = val); 254 | 255 | /* istanbul ignore else */ 256 | if (--remain <= 0) { 257 | dfd.resolve(values); 258 | } 259 | }, 260 | _failFn = function (err) { 261 | dfd.reject([err]); 262 | } 263 | ; 264 | 265 | if (remain === 0) { 266 | _doneFn(); 267 | } 268 | else { 269 | for (; i < n; i++) { 270 | d = iterable[i]; 271 | 272 | if (d && typeof d.then === 'function') { 273 | _fn = _doneFn.bind(null, i); // todo: тест 274 | 275 | d.__noLog = true; 276 | 277 | if (d.done && d.fail) { 278 | d.done(_fn).fail(_failFn); 279 | } else { 280 | d.then(_fn, _failFn); 281 | } 282 | 283 | d.__noLog = false; 284 | } 285 | else { 286 | _doneFn(i, d); 287 | } 288 | } 289 | } 290 | 291 | return dfd; 292 | }; 293 | 294 | 295 | /** 296 | * Дождаться «разрешения» всех обещаний и вернуть результат последнего 297 | * @static 298 | * @memberOf Promise 299 | * @param {Array} iterable массив значений/обещаний 300 | * @returns {Promise} 301 | */ 302 | Promise.race = function (iterable) { 303 | return Promise.all(iterable).then(function (values) { 304 | return values.pop(); 305 | }); 306 | }; 307 | 308 | 309 | /** 310 | * Привести значение к «Обещанию» 311 | * @static 312 | * @memberOf Promise 313 | * @param {*} value переменная или объект имеющий метод then 314 | * @returns {Promise} 315 | */ 316 | Promise.cast = function (value) { 317 | var promise = Promise().resolve(value); 318 | return value && typeof value.then === 'function' 319 | ? promise.then(function () { return value; }) 320 | : promise 321 | ; 322 | }; 323 | 324 | 325 | /** 326 | * Вернуть «разрешенное» обещание 327 | * @static 328 | * @memberOf Promise 329 | * @param {*} value переменная 330 | * @returns {Promise} 331 | */ 332 | Promise.resolve = function (value) { 333 | return (value && value.constructor === Promise) ? value : Promise().resolve(value); 334 | }; 335 | 336 | 337 | /** 338 | * Вернуть «отклоненное» обещание 339 | * @static 340 | * @memberOf Promise 341 | * @param {*} value переменная 342 | * @returns {Promise} 343 | */ 344 | Promise.reject = function (value) { 345 | return Promise().reject(value); 346 | }; 347 | 348 | 349 | /** 350 | * Дождаться «разрешения» всех обещаний 351 | * @param {Object} map «Ключь» => «Обещание» 352 | * @returns {Promise} 353 | */ 354 | Promise.map = function (map) { 355 | var array = [], key, idx = 0, results = {}; 356 | 357 | for (key in map) { 358 | array.push(map[key]); 359 | } 360 | 361 | return Promise.all(array).then(function (values) { 362 | /* jshint -W088 */ 363 | for (key in map) { 364 | results[key] = values[idx++]; 365 | } 366 | 367 | return results; 368 | }); 369 | }; 370 | 371 | 372 | 373 | // Версия модуля 374 | Promise.version = "0.3.1"; 375 | 376 | 377 | /* istanbul ignore else */ 378 | if (!window.Promise) { 379 | window.Promise = Promise; 380 | } 381 | 382 | 383 | // exports 384 | if (typeof define === "function" && (define.amd || /* istanbul ignore next */ define.ajs)) { 385 | define('Promise', [], function () { 386 | return Promise; 387 | }); 388 | } else if (typeof module != "undefined" && module.exports) { 389 | module.exports = Promise; 390 | } 391 | else { 392 | window.Deferred = Promise; 393 | } 394 | })(); 395 | -------------------------------------------------------------------------------- /vendors/performance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User Timing polyfill (http://www.w3.org/TR/user-timing/) 3 | * @author RubaXa 4 | */ 5 | (function (window){ 6 | var 7 | startOffset = Date.now ? Date.now() : +(new Date) 8 | , performance = window.performance || {} 9 | 10 | , _entries = [] 11 | , _marksIndex = {} 12 | 13 | , _filterEntries = function (key, value){ 14 | var i = 0, n = _entries.length, result = []; 15 | for( ; i < n; i++ ){ 16 | if( _entries[i][key] == value ){ 17 | result.push(_entries[i]); 18 | } 19 | } 20 | return result; 21 | } 22 | 23 | , _clearEntries = function (type, name){ 24 | var i = _entries.length, entry; 25 | while( i-- ){ 26 | entry = _entries[i]; 27 | if( entry.entryType == type && (name === void 0 || entry.name == name) ){ 28 | _entries.splice(i, 1); 29 | } 30 | } 31 | } 32 | ; 33 | 34 | 35 | if( !performance.now ){ 36 | performance.now = performance.webkitNow || performance.mozNow || performance.msNow || function (){ 37 | return (Date.now ? Date.now() : +(new Date)) - startOffset; 38 | }; 39 | } 40 | 41 | 42 | if( !performance.mark ){ 43 | performance.mark = performance.webkitMark || function (name){ 44 | var mark = { 45 | name: name 46 | , entryType: 'mark' 47 | , startTime: performance.now() 48 | , duration: 0 49 | }; 50 | _entries.push(mark); 51 | _marksIndex[name] = mark; 52 | }; 53 | } 54 | 55 | 56 | if( !performance.measure ){ 57 | performance.measure = performance.webkitMeasure || function (name, startMark, endMark){ 58 | startMark = _marksIndex[startMark].startTime; 59 | endMark = _marksIndex[endMark].startTime; 60 | 61 | _entries.push({ 62 | name: name 63 | , entryType: 'measure' 64 | , startTime: startMark 65 | , duration: endMark - startMark 66 | }); 67 | }; 68 | } 69 | 70 | 71 | if( !performance.getEntriesByType ){ 72 | performance.getEntriesByType = performance.webkitGetEntriesByType || function (type){ 73 | return _filterEntries('entryType', type); 74 | }; 75 | } 76 | 77 | 78 | if( !performance.getEntriesByName ){ 79 | performance.getEntriesByName = performance.webkitGetEntriesByName || function (name){ 80 | return _filterEntries('name', name); 81 | }; 82 | } 83 | 84 | 85 | if( !performance.clearMarks ){ 86 | performance.clearMarks = performance.webkitClearMarks || function (name){ 87 | _clearEntries('mark', name); 88 | }; 89 | } 90 | 91 | 92 | if( !performance.clearMeasures ){ 93 | performance.clearMeasures = performance.webkitClearMeasures || function (name){ 94 | _clearEntries('measure', name); 95 | }; 96 | } 97 | 98 | 99 | // exports 100 | window.performance = performance; 101 | 102 | if( typeof define === 'function' && define.amd ){ 103 | define(function (){ return performance }); 104 | } 105 | })(window); 106 | --------------------------------------------------------------------------------