UNPKG

chondric

Version:

ChondricJS App Framework

787 lines (654 loc) 29.5 kB
import {RouteCollection} from "./routecollection.js"; export class App { constructor(options) { this.options = options; this.title = options.title; this.moduleName = options.moduleName || "chondric"; var deps = []; try { angular.module("ngAnimate"); deps.push("ngAnimate"); } catch (err) { console.log("ngAnimate not loaded"); } this.module = angular.module(this.moduleName, deps); this.debugMode = window.debugMode || false; this.registerOptionalDirective(require("./directives/ng-tap"), "ngTap"); this.registerOptionalDirective(require("./directives/ng-style-prefixer"), "ngStylePrefixer"); this.registerOptionalDirective(require("./directives/cjs-shared-component"), "cjsSharedComponent"); this.registerOptionalDirective(require("./directives/chondric-page"), "chondricPage"); this.registerOptionalDirective(require("./directives/cjs-popover")); this.registerOptionalDirective(require("./directives/chondric-viewport")); this.registerOptionalDirective(require("./directives/viewport2")); this.registerOptionalDirective(require("./loadstatus/cjs-loading-overlay")); this.registerOptionalDirective(require("./loadstatus/cjs-show-after-load")); this.registerFactory('sharedUi', require("./sharedui/shareduiprovider.js")); this.registerFactory('loadStatus', require("./loadstatus/loadstatusprovider.js")); this.sharedUiComponents = {}; this.additionalInjections = []; // this.sharedUiComponents.popup = new require("./sharedui/popup.js").SharedPopup(); this.allRoutes = {}; this.noop = function() { }; this.hostSettings = require("build/hostsettings"); this.topLevelRoutes = new RouteCollection(); } registerFactory(name, factory) { if (factory.default) factory = factory.default; this.module.factory(name, factory); } registerPage(pageclass, route, options) { if (pageclass.default) pageclass = pageclass.default; route = route || pageclass.routeTemplate; console.log("Registering page " + pageclass.name + " on route " + route); // move options into annotations so that constructor if (!pageclass.annotations || !pageclass.annotations.length) { // in new model, classes are not reusable across multiple routes, so create a new subclass. class pc extends pageclass { constructor(a, b, c) { super(a, b, c); } } pc.annotations = [new Route({ route: route, options: options })]; this.allRoutes[route] = { pageclass: pc, options: options || {} }; } else { this.allRoutes[route] = { pageclass: pageclass, options: options || {} }; } } registerSection(pageclass, route, options) { this.registerPage(pageclass, route, options); } registerSharedUiComponent(componentClass) { if (componentClass.default) componentClass = componentClass.default; console.log("Registering shared UI component " + componentClass.name); var component = new componentClass(); component.app = this; component.componentId = component.componentId || component.componentName || componentClass.componentName || componentClass.name; this.sharedUiComponents[component.componentId] = component; } registerOptionalDirective(options, name2) { var app = this; this.knownOptionalDirectives = this.knownOptionalDirectives || []; if (options.default) options = options.default; var name, injections, fn, arr; if (name2) { name = name2; arr = options; } else { if (typeof options == "function") { // annotated class // todo: find annotation with type Directive properly var annotation = options.annotations[0]; name = annotation.selector; injections = annotation.injections; fn = function(a, b, c, d, e, f, g) { console.log("vp2 init"); return { template: annotation.template, scope: true, link: function(scope, element, attrs) { console.log("vp2 link"); var obj = new options(scope, element, attrs, a, b, c, d, e, f, g); obj.scope = scope; obj.app = app; scope[name] = scope.directive = obj; } }; }; } else if (typeof options == "object") { // object with name, injector and fn properties name = options.name; injections = options.injections || []; fn = options.fn; } arr = injections || []; arr = arr.concat([fn]); } if (this.knownOptionalDirectives.indexOf(name) >= 0) return; this.knownOptionalDirectives.push(name); if (!arr) { console.error("No definition for directive " + name); } this.module.directive(name, arr); } registerOptionalFilter(options) { this.knownOptionalFilters = this.knownOptionalFilters || []; if (options.default) options = options.default; if (this.knownOptionalFilters.indexOf(options.name) >= 0) return; this.knownOptionalFilters.push(options.name); var arr = options.injections || []; arr = arr.concat([options.fn]); this.module.filter(options.name, arr); } updateOpenViewArray(parentObject, parentArray) { parentArray.splice(0, parentArray.length); for (var k in parentObject) { var v = parentObject[k]; parentArray.push(v); if (v.subsections) { v.subsectionArray = v.subsectionArray || []; this.updateOpenViewArray(v.subsections, v.subsectionArray); } } } loadView(url, position) { var app = this; var $scope = app.scope; if (!url) { // first run - load start page throw new Error("loadView requires a valid route URL"); } var matchingRoutes = []; var parts = url.split("/"); routeLoop: for (var r in app.allRoutes) { var rparts = r.split("/"); for (var i = 0; i < rparts.length; i++) { if (rparts[i] == parts[i]) continue; if (rparts[i][0] == "$") continue; continue routeLoop; } matchingRoutes.push(r); } matchingRoutes.sort(function(a, b) { return a.split("/").length - b.split("/").length; }); // matching routes list should be section heirarchy var openViews = $scope.openViews; for (var i2 = 0; i2 < matchingRoutes.length; i2++) { var template = $scope.allRoutes[matchingRoutes[i2]]; var mrp = matchingRoutes[i2].split("/"); var ar = ""; var params = {}; for (var j = 0; j < mrp.length; j++) { if (mrp[j][0] == "$" && parts[j]) params[mrp[j].substr(1)] = decodeURIComponent(parts[j]); if (parts[j]) ar += "/" + parts[j]; } var page = openViews[ar]; if (!page) { openViews[ar] = page = new template.pageclass(ar, params, template.options); page.initSharedUiComponents(app); } if (page.subsections) openViews = page.subsections; if (position) page.position = position; app.updateOpenViewArray($scope.openViews, $scope.openViewArray); } } transitionComponents(fromRoute, toRoute, progress) { if (!toRoute) return; var app = this; var fromStates = app.componentStatesForRoutes[fromRoute] || {}; var toStates = app.componentStatesForRoutes[toRoute] || {}; for (var k in app.sharedUiComponents) { var component = app.sharedUiComponents[k]; var fromState = fromStates[k] || { route: fromRoute, active: false, available: false, data: {} }; var toState = toStates[k] || { route: toRoute, active: false, available: false, data: {} }; if (component.setStatePartial) { component.setStatePartial(component, fromState, toState, progress); } else { if (progress > 0.5) { component.setState(component, toState.route, toState.active, toState.available, toState.data); } else if (fromState.route) { component.setState(component, fromState.route, fromState.active, fromState.available, fromState.data); } } } } pushPopup(p) { if (!p) return; var r; if (p instanceof Array) { r = ""; for (let i = 0; i < p.length; i++) { r += "/" + p[i]; } } else { r = p; } this.changePage(this.scope.route + ";" + r); } changePopup(p) { if (!p) return; var r; if (p instanceof Array) { r = ""; for (let i = 0; i < p.length; i++) { r += "/" + p[i]; } } else { r = p; } let ss = this.scope.route.split(";"); this.changePage(ss[0] + ";" + r); this.changePage(this.scope.route + ";" + r); } closePopup() { let ss = this.scope.route.split(";"); this.changePageInternal(ss[0], ss[0], []); } popPopup() { // todo: remove last item from route let ss = this.scope.route.split(";"); var l = ss.length - 1; while (l > 1 && !ss[l]) l--; this.changePageInternal(ss[0], ss[0], ss.slice(1, l)); } changePage(p, transition, originElement) { console.log("Changing page to " + p); var $scope = this.scope; var lastRoute, mainRoute; var popups = []; if (p instanceof Array) { if (p[0] instanceof Array) { // parameter is [["main"], ["popup"]] mainRoute = ""; for (let i = 0; i < p[0].length; i++) { mainRoute += "/" + p[0][i]; } for (let j = 1; j < p.length; j++) { popups[j] = ""; for (let i = 0; i < p[j].length; i++) { popups[j - 1] += "/" + p[j][i]; } } } else if (p[0].indexOf("/") >= 0) { // parameter is ["/main", "/popup"] mainRoute = p[0]; for (let j = 1; j < p.length; j++) { if (p[j]) popups[j - 1] = p[j]; } } else { // old syntax - single route with array of path components mainRoute = ""; for (let i = 0; i < p.length; i++) { mainRoute += "/" + p[i]; } } } else { // parameter is /main;/popup let ss = p.split(';'); mainRoute = ss[0]; for (let j = 1; j < ss.length; j++) { if (ss[j]) popups[j - 1] = ss[j]; } } if ($scope.route) { let ss = $scope.route.split(";"); lastRoute = ss[0]; } this.changePageInternal(lastRoute, mainRoute, popups, transition, originElement); } changePageInternal(lastPageRoute, currentPageRoute, popups, transition, originElement) { if (lastPageRoute) { var path = require("./path"); currentPageRoute = path.resolve(lastPageRoute, currentPageRoute); for (let i = 0; i < popups.length; i++) { if (!popups[i]) continue; popups[i] = path.resolve(lastPageRoute, popups[i]); } } console.log("changePageInternal"); console.log(popups); console.log(currentPageRoute); var fullRoute = currentPageRoute + ";" + popups.join(";"); if (fullRoute[fullRoute.length - 1] == ";") fullRoute = fullRoute.substr(0, fullRoute.length - 1); // if ($scope.route == r) return; // if ($scope.lastRoute == r) $scope.lastRoute = null; var app = this; var $scope = this.scope; var r = currentPageRoute; var fromRoute = lastPageRoute; var toRoute = currentPageRoute; var fromRect = null; if (fromRoute) { app.scrollPosForRoutes[fromRoute] = { x: window.scrollX, y: window.scrollY }; if (originElement && originElement.length) { // todo: find parent element if necessary and set appropriate origin rect fromRect = app.transitionOriginForRoutes[fromRoute] = originElement[0].getBoundingClientRect(); } else { fromRect = app.transitionOriginForRoutes[fromRoute] = null; } } if (app.transitionMode == "none" || transition == "none") { app.loadView(r); if (popups[popups.length - 1]) app.loadView(popups[popups.length - 1]); app.$timeout(function() { $scope.route = fullRoute; $scope.activePopups = popups; //$scope.$apply(); app.$timeout(function() { app.transitionComponents(fromRoute, toRoute, 1); // $scope.$apply(); }, 0); }, 0); } else if (app.transitionMode == "native") { // disable pointer events for 300ms to prevent ghost clicks. if (window.jstimer) window.jstimer.start("transitioningTimeout"); angular.element(document.body).addClass("cjs-transitioning"); window.setTimeout(function() { if (window.jstimer) window.jstimer.finish("transitioningTimeout"); angular.element(document.body).removeClass("cjs-transitioning"); }, 300); var actualTransition = "crossfade"; var originRect = null; if (transition == "zoomin" && fromRect) { actualTransition = "zoomin"; originRect = fromRect; } if (transition == "zoomout" && app.transitionOriginForRoutes[toRoute]) { actualTransition = "zoomout"; originRect = app.transitionOriginForRoutes[toRoute]; } window.NativeNav.startNativeTransition(actualTransition, originRect, function() { // angular.element(".chondric-page.active").removeClass("active"); if (window.jstimer) window.jstimer.finish("transitioningCallback1"); if (window.jstimer) window.jstimer.start("transitioningTimeout2"); window.setTimeout(function() { if (window.jstimer) window.jstimer.finish("transitioningTimeout2"); if (window.jstimer) window.jstimer.start("transitioningTimeout3"); app.loadView(r); if (popups[popups.length - 1]) app.loadView(popups[popups.length - 1]); $scope.route = fullRoute; $scope.activePopups = popups; $scope.$apply(); app.transitionComponents(fromRoute, toRoute, 1); $scope.$apply(); window.NativeNav.finishNativeTransition(); if (window.jstimer) window.jstimer.finish("transitioningTimeout3"); }, 0); }); } else { $scope.transition.type = transition || "crossfade"; $scope.noTransition = true; app.loadView(r); if (popups[popups.length - 1]) app.loadView(popups[popups.length - 1]); $scope.nextRoute = r; $scope.transition.progress = 0; $scope.transition.from = $scope.route; $scope.transition.to = $scope.nextRoute; $scope.transition.fromRect = fromRect; if (fromRoute) { $scope.transition.fromScroll = app.scrollPosForRoutes[fromRoute]; $scope.transition.fromRect = app.transitionOriginForRoutes[fromRoute]; } $scope.transition.toRect = app.transitionOriginForRoutes[$scope.transition.to]; $scope.transition.toScroll = app.scrollPosForRoutes[$scope.transition.to] || { x: 0, y: 0 }; window.setTimeout(function() { $scope.noTransition = false; $scope.route = fullRoute; $scope.activePopups = popups; $scope.transition.progress = 1; $scope.$apply(); }, 100); } } customInit() { console.log("Base custom init"); this.loadStartPage(); } initController() { console.log("initController"); var app = this; var inj = ["$scope", "$location", "$element", "$attrs", "$rootScope", "$timeout"].concat(this.additionalInjections); var appCtrl = function($scope, $location, $element, $attrs, $rootScope, $timeout, a, b, c, d, e) { console.log("running app module controller"); app.scope = $scope; app.rootScope = $rootScope; app.$timeout = $timeout; if ($attrs.startPage) { app.startPageFromHtml = $attrs.startPage; } $scope.app = app; $scope.allRoutes = app.allRoutes; $scope.route = null; $scope.nextRoute = null; $scope.lastRoute = null; $scope.transition = { type: "crossfade", progress: 0 }; $scope.openViews = {}; $scope.openViewArray = []; $scope.sharedUiComponents = app.sharedUiComponents; // these will usually get overridden on a child scope - otherwise names have to be globally unique $scope.showModal = function(name, lastTap) { $scope[name] = lastTap; }; $scope.hideModal = function(name) { $scope[name] = null; }; $scope.getSharedUiComponentState = app.getSharedUiComponentState = function(routeScope, componentId) { app.scopesForRoutes[routeScope.pageRoute] = routeScope; var component = app.sharedUiComponents[componentId]; if (!component) { throw new Error( "Shared UI Component " + componentId + " not found" ); } var csfr = app.componentStatesForRoutes[routeScope.pageRoute] = app.componentStatesForRoutes[routeScope.pageRoute] || {}; var cs = csfr[componentId] = csfr[componentId] || { route: routeScope.pageRoute, active: false, available: false, data: {} }; return cs; }; $scope.setSharedUiComponentState = app.setSharedUiComponentState = function(routeScope, componentId, active, available, data) { console.log("setSharedUiComponentState"); console.log(data); var cs = app.getSharedUiComponentState(routeScope, componentId); // if parameters are undefined, the previous value will be used if (active === true || active === false) cs.active = active; if (available === true || available === false) cs.available = available; if (data !== undefined) cs.data = data; var uc = routeScope.usedComponents; var uci = uc.asArray.indexOf("uses-" + componentId); if (available && uci < 0) { uc.asArray.push("uses-" + componentId); uc.asString = uc.asArray.join(" "); } else if (!available && uci >= 0) { uc.asArray.splice(uci, 1); uc.asString = uc.asArray.join(" "); } var component = app.sharedUiComponents[componentId]; //if (component.getSwipeNav) app.updateSwipeNav(routeScope, component.getSwipeNav(component, cs.active, cs.available)); if ($scope.route && $scope.route.split(";")[0] == routeScope.pageRoute) { component.setState(component, routeScope.pageRoute, cs.active, cs.available, cs.data); } }; app.scopesForRoutes = {}; app.scrollPosForRoutes = {}; app.swipeNavForRoutes = {}; app.transitionOriginForRoutes = {}; app.componentStatesForRoutes = {}; $scope.changePage = function(a1, b1, c1) { // because angular expressions can pass in lastTap but not lastTap.element in angular 1.3 if (c1 && c1.element) c1 = c1.element; app.changePage(a1, b1, c1); }; $scope.pushPopup = function(a1, b1, c1) { app.pushPopup(a1, b1, c1); }; $scope.closePopup = function(a1, b1, c1) { app.closePopup(a1, b1, c1); }; $scope.popPopup = function(a1, b1, c1) { app.popPopup(a1, b1, c1); }; $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { if (!oldUrl || !newUrl || oldUrl == newUrl) return; var ind = newUrl.indexOf("#"); if (ind < 0) return; var hash = newUrl.substr(ind + 1); if (hash.indexOf("access_token=") >= 0) return; if (hash == $scope.route) return; app.changePage(hash); }); function viewCleanup(viewCollection, preservedRoutes) { for (var k in viewCollection) { if (k.indexOf("/") !== 0) continue; var keep = false; for (var i = 0; i < preservedRoutes.length; i++) { var r = preservedRoutes[i]; if (!r) continue; if (r.indexOf(k) === 0) { keep = true; break; } } if (!keep) { for (var csfrk in app.componentStatesForRoutes) { if (csfrk.indexOf(k) === 0) delete app.componentStatesForRoutes[csfrk]; } for (var sfrk in app.scopesForRoutes) { if (sfrk.indexOf(k) === 0) delete app.scopesForRoutes[sfrk]; } delete viewCollection[k]; continue; } if (viewCollection[k].subsections) { viewCleanup(viewCollection[k].subsections, preservedRoutes); } } app.updateOpenViewArray($scope.openViews, $scope.openViewArray); } $scope.$watch("transition", function(transition) { if (!transition) return; if (!transition.to) return; app.transitionComponents(transition.from, transition.to, transition.progress); }, true); $scope.$watch("route", function(url, oldVal) { console.log("route watch: " + oldVal + " -> " + url); if (!url) return; if (document.activeElement && app.transitionMode != "native" && document.activeElement.tagName != "BODY") { if (angular.element(document.activeElement).closest(".body").length > 0) { // only blur if the active element was inside a page body - page headers etc can remain focused. document.activeElement.blur(); } } $scope.nextRoute = null; $scope.lastRoute = oldVal; $location.path(url).replace(); $scope.activeRoutes = url.split(";"); $scope.activePageRoute = $scope.activeRoutes[0]; app.loadView($scope.activeRoutes[0]); viewCleanup($scope.openViews, [$scope.route, $scope.nextRoute, $scope.lastRoute].concat($scope.activePopups).concat(app.preloadedRoutes || [])); if (window.NativeNav) { window.NativeNav.setValidGestures(app.swipeNavForRoutes[url] || {}); } // this doesn't make sense in embedded mode if (!$element.hasClass("embedded")) { window.setTimeout(function() { var sp = app.scrollPosForRoutes[url]; if (sp) { window.scrollTo(sp.x, sp.y); } else { window.scrollTo(0, 0); } }, 10); } }); app.attrs = $attrs; app.element = $element; app.appCtrl($scope, a, b, c, d, e); app.init(); }; // end appCtrl app.module.controller("appCtrl", inj.concat([appCtrl])); } init() { console.log("App Init"); var app = this; if (window.NativeNav) { app.transitionMode = "native"; window.NativeNav.handleAction = function(route, action) { var routeScope = app.scopesForRoutes[route]; if (routeScope) { routeScope.$apply(action); } }; var gestureOpenedComponent = null; window.NativeNav.updateViewWithComponent = function(componentId) { // fill the frame with a side panel console.log("NativeNav requested component " + componentId); gestureOpenedComponent = app.sharedUiComponents[componentId]; if (gestureOpenedComponent.forceShow) gestureOpenedComponent.forceShow(gestureOpenedComponent); window.NativeNav.setCloseModalCallback(gestureOpenedComponent.scope.hideModal); app.scope.$apply(); }; window.NativeNav.updateViewWithRoute = function(newRoute) { // move to the next route console.log("NativeNav requested route " + newRoute); }; window.NativeNav.cancelGesture = function() { console.log("Gesture canceled"); if (gestureOpenedComponent) { if (gestureOpenedComponent.forceHide) gestureOpenedComponent.forceHide(gestureOpenedComponent); app.scope.$apply(); } }; } this.customInit(); } appCtrl($scope, $http) { console.log("App controller on core"); } getStartPage(route) { // start page may come from one of several places: // parameter set by calling customInit function // hash from url - optional. App may disable this if pages have to be accessed in a particular order. // attribute set in calling html // default from this.defaultStartPage if (route) return route; if ((this.hostSettings.useLocationHash !== undefined ? this.hostSettings.useLocationHash : this.options.useLocationHash) && location.hash.length > 1 && location.hash.indexOf("access_token=") < 0) { var allpaths = location.hash.substr(2).split(";"); var parts = allpaths[0].split("/"); for (var i = 0; i < parts.length; i++) { parts[i] = decodeURIComponent(parts[i]); } return parts; } if (this.startPageFromHtml) return this.startPageFromHtml; if (this.options.defaultStartPage) return this.options.defaultStartPage; return "/start"; } loadStartPage(route) { route = this.getStartPage(route); this.changePage(route, "none"); this.ready = true; var event = document.createEvent("HTMLEvents"); event.initEvent("chondric.appready", true, true); document.dispatchEvent(event); } start() { var app = this; this.initController(); angular.element(document).ready(function() { angular.bootstrap(document, [app.moduleName]); }); } }