├── .gitignore ├── README.md ├── package.json ├── sources ├── JetApp.ts ├── JetAppBase.ts ├── JetBase.ts ├── JetView.ts ├── JetViewRaw.ts ├── Route.ts ├── errors.ts ├── es5.ts ├── helpers.ts ├── index.ts ├── interfaces.ts ├── patch.ts ├── plugins │ ├── Guard.ts │ ├── Locale.ts │ ├── Menu.ts │ ├── Status.ts │ ├── Theme.ts │ ├── UrlParam.ts │ └── User.ts ├── routers │ ├── EmptyRouter.ts │ ├── HashRouter.ts │ ├── StoreRouter.ts │ ├── SubRouter.ts │ └── UrlRouter.ts └── typings │ └── webix-polyglot.d.ts ├── tests ├── add_remove_view.spec.js ├── app_in_app.spec.js ├── basic_app.spec.js ├── destructor.spec.js ├── events.spec.js ├── jetappui.spec.js ├── locale.spec.js ├── menu.spec.js ├── refresh.spec.js ├── routers.spec.js ├── stubs │ ├── helpers.js │ └── views.js ├── subviews.spec.js ├── url.spec.js ├── urlparams.spec.js ├── view_stages.spec.js └── window.spec.js ├── tsconfig.json ├── vite.config.js ├── whatsnew.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sources/**/*.js 3 | *.zip 4 | .Ds_store 5 | *.tgz 6 | *.log 7 | .vscode 8 | dist 9 | coverage -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Webix Jet 2 | ============ 3 | 4 | [![npm version](https://badge.fury.io/js/webix-jet.svg)](https://badge.fury.io/js/webix-jet) 5 | 6 | Micro-framework for Webix UIMicro-framework for [Webix UI](https://webix.com) 7 | 8 | 9 | ### Useful links 10 | 11 | - Documentation: https://webix.gitbook.io/webix-jet/ 12 | - Support Forum: https://forum.webix.com 13 | - Demos 14 | - Starter app - https://github.com/webix-hub/jet-start 15 | - Functionality demos - https://github.com/webix-hub/jet-demos 16 | - Interface demo - https://github.com/webix-hub/webix-adminapp-demo 17 | 18 | ### Working with sources 19 | 20 | ``` 21 | //compile to es5 22 | npm run dist 23 | 24 | //run lint and test 25 | npm run lint && npm run test 26 | ``` 27 | 28 | ### License terms 29 | 30 | The MIT License (MIT) 31 | Copyright (c) 2016 XBSoftware 32 | 33 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webix-jet", 3 | "version": "3.0.3", 4 | "description": "Webix Jet micro-framework", 5 | "main": "dist/jet.umd.js", 6 | "module": "dist/jet.mjs", 7 | "types": "dist/types/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": { 11 | "types": "./dist/types/index.d.ts", 12 | "default": "./dist/jet.mjs" 13 | }, 14 | "require": { 15 | "types": "./dist/types/index.d.ts", 16 | "default": "./dist/jet.umd.js" 17 | } 18 | }, 19 | "./package.json": "./package.json" 20 | }, 21 | "scripts": { 22 | "test": "vitest run", 23 | "dist": "vite build", 24 | "watch": "vite", 25 | "watch-test": "vitest" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/webix-hub/webix-jet.git" 30 | }, 31 | "keywords": [ 32 | "webix", 33 | "jet", 34 | "framework", 35 | "rich", 36 | "ui" 37 | ], 38 | "files": [ 39 | "/dist", 40 | "README.md" 41 | ], 42 | "devDependencies": { 43 | "@vitest/browser": "^0.31.0", 44 | "vite": "^4.3.5", 45 | "vite-plugin-dts": "^2.3.0", 46 | "vitest": "^0.31.0", 47 | "webdriverio": "^8.10.1", 48 | "webix-polyglot": "^3.0.0" 49 | }, 50 | "author": "Maksim Kozhukh", 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/webix-hub/webix-jet/issues" 54 | }, 55 | "homepage": "https://github.com/webix-hub/webix-jet#readme" 56 | } 57 | -------------------------------------------------------------------------------- /sources/JetApp.ts: -------------------------------------------------------------------------------- 1 | import { IJetApp } from "./interfaces"; 2 | import { JetAppBase } from "./JetAppBase"; 3 | import { HashRouter } from "./routers/HashRouter"; 4 | 5 | import patch from "./patch"; 6 | 7 | // webpack require 8 | declare function require(_$url: string): any; 9 | 10 | // webpack require 11 | declare function require(_$url: string): any; 12 | 13 | export class JetApp extends JetAppBase implements IJetApp { 14 | constructor(config: any) { 15 | config.router = config.router || HashRouter; 16 | super(config); 17 | patch(this.webix); 18 | } 19 | require(type:string, url:string){ 20 | return require(type+"/"+url); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sources/JetAppBase.ts: -------------------------------------------------------------------------------- 1 | import { JetBase } from "./JetBase"; 2 | import { JetViewRaw } from "./JetViewRaw"; 3 | import { JetView } from "./JetView"; 4 | import {SubRouter} from "./routers/SubRouter"; 5 | import {NavigationBlocked} from "./errors"; 6 | 7 | import { 8 | IBaseView, IJetApp, IJetConfig, IJetRouter, 9 | IJetURL, IJetURLChunk, IHash, 10 | IJetView, IRoute, ISubView, IViewConfig } from "./interfaces"; 11 | 12 | import { Route } from "./Route"; 13 | 14 | let _once = true; 15 | 16 | export class JetAppBase extends JetBase implements IJetView { 17 | public config: IJetConfig; 18 | public app: IJetApp; 19 | public ready: Promise; 20 | 21 | callEvent: (name: string, parameters: any[]) => boolean; 22 | attachEvent: (name: string, handler: any) => void; 23 | 24 | private $router: IJetRouter; 25 | private _services: { [name: string]: any }; 26 | private _subSegment: IRoute; 27 | 28 | constructor(config?: any) { 29 | const webix = (config || {}).webix || (window as any).webix; 30 | config = webix.extend({ 31 | name: "App", 32 | version: "1.0", 33 | start: "/home" 34 | }, config, true); 35 | 36 | super(webix, config); 37 | 38 | this.config = config; 39 | this.app = this.config.app; 40 | this.ready = Promise.resolve(); 41 | this._services = {}; 42 | 43 | this.webix.extend(this, this.webix.EventSystem); 44 | } 45 | getUrl():IJetURL{ 46 | return this._subSegment.suburl(); 47 | } 48 | getUrlString(){ 49 | return this._subSegment.toString(); 50 | } 51 | getService(name: string) { 52 | let obj = this._services[name]; 53 | if (typeof obj === "function") { 54 | obj = this._services[name] = obj(this); 55 | } 56 | return obj; 57 | } 58 | setService(name: string, handler: any) { 59 | this._services[name] = handler; 60 | } 61 | destructor(){ 62 | this.getSubView().destructor(); 63 | super.destructor(); 64 | } 65 | // copy object and collect extra handlers 66 | copyConfig(obj: any, target: any, config: IViewConfig) { 67 | // raw ui config 68 | if (obj instanceof JetBase || 69 | (typeof obj === "function" && obj.prototype instanceof JetBase)){ 70 | obj = { $subview: obj }; 71 | } 72 | 73 | // subview placeholder 74 | if (typeof obj.$subview != "undefined") { 75 | return this.addSubView(obj, target, config); 76 | } 77 | 78 | // process sub-properties 79 | const isArray = obj instanceof Array; 80 | target = target || (isArray ? [] : {}); 81 | for (const method in obj) { 82 | let point = obj[method]; 83 | 84 | // view class 85 | if (typeof point === "function" && point.prototype instanceof JetBase) { 86 | point = { $subview : point }; 87 | } 88 | 89 | if (point && typeof point === "object" && 90 | !(point instanceof this.webix.DataCollection) && !(point instanceof RegExp) && !(point instanceof Map)) { 91 | if (point instanceof Date) { 92 | target[method] = new Date(point); 93 | } else { 94 | const copy = this.copyConfig( 95 | point, 96 | (point instanceof Array ? [] : {}), 97 | config); 98 | if (copy !== null){ 99 | if (isArray) target.push(copy); 100 | else target[method] = copy; 101 | } 102 | } 103 | } else { 104 | target[method] = point; 105 | } 106 | } 107 | 108 | return target; 109 | } 110 | 111 | getRouter() { 112 | return this.$router; 113 | } 114 | 115 | clickHandler(e: Event, target?: HTMLElement) { 116 | if (e) { 117 | target = target || (e.target || e.srcElement) as HTMLElement; 118 | if (target && target.getAttribute) { 119 | const trigger: string = target.getAttribute("trigger"); 120 | if (trigger) { 121 | this._forView(target, view => view.app.trigger(trigger)); 122 | e.cancelBubble = true; 123 | return e.preventDefault(); 124 | } 125 | const route: string = target.getAttribute("route"); 126 | if (route) { 127 | this._forView(target, view => view.show(route)); 128 | e.cancelBubble = true; 129 | return e.preventDefault(); 130 | } 131 | } 132 | } 133 | 134 | const parent = target.parentNode as HTMLElement; 135 | if (parent){ 136 | this.clickHandler(e, parent); 137 | } 138 | } 139 | 140 | getRoot(){ 141 | return this.getSubView().getRoot(); 142 | } 143 | 144 | refresh(){ 145 | if (!this._subSegment){ 146 | return Promise.resolve(null); 147 | } 148 | 149 | return this.getSubView().refresh().then(view => { 150 | this.callEvent("app:route", [this.getUrl()]); 151 | return view; 152 | }); 153 | } 154 | 155 | loadView(url:string): Promise { 156 | const views = this.config.views; 157 | let result = null; 158 | 159 | if (url === ""){ 160 | return Promise.resolve( 161 | this._loadError("", new Error("Webix Jet: Empty url segment")) 162 | ); 163 | } 164 | 165 | try { 166 | if (views) { 167 | if (typeof views === "function") { 168 | // custom loading strategy 169 | result = views(url); 170 | } else { 171 | // predefined hash 172 | result = views[url]; 173 | } 174 | if (typeof result === "string"){ 175 | url = result; 176 | result = null; 177 | } 178 | } 179 | 180 | if (!result){ 181 | if (url === "_hidden"){ 182 | result = { hidden:true }; 183 | } else if (url === "_blank"){ 184 | result = {}; 185 | } else { 186 | url = url.replace(/\./g, "/"); 187 | result = this.require("jet-views", url); 188 | } 189 | } 190 | } catch(e){ 191 | result = this._loadError(url, e); 192 | } 193 | 194 | // custom handler can return view or its promise 195 | if (!result.then){ 196 | result = Promise.resolve(result); 197 | } 198 | 199 | // set error handler 200 | result = result 201 | .then(module => module.__esModule ? module.default : module) 202 | .catch(err => this._loadError(url, err)); 203 | 204 | return result; 205 | } 206 | 207 | _forView(target: HTMLElement, handler){ 208 | const view = this.webix.$$(target as any); 209 | if (view) { 210 | handler((view as any).$scope) 211 | } 212 | } 213 | 214 | _loadViewDynamic(url){ 215 | return null; 216 | } 217 | 218 | createFromURL(chunk: IJetURLChunk): Promise { 219 | let view:Promise; 220 | 221 | if (chunk.isNew || !chunk.view) { 222 | view = this.loadView(chunk.page) 223 | .then(ui => this.createView(ui, "", chunk.params)); 224 | } else { 225 | view = Promise.resolve(chunk.view); 226 | } 227 | 228 | return view; 229 | } 230 | 231 | _override(ui) { 232 | const over = this.config.override; 233 | if (over){ 234 | let dv; 235 | while(ui){ 236 | dv = ui; 237 | ui = over.get(ui); 238 | } 239 | return dv; 240 | } 241 | return ui; 242 | } 243 | createView(ui:any, name?:string, params?:IHash){ 244 | ui = this._override(ui); 245 | 246 | let obj; 247 | if (typeof ui === "function") { 248 | if (ui.prototype instanceof JetAppBase) { 249 | // UI class 250 | return new ui({ app: this, name, params, router:SubRouter }); 251 | } else if (ui.prototype instanceof JetBase) { 252 | // UI class 253 | return new ui(this, { name, params }); 254 | } else { 255 | // UI factory functions 256 | ui = ui(this); 257 | } 258 | } 259 | 260 | if (ui instanceof JetBase){ 261 | obj = ui; 262 | } else { 263 | // UI object 264 | obj = new JetViewRaw(this, { name, ui }); 265 | } 266 | return obj; 267 | } 268 | 269 | // show view path 270 | show(url: string, config?:any) : Promise { 271 | if (url && this.app && url.indexOf("//") == 0) 272 | return this.app.show(url.substr(1), config); 273 | 274 | return this.render(this._container, url || this.config.start, config); 275 | } 276 | 277 | // event helpers 278 | trigger(name: string, ...rest: any[]) { 279 | this.apply(name, rest); 280 | } 281 | apply(name: string, data: any[]) { 282 | this.callEvent(name, data); 283 | } 284 | action(name: string) { 285 | return this.webix.bind(function(...rest: any[]) { 286 | this.apply(name, rest); 287 | }, this); 288 | } 289 | on(name: string, handler) { 290 | this.attachEvent(name, handler); 291 | } 292 | 293 | use(plugin, config){ 294 | plugin(this, null, config); 295 | } 296 | 297 | error(name:string, er:any[]){ 298 | this.callEvent(name, er); 299 | this.callEvent("app:error", er); 300 | 301 | /* tslint:disable */ 302 | if (this.config.debug){ 303 | for (var i=0; i${text}`; 310 | } else { 311 | text += "

Check console for more details"; 312 | this.webix.message({ type:"error", text:text, expire:-1 }); 313 | } 314 | 315 | } 316 | } 317 | debugger; 318 | } 319 | /* tslint:enable */ 320 | } 321 | 322 | // renders top view 323 | render( 324 | root?: string | HTMLElement | ISubView, 325 | url?: IRoute | string, 326 | config?:any): Promise { 327 | 328 | this._container = (typeof root === "string") ? 329 | this.webix.toNode(root): 330 | (root || document.body); 331 | 332 | const firstInit = !this.$router; 333 | let path:string = null; 334 | if (firstInit){ 335 | if (_once && "tagName" in this._container){ 336 | this.webix.event(document.body, "click", e => this.clickHandler(e)); 337 | _once = false; 338 | } 339 | 340 | if (typeof url === "string"){ 341 | url = new Route(url, 0); 342 | } 343 | this._subSegment = this._first_start(url); 344 | this._subSegment.route.linkRouter = true; 345 | } else { 346 | if (typeof url === "string"){ 347 | path = url; 348 | } else { 349 | if (this.app){ 350 | path = url.split().route.path || this.config.start; 351 | } else { 352 | path = url.toString(); 353 | } 354 | } 355 | } 356 | 357 | const params = config ? config.params : this.config.params || null; 358 | const top = this.getSubView(); 359 | const segment = this._subSegment; 360 | const ready = segment 361 | .show({ url: path, params }, top) 362 | .then(() => this.createFromURL(segment.current())) 363 | .then(view => view.render(root, segment)) 364 | .then(base => { 365 | this.$router.set(segment.route.path, { silent:true }); 366 | this.callEvent("app:route", [this.getUrl()]); 367 | return base; 368 | }); 369 | 370 | this.ready = this.ready.then(() => ready); 371 | return ready; 372 | } 373 | 374 | getSubView():IJetView{ 375 | if (this._subSegment){ 376 | const view = this._subSegment.current().view; 377 | if (view) 378 | return view; 379 | } 380 | return new JetView(this, {}); 381 | } 382 | 383 | require(type:string, url:string):any{ return null; } 384 | 385 | private _first_start(route: IRoute) : IRoute{ 386 | this._segment = route; 387 | 388 | const cb = (a:string) => setTimeout(() => { 389 | (this as JetAppBase).show(a).catch(e => { 390 | if (!(e instanceof NavigationBlocked)) 391 | throw e; 392 | }); 393 | },1); 394 | this.$router = new (this.config.router)(cb, this.config, this); 395 | 396 | // start animation for top-level app 397 | if (this._container === document.body && this.config.animation !== false) { 398 | const node = this._container as HTMLElement; 399 | this.webix.html.addCss(node, "webixappstart"); 400 | setTimeout(() => { 401 | this.webix.html.removeCss(node, "webixappstart"); 402 | this.webix.html.addCss(node, "webixapp"); 403 | }, 10); 404 | } 405 | 406 | if (!route){ 407 | // if no url defined, check router first 408 | let urlString = this.$router.get(); 409 | if (!urlString){ 410 | urlString = this.config.start; 411 | this.$router.set(urlString, { silent: true }); 412 | } 413 | route = new Route(urlString, 0); 414 | } else if (this.app) { 415 | const oldRoute = route; 416 | const oldView = route.current().view; 417 | 418 | route.current().view = this; 419 | if (route.next()){ 420 | route.refresh(); 421 | route = route.split(); 422 | } else { 423 | route = new Route(this.config.start, 0); 424 | } 425 | oldRoute.current().view = oldView || this; 426 | } 427 | 428 | return route; 429 | } 430 | 431 | // error during view resolving 432 | private _loadError(url: string, err: Error):any{ 433 | this.error("app:error:resolve", [err, url]); 434 | return { template:" " }; 435 | } 436 | 437 | private addSubView(obj, target, config:IViewConfig) : ISubView { 438 | const url = obj.$subview !== true ? obj.$subview : null; 439 | const name: string = obj.name || (url ? this.webix.uid() : "default"); 440 | target.id = obj.id || "s" + this.webix.uid(); 441 | 442 | const view : ISubView = config[name] = { 443 | id: target.id, 444 | url, 445 | branch: obj.branch, 446 | popup: obj.popup, 447 | params: obj.params 448 | }; 449 | 450 | return view.popup ? null : target; 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /sources/JetBase.ts: -------------------------------------------------------------------------------- 1 | import { IBaseView, IJetApp, IJetURL, IJetView, 2 | IRoute, ISubView, ISubViewInfo, IWebixFacade, IHash } from "./interfaces"; 3 | 4 | export abstract class JetBase implements IJetView{ 5 | public app: IJetApp; 6 | public webix: IWebixFacade; 7 | public webixJet = true; 8 | 9 | protected _name: string; 10 | protected _parent: IJetView; 11 | protected _container: HTMLElement | ISubView; 12 | protected _root: IBaseView; 13 | protected _events:{ id:string, obj: any }[]; 14 | protected _subs:{[name:string]:ISubView}; 15 | protected _initUrl:IJetURL; 16 | protected _segment: IRoute; 17 | 18 | private _data:{[name:string]:any}; 19 | 20 | 21 | constructor(webix:IWebixFacade, config?:any){ 22 | this.webix = webix; 23 | this._events = []; 24 | this._subs = {}; 25 | this._data = {}; 26 | 27 | if (config && config.params) 28 | webix.extend(this._data, config.params); 29 | } 30 | 31 | getRoot(): IBaseView { 32 | return this._root; 33 | } 34 | 35 | destructor() { 36 | this._detachEvents(); 37 | this._destroySubs(); 38 | this._events = this._container = this.app = this._parent = this._root = null; 39 | } 40 | setParam(id:string, value:any, url?:boolean):void|Promise{ 41 | if (this._data[id] !== value){ 42 | this._data[id] = value; 43 | this._segment.update(id, value, 0); 44 | if (url){ 45 | return this.show(null); 46 | } 47 | } 48 | } 49 | getParam(id:string, parent:boolean):any{ 50 | const value = this._data[id]; 51 | if (typeof value !== "undefined" || !parent){ 52 | return value; 53 | } 54 | 55 | const view = this.getParentView(); 56 | if (view){ 57 | return view.getParam(id, parent); 58 | } 59 | } 60 | getUrl():IJetURL{ 61 | return this._segment.suburl(); 62 | } 63 | getUrlString():string{ 64 | return this._segment.toString(); 65 | } 66 | 67 | getParentView() : IJetView{ 68 | return this._parent; 69 | } 70 | 71 | $$(id:string | IBaseView):T{ 72 | if (typeof id === "string"){ 73 | const root = this.getRoot() as any; 74 | return root.queryView( 75 | (obj => (obj.config.id === id || obj.config.localId === id) && 76 | (obj.$scope === root.$scope) 77 | ), 78 | "self"); 79 | } else { 80 | return id as undefined as T; 81 | } 82 | } 83 | 84 | on(obj, name, code){ 85 | const id = obj.attachEvent(name, code); 86 | this._events.push({ obj, id }); 87 | return id; 88 | } 89 | 90 | contains(view: IJetView){ 91 | for (const key in this._subs){ 92 | const kid = this._subs[key].view; 93 | if (kid && (kid === view || kid.contains(view))){ 94 | return true; 95 | } 96 | } 97 | return false; 98 | } 99 | 100 | getSubView(name?:string):IJetView{ 101 | const sub = this.getSubViewInfo(name); 102 | if (sub){ 103 | return sub.subview.view; 104 | } 105 | } 106 | 107 | getSubViewInfo(name?:string):ISubViewInfo{ 108 | const sub = this._subs[name || "default"]; 109 | if (sub){ 110 | return { subview:sub, parent:this }; 111 | } 112 | 113 | if (name === "_top"){ 114 | this._subs[name] = { url:"", id:null, popup:true }; 115 | return this.getSubViewInfo(name); 116 | } 117 | 118 | // when called from a child view, searches for nearest parent with subview 119 | if (this._parent){ 120 | return this._parent.getSubViewInfo(name); 121 | } 122 | return null; 123 | } 124 | 125 | public abstract refresh(); 126 | public abstract show(path:any, config?:any); 127 | public abstract render( 128 | root?: string | HTMLElement | ISubView, 129 | url?: IRoute, parent?: IJetView): Promise; 130 | 131 | protected _detachEvents(){ 132 | const events = this._events; 133 | for (let i = events.length - 1; i >= 0; i--){ 134 | events[i].obj.detachEvent(events[i].id); 135 | } 136 | } 137 | protected _destroySubs(){ 138 | // destroy sub views 139 | for (const key in this._subs){ 140 | const subView = this._subs[key].view; 141 | // it possible that subview was not loaded with any content yet 142 | // so check on null 143 | if (subView){ 144 | subView.destructor(); 145 | } 146 | } 147 | 148 | // reset to prevent memory leaks 149 | this._subs = {}; 150 | } 151 | protected _init_url_data(){ 152 | const url = this._segment.current(); 153 | this._data = {}; 154 | this.webix.extend(this._data, url.params, true); 155 | } 156 | 157 | protected _getDefaultSub(){ 158 | if (this._subs.default){ 159 | return this._subs.default; 160 | } 161 | for (const key in this._subs){ 162 | const sub = this._subs[key]; 163 | if (!sub.branch && sub.view && key !== "_top"){ 164 | const child = (sub.view as JetBase)._getDefaultSub(); 165 | if (child){ 166 | return child; 167 | } 168 | } 169 | } 170 | } 171 | 172 | protected _routed_view() { 173 | const parent = this.getParentView() as JetBase; 174 | if (!parent){ 175 | return true; 176 | } 177 | 178 | const sub = parent._getDefaultSub(); 179 | if (!sub && sub !== this){ 180 | return false; 181 | } 182 | 183 | return parent._routed_view(); 184 | } 185 | 186 | 187 | } 188 | -------------------------------------------------------------------------------- /sources/JetView.ts: -------------------------------------------------------------------------------- 1 | import {JetBase} from "./JetBase"; 2 | import {JetAppBase} from "./JetAppBase"; 3 | 4 | import { 5 | IBaseConfig, IBaseView, IJetApp, IJetURL, 6 | IJetView, IJetViewFactory, ISubView, IUIConfig, IRoute, IJetUrlTarget } from "./interfaces"; 7 | import { Route } from "./Route"; 8 | 9 | 10 | export class JetView extends JetBase{ 11 | private _children:IJetView[]; 12 | 13 | constructor(app : IJetApp, config : any){ 14 | super(app.webix); 15 | 16 | this.app = app; 17 | //this.$config = config; 18 | 19 | this._children = []; 20 | } 21 | 22 | ui( 23 | ui:IBaseConfig|IJetViewFactory, 24 | config?: IUIConfig 25 | ) : IBaseView | IJetView{ 26 | config = config || {}; 27 | const container = config.container || (ui as IBaseConfig).container; 28 | 29 | const jetview = this.app.createView(ui); 30 | this._children.push(jetview); 31 | 32 | jetview.render(container, this._segment, this); 33 | 34 | if (typeof ui !== "object" || (ui instanceof JetBase)){ 35 | // raw webix UI 36 | return jetview; 37 | } else { 38 | return jetview.getRoot(); 39 | } 40 | } 41 | 42 | show(path:any, config?:any):Promise{ 43 | config = config || {}; 44 | 45 | // convert parameters object to url 46 | if (typeof path === "object"){ 47 | for (const key in path){ 48 | this.setParam(key, path[key]); 49 | } 50 | path = null; 51 | } else { 52 | 53 | // deligate to app in case of root prefix 54 | if (path.substr(0,1) === "/"){ 55 | return this.app.show(path, config); 56 | } 57 | 58 | // local path, do nothing 59 | if (path.indexOf("./") === 0){ 60 | path = path.substr(2); 61 | } 62 | 63 | // parent path, call parent view 64 | if (path.indexOf("../") === 0){ 65 | const parent = this.getParentView(); 66 | if (parent){ 67 | return parent.show(path.substr(3), config); 68 | } else { 69 | return this.app.show("/"+path.substr(3)); 70 | } 71 | } 72 | 73 | const sub = this.getSubViewInfo(config.target); 74 | if (sub){ 75 | if (sub.parent !== this){ 76 | return sub.parent.show(path, config); 77 | } else if (config.target && config.target !== "default"){ 78 | return this._renderFrameLock(config.target, sub.subview, { 79 | url: path, 80 | params: config.params, 81 | }); 82 | } 83 | } else { 84 | if (path){ 85 | return this.app.show("/" + path, config); 86 | } 87 | } 88 | 89 | } 90 | 91 | return this._show( 92 | this._segment, 93 | { url: path, params: config.params }, 94 | this 95 | ); 96 | } 97 | 98 | _show(segment:IRoute, path:IJetUrlTarget, view:IJetView){ 99 | return segment.show(path, view, true).then(() => { 100 | this._init_url_data(); 101 | return this._urlChange(); 102 | }).then(() => { 103 | if (segment.route.linkRouter){ 104 | this.app.getRouter().set(segment.route.path, { silent: true }); 105 | this.app.callEvent("app:route", [segment.route.path]); 106 | } 107 | }); 108 | } 109 | 110 | init(_$view:IBaseView, _$: IJetURL){ 111 | // stub 112 | } 113 | ready(_$view:IBaseView, _$url: IJetURL){ 114 | // stub 115 | } 116 | config() : any { 117 | this.app.webix.message("View:Config is not implemented"); 118 | } 119 | urlChange(_$view: IBaseView, _$url : IJetURL){ 120 | // stub 121 | } 122 | 123 | destroy(){ 124 | // stub 125 | } 126 | 127 | destructor(){ 128 | this.destroy(); 129 | this._destroyKids(); 130 | 131 | // destroy actual UI 132 | if (this._root) { 133 | this._root.destructor(); 134 | super.destructor(); 135 | } 136 | } 137 | 138 | use(plugin, config){ 139 | plugin(this.app, this, config); 140 | } 141 | 142 | refresh(){ 143 | const url = this.getUrl(); 144 | this.destroy(); 145 | this._destroyKids(); 146 | this._destroySubs(); 147 | this._detachEvents(); 148 | 149 | if ((this._container as any).tagName){ 150 | this._root.destructor(); 151 | } 152 | 153 | this._segment.refresh(); 154 | return this._render(this._segment); 155 | } 156 | 157 | render( 158 | root: string | HTMLElement | ISubView, 159 | url: IRoute, parent?: IJetView): Promise { 160 | 161 | if (typeof url === "string"){ 162 | url = new Route(url, 0); 163 | } 164 | 165 | this._segment = url; 166 | 167 | this._parent = parent; 168 | this._init_url_data(); 169 | 170 | root = root || document.body; 171 | const _container = (typeof root === "string") ? this.webix.toNode(root) : root; 172 | 173 | if (this._container !== _container) { 174 | this._container = _container; 175 | return this._render(url); 176 | } else { 177 | return this._urlChange().then(() => this.getRoot()); 178 | } 179 | } 180 | 181 | protected _render(url: IRoute):Promise{ 182 | const config = this.config(); 183 | if (config.then){ 184 | return config.then(cfg => this._render_final(cfg, url)); 185 | } else { 186 | return this._render_final(config, url); 187 | } 188 | } 189 | 190 | protected _render_final(config:any, url:IRoute):Promise{ 191 | // get previous view in the same slot 192 | let slot:ISubView = null; 193 | let container:string|HTMLElement|IBaseView = null; 194 | let show = false; 195 | if (!(this._container as HTMLElement).tagName){ 196 | slot = (this._container as ISubView); 197 | if (slot.popup){ 198 | container = document.body; 199 | show = true; 200 | } else { 201 | container = this.webix.$$(slot.id); 202 | } 203 | } else { 204 | container = this._container as HTMLElement; 205 | } 206 | 207 | // view already destroyed 208 | if (!this.app || !container){ 209 | return Promise.reject(null); 210 | } 211 | 212 | let response:Promise; 213 | const current = this._segment.current(); 214 | 215 | // using wrapper object, so ui can be changed from app:render event 216 | const result:any = { ui: {} }; 217 | this.app.copyConfig(config, result.ui, this._subs); 218 | this.app.callEvent("app:render", [this, url, result]); 219 | result.ui.$scope = this; 220 | 221 | /* destroy old HTML attached views before creating new one */ 222 | if (!slot && current.isNew && current.view){ 223 | current.view.destructor(); 224 | } 225 | 226 | try { 227 | // special handling for adding inside of multiview - preserve old id 228 | if (slot && !show){ 229 | const oldui = container as IBaseView; 230 | const parent = oldui.getParentView(); 231 | if (parent && parent.name === "multiview" && !result.ui.id){ 232 | result.ui.id = oldui.config.id; 233 | } 234 | } 235 | 236 | this._root = this.app.webix.ui(result.ui, container); 237 | const asWin = this._root as any; 238 | // check for url added to ignore this.ui calls 239 | if (show && asWin.setPosition && !asWin.isVisible()){ 240 | asWin.show(); 241 | } 242 | 243 | // check, if we are replacing some older view 244 | if (slot){ 245 | if (slot.view && slot.view !== this && slot.view !== this.app){ 246 | slot.view.destructor(); 247 | } 248 | 249 | slot.id = this._root.config.id as string; 250 | if (this.getParentView() || !this.app.app) 251 | slot.view = this; 252 | else { 253 | // when we have subapp, set whole app as a view 254 | // so on destruction, the whole app will be destroyed 255 | slot.view = this.app as any; 256 | } 257 | } 258 | 259 | if (current.isNew){ 260 | current.view = this; 261 | current.isNew = false; 262 | } 263 | 264 | response = Promise.resolve(this._init(this._root, url)).then(() => { 265 | return this._urlChange().then(() => { 266 | this._initUrl = null; 267 | return this.ready(this._root, url.suburl()); 268 | }); 269 | }); 270 | } catch(e){ 271 | response = Promise.reject(e); 272 | } 273 | 274 | return response.catch(err => this._initError(this, err)); 275 | } 276 | 277 | protected _init(view:IBaseView, url: IRoute){ 278 | return this.init(view, url.suburl()); 279 | } 280 | 281 | protected _urlChange():Promise{ 282 | this.app.callEvent("app:urlchange", [this, this._segment]); 283 | 284 | const waits = []; 285 | for (const key in this._subs){ 286 | const frame = this._subs[key]; 287 | const wait = this._renderFrameLock(key, frame, null); 288 | if (wait){ 289 | waits.push(wait); 290 | } 291 | } 292 | 293 | return Promise.all(waits).then(() => { 294 | return this.urlChange(this._root, this._segment.suburl()); 295 | }); 296 | } 297 | 298 | protected _renderFrameLock(key:string, frame:ISubView, path: IJetUrlTarget):Promise{ 299 | // if subview is not occupied by some rendering yet 300 | if (!frame.lock) { 301 | // retreive and store rendering end promise 302 | const lock = this._renderFrame(key, frame, path); 303 | if (lock){ 304 | // clear lock after frame rendering 305 | // as promise.finally is not supported by Webix lesser than 6.2 306 | // using a more verbose notation 307 | frame.lock = lock.then(() => frame.lock = null, () => frame.lock = null) 308 | } 309 | } 310 | 311 | // return rendering end promise 312 | return frame.lock; 313 | } 314 | 315 | protected _renderFrame(key:string, frame:ISubView, path: IJetUrlTarget):Promise{ 316 | //default route 317 | if (key === "default"){ 318 | if (this._segment.next()){ 319 | // we have a next segment in url, render it 320 | let params = path ? path.params : null; 321 | if (frame.params) { 322 | params = this.webix.extend(params || {}, frame.params); 323 | } 324 | return this._createSubView(frame, this._segment.shift(params)); 325 | } else if (frame.view && frame.popup) { 326 | // there is no next segment, delete the existing sub-view 327 | frame.view.destructor(); 328 | frame.view = null; 329 | } 330 | } 331 | 332 | //if new path provided, set it to the frame 333 | if (path !== null){ 334 | frame.url = path.url; 335 | if (frame.params) { 336 | path.params = this.webix.extend(path.params || {}, frame.params); 337 | } 338 | } 339 | 340 | // in case of routed sub-view 341 | if (frame.route){ 342 | // we have a new path for sub-view 343 | if (path !== null){ 344 | return frame.route.show(path, frame.view).then(() => { 345 | return this._createSubView(frame, frame.route); 346 | }); 347 | } 348 | 349 | // do not trigger onChange for isolated sub-views 350 | if (frame.branch){ 351 | return; 352 | } 353 | } 354 | 355 | let view = frame.view; 356 | // if view doesn't exists yet, init it 357 | if (!view && frame.url){ 358 | if (typeof frame.url === "string"){ 359 | // string, so we have isolated subview url 360 | frame.route = new Route(frame.url, 0); 361 | if (path) frame.route.setParams(frame.route.route.url, path.params, 0); 362 | if (frame.params) 363 | frame.route.setParams(frame.route.route.url, frame.params, 0); 364 | return this._createSubView(frame, frame.route); 365 | } else { 366 | // object, so we have an embeded subview 367 | if (typeof frame.url === "function" && !(view instanceof frame.url)){ 368 | const rview = (this.app as any)._override(frame.url); 369 | if (rview.prototype instanceof JetAppBase) { 370 | view = new rview({ app: this.app }); 371 | } else { 372 | view = new rview(this.app, ""); 373 | } 374 | } 375 | if (!view){ 376 | view = frame.url as any; 377 | } 378 | } 379 | } 380 | 381 | // trigger onChange for already existed view 382 | if (view){ 383 | return view.render(frame, (frame.route || this._segment), this); 384 | } 385 | } 386 | 387 | private _initError(view: any, err: any){ 388 | /* 389 | if view is destroyed, ignore any view related errors 390 | */ 391 | if (this.app){ 392 | this.app.error("app:error:initview", [err, view]); 393 | } 394 | return true; 395 | } 396 | 397 | private _createSubView( 398 | sub:ISubView, 399 | suburl:IRoute):Promise{ 400 | return this.app.createFromURL(suburl.current()).then(view => { 401 | return view.render(sub, suburl, this); 402 | }); 403 | } 404 | 405 | private _destroyKids(){ 406 | // destroy child views 407 | const uis = this._children; 408 | for (let i = uis.length - 1; i >= 0; i--){ 409 | if (uis[i] && uis[i].destructor){ 410 | uis[i].destructor(); 411 | } 412 | } 413 | 414 | // reset vars for better GC processing 415 | this._children = []; 416 | } 417 | } -------------------------------------------------------------------------------- /sources/JetViewRaw.ts: -------------------------------------------------------------------------------- 1 | import {IJetApp} from "./interfaces"; 2 | import {JetView} from "./JetView"; 3 | 4 | 5 | // wrapper for raw objects and Jet 1.x structs 6 | export class JetViewRaw extends JetView{ 7 | private _ui:any; 8 | 9 | constructor(app:IJetApp, config:any){ 10 | super(app, config); 11 | this._ui = config.ui; 12 | } 13 | 14 | config(){ 15 | return this._ui; 16 | } 17 | } -------------------------------------------------------------------------------- /sources/Route.ts: -------------------------------------------------------------------------------- 1 | import { IJetURL, IJetView, IPath, IRoute, IJetURLChunk, IJetUrlTarget } from "./interfaces"; 2 | import {NavigationBlocked} from "./errors"; 3 | 4 | import {parse, url2str} from "./helpers"; 5 | 6 | export class Route implements IRoute{ 7 | public route: IPath; 8 | private index: number; 9 | private _next: number = 1; 10 | 11 | constructor(route: string|IPath, index: number){ 12 | if (typeof route === "string"){ 13 | this.route = { 14 | url:parse(route), 15 | path: route 16 | }; 17 | } else { 18 | this.route = route; 19 | } 20 | 21 | this.index = index; 22 | } 23 | current():IJetURLChunk{ 24 | return this.route.url[this.index]; 25 | } 26 | next():IJetURLChunk{ 27 | return this.route.url[this.index + this._next]; 28 | } 29 | 30 | suburl():IJetURL{ 31 | return this.route.url.slice(this.index); 32 | } 33 | shift(params) { 34 | const route = new Route(this.route, this.index + this._next); 35 | route.setParams(route.route.url, params, route.index); 36 | return route; 37 | } 38 | setParams(url, params, index) { 39 | if (params) { 40 | const old = url[index].params; 41 | for (var key in params) old[key] = params[key]; 42 | } 43 | } 44 | refresh(){ 45 | const url = this.route.url; 46 | for (let i=this.index+1; i{ 91 | const url = this._join(path.url, kids); 92 | this.setParams(url, path.params, this.index + (kids ? this._next : 0)); 93 | 94 | 95 | return new Promise((res, rej) => { 96 | const redirect = url2str(url); 97 | const obj = { 98 | url, 99 | redirect, 100 | confirm: Promise.resolve() 101 | }; 102 | 103 | const app = view ? view.app : null; 104 | // when creating a new route, it possible that it will not have any content 105 | // guard is not necessary in such case 106 | if (app){ 107 | const result = app.callEvent("app:guard", [obj.redirect, view, obj]); 108 | if (!result){ 109 | rej(new NavigationBlocked()); 110 | return; 111 | } 112 | } 113 | 114 | let err; 115 | obj.confirm.catch(err => rej(err)).then(() => { 116 | if (obj.redirect === null){ 117 | rej(new NavigationBlocked()); 118 | return; 119 | } 120 | 121 | if (obj.redirect !== redirect){ 122 | app.show(obj.redirect); 123 | rej(new NavigationBlocked()); 124 | return; 125 | } 126 | 127 | this.route.path = redirect; 128 | this.route.url = url; 129 | res(); 130 | }); 131 | }); 132 | } 133 | size(n:number){ 134 | this._next = n; 135 | } 136 | split():IRoute{ 137 | const route = { 138 | url: this.route.url.slice(this.index+1), 139 | path:"" 140 | }; 141 | 142 | if (route.url.length){ 143 | route.path = url2str(route.url); 144 | } 145 | 146 | return new Route(route, 0); 147 | } 148 | update(name:string, value: string, index?:number){ 149 | const chunk = this.route.url[this.index + (index || 0)]; 150 | if (!chunk){ 151 | this.route.url.push({ page:"", params:{} }); 152 | return this.update(name, value, index); 153 | } 154 | 155 | if (name === ""){ 156 | chunk.page = value; 157 | } else { 158 | chunk.params[name] = value; 159 | } 160 | 161 | this.route.path = url2str(this.route.url); 162 | } 163 | } -------------------------------------------------------------------------------- /sources/errors.ts: -------------------------------------------------------------------------------- 1 | export class NavigationBlocked {} -------------------------------------------------------------------------------- /sources/es5.ts: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | Copyright (c) 2019 XB Software 4 | */ 5 | 6 | import * as jet from "./index"; 7 | 8 | const w = window as any; 9 | if (!w.webix){ 10 | w.webix = {}; 11 | } 12 | 13 | w.webix.jet = {}; 14 | w.webix.extend(w.webix.jet, jet, true); 15 | 16 | -------------------------------------------------------------------------------- /sources/helpers.ts: -------------------------------------------------------------------------------- 1 | import {IJetURLChunk} from "./interfaces"; 2 | 3 | 4 | export function parse(url:string):IJetURLChunk[]{ 5 | // remove starting / 6 | if (url[0] === "/"){ 7 | url = url.substr(1); 8 | } 9 | 10 | // split url by "/" 11 | const parts = url.split("/"); 12 | const chunks:IJetURLChunk[] = []; 13 | 14 | // for each page in url 15 | for (let i = 0; i < parts.length; i++){ 16 | const test = parts[i]; 17 | const result = {}; 18 | 19 | // detect params 20 | // support old some:a=b:c=d 21 | // and new notation some?a=b&c=d 22 | let pos = test.indexOf(":"); 23 | if (pos === -1){ 24 | pos = test.indexOf("?"); 25 | } 26 | 27 | if (pos !== -1){ 28 | const params = test.substr(pos+1).split(/[\:\?\&]/g); 29 | // create hash of named params 30 | for (const param of params) { 31 | const dchunk = param.split("="); 32 | result[dchunk[0]] = decodeURIComponent(dchunk[1]); 33 | } 34 | } 35 | 36 | // store parsed values 37 | chunks[i] = { 38 | page: (pos > -1 ? test.substr(0, pos) : test), 39 | params:result, 40 | isNew:true 41 | }; 42 | } 43 | 44 | // return array of page objects 45 | return chunks; 46 | } 47 | 48 | export function url2str(stack:IJetURLChunk[]):string{ 49 | const url = []; 50 | 51 | for (const chunk of stack){ 52 | url.push("/"+chunk.page); 53 | const params = obj2str(chunk.params); 54 | if (params){ 55 | url.push("?"+params); 56 | } 57 | } 58 | 59 | return url.join(""); 60 | } 61 | 62 | function obj2str(obj){ 63 | const str = []; 64 | for (const key in obj){ 65 | if (typeof obj[key] === "object") continue; 66 | if (str.length){ 67 | str.push("&"); 68 | } 69 | str.push(key+"="+encodeURIComponent(obj[key])); 70 | } 71 | 72 | return str.join(""); 73 | } -------------------------------------------------------------------------------- /sources/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | Copyright (c) 2019 XB Software 4 | */ 5 | 6 | import type { IJetApp, IJetView } from "./interfaces"; 7 | export { IJetApp, IJetView }; 8 | 9 | import {NavigationBlocked} from "./errors"; 10 | 11 | export { JetApp } from "./JetApp"; 12 | export { JetView } from "./JetView"; 13 | 14 | export { HashRouter } from "./routers/HashRouter"; 15 | export { StoreRouter } from "./routers/StoreRouter"; 16 | export { UrlRouter } from "./routers/UrlRouter"; 17 | export { EmptyRouter } from "./routers/EmptyRouter"; 18 | export { SubRouter } from "./routers/SubRouter"; 19 | 20 | import {UnloadGuard} from "./plugins/Guard"; 21 | import {Locale} from "./plugins/Locale"; 22 | import {Menu} from "./plugins/Menu"; 23 | import {Status} from "./plugins/Status"; 24 | import {Theme} from "./plugins/Theme"; 25 | import {UrlParam} from "./plugins/UrlParam"; 26 | import {User} from "./plugins/User"; 27 | 28 | import patch from "./patch"; 29 | let webix = (window as any).webix; 30 | if (webix){ 31 | patch(webix); 32 | } 33 | export { patch }; 34 | 35 | export const plugins = { 36 | UnloadGuard, Locale, Menu, Theme, User, Status, UrlParam 37 | }; 38 | 39 | export const errors = { NavigationBlocked }; 40 | 41 | const w = window as any; 42 | if (!w.Promise){ 43 | w.Promise = w.webix.promise; 44 | } -------------------------------------------------------------------------------- /sources/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IBaseView { 2 | config: IBaseConfig; 3 | name:string; 4 | getParentView():IBaseView; 5 | destructor():void; 6 | } 7 | 8 | export interface IWebixHTMLHelper{ 9 | addCss(node:HTMLElement, css:string):void; 10 | removeCss(node:HTMLElement, css:string):void; 11 | } 12 | export interface IBaseConfig { 13 | id?:string | number; 14 | container?: string|HTMLElement; 15 | } 16 | 17 | export interface IWebixFacade{ 18 | storage:any; 19 | i18n: any; 20 | html: IWebixHTMLHelper; 21 | EventSystem: any; 22 | DataCollection: any; 23 | 24 | $$(id: string):IBaseView; 25 | isArray(input:any):boolean; 26 | event(node:HTMLElement, name:string, handler:any); 27 | uid():number; 28 | ui( 29 | config: any, 30 | cont?: string|HTMLElement|IBaseView) : IBaseView; 31 | bind(handler:any, master:any); 32 | message(text:any); 33 | dp(id:string):any; 34 | toNode(id:string):HTMLElement; 35 | extend(a: any, b: any, force?:boolean):any; 36 | attachEvent(name: string, handler:any):string; 37 | } 38 | 39 | export interface IUIConfig{ 40 | container? : string|HTMLElement; 41 | } 42 | 43 | export interface IJetApp extends IJetView{ 44 | webix: IWebixFacade; 45 | config: IJetConfig; 46 | app: IJetApp; 47 | getUrl():IJetURL; 48 | require(type:string, url:string):any; 49 | getService(name:string):any; 50 | setService(name:string, obj: any):void; 51 | callEvent(name:string, parameters:any[]):boolean; 52 | attachEvent(name:string, handler:any):void; 53 | createFromURL(chunk:IJetURLChunk) : Promise; 54 | show(path:any, config?:any):Promise; 55 | createView(obj:any, name?:string, params?:IHash):IJetView; 56 | refresh():Promise; 57 | error(name:string, data:any[]); 58 | copyConfig(source:any, target:any, config?:IViewConfig); 59 | getRouter(): IJetRouter; 60 | destructor(): void; 61 | } 62 | 63 | export interface IJetURLChunk{ 64 | page:string; 65 | params:{ [name:string]:string }; 66 | 67 | view?:IJetView; 68 | isNew?:boolean; 69 | } 70 | 71 | export interface IJetUrlTarget{ 72 | url:string; 73 | params?:IHash; 74 | } 75 | 76 | export type IJetURL = IJetURLChunk[]; 77 | 78 | export interface IJetView{ 79 | app: IJetApp; 80 | $$(name:string):T; 81 | contains(view: IJetView):boolean; 82 | getSubView(name?:string):IJetView; 83 | getSubViewInfo(name?:string):ISubViewInfo; 84 | getRoot() : IBaseView; 85 | setParam(id:string, value:any, url?:boolean); 86 | getParam(id:string, parent:boolean):any; 87 | getUrl():IJetURL; 88 | getUrlString():string; 89 | getParentView() : IJetView; 90 | refresh():Promise; 91 | render( 92 | area: ISubView|string|HTMLElement, 93 | url? : IRoute, 94 | parent?: IJetView) : Promise; 95 | destructor(); 96 | on(obj:any, name:string, code:any); 97 | show(path:any, config?:any):Promise; 98 | } 99 | 100 | export interface IHash{ 101 | [name:string]:any; 102 | } 103 | 104 | export interface IJetConfig{ 105 | debug?:boolean; 106 | app?: IJetApp; 107 | name?:string; 108 | version?:string; 109 | start?:string; 110 | webix?:IWebixFacade; 111 | container:HTMLElement | string; 112 | animation:boolean; 113 | router: IJetRouterFactory; 114 | views: Function | IHash; 115 | params: IHash; 116 | override: IHash; 117 | } 118 | 119 | export interface IJetRouterOptions{ 120 | [name:string]:any; 121 | } 122 | 123 | export interface IJetRouterFactory{ 124 | new (cb:IJetRouterCallback, config:any, app:IJetApp); 125 | } 126 | export interface IJetViewFactory{ 127 | new (app:IJetApp, name:string); 128 | } 129 | 130 | export interface IJetRouter{ 131 | get():string; 132 | set(name:string, options?:IJetRouterOptions):void; 133 | } 134 | export type IJetRouterCallback = (url?:string) => any; 135 | 136 | export interface IViewConfig{ 137 | [name:string]:ISubView; 138 | } 139 | 140 | export interface ISubView{ 141 | view?: IJetView; 142 | url: string | IJetViewFactory; 143 | name?: string; 144 | popup?: boolean; 145 | id: string; 146 | branch?: boolean; 147 | route?: IRoute; 148 | lock?: Promise; 149 | params?: IHash; 150 | } 151 | 152 | export interface ISubViewInfo{ 153 | subview: ISubView; 154 | parent: IJetView; 155 | } 156 | 157 | export interface IPath{ 158 | path: string; 159 | url: IJetURL; 160 | linkRouter?: boolean 161 | } 162 | 163 | export interface IRoute{ 164 | route: IPath; 165 | 166 | current():IJetURLChunk; 167 | next():IJetURLChunk; 168 | 169 | suburl():IJetURL; 170 | shift(IHash):IRoute; 171 | setParams(url, params:IHash, index:number); 172 | show(url:IJetUrlTarget, view:IJetView, kids?: boolean):Promise; 173 | refresh():void; 174 | size(n:number); 175 | update(name: string, value: string, index?:number); 176 | split():IRoute; 177 | append(path:string):string; 178 | toString():string; 179 | } 180 | 181 | export interface IDestructable{ 182 | destructor():void; 183 | } -------------------------------------------------------------------------------- /sources/patch.ts: -------------------------------------------------------------------------------- 1 | let isPatched = false; 2 | export default function patch(w: any){ 3 | if (isPatched || !w){ return; } 4 | isPatched = true; 5 | 6 | // custom promise for IE8 7 | const win = window as any; 8 | if (!win.Promise){ 9 | win.Promise = w.promise; 10 | } 11 | 12 | const version = w.version.split(".") as any[]; 13 | 14 | // will be fixed in webix 5.3 15 | if (version[0]*10+version[1]*1 < 53) { 16 | w.ui.freeze = function(handler):any{ 17 | // disabled because webix jet 5.0 can't handle resize of scrollview correctly 18 | // w.ui.$freeze = true; 19 | const res = handler(); 20 | if (res && res.then){ 21 | res.then(function(some){ 22 | w.ui.$freeze = false; 23 | w.ui.resize(); 24 | return some; 25 | }); 26 | } else { 27 | w.ui.$freeze = false; 28 | w.ui.resize(); 29 | } 30 | return res; 31 | }; 32 | } 33 | 34 | // adding views as classes 35 | const baseAdd = w.ui.baselayout.prototype.addView as any; 36 | const baseRemove = w.ui.baselayout.prototype.removeView as any; 37 | 38 | const config = { 39 | addView(view, index){ 40 | // trigger logic only for widgets inside of jet-view 41 | // ignore case when addView used with already initialized widget 42 | if (this.$scope && this.$scope.webixJet && !view.queryView){ 43 | const jview = this.$scope; 44 | const subs = {}; 45 | 46 | view = jview.app.copyConfig(view, {}, subs); 47 | baseAdd.apply(this, [view, index]); 48 | 49 | for (const key in subs){ 50 | jview._renderFrame(key, subs[key], null).then(() => { 51 | jview._subs[key] = subs[key]; 52 | }); 53 | } 54 | 55 | return view.id; 56 | } else { 57 | return baseAdd.apply(this, arguments); 58 | } 59 | }, 60 | removeView(){ 61 | baseRemove.apply(this, arguments); 62 | if (this.$scope && this.$scope.webixJet){ 63 | const subs = this.$scope._subs; 64 | // check all sub-views, destroy and clean the removed one 65 | for(const key in subs){ 66 | const test = subs[key]; 67 | if (!w.$$(test.id)){ 68 | test.view.destructor(); 69 | delete subs[key]; 70 | } 71 | } 72 | } 73 | } 74 | }; 75 | 76 | w.extend(w.ui.layout.prototype, config, true); 77 | w.extend(w.ui.baselayout.prototype, config, true); 78 | 79 | // wrapper for using Jet Apps as views 80 | 81 | w.protoUI({ 82 | name:"jetapp", 83 | $init(cfg){ 84 | this.$app = new this.app(cfg); 85 | 86 | const id = w.uid().toString(); 87 | cfg.body = { id }; 88 | 89 | this.$ready.push(function(){ 90 | this.callEvent("onInit", [this.$app]); 91 | this.$app.render({ id }); 92 | }); 93 | } 94 | }, (w.ui as any).proxy, w.EventSystem); 95 | } -------------------------------------------------------------------------------- /sources/plugins/Guard.ts: -------------------------------------------------------------------------------- 1 | import {NavigationBlocked} from "../errors"; 2 | import {IJetApp, IJetURL, IJetView} from "../interfaces"; 3 | 4 | export function UnloadGuard(app: IJetApp, view: IJetView, config: any){ 5 | view.on(app, `app:guard`, function(_$url:IJetURL, point:IJetView, promise:any){ 6 | if (point === view || point.contains(view)){ 7 | const res = config(); 8 | if (res === false){ 9 | promise.confirm = Promise.reject(new NavigationBlocked()); 10 | } else { 11 | promise.confirm = promise.confirm.then(() => res); 12 | } 13 | } 14 | }); 15 | } -------------------------------------------------------------------------------- /sources/plugins/Locale.ts: -------------------------------------------------------------------------------- 1 | import Polyglot from "webix-polyglot"; 2 | import {IJetApp, IJetView} from "../interfaces"; 3 | 4 | export function Locale(app: IJetApp, _view: IJetView, config: any){ 5 | config = config || {}; 6 | const storage = config.storage; 7 | let lang = storage ? (storage.get("lang") || "en") : (config.lang || "en"); 8 | 9 | function setLangData(name, data: any, silent?: boolean) : Promise{ 10 | if (data.__esModule) { 11 | data = data.default; 12 | } 13 | 14 | const pconfig = { phrases:data }; 15 | if (config.polyglot){ 16 | app.webix.extend(pconfig, config.polyglot); 17 | } 18 | 19 | const poly = service.polyglot = new Polyglot(pconfig); 20 | poly.locale(name); 21 | 22 | service._ = app.webix.bind(poly.t, poly); 23 | lang = name; 24 | 25 | if (storage){ 26 | storage.put("lang", lang); 27 | } 28 | 29 | if (config.webix){ 30 | const locName = config.webix[name]; 31 | if (locName){ 32 | app.webix.i18n.setLocale(locName); 33 | } 34 | } 35 | 36 | if (!silent){ 37 | return app.refresh(); 38 | } 39 | 40 | return Promise.resolve(); 41 | } 42 | function getLang(){ return lang; } 43 | function setLang(name:string, silent? : boolean){ 44 | // ignore setLang if loading by path is disabled 45 | if (config.path === false){ 46 | return; 47 | } 48 | 49 | if (typeof config.path === "function"){ 50 | config.path(name).then(data => setLangData(name, data, silent)); 51 | return; 52 | } 53 | 54 | const path = (config.path ? config.path + "/" : "") + name; 55 | const data = app.require("jet-locales",path); 56 | 57 | setLangData(name, data, silent); 58 | } 59 | 60 | const service = { 61 | getLang, setLang, setLangData, _:null, polyglot:null 62 | }; 63 | 64 | app.setService("locale", service); 65 | setLang(lang, true); 66 | } -------------------------------------------------------------------------------- /sources/plugins/Menu.ts: -------------------------------------------------------------------------------- 1 | import {IJetApp, IJetView} from "../interfaces"; 2 | 3 | function show(view, config, value){ 4 | if (config.urls){ 5 | value = config.urls[value] || value; 6 | } else if (config.param){ 7 | value = { [config.param]:value }; 8 | } 9 | 10 | view.show(value); 11 | } 12 | export function Menu(app: IJetApp, view: IJetView, config: any){ 13 | const frame = view.getSubViewInfo().parent; 14 | const ui = view.$$(config.id || config) as any; 15 | let silent = false; 16 | 17 | ui.attachEvent("onchange", function(){ 18 | if (!silent){ 19 | show(frame, config, this.getValue()); 20 | } 21 | }); 22 | ui.attachEvent("onafterselect", function(){ 23 | if (!silent){ 24 | let id = null; 25 | if (ui.setValue){ 26 | id = this.getValue(); 27 | } else if (ui.getSelectedId){ 28 | id = ui.getSelectedId(); 29 | } 30 | show(frame, config, id); 31 | } 32 | }); 33 | 34 | view.on(app, `app:route`, function(){ 35 | let name = ""; 36 | if (config.param){ 37 | name = view.getParam(config.param, true); 38 | } else { 39 | const segment = frame.getUrl()[1]; 40 | if (segment){ 41 | name = segment.page; 42 | } 43 | } 44 | 45 | if (name){ 46 | silent = true; 47 | if (ui.setValue && ui.getValue() !== name){ 48 | ui.setValue(name); 49 | } else if (ui.select && ui.exists(name) && ui.getSelectedId() !== name){ 50 | ui.select(name); 51 | } 52 | silent = false; 53 | } 54 | }); 55 | } -------------------------------------------------------------------------------- /sources/plugins/Status.ts: -------------------------------------------------------------------------------- 1 | import {IJetApp, IJetView, IWebixFacade} from "../interfaces"; 2 | 3 | const baseicons = { 4 | good: "check", 5 | error: "warning", 6 | saving: "refresh fa-spin" 7 | }; 8 | 9 | const basetext = { 10 | good: "Ok", 11 | error: "Error", 12 | saving: "Connecting..." 13 | }; 14 | 15 | export function Status(app: IJetApp, view: IJetView, config: any){ 16 | 17 | let status = "good"; 18 | let count = 0; 19 | let iserror = false; 20 | let expireDelay = config.expire; 21 | if (!expireDelay && expireDelay !== false){ 22 | expireDelay = 2000; 23 | } 24 | const texts = config.texts || basetext; 25 | const icons = config.icons || baseicons; 26 | 27 | if (typeof config === "string"){ 28 | config = { target:config }; 29 | } 30 | 31 | function refresh(content? : string) { 32 | const area = view.$$(config.target); 33 | if (area) { 34 | if (!content){ 35 | content = "
" + texts[status] + "
"; 39 | } 40 | (area as any).setHTML(content); 41 | } 42 | } 43 | function success(){ 44 | count--; 45 | setStatus("good"); 46 | } 47 | function fail(err){ 48 | count--; 49 | setStatus("error", err); 50 | } 51 | function start(promise){ 52 | count++; 53 | setStatus("saving"); 54 | if (promise && promise.then){ 55 | promise.then(success, fail); 56 | } 57 | } 58 | function getStatus(){ 59 | return status; 60 | } 61 | function hideStatus(){ 62 | if (count === 0){ 63 | refresh(" "); 64 | } 65 | } 66 | function setStatus(mode, err?){ 67 | if (count < 0){ 68 | count = 0; 69 | } 70 | 71 | if (mode === "saving"){ 72 | status = "saving"; 73 | refresh(); 74 | } else { 75 | iserror = (mode === "error"); 76 | if (count === 0){ 77 | status = iserror ? "error" : "good"; 78 | if (iserror){ 79 | app.error("app:error:server", [err.responseText || err]); 80 | } else { 81 | if (expireDelay){ 82 | setTimeout(hideStatus, expireDelay); 83 | } 84 | } 85 | 86 | refresh(); 87 | } 88 | } 89 | } 90 | function track(data){ 91 | const dp = app.webix.dp(data); 92 | if (dp){ 93 | view.on(dp, "onAfterDataSend", start); 94 | view.on(dp, "onAfterSaveError", (_id, _obj, response) => fail(response)); 95 | view.on(dp, "onAfterSave", success); 96 | } 97 | } 98 | 99 | app.setService("status", { 100 | getStatus, 101 | setStatus, 102 | track 103 | }); 104 | 105 | if (config.remote){ 106 | view.on(app.webix, "onRemoteCall", start); 107 | } 108 | 109 | if (config.ajax){ 110 | view.on(app.webix, "onBeforeAjax", 111 | (_mode, _url, _data, _request, _headers, _files, promise) => { 112 | start(promise); 113 | }); 114 | } 115 | 116 | if (config.data){ 117 | track(config.data); 118 | } 119 | } -------------------------------------------------------------------------------- /sources/plugins/Theme.ts: -------------------------------------------------------------------------------- 1 | import {IJetApp, IJetView, IWebixFacade} from "../interfaces"; 2 | 3 | export function Theme(app: IJetApp, _view: IJetView, config: any){ 4 | config = config || {}; 5 | const storage = config.storage; 6 | let theme = storage ? 7 | (storage.get("theme")||"material-default") 8 | : 9 | (config.theme || "material-default"); 10 | 11 | const service = { 12 | getTheme(){ return theme; }, 13 | setTheme(name:string, silent?:boolean){ 14 | const parts = name.split("-"); 15 | const links = document.getElementsByTagName("link"); 16 | 17 | for (let i=0; i= 0){ 27 | data[name] = value; 28 | this._segment.update("", value, index+1); 29 | if (show){ 30 | return view.show(null); 31 | } 32 | } else { 33 | return os.call(this, name, value, show); 34 | } 35 | }; 36 | 37 | view.getParam = function(key, mode){ 38 | const val = data[key]; 39 | if (typeof val !== "undefined") { return val; } 40 | return og.call(this, key, mode); 41 | }; 42 | 43 | copyParams(data, view.getUrl(), route); 44 | } 45 | -------------------------------------------------------------------------------- /sources/plugins/User.ts: -------------------------------------------------------------------------------- 1 | import {IJetApp, IJetView} from "../interfaces"; 2 | 3 | export function User(app: IJetApp, _view: IJetView, config: any){ 4 | config = config || {}; 5 | 6 | const login = config.login || "/login"; 7 | const logout = config.logout || "/logout"; 8 | const afterLogin = config.afterLogin || app.config.start; 9 | const afterLogout = config.afterLogout || "/login"; 10 | const ping = config.ping || 5*60*1000; 11 | const model = config.model; 12 | let user = config.user; 13 | 14 | const service = { 15 | getUser(){ 16 | return user; 17 | }, 18 | getStatus(server? : boolean){ 19 | if (!server){ 20 | return user !== null; 21 | } 22 | 23 | return model.status().catch(() => null).then(data => { 24 | user = data; 25 | }); 26 | }, 27 | login(name:string, pass:string){ 28 | return model.login(name, pass).then(data => { 29 | user = data; 30 | if (!data){ 31 | throw new Error("Access denied"); 32 | } 33 | 34 | app.callEvent("app:user:login", [ user ]); 35 | app.show(afterLogin); 36 | }); 37 | }, 38 | logout(){ 39 | user = null; 40 | return model.logout().then(res => { 41 | app.callEvent("app:user:logout",[]); 42 | return res; 43 | }); 44 | } 45 | }; 46 | 47 | function canNavigate(url, obj){ 48 | if (url === logout){ 49 | service.logout(); 50 | obj.redirect = afterLogout; 51 | } else if (url !== login && !service.getStatus()){ 52 | obj.redirect = login; 53 | } 54 | } 55 | 56 | app.setService("user", service); 57 | 58 | app.attachEvent(`app:guard`, function(url: string, _$root: any, obj:any){ 59 | if (config.public && config.public(url)){ 60 | return true; 61 | } 62 | 63 | if (typeof user === "undefined"){ 64 | obj.confirm = service.getStatus(true).then(() => canNavigate(url, obj)); 65 | } 66 | 67 | return canNavigate(url, obj); 68 | }); 69 | 70 | if (ping){ 71 | setInterval(() => service.getStatus(true), ping); 72 | } 73 | } -------------------------------------------------------------------------------- /sources/routers/EmptyRouter.ts: -------------------------------------------------------------------------------- 1 | import {IJetRouter, IJetRouterCallback, IJetRouterOptions} from "../interfaces"; 2 | 3 | export class EmptyRouter implements IJetRouter{ 4 | private path: string; 5 | private cb: IJetRouterCallback; 6 | 7 | constructor(cb: IJetRouterCallback, _$config:any){ 8 | this.path = ""; 9 | this.cb = cb; 10 | } 11 | set(path:string, config?:IJetRouterOptions){ 12 | this.path = path; 13 | if (!config || !config.silent){ 14 | setTimeout(() => this.cb(path), 1); 15 | } 16 | } 17 | get(){ 18 | return this.path; 19 | } 20 | } -------------------------------------------------------------------------------- /sources/routers/HashRouter.ts: -------------------------------------------------------------------------------- 1 | import {IJetRouter, IJetRouterCallback, IJetRouterOptions} from "../interfaces"; 2 | 3 | export class HashRouter implements IJetRouter{ 4 | protected config:any; 5 | protected prefix:string; 6 | protected sufix:string; 7 | private cb: IJetRouterCallback; 8 | 9 | constructor(cb: IJetRouterCallback, config:any){ 10 | this.config = config || {}; 11 | this._detectPrefix(); 12 | this.cb = cb; 13 | window.onpopstate = () => this.cb(this.get()); 14 | } 15 | 16 | set(path:string, config?:IJetRouterOptions){ 17 | if (this.config.routes){ 18 | const compare = path.split("?",2); 19 | for (const key in this.config.routes){ 20 | if (this.config.routes[key] === compare[0]){ 21 | path = key+(compare.length > 1 ? "?"+compare[1] : ""); 22 | break; 23 | } 24 | } 25 | } 26 | 27 | if (this.get() !== path){ 28 | window.history.pushState(null, null, this.prefix + this.sufix + path); 29 | } 30 | if (!config || !config.silent){ 31 | setTimeout(() => this.cb(path), 1); 32 | } 33 | } 34 | get(){ 35 | let path = this._getRaw().replace(this.prefix, "").replace(this.sufix, ""); 36 | path = (path !== "/" && path !== "#") ? path : ""; 37 | 38 | if (this.config.routes){ 39 | const compare = path.split("?",2); 40 | const key = this.config.routes[compare[0]]; 41 | if (key){ 42 | path = key+(compare.length > 1 ? "?"+compare[1] : ""); 43 | } 44 | } 45 | return path; 46 | } 47 | protected _detectPrefix(){ 48 | // use "#!" for backward compatibility 49 | const sufix = this.config.routerPrefix; 50 | this.sufix = "#" + ((typeof sufix === "undefined") ? "!" : sufix); 51 | 52 | this.prefix = document.location.href.split("#", 2)[0]; 53 | } 54 | 55 | protected _getRaw(){ 56 | return document.location.href; 57 | } 58 | } -------------------------------------------------------------------------------- /sources/routers/StoreRouter.ts: -------------------------------------------------------------------------------- 1 | import {IJetApp, IJetRouter, IJetRouterCallback, IJetRouterOptions} from "../interfaces"; 2 | 3 | export class StoreRouter implements IJetRouter{ 4 | private name:string; 5 | private storage: any; 6 | private cb: IJetRouterCallback; 7 | 8 | constructor(cb: IJetRouterCallback, config:any, app: IJetApp){ 9 | this.storage = config.storage || app.webix.storage.session; 10 | this.name = (config.storeName || config.id+":route"); 11 | this.cb = cb; 12 | } 13 | set(path:string, config?:IJetRouterOptions){ 14 | this.storage.put(this.name, path); 15 | if (!config || !config.silent){ 16 | setTimeout(() => this.cb(path), 1); 17 | } 18 | } 19 | get(){ 20 | return this.storage.get(this.name); 21 | } 22 | } -------------------------------------------------------------------------------- /sources/routers/SubRouter.ts: -------------------------------------------------------------------------------- 1 | import {IJetApp, IJetRouter, IJetRouterCallback, IJetRouterOptions} from "../interfaces"; 2 | 3 | import {url2str} from "../helpers"; 4 | 5 | export class SubRouter implements IJetRouter{ 6 | private app: IJetApp; 7 | private path: string; 8 | private prefix: string; 9 | 10 | constructor(cb: IJetRouterCallback, config:any, app:IJetApp){ 11 | this.path = ""; 12 | this.app = app; 13 | } 14 | set(path:string, config?:IJetRouterOptions){ 15 | this.path = path; 16 | const a = this.app as any; 17 | a.app.getRouter().set(a._segment.append(this.path), { silent:true }); 18 | } 19 | get(){ 20 | return this.path; 21 | } 22 | } -------------------------------------------------------------------------------- /sources/routers/UrlRouter.ts: -------------------------------------------------------------------------------- 1 | import {IJetRouter, IJetRouterCallback, IJetRouterOptions} from "../interfaces"; 2 | import { HashRouter } from "./HashRouter"; 3 | 4 | export class UrlRouter extends HashRouter implements IJetRouter{ 5 | protected _detectPrefix(){ 6 | this.prefix = ""; 7 | this.sufix = this.config.routerPrefix || ""; 8 | } 9 | protected _getRaw(){ 10 | return document.location.pathname + (document.location.search||""); 11 | } 12 | } -------------------------------------------------------------------------------- /sources/typings/webix-polyglot.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for node-polyglot v0.4.1 2 | // Project: https://github.com/airbnb/polyglot.js 3 | // Definitions by: Tim Jackson-Kiely 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | interface IInterpolationOptions { 7 | smart_count?: number; 8 | _?: string; 9 | } 10 | interface IPolyglotOptions { 11 | phrases?: any; 12 | locale?: string; 13 | } 14 | 15 | export default class Polyglot { 16 | constructor(options?: IPolyglotOptions); 17 | extend(phrases: any): void; 18 | t(phrase: string, smartCount?: number|IInterpolationOptions): string; 19 | clear(): void; 20 | replace(phrases: any): void; 21 | locale(): string; 22 | locale(locale: string): void; 23 | } 24 | -------------------------------------------------------------------------------- /tests/add_remove_view.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, it, expect } from "vitest"; 3 | 4 | import { waitTime, loadWebix } from "./stubs/helpers"; 5 | import { TopView, SubView1, SubView2, getEvents, resetEvents } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | it("create/destory views on addView / removeView calls", async () => { 17 | resetEvents(); 18 | 19 | app = new JetApp({ 20 | start:"/Top/Sub1", router: EmptyRouter, 21 | debug: true, 22 | version: 2, 23 | views:{ 24 | "Top":TopView, 25 | "Sub1":SubView1, 26 | "Sub2":SubView2 27 | } 28 | }); 29 | 30 | let view = new SubView2(app, ""); 31 | 32 | await app.render(document.body); 33 | 34 | resetEvents(); 35 | app.getRoot().addView(view); 36 | 37 | await waitTime(20); //addView doesn't return promise, so need to wait 38 | 39 | expect(getEvents(), "after addView").deep.equal([ 40 | "sub2-config", "sub2-init", "sub2-urlChange", "sub2-ready" 41 | ]); 42 | expect(app.getSubView().contains(view)).to.be.true 43 | 44 | resetEvents(); 45 | view.getRoot().getParentView().removeView(view.getRoot()); 46 | 47 | await waitTime(20); 48 | 49 | expect(getEvents(), "after removeView").deep.equal(["sub2-destroy"]); 50 | expect(app.getSubView().contains(view)).to.be.false 51 | }); -------------------------------------------------------------------------------- /tests/app_in_app.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { SubApp } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | 17 | const href = () => document.location.href.split("#")[1].slice(1); 18 | const baseUrl = document.location.href; 19 | beforeEach(function(){ 20 | app = new JetApp({ 21 | start:"/Demo/SubApp", 22 | debug: true, 23 | version: 2, 24 | views:{ 25 | "Demo":{ rows:[{ $subview:true }] }, 26 | "Details":{ template:"App" }, 27 | "SubApp" : SubApp 28 | } 29 | }); 30 | return app.render(document.body, "Demo/SubApp"); 31 | }); 32 | 33 | afterEach(function(){ 34 | window.history.pushState(null, null, baseUrl); 35 | }); 36 | 37 | it("can navigate", async () => { 38 | expect(app.getUrlString()).to.equal("Demo/SubApp/top/body"); 39 | expect(href()).to.equal("/Demo/SubApp/top/body"); 40 | 41 | await $$("sb-tp").$scope.show("body2"); 42 | expect(app.getUrlString()).to.equal("Demo/SubApp/top/body2"); 43 | expect(href()).to.equal("/Demo/SubApp/top/body2"); 44 | 45 | await app.show("/Demo/Details"); 46 | expect(app.getUrlString()).to.equal("Demo/Details"); 47 | expect(href()).to.equal("/Demo/Details"); 48 | 49 | await app.show("/Demo/SubApp/top"); 50 | expect(app.getUrlString()).to.equal("Demo/SubApp/top"); 51 | expect(href()).to.equal("/Demo/SubApp/top"); 52 | 53 | await app.show("/Demo/SubApp/body"); 54 | expect(app.getUrlString()).to.equal("Demo/SubApp/body"); 55 | expect(href()).to.equal("/Demo/SubApp/body"); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/basic_app.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { ChangeRouteFromInit, ChangeRouteFromUrlChange } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | let guard_counter=0; 17 | 18 | beforeEach(async () => { 19 | app = new JetApp({ 20 | start:"/Demo/Details", router: EmptyRouter, 21 | debug: true, 22 | version: 2, 23 | views:{ 24 | "Demo":{ rows:[{ $subview:true }] }, 25 | "Details":{ template:"App" }, 26 | "Other":new Promise(r => r({ template:"Other" })), 27 | "Change1":ChangeRouteFromInit, 28 | "Change2":ChangeRouteFromUrlChange 29 | } 30 | }); 31 | app.attachEvent("app:guard", () => ++guard_counter ); 32 | await app.render(document.body); 33 | }) 34 | 35 | it("can init", () => { 36 | // initialized 37 | expect(app).be.instanceOf(JetApp); 38 | // config set 39 | expect(app.config.version).eq(2); 40 | // guard event triggered 41 | expect(guard_counter).eq(1); 42 | 43 | // default route applied 44 | expect(app.getRouter().get()).eq("/Demo/Details"); 45 | expect(app.getRoot().name).eq("layout") 46 | expect(app.getRoot().getChildViews()[0].name).eq("template") 47 | }); 48 | 49 | it("can set empty url", async () => { 50 | guard_counter = 0; 51 | expect(app.getUrlString()).to.eq("Demo/Details"); 52 | 53 | await app.show("/Demo/Other") 54 | expect(app.getUrlString()).to.eq("Demo/Other"); 55 | expect(guard_counter).eq(1); 56 | 57 | await app.show("") 58 | expect(app.getUrlString()).to.eq("Demo/Details"); 59 | expect(guard_counter).eq(2); 60 | }); 61 | 62 | it("can change ulr from handler", async () => { 63 | guard_counter = 0; 64 | await app.show("Demo/Change1/Details") 65 | expect(app.getUrlString()).to.eq("Demo/Change1/Other"); 66 | expect(guard_counter).eq(2); 67 | 68 | await app.show("Demo/Change2/Details") 69 | expect(app.getUrlString()).to.eq("Demo/Other"); 70 | expect(guard_counter).eq(4); 71 | 72 | const a1 = app.show("Demo/Other") 73 | const a2 = app.show("Demo/Details") 74 | await Promise.all([a1, a2]) 75 | expect(app.getUrlString()).to.eq("Demo/Details"); 76 | expect(guard_counter).eq(6); 77 | }); -------------------------------------------------------------------------------- /tests/destructor.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { UrlParamsView } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | it("must remove all HTML on destruction (top-level navigation)", () => { 17 | app = new JetApp({ 18 | router: EmptyRouter, 19 | start:"/there", 20 | debug: true, 21 | views:{ 22 | "some":{ id:"s1", rows:[{ $subview:true }] }, 23 | "url":{ id:"s2", rows:[{ $subview:true }] }, 24 | "here":{ id:"s31", template:"s1", localId:"h1" }, 25 | "there":{ id:"s32", template:"s2", localId:"t1" } 26 | } 27 | }); 28 | 29 | return app.render(document.body).then(() => { 30 | expect(!!$$("s1") ).to.equal(false); 31 | expect(!!$$("s2") ).to.equal(false); 32 | expect(!!$$("s31") ).to.equal(false); 33 | expect(!!$$("s32") ).to.equal(true); 34 | 35 | return app.show("/some/url/here") 36 | }).then(() => { 37 | 38 | expect(!!$$("s1") ).to.equal(true); 39 | expect(!!$$("s2") ).to.equal(true); 40 | expect(!!$$("s31") ).to.equal(true); 41 | expect(!!$$("s32") ).to.equal(false); 42 | 43 | app.destructor(); 44 | 45 | expect(!!$$("s1") ).to.equal(false); 46 | expect(!!$$("s2") ).to.equal(false); 47 | expect(!!$$("s31") ).to.equal(false); 48 | expect(!!$$("s32") ).to.equal(false); 49 | }); 50 | }); 51 | 52 | it("must remove all HTML on destruction (sub-level navigation)", () => { 53 | app = new JetApp({ 54 | router: EmptyRouter, 55 | start:"/some/url/there", 56 | debug: true, 57 | views:{ 58 | "some":{ id:"s1", rows:[{ $subview:true }] }, 59 | "url":{ id:"s2", rows:[{ $subview:true }] }, 60 | "here":{ id:"s31", template:"s1", localId:"h1" }, 61 | "there":{ id:"s32", template:"s2", localId:"t1" } 62 | } 63 | }); 64 | 65 | return app.render(document.body).then(() => { 66 | expect(!!$$("s1") ).to.equal(true); 67 | expect(!!$$("s2") ).to.equal(true); 68 | expect(!!$$("s31") ).to.equal(false); 69 | expect(!!$$("s32") ).to.equal(true); 70 | 71 | return app.show("/some/url/here") 72 | }).then(() => { 73 | 74 | expect(!!$$("s1") ).to.equal(true); 75 | expect(!!$$("s2") ).to.equal(true); 76 | expect(!!$$("s31") ).to.equal(true); 77 | expect(!!$$("s32") ).to.equal(false); 78 | 79 | app.destructor(); 80 | 81 | expect(!!$$("s1") ).to.equal(false); 82 | expect(!!$$("s2") ).to.equal(false); 83 | expect(!!$$("s31") ).to.equal(false); 84 | expect(!!$$("s32") ).to.equal(false); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/events.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { UrlParamsView } from "./stubs/views"; 6 | import { errors } from "../sources/index"; 7 | 8 | let app; 9 | 10 | beforeAll(async () => { 11 | await loadWebix(); 12 | }); 13 | afterEach(function(){ 14 | try{ app.destructor(); } catch(e){} 15 | }); 16 | 17 | 18 | beforeEach(function(){ 19 | app = new JetApp({ 20 | router: EmptyRouter, 21 | start:"/some/url/here", 22 | debug: false, 23 | views:{ 24 | "some":{ rows:[{ $subview:true }] }, 25 | "url":{ rows:[{ $subview:true }] }, 26 | "here":{ template:() => app.config.counter, localId:"h1" }, 27 | "there":{ template:"s2", localId:"t1" } 28 | } 29 | }); 30 | return app.render(document.body) 31 | }); 32 | 33 | 34 | it("app:guard", async () => { 35 | let counter = 0; 36 | app.on("app:guard", () => counter++) 37 | await app.show("/some/url/there"); 38 | expect(counter).to.equal(1); 39 | }); 40 | 41 | it("app:guard", async () => { 42 | app.on("app:guard", () => false ) 43 | let error = null; 44 | 45 | await (app.show("/some/url/there").catch(e => { 46 | error = e; 47 | })); 48 | 49 | expect(error).instanceof(errors.NavigationBlocked); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/jetappui.spec.js: -------------------------------------------------------------------------------- 1 | import { patch, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { SubApp } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | 17 | 18 | it("can use inner navigation", async () => { 19 | patch(webix); 20 | 21 | webix.protoUI({ 22 | name:"subapp", 23 | app: SubApp 24 | }, webix.ui.jetapp); 25 | 26 | const ui = webix.ui({ view:"subapp", id:"d1", router: EmptyRouter }); 27 | await(ui.$app.ready); 28 | const topView = ui.$app.getSubView(); 29 | await ui.$app.show("body2"); 30 | 31 | expect(ui.$app.getUrlString()).to.eq("body2"); 32 | expect(!!topView.getSubView()).to.eq(false); 33 | 34 | ui.destructor(); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/locale.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { JetView, plugins } from "../sources/index"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | 17 | 18 | beforeEach(function(){ 19 | class AppView extends JetView { 20 | config(){ 21 | const _ = this.app.getService("locale")._; 22 | return { id:"b1", view:"button", value: _("test") } 23 | } 24 | } 25 | 26 | app = new JetApp({ 27 | router: EmptyRouter, 28 | start:"/here", 29 | debug: true, 30 | views:{ 31 | here: AppView 32 | } 33 | }); 34 | 35 | app.use(plugins.Locale, { path:false, webix:{ en:"en-US", es:"es-ES" } }); 36 | app.getService("locale").setLangData("en", { test:"Test" }); 37 | 38 | return app.render(document.body) 39 | }); 40 | 41 | it("must apply default locale", () => { 42 | expect($$("b1").getValue()).to.equal("Test") 43 | }); 44 | 45 | it("must set custom object as locale", () => { 46 | const locale = app.getService("locale"); 47 | return locale.setLangData("ru", { test:"Тест" }).then(() => { 48 | expect($$("b1").getValue()).to.equal("Тест"); 49 | }); 50 | }); 51 | 52 | it("must work with webix.i18n", () => { 53 | const locale = app.getService("locale"); 54 | locale.setLangData("en", { test:"" }); 55 | expect(app.webix.i18n.calendar.hours).to.eq("Hours"); 56 | locale.setLangData("es", { test:"" }); 57 | expect(app.webix.i18n.calendar.hours).to.eq("Horas"); 58 | locale.setLangData("fr", { test:"" }); 59 | expect(app.webix.i18n.calendar.hours).to.eq("Horas"); 60 | }); -------------------------------------------------------------------------------- /tests/menu.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { TopMenuView, MenuView, SubMenuView } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | 17 | 18 | it("must mark page on url change", async () => { 19 | app = new JetApp({ 20 | router: EmptyRouter, 21 | start:"/some/two", 22 | debug: true, 23 | views:{ 24 | "some": MenuView, 25 | "one":{ }, 26 | "two":{ }, 27 | "three": { } 28 | } 29 | }); 30 | 31 | await app.render(document.body); 32 | expect(webix.$$("s1").getValue()).to.equal("two"); 33 | 34 | await app.show("/some/one"); 35 | expect(webix.$$("s1").getValue()).to.equal("one"); 36 | 37 | await app.show("/some/two"); 38 | expect(webix.$$("s1").getValue()).to.equal("two"); 39 | 40 | await app.show("/some/three"); 41 | expect(webix.$$("s1").getValue()).to.equal("three"); 42 | 43 | await app.show("/some"); 44 | }); 45 | 46 | it("must work from sub-views", async () => { 47 | app = new JetApp({ 48 | router: EmptyRouter, 49 | start:"/some/two", 50 | debug: true, 51 | views:{ 52 | "some": TopMenuView, 53 | "submenu" : SubMenuView, 54 | "one":{ }, 55 | "two":{ }, 56 | "three": { } 57 | } 58 | }); 59 | 60 | await app.render(document.body); 61 | expect(webix.$$("s1").getValue()).to.equal("two"); 62 | 63 | await app.show("/some/one"); 64 | expect(webix.$$("s1").getValue()).to.equal("one"); 65 | 66 | await app.show("/some/two"); 67 | expect(webix.$$("s1").getValue()).to.equal("two"); 68 | 69 | await app.show("/some/three"); 70 | expect(webix.$$("s1").getValue()).to.equal("three"); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /tests/refresh.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { waitTime, loadWebix } from "./stubs/helpers"; 5 | 6 | let app; 7 | 8 | beforeAll(async () => { 9 | await loadWebix(); 10 | }); 11 | afterEach(function(){ 12 | try{ app.destructor(); } catch(e){} 13 | }); 14 | 15 | 16 | beforeEach(function(){ 17 | app = new JetApp({ 18 | router: EmptyRouter, 19 | start:"/some/url/here", 20 | debug: true, 21 | counter: 1, 22 | views:{ 23 | "some":{ rows:[{ $subview:true }] }, 24 | "url":{ rows:[{ $subview:true }] }, 25 | "here":{ template:() => app.config.counter, localId:"h1" }, 26 | "there":{ template:"s2", localId:"t1" } 27 | } 28 | }); 29 | return app.render(document.body) 30 | }); 31 | 32 | it("must refresh a single view", async () => { 33 | const view = app.getRoot().queryView({ localId : "h1" }).$scope; 34 | let text = view.getRoot().$view.innerText.trim(); 35 | expect(text).to.equal("1"); 36 | 37 | app.config.counter++; 38 | await view.refresh(); 39 | 40 | await waitTime(100); 41 | text = view.getRoot().$view.innerText.trim(); 42 | expect(text).to.equal("2"); 43 | 44 | await view.show("./there"); 45 | const there = app.getRoot().queryView({ localId : "t1" }); 46 | expect( !!there ).to.equal(true); 47 | 48 | await app.show("/some/url/here"); 49 | }); 50 | 51 | it("must refresh a whole app", async () => { 52 | let view = app.getRoot().queryView({ localId : "h1" }).$scope; 53 | let text = view.getRoot().$view.innerText.trim(); 54 | expect(text).to.equal("1"); 55 | 56 | app.config.counter++; 57 | await app.refresh(); 58 | 59 | view = app.getRoot().queryView({ localId : "h1" }).$scope; 60 | text = view.getRoot().$view.innerText.trim(); 61 | expect(text).to.equal("2"); 62 | 63 | await view.show("./there"); 64 | 65 | let there = app.getRoot().queryView({ localId : "t1" }); 66 | expect( !!there ).to.equal(true); 67 | 68 | await app.show("/url/there"); 69 | 70 | there = app.getRoot().queryView({ localId : "t1" }); 71 | expect( !!there ).to.equal(true); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/routers.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter, HashRouter, StoreRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | 6 | let app; 7 | 8 | beforeAll(async () => { 9 | await loadWebix(); 10 | }); 11 | afterEach(function(){ 12 | try{ app.destructor(); } catch(e){} 13 | }); 14 | 15 | const baseUrl = document.location.href; 16 | async function runTests(router, routerPrefix, href){ 17 | app = new JetApp({ 18 | start:"/Demo/Details", 19 | routerPrefix, 20 | debug: true, 21 | router, 22 | version: 2, 23 | views:{ 24 | "Demo":{ id:"tp", rows:[{ $subview:true }] }, 25 | "Details":{ template:"App" }, 26 | "Other" : { template:"Other" } 27 | } 28 | }); 29 | 30 | await app.render(document.body); 31 | expect(app.getUrlString()).to.equal("Demo/Details"); 32 | if (href) expect(href()).to.equal("/Demo/Details"); 33 | 34 | await $$("tp").$scope.show("Other"); 35 | expect(app.getUrlString()).to.equal("Demo/Other"); 36 | if (href) expect(href()).to.equal("/Demo/Other"); 37 | 38 | await app.show("/Other"); 39 | expect(app.getUrlString()).to.equal("Other"); 40 | if (href) expect(href()).to.equal("/Other"); 41 | 42 | await app.show("/Demo/Details"); 43 | expect(app.getUrlString()).to.equal("Demo/Details"); 44 | if (href) expect(href()).to.equal("/Demo/Details"); 45 | } 46 | 47 | afterEach(function(){ 48 | window.history.pushState(null, null, baseUrl); 49 | }) 50 | 51 | it("hash router", async () => { 52 | await runTests(HashRouter, "", () => document.location.href.split("#")[1]); 53 | }); 54 | 55 | // can be executed in browser only 56 | // it("url router", async () => { 57 | // const base = document.location.pathname; 58 | // await runTests(UrlRouter, base, () => document.location.pathname.replace(base, "")); 59 | // }); 60 | 61 | it("empty router", async () => { 62 | await runTests(EmptyRouter, ""); 63 | }); 64 | 65 | it("store router", async () => { 66 | await runTests(StoreRouter, ""); 67 | }); -------------------------------------------------------------------------------- /tests/stubs/helpers.js: -------------------------------------------------------------------------------- 1 | export const waitTime = function(time){ 2 | return new Promise(res => setTimeout(res, time)); 3 | } 4 | 5 | export const loadWebix = function(){ 6 | return new Promise(res => { 7 | const script = document.createElement('script'); 8 | script.onload = () => { 9 | res(); 10 | }; 11 | script.src = "https://cdn.webix.com/edge/webix.js"; 12 | document.head.appendChild(script); 13 | }); 14 | } -------------------------------------------------------------------------------- /tests/stubs/views.js: -------------------------------------------------------------------------------- 1 | import { JetView, JetApp, plugins } from "../../sources/index"; 2 | 3 | let events = []; 4 | export function getEvents(){ 5 | return events; 6 | } 7 | export function resetEvents(){ 8 | events = []; 9 | } 10 | 11 | 12 | export class TopView extends JetView { 13 | init(){ 14 | events.push("top-init"); 15 | } 16 | config(){ 17 | events.push("top-config"); 18 | return { rows:[ {$subview:true} ] }; 19 | } 20 | ready(){ 21 | events.push("top-ready"); 22 | } 23 | urlChange(){ 24 | events.push("top-urlChange"); 25 | } 26 | destroy(){ 27 | events.push("top-destroy"); 28 | } 29 | } 30 | 31 | export class SubView1 extends JetView { 32 | init(){ 33 | events.push("sub1-init"); 34 | } 35 | config(){ 36 | events.push("sub1-config"); 37 | return { template:"a" }; 38 | } 39 | ready(){ 40 | events.push("sub1-ready"); 41 | } 42 | urlChange(){ 43 | events.push("sub1-urlChange"); 44 | } 45 | destroy(){ 46 | events.push("sub1-destroy"); 47 | } 48 | } 49 | 50 | export class SubView2 extends JetView { 51 | init(){ 52 | events.push("sub2-init"); 53 | } 54 | config(){ 55 | events.push("sub2-config"); 56 | return { template:"a" }; 57 | } 58 | ready(){ 59 | events.push("sub2-ready"); 60 | } 61 | urlChange(){ 62 | events.push("sub2-urlChange"); 63 | } 64 | destroy(){ 65 | events.push("sub2-destroy"); 66 | } 67 | } 68 | 69 | export class MenuView extends JetView { 70 | init(){ 71 | this.use(plugins.Menu, "s1"); 72 | } 73 | config(){ 74 | return { rows:[{ view:"segmented", id:"s1", options:["one", "two", "three"] }, { $subview:true }] }; 75 | } 76 | } 77 | 78 | export class ChangeRouteFromInit extends JetView{ 79 | config(){ 80 | return { rows:[{$subview:true}] }; 81 | } 82 | init(){ 83 | this.show("./Other") 84 | } 85 | } 86 | 87 | export class ChangeRouteFromUrlChange extends JetView{ 88 | config(){ 89 | return { rows:[{$subview:true}] }; 90 | } 91 | urlChange(){ 92 | this.show("../Other") 93 | } 94 | } 95 | 96 | export class TopMenuView extends JetView { 97 | config(){ 98 | return { rows:[{ $subview:SubMenuView }, { $subview:true }] }; 99 | } 100 | } 101 | 102 | export class SubMenuView extends JetView { 103 | init(){ 104 | this.use(plugins.Menu, "s1"); 105 | } 106 | config(){ 107 | return { view:"segmented", id:"s1", options:["one", "two", "three"] }; 108 | } 109 | } 110 | 111 | export class UrlParamsView extends JetView { 112 | init(){ 113 | this.use(plugins.UrlParam, ["mode", "id"]); 114 | } 115 | config(){ 116 | return { rows:[ { template:"Now" }, { $subview:true } ]}; 117 | } 118 | } 119 | 120 | export class SubApp extends JetApp { 121 | constructor(config){ 122 | config.views = { 123 | top:{ row:[{ template:"Header" }, { $subview:true }] }, 124 | body:{ template:"Body"} 125 | }; 126 | config.start = config.start || "/top/body"; 127 | config.views = { 128 | "top" : { id:"sb-tp", rows:[ {$subview: true} ] }, 129 | "body2": { template:"body 2" }, 130 | "body": { template:"body" } 131 | }; 132 | super(config); 133 | } 134 | } -------------------------------------------------------------------------------- /tests/subviews.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { resetEvents, getEvents, SubView1 } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | 17 | beforeEach(async() => { 18 | app = new JetApp({ 19 | start:"/Demo/Details", 20 | debug: true, 21 | version: 2, 22 | views:{ 23 | "Demo":{ id:"tp", rows:[{ $subview:true }] }, 24 | "Normal":{ rows:[{ $subview:"SubView1" }, { $subview:true }]}, 25 | "Branch":{ rows:[{ $subview:"SubView1", branch:true }, { $subview:true }]}, 26 | "Names":{ rows:[{ $subview:"Details", name:"sideway" }, { $subview:true }]}, 27 | "Details":{ template:"App", localId:"details" }, 28 | "Other" : { template:"Other", localId:"other" }, 29 | "SubView1": SubView1 30 | } 31 | }); 32 | 33 | return app.render(document.body); 34 | }); 35 | 36 | it("support branch option", async() => { 37 | await app.show("/Other") 38 | resetEvents(); 39 | await app.show("/Demo/Normal/Details") 40 | await app.show("/Demo/Normal/Other") 41 | expect(getEvents()).to.deep.eq(["sub1-config", "sub1-init", "sub1-urlChange", "sub1-ready", "sub1-urlChange"]) 42 | 43 | await app.show("/Other") 44 | resetEvents(); 45 | await app.show("/Demo/Branch/Details") 46 | await app.show("/Demo/Branch/Other") 47 | expect(getEvents()).to.deep.eq(["sub1-config", "sub1-init", "sub1-urlChange", "sub1-ready"]); 48 | }); 49 | 50 | it("support branch navigation from master view", async() => { 51 | await app.show("/Demo/Names/SubView1") 52 | let view = app.getRoot().queryView({ localId:"details"}).$scope; 53 | await view.show("Other", { target:"sideway" }); 54 | 55 | //old view destroyed 56 | expect(!!view.getRoot()).to.eq(false) 57 | //new view created 58 | view = app.getRoot().queryView({ localId:"other"}).$scope; 59 | expect(!!view).to.eq(true) 60 | 61 | await view.show("Normal/Other", { target:"sideway" }); 62 | view = app.getRoot().queryView({ localId:"other"}).$scope; 63 | const jview = view.getParentView(); 64 | 65 | await view.show("Normal/Normal/Other", { target:"sideway" }); 66 | view = app.getRoot().queryView({ localId:"other"}).$scope; 67 | const jview2 = view.getParentView().getParentView(); 68 | expect(jview).to.eq(jview2) 69 | 70 | expect(jview.getUrlString()).to.eq("Normal/Normal/Other") 71 | 72 | await app.show("/SubView1") 73 | expect(!!jview.getRoot()).to.eq(false) 74 | }); 75 | 76 | -------------------------------------------------------------------------------- /tests/url.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { UrlParamsView } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | function filterUrl(url){ 17 | const data = url.slice(0); 18 | for (var i=0; i { 40 | return app.show("/some/url/here") 41 | .then(_ =>{ 42 | expect(filterUrl(app.getSubView().getUrl())).to.deep.equal([ 43 | { page:"some", params:{} }, 44 | { page:"url", params:{} }, 45 | { page:"here", params:{} } 46 | ]); 47 | }); 48 | }); 49 | 50 | it("must work without leading /", () => { 51 | return app.show("some/url/here") 52 | .then(_ =>{ 53 | expect(filterUrl(app.getSubView().getUrl())).to.deep.equal([ 54 | { page:"some", params:{} }, 55 | { page:"url", params:{} }, 56 | { page:"here", params:{} } 57 | ]); 58 | }); 59 | }); 60 | 61 | it("must parse url parameters", () => { 62 | return app.show("/some:test1=1:test2=2/url?test3=3&test4=4/here") 63 | .then(_ =>{ 64 | expect(filterUrl(app.getSubView().getUrl())).to.deep.equal([ 65 | { page:"some", params:{ test1: "1", test2 : "2" } }, 66 | { page:"url", params:{ test3: "3", test4 : "4" } }, 67 | { page:"here", params:{} } 68 | ]); 69 | }); 70 | }); 71 | 72 | 73 | it("must merge parameters from string", async () => { 74 | await app.show("some/url/here"); 75 | var hereView = app.getRoot().queryView({ localId: "h1"}).$scope; 76 | await hereView.show("there?id=123&t=245"); 77 | 78 | var thereView = app.getRoot().queryView({ localId: "t1"}).$scope; 79 | expect(filterUrl(thereView.getUrl())).to.deep.equal([{ 80 | page:"there", params:{ id:"123", t:"245" } 81 | }]); 82 | expect(thereView.getUrlString()).eq("there?id=123&t=245"); 83 | 84 | expect(filterUrl(thereView.getParentView().getUrl())).to.deep.equal([ 85 | { page:"url", params:{} }, 86 | { page:"there", params:{ id:"123", t:"245" } } 87 | ]); 88 | expect(thereView.getParentView().getUrlString()).eq("url/there?id=123&t=245"); 89 | 90 | expect(filterUrl(app.getSubView().getUrl())).to.deep.equal([ 91 | { page:"some", params:{} }, 92 | { page:"url", params:{} }, 93 | { page:"there", params:{ id:"123", t:"245" } } 94 | ]); 95 | expect(app.getSubView().getUrlString()).eq("some/url/there?id=123&t=245"); 96 | 97 | expect(filterUrl(app.getUrl())).to.deep.equal([ 98 | { page:"some", params:{} }, 99 | { page:"url", params:{} }, 100 | { page:"there", params:{ id:"123", t:"245" } } 101 | ]); 102 | expect(app.getUrlString()).eq("some/url/there?id=123&t=245"); 103 | }); 104 | 105 | it("must merge parameters from object", async () => { 106 | await app.show("some/url/there") 107 | 108 | var hereView = app.getRoot().queryView({ localId: "t1"}).$scope; 109 | expect(hereView.getParam("t")).eq(undefined); 110 | await hereView.show({ id:123, t:245 }); 111 | 112 | var thereView = app.getRoot().queryView({ localId: "t1"}).$scope; 113 | expect(thereView.getUrlString()).eq("there?id=123&t=245"); 114 | expect(app.getUrlString()).eq("some/url/there?id=123&t=245"); 115 | 116 | expect(thereView.getParam("t")).eq(245); 117 | thereView.setParam("t", "246"); 118 | expect(thereView.getParam("t")).eq("246"); 119 | await thereView.setParam("z", 3, true); 120 | 121 | 122 | var thereView = app.getRoot().queryView({ localId: "t1"}).$scope; 123 | expect(thereView.getUrlString()).eq("there?id=123&t=246&z=3"); 124 | }); 125 | 126 | it("must understand jet 0.x parameters syntax", () => { 127 | return app.show("/some?test1=1&test2=2/url?test3=3&test4=4/here") 128 | .then(_ => { 129 | const url = app.getUrl(); 130 | expect(url[0].params).deep.eq({test1:"1", test2:"2"}); 131 | expect(url[1].params).deep.eq({test3:"3", test4:"4"}); 132 | 133 | return app.show("/some:test1=1:test2=2/url:test3=3:test4=4/here"); 134 | }) 135 | .then(_ =>{ 136 | const url = app.getUrl(); 137 | 138 | expect(url[0].params).deep.eq({test1:"1", test2:"2"}); 139 | expect(url[1].params).deep.eq({test3:"3", test4:"4"}); 140 | }); 141 | }); 142 | 143 | it("must work with different navigation patterns", async () => { 144 | await app.show("some/url/there") 145 | 146 | var view = app.getRoot().queryView({ localId: "t1"}).$scope; 147 | await view.show("here"); 148 | expect(app.getUrlString(),"{name}").eq("some/url/here"); 149 | 150 | var view = app.getRoot().queryView({ localId: "h1"}).$scope; 151 | await view.show("./there"); 152 | expect(app.getUrlString(),"./{name}").eq("some/url/there"); 153 | 154 | var view = app.getRoot().queryView({ localId: "t1"}).$scope; 155 | await view.show("../../here"); 156 | expect(app.getUrlString(),"../{name}").eq("some/here"); 157 | 158 | var view = app.getRoot().queryView({ localId: "h1"}).$scope; 159 | await view.show("/some/url/there"); 160 | expect(app.getUrlString(),"/{name}").eq("some/url/there"); 161 | 162 | await app.show("/some/url/here"); 163 | expect(app.getUrlString(),"/{name}").eq("some/url/here"); 164 | }); 165 | -------------------------------------------------------------------------------- /tests/urlparams.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { UrlParamsView } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | beforeEach(function(){ 17 | app = new JetApp({ 18 | router: EmptyRouter, 19 | start:"/some", 20 | debug: true, 21 | views:{ 22 | "some":{ rows:[{ $subview:true }] }, 23 | "url":{ rows:[{ $subview:true }] }, 24 | "here":{ template:"s1", localId:"h1" }, 25 | "params": UrlParamsView 26 | } 27 | }); 28 | return app.render(document.body) 29 | }) 30 | 31 | it("must parse params from the url", async () => { 32 | await app.show("/some/params") 33 | expect(app.getUrlString()).to.equal("some/params"); 34 | 35 | const params = app.getSubView().getSubView(); 36 | 37 | await app.show("/some/params/one/12") 38 | expect( params.getParam("mode") ).to.equal("one"); 39 | expect( params.getParam("id") ).to.equal("12"); 40 | 41 | await app.show("/some/params/two/14") 42 | expect( params.getParam("mode") ).to.equal("two"); 43 | expect( params.getParam("id") ).to.equal("14"); 44 | 45 | expect(app.getUrlString()).to.equal("some/params/two/14"); 46 | expect(params.getUrlString()).to.equal("params/two/14"); 47 | }); 48 | 49 | it("must parse params on navigation", async () => { 50 | await app.show("/some/params/two/14") 51 | expect(app.getUrlString()).to.equal("some/params/two/14"); 52 | 53 | const params = app.getSubView().getSubView(); 54 | await params.setParam("id", 12, true); 55 | expect(params.getUrlString()).to.equal("params/two/12"); 56 | 57 | await params.show({ id : 11 }); 58 | expect(params.getUrlString()).to.equal("params/two/11"); 59 | expect(params.getParam("id")+"").to.equal("11") 60 | 61 | await params.show("../params/two/10"); 62 | expect(params.getUrlString()).to.equal("params/two/10"); 63 | expect(params.getParam("id")+"").to.equal("10") 64 | 65 | await params.show("here"); 66 | expect(params.getUrlString()).to.equal("params/two/10/here"); 67 | expect(params.getParam("id")+"").to.equal("10") 68 | 69 | await params.show("some"); 70 | expect(params.getUrlString()).to.equal("params/two/10/some"); 71 | expect(params.getParam("id")+"").to.equal("10") 72 | }); 73 | 74 | it("must preserve params on refresh", async () => { 75 | await app.show("/some/params/two/14") 76 | expect(app.getUrlString()).to.equal("some/params/two/14"); 77 | 78 | await app.refresh(); 79 | expect(app.getUrlString()).to.equal("some/params/two/14"); 80 | 81 | const params = app.getSubView().getSubView(); 82 | await params.refresh(); 83 | expect(params.getUrlString()).to.equal("params/two/14"); 84 | expect(app.getUrlString()).to.equal("some/params/two/14"); 85 | }); 86 | 87 | it("must correctly process force-show flag", async () => { 88 | await app.show("/some/params/two/14") 89 | expect(app.getUrlString()).to.equal("some/params/two/14"); 90 | 91 | await app.getSubView().getSubView().setParam("mode", "five", true); 92 | expect(app.getUrlString()).to.equal("some/params/five/14"); 93 | }); 94 | -------------------------------------------------------------------------------- /tests/view_stages.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { getEvents, resetEvents, TopView, SubView1, SubView2 } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | it("process init, config, ready, destroy in the correct order", () => { 17 | resetEvents(); 18 | 19 | app = new JetApp({ 20 | start:"/Top/Sub1", router: EmptyRouter, 21 | debug: true, 22 | version: 2, 23 | views:{ 24 | "Top":TopView, 25 | "Sub1":SubView1, 26 | "Sub2":SubView2 27 | } 28 | }); 29 | 30 | return app.render(document.body).then( _ => { 31 | expect(getEvents(), "after init").deep.equal([ 32 | "top-config", "top-init", 33 | "sub1-config", "sub1-init", "sub1-urlChange", "sub1-ready", 34 | "top-urlChange", "top-ready"]); 35 | 36 | resetEvents(); 37 | return app.show("Top/Sub2"); 38 | }).then(_ => { 39 | expect(getEvents(), "after show").deep.equal([ 40 | "sub2-config", 41 | "sub1-destroy", 42 | "sub2-init", "sub2-urlChange", "sub2-ready", 43 | "top-urlChange" 44 | ]); 45 | 46 | resetEvents(); 47 | return app.refresh(); 48 | }).then(_ => { 49 | expect(getEvents(), "after refresh").deep.equal([ 50 | "top-destroy", "sub2-destroy", 51 | "top-config", "top-init", 52 | "sub2-config", "sub2-init", "sub2-urlChange", "sub2-ready", 53 | "top-urlChange", "top-ready"]); 54 | 55 | resetEvents(); 56 | return app.destructor(); 57 | }).then(_ => { 58 | expect(getEvents()).deep.equal([ "top-destroy", "sub2-destroy"]); 59 | }).catch(err => { 60 | resetEvents(); 61 | 62 | app.destructor(); 63 | throw err; 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/window.spec.js: -------------------------------------------------------------------------------- 1 | import { JetApp, EmptyRouter } from "../sources/index"; 2 | import { afterEach, beforeAll, beforeEach, it, expect } from "vitest"; 3 | 4 | import { loadWebix } from "./stubs/helpers"; 5 | import { ChangeRouteFromInit, ChangeRouteFromUrlChange } from "./stubs/views"; 6 | 7 | let app; 8 | 9 | beforeAll(async () => { 10 | await loadWebix(); 11 | }); 12 | afterEach(function(){ 13 | try{ app.destructor(); } catch(e){} 14 | }); 15 | 16 | 17 | beforeEach(function(){ 18 | app = new JetApp({ 19 | router: EmptyRouter, 20 | start:"/some/url/there", 21 | debug: true, 22 | counter: 1, 23 | views:{ 24 | "some":{ rows:[{ $subview:true }] }, 25 | "url":{ rows:[{ $subview:true, popup:true }] }, 26 | "here":{ view:"window", modal:true, id:"h1", body:{ $subview:true }}, 27 | "there":{ template:"s2", id:"t1" }, 28 | "dummy":{ template:"dummy" } 29 | } 30 | }); 31 | return app.render(document.body) 32 | }); 33 | 34 | 35 | it("can use default router to window", async () => { 36 | await app.show("/some/url/here/there"); 37 | const win = webix.$$("h1"); 38 | expect( !!win ).to.equal( true ); 39 | expect( !!win.isVisible() ).to.equal( true ); 40 | expect( win.$scope ).to.equal( app.getSubView().getSubView().getSubView() ); 41 | 42 | await win.$scope.show("./some/there"); 43 | expect(win.queryView({ id:"t1" }).config.id).to.equal("t1"); 44 | expect(app.getUrlString()).to.equal("some/url/here/some/there") 45 | 46 | await app.show("/there"); 47 | expect( !!webix.$$("h1") ).to.equal( false ); 48 | }); 49 | 50 | it("can use show to window", async () => { 51 | await app.show("/dummy"); 52 | const top = app.getSubView(); 53 | 54 | await top.show("./here/there", { target:"_top" }); 55 | 56 | const win = webix.$$("h1"); 57 | expect( !!win ).to.equal( true ); 58 | expect( !!win.isVisible() ).to.equal( true ); 59 | expect( !!webix.$$("t1") ).to.equal( true ); 60 | expect( win.getBody().config.id ).to.equal( "t1") 61 | expect( win.$scope.getParentView() ).to.equal( top ); 62 | expect( app.getUrlString() ).to.equal( "dummy" ); 63 | 64 | await win.$scope.show("./url"); 65 | expect( !!webix.$$("h1") ).to.equal( true ); 66 | expect( !!webix.$$("t1") ).to.equal( false ); 67 | expect( app.getUrlString() ).to.equal( "dummy" ); 68 | 69 | await top.show("/some"); 70 | expect( !!webix.$$("h1") ).to.equal( false ); 71 | }); 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "nodenext", 4 | "module": "esnext", 5 | "target": "es6", 6 | "lib": ["es2018", "dom"], 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationDir":"dist/types" 10 | }, 11 | "include":[ 12 | "sources/**/*.ts" 13 | ] 14 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import dts from "vite-plugin-dts"; 3 | 4 | export default defineConfig({ 5 | plugins: [dts({ 6 | outputDir: "dist/types", 7 | insertTypesEntry: true, 8 | })], 9 | resolve: { 10 | extensions: ['.ts', '.js', '.json'] 11 | }, 12 | build: { 13 | outDir: 'dist', 14 | assetsDir: '', 15 | lib: { 16 | entry: 'sources/index.ts', 17 | name: 'webixJet', 18 | fileName: 'jet' 19 | } 20 | }, 21 | test: { 22 | browser: { 23 | include: ['tests/**/*.spec.{ts,js}'], 24 | enabled: true, 25 | headless: true, 26 | name: 'chrome', 27 | }, 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /whatsnew.md: -------------------------------------------------------------------------------- 1 | ### 3.0.3 2 | 3 | - [fix] UnloadGuard can throw an error in some subviews are not loaded yet 4 | --------------------------------------------------------------------------------