From d58b0fbf980daa4efe51347a4cf6780e8899fb9b Mon Sep 17 00:00:00 2001 From: Kersten Lorenz Date: Mon, 17 Jul 2017 13:25:46 +0200 Subject: [PATCH] Init --- .editorconfig | 15 ++ .gitignore | 14 ++ config/helpers.js | 37 ++++ config/webpack.common.js | 190 ++++++++++++++++++ config/webpack.dev.js | 80 ++++++++ config/webpack.prod.js | 83 ++++++++ package.json | 104 ++++++++++ .../app-holder/app-holder.component.html | 6 + .../app-holder/app-holder.component.ts | 78 +++++++ .../app-holder/app-holder.controller.ts | 32 +++ src/app/components/app-holder/index.ts | 1 + src/app/components/index.ts | 2 + src/app/components/portal/index.ts | 1 + .../components/portal/portal.component.html | 25 +++ src/app/components/portal/portal.component.ts | 15 ++ .../components/portal/portal.controller.ts | 12 ++ src/app/index.ts | 9 + src/app/module-app.ts | 62 ++++++ src/app/module-portal.ts | 53 +++++ src/app/portal.states.ts | 16 ++ src/app/sample-app/app.component.html | 13 ++ src/app/sample-app/app.component.ts | 15 ++ src/app/sample-app/app.controller.ts | 5 + src/app/sample-app/app.states.ts | 29 +++ src/app/sample-app/index.ts | 2 + src/app/shared/component.decorator.ts | 8 + src/app/shared/convert-dash-to-camel-case.ts | 11 + src/app/shared/current-window-service.ts | 21 ++ src/app/shared/directive.decorator.ts | 8 + src/app/shared/index.ts | 9 + src/app/shared/navigation-command.enum.ts | 3 + src/app/shared/navigation-message.enum.ts | 3 + src/app/shared/onload.directive.ts | 37 ++++ src/app/shared/service.decorator.ts | 8 + src/app/shared/state-config.decorator.ts | 12 ++ src/assets/.gitkeep | 0 src/backend-mock/index.js | 21 ++ src/custom-typings.d.ts | 17 ++ src/iframe.html | 29 +++ src/index.html | 30 +++ src/main.ts | 3 + src/styles/index.scss | 1 + src/styles/portal/_index.scss | 1 + src/styles/portal/_portal-base.scss | 64 ++++++ tsconfig.common.json | 29 +++ tsconfig.json | 10 + tsconfig.webpack.json | 11 + tslint.json | 119 +++++++++++ 48 files changed, 1354 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 config/helpers.js create mode 100644 config/webpack.common.js create mode 100644 config/webpack.dev.js create mode 100644 config/webpack.prod.js create mode 100644 package.json create mode 100644 src/app/components/app-holder/app-holder.component.html create mode 100644 src/app/components/app-holder/app-holder.component.ts create mode 100644 src/app/components/app-holder/app-holder.controller.ts create mode 100644 src/app/components/app-holder/index.ts create mode 100644 src/app/components/index.ts create mode 100644 src/app/components/portal/index.ts create mode 100644 src/app/components/portal/portal.component.html create mode 100644 src/app/components/portal/portal.component.ts create mode 100644 src/app/components/portal/portal.controller.ts create mode 100644 src/app/index.ts create mode 100644 src/app/module-app.ts create mode 100644 src/app/module-portal.ts create mode 100644 src/app/portal.states.ts create mode 100644 src/app/sample-app/app.component.html create mode 100644 src/app/sample-app/app.component.ts create mode 100644 src/app/sample-app/app.controller.ts create mode 100644 src/app/sample-app/app.states.ts create mode 100644 src/app/sample-app/index.ts create mode 100644 src/app/shared/component.decorator.ts create mode 100644 src/app/shared/convert-dash-to-camel-case.ts create mode 100644 src/app/shared/current-window-service.ts create mode 100644 src/app/shared/directive.decorator.ts create mode 100644 src/app/shared/index.ts create mode 100644 src/app/shared/navigation-command.enum.ts create mode 100644 src/app/shared/navigation-message.enum.ts create mode 100644 src/app/shared/onload.directive.ts create mode 100644 src/app/shared/service.decorator.ts create mode 100644 src/app/shared/state-config.decorator.ts create mode 100644 src/assets/.gitkeep create mode 100644 src/backend-mock/index.js create mode 100644 src/custom-typings.d.ts create mode 100644 src/iframe.html create mode 100644 src/index.html create mode 100644 src/main.ts create mode 100644 src/styles/index.scss create mode 100644 src/styles/portal/_index.scss create mode 100644 src/styles/portal/_portal-base.scss create mode 100644 tsconfig.common.json create mode 100644 tsconfig.json create mode 100644 tsconfig.webpack.json create mode 100644 tslint.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ced7e20 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a73b4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +node_modules/ +/.idea +webpack.userconfig.json +generator.log +test-results +build/ +coverage/ +dist/ +test-results/ +dll/ +wars/ +target/ +doc/ +fallback-portal.config.ts diff --git a/config/helpers.js b/config/helpers.js new file mode 100644 index 0000000..339115e --- /dev/null +++ b/config/helpers.js @@ -0,0 +1,37 @@ +/** + * @author: @AngularClass + */ + +const path = require('path'); + +const EVENT = process.env.npm_lifecycle_event || ''; + +// Helper functions +const ROOT = path.resolve(__dirname, '..'); + +function hasProcessFlag(flag) { + return process.argv.join('').indexOf(flag) > -1; +} + +function hasNpmFlag(flag) { + return EVENT.includes(flag); +} + +function isWebpackDevServer() { + return process.argv[1] && !!(/webpack-dev-server/.exec(process.argv[1])); +} + +const root = path.join.bind(path, ROOT); + +const packageJson = require(path.join(ROOT, 'package.json')); +const moduleName = packageJson.name; +const appName = moduleName.split('.').pop(); +const currentVersion = packageJson.version; + +exports.hasProcessFlag = hasProcessFlag; +exports.hasNpmFlag = hasNpmFlag; +exports.isWebpackDevServer = isWebpackDevServer; +exports.moduleName = moduleName; +exports.appName = appName; +exports.currentVersion = currentVersion; +exports.root = root; diff --git a/config/webpack.common.js b/config/webpack.common.js new file mode 100644 index 0000000..2336880 --- /dev/null +++ b/config/webpack.common.js @@ -0,0 +1,190 @@ +const webpack = require('webpack'); +const helpers = require('./helpers'); + +const AssetsPlugin = require('assets-webpack-plugin'); +const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin'); +const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); +const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); + +const METADATA = { + baseUrl: '/aol/', + port: '9080', + isDevServer: helpers.isWebpackDevServer() +}; + +module.exports = function () { + return { + + devtool: 'source-map', + + resolve: { + extensions: ['.ts', '.js', '.json'], + modules: [helpers.root('src'), helpers.root('node_modules')], + + alias: { + angular: helpers.root('node_modules/angular/angular.min.js'), + 'angular-animate': helpers.root('node_modules/angular-animate/angular-animate.min.js'), + 'angular-messages': helpers.root('node_modules/angular-messages/angular-messages.min.js'), + 'angular-route': helpers.root('node_modules/angular-route/angular-route.min.js'), + 'angular-sanitize': helpers.root('node_modules/angular-sanitize/angular-sanitize.min.js'), + lodash: helpers.root('node_modules/lodash/lodash.min.js'), + } + }, + + module: { + + rules: [ + { + enforce: 'pre', + test: /\.js$/, + loader: 'source-map-loader', + exclude: [helpers.root('node_modules/@uirouter')] + }, + + // Add "module.exports = angular" to the end of the file + { + test: helpers.root('node_modules/angular/angular.min.js'), + use: [ + { + loader: 'exports-loader', + options: 'angular' + } + ] + }, + + { + test: /\.ts$/, + use: [ + { + loader: 'babel-loader', + options: { + plugins: [ + ['angularjs-annotate', {'explicitOnly': true}] + ] + } + }, + { + loader: 'ts-loader', + options: { + configFileName: 'tsconfig.webpack.json' + } + } + ], + exclude: [/\.(spec|e2e)\.ts$/] + }, + + + // JSON + { + test: /\.json$/, + use: 'json-loader' + }, + + // Angular templates + { + test: /\.ngtpl\.html$/, + use: [ + { + loader: 'ngtemplate-loader', + options: { + relativeTo: helpers.root() + } + }, + { + loader: 'html-loader', + options: { + minimize: true + } + } + ], + exclude: [helpers.root('src/index.html'), helpers.root('src/iframe.html')] + }, + + // HTML + { + test: /\.html$/, + use: [ + { + loader: 'html-loader', + options: { + minimize: true + } + } + ], + exclude: [helpers.root('src/index.html'), /\.ngtpl/, helpers.root('src/iframe.html')] + }, + + // Images + { + test: /\.(jpg|png|gif|svg)$/, + use: 'file-loader?name=assets/[name].[ext]' + }, + + // Fonts + { + test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, + use: 'url-loader?limit=10000&name=fonts/[name].[ext]&mimetype=application/font-woff' + }, + { + test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, + use: 'url-loader?limit=10000&name=fonts/[name].[ext]&mimetype=application/font-woff' + }, + { + test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, + use: 'url-loader?limit=10000&name=fonts/[name].[ext]&mimetype=application/octet-stream' + }, + { + test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, + use: 'file-loader?name=fonts/[name].[ext]' + }, + { + test: /(ionicons|glyphicons)+.*\.svg(\?v=\d+\.\d+\.\d+)?$/, + use: 'url-loader?limit=10000&name=fonts/[name].[ext]&mimetype=image/svg+xml' + }, + + ], + + }, + + plugins: [ + + new AssetsPlugin({ + path: helpers.root('dist'), + filename: 'webpack-assets.json', + prettyPrint: true + }), + + new CopyWebpackPlugin([ + {from: 'src/assets', to: 'assets'}, + ], { + ignore: [ + '.gitkeep' + ], + }), + + new HtmlWebpackPlugin({ + template: 'src/index.html', + chunksSortMode: 'dependency', + metadata: METADATA, + inject: 'head' + }), + + new HtmlWebpackPlugin({ + template: 'src/iframe.html', + filename: 'iframe-content.html', + chunksSortMode: 'dependency', + metadata: METADATA, + inject: 'head' + }), + + new ScriptExtHtmlWebpackPlugin({ + defaultAttribute: 'defer' + }), + + ], + + }; +}; diff --git a/config/webpack.dev.js b/config/webpack.dev.js new file mode 100644 index 0000000..1faf284 --- /dev/null +++ b/config/webpack.dev.js @@ -0,0 +1,80 @@ +const fs = require('fs'); +const helpers = require('./helpers'); +const webpackMerge = require('webpack-merge'); // used to merge webpack configs +const commonConfig = require('./webpack.common.js'); // the settings that are common to prod and dev + +const DefinePlugin = require('webpack/lib/DefinePlugin'); +const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin'); +const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const extractFrameworkCSS = new ExtractTextPlugin({ filename: 'framework.css', allChunks: true }); +const extractAppSCSS = new ExtractTextPlugin('app.css'); + +module.exports = function (env) { + return webpackMerge(commonConfig(), { + + entry: { + 'main': './src/main.ts', + }, + + output: { + path: helpers.root('dist'), + filename: '[name].bundle.js', + sourceMapFilename: '[file].map', + chunkFilename: '[id].chunk.js', + library: 'app_[name]', + libraryTarget: 'var', + }, + + module: { + rules: [ + { + test: /\.css$/, + use: extractFrameworkCSS.extract({ + fallback: 'style-loader', + use: ['css-loader?sourceMap=true', 'source-map-loader'] + }) + }, + { + test: /\.scss$/, + loader: extractAppSCSS.extract({ + fallback: 'style-loader', + use: ['css-loader', 'sass-loader'] + }) + }, + ] + }, + + plugins: [ + + // extracts imported CSS files into external stylesheet + extractFrameworkCSS, + extractAppSCSS, + + new DefinePlugin({ + 'process.env': { + 'NODE_ENV': JSON.stringify(env.NODE_ENV), + } + }), + + ], + + devServer: { + port: '9080', + contentBase: helpers.root('dist'), + publicPath: '/aol/', + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'accept, authorization, x-requested-with', + }, + proxy: { + '/aol/**': { + secure: false, + target: 'https://apa279002.system-a.local', + }, + } + }, + + }); +}; diff --git a/config/webpack.prod.js b/config/webpack.prod.js new file mode 100644 index 0000000..b798b01 --- /dev/null +++ b/config/webpack.prod.js @@ -0,0 +1,83 @@ +const helpers = require('./helpers'); +const webpackMerge = require('webpack-merge'); // used to merge webpack configs +const commonConfig = require('./webpack.common.js'); // the settings that are common to prod and dev + +const DefinePlugin = require('webpack/lib/DefinePlugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const IgnorePlugin = require('webpack/lib/IgnorePlugin'); +const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin'); +const ProvidePlugin = require('webpack/lib/ProvidePlugin'); +const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); +const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); +const CircularDependencyPlugin = require('circular-dependency-plugin'); +const WebpackMd5Hash = require('webpack-md5-hash'); + +module.exports = function (env) { + return webpackMerge(commonConfig(), { + + entry: { + 'vendor': [ + 'angular', + 'angular-messages', + 'angular-mocks', + 'angular-route', + 'angular-sanitize', + 'angular-animate', + 'angular-ui-bootstrap', + 'core-js', + 'lodash' + ], + 'main': './src/main.ts', + }, + + output: { + path: helpers.root('dist'), + filename: '[name].[chunkhash].bundle.js', + sourceMapFilename: '[file].map', + chunkFilename: '[id].[chunkhash].chunk.js' + }, + + module: { + rules: [ + { + test: /\.css$/, + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: ['css-loader?sourceMap=true', 'source-map-loader'] + }) + }, + { + test: /\.scss$/, + loader: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: ['css-loader', 'sass-loader'] + }) + }, + ] + }, + + plugins: [ + + new ExtractTextPlugin('[name].[contenthash].css'), + + new WebpackMd5Hash(), + + new DefinePlugin({ + 'process.env': { + 'NODE_ENV': JSON.stringify(env.NODE_ENV), + } + }), + + new CommonsChunkPlugin({ + names: ['vendor'] + }), + + new CircularDependencyPlugin({ + exclude: /a\.js|node_modules/, + failOnError: false + }), + + ] + + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4bd110f --- /dev/null +++ b/package.json @@ -0,0 +1,104 @@ +{ + "author": "Fabian Raetz ", + "Kersten Lorenz " + ], + "description": "aoAWL Portalrahmen", + "dependencies": { + "@uirouter/angularjs": "^1.0.5", + "angular": "^1.6.3", + "angular-animate": "^1.6.3", + "angular-i18n": "^1.6.3", + "angular-messages": "^1.6.3", + "angular-mocks": "^1.6.3", + "angular-route": "^1.6.3", + "angular-sanitize": "^1.6.3", + "core-js": "^2.4.1", + "lodash": "^4.17.4" + }, + "devDependencies": { + "@types/angular": "^1.6.9", + "@types/angular-animate": "^1.5.6", + "@types/angular-mocks": "^1.5.9", + "@types/angular-route": "^1.3.3", + "@types/angular-sanitize": "^1.3.4", + "@types/jasmine": "2.5.45", + "@types/jquery": "^3.2.1", + "@types/lodash": "^4.14.55", + "@types/node": "^7.0.8", + "@types/source-map": "^0.5.0", + "@types/uglify-js": "^2.6.28", + "@types/webpack": "^2.2.11", + "add-asset-html-webpack-plugin": "^1.0.2", + "assets-webpack-plugin": "^3.5.1", + "babel-core": "^6.24.1", + "babel-loader": "^6.4.1", + "babel-plugin-angularjs-annotate": "^0.7.0", + "circular-dependency-plugin": "^2.0.0", + "copy-webpack-plugin": "^4.0.1", + "css-loader": "^0.27.3", + "exports-loader": "^0.6.4", + "express": "^4.15.3", + "extract-text-webpack-plugin": "^2.1.0", + "file-loader": "^0.10.1", + "html-loader": "^0.4.5", + "html-webpack-plugin": "^2.28.0", + "istanbul-instrumenter-loader": "^2.0.0", + "live-server": "^1.2.0", + "ngtemplate-loader": "^1.3.1", + "node-sass": "^4.5.3", + "npm-run-all": "^4.0.2", + "post-robot": "^7.0.4", + "raw-loader": "^0.5.1", + "rimraf": "~2.6.1", + "sass-loader": "^6.0.3", + "script-ext-html-webpack-plugin": "^1.7.1", + "source-map-loader": "^0.2.0", + "style-loader": "^0.16.0", + "to-string-loader": "^1.1.5", + "ts-loader": "^2.0.3", + "tslib": "^1.6.0", + "tslint": "^4.5.1", + "typescript": "~2.4.0", + "url-loader": "^0.5.8", + "webpack": "^2.4.1", + "webpack-dev-server": "^2.4.2", + "webpack-md5-hash": "0.0.5", + "webpack-merge": "^4.1.0" + }, + "keywords": [], + "license": "UNLICENSED", + "main": "app/index.ts", + "name": "si.aol.portal", + "publishConfig": { + "registry": "http://m2repo.system.local/content/repositories/si-aol/" + }, + "repository": { + "type": "git", + "url": "http://git.system.local/scm/aol/si.aol.portal.git" + }, + "scripts": { + "build:dev": "npm run clean:dist && webpack --config config/webpack.dev.js --profile --env.NODE_ENV=development", + "build:staging": "npm run clean:dist && webpack --config config/webpack.prod.js --env.NODE_ENV=test --profile --bail", + "build:prod": "npm run clean:dist && webpack --config config/webpack.prod.js --env.NODE_ENV=production --profile --bail", + "build:spec": "npm run clean:dist && webpack --config config/webpack.karma.js --env.NODE_ENV=production --profile --bail", + "build": "npm run build:dev", + "clean:dist": "rimraf dist", + "clean:install": "npm set progress=false && npm install", + "clean:all": "npm cache clean && rimraf node_modules doc test-results coverage dist dll", + "preclean:install": "npm run clean:all", + "server:dev": "webpack-dev-server --config config/webpack.dev.js --profile --watch --env.NODE_ENV=development", + "serve:dist": "live-server dist --port=3000 --cors --open=/aol/ --mount=/aol:./dist", + "start": "npm run server:dev", + "watch:dev": "npm run build:dev -- --watch", + "watch:prod": "npm run build:prod -- --watch", + "watch:test": "npm run test -- --auto-watch --no-single-run", + "watch": "npm run watch:dev", + "mock-backend": "node src/backend-mock/index.js" + }, + "version": "0.0.1" +} diff --git a/src/app/components/app-holder/app-holder.component.html b/src/app/components/app-holder/app-holder.component.html new file mode 100644 index 0000000..edbd1e5 --- /dev/null +++ b/src/app/components/app-holder/app-holder.component.html @@ -0,0 +1,6 @@ +

UID: {{vm.uid}}

+ diff --git a/src/app/components/app-holder/app-holder.component.ts b/src/app/components/app-holder/app-holder.component.ts new file mode 100644 index 0000000..982120c --- /dev/null +++ b/src/app/components/app-holder/app-holder.component.ts @@ -0,0 +1,78 @@ +import * as robot from 'post-robot'; +import {Transition} from '@uirouter/core'; +import {Ng1StateDeclaration} from '@uirouter/angularjs'; + +import {AppHolderController} from './app-holder.controller'; +import * as template from './app-holder.component.html'; +import {portal} from '../../module-portal'; +import {StateConfig} from '../../shared/state-config.decorator'; +import {NavigationCommand} from '../../shared/navigation-command.enum'; +import {CurrentWindowService} from '../../shared/current-window-service'; +import {Component} from '../../shared/component.decorator'; + +export class AppHolder { + + @Component('si-app-holder', portal) + static create(): angular.IComponentOptions { + return { + controller: AppHolderController, + controllerAs: 'vm', + template: template, + bindings: { + src: '', + resolve: { + frameUrl: function () { + const host = 'http://localhost'; + const port = Math.floor(Math.random() * (15203 - 15201 + 1)) + 15201; + const content = 'static/iframe-content.html'; + return `${host}:${port}/${content}`; + } + }, + }; + } + + @StateConfig(portal) + static configureB(): Ng1StateDeclaration { + return { + name: 'ext.holder', + url: '/appz/:app', + component: 'siAppHolder', + params: {app: null}, + }; + } + + @StateConfig(portal) + static configureC(): Ng1StateDeclaration { + return { + name: 'ext.holder.internal', + url: '/*tail', + params: {tail: null}, + onEnter: function ($transition$: Transition, currentWindowService: CurrentWindowService) { + 'ngInject'; + + console.log('onEnter to', $transition$.to()); + console.log('onEnter from', $transition$.from()); + console.log('with', $transition$, $transition$.params('to'), $transition$.params('from'), currentWindowService.currentWindow); + + // propagate event only when running in same app + // because the addressed iframe will get unloaded + // the the app holder is responsible to trigger that event + if (currentWindowService.currentWindow && $transition$.params('to').app === $transition$.params('from').app) { + robot.send(currentWindowService.currentWindow, NavigationCommand.TRY_TO_NAVIGATE, {url: '/' + $transition$.params().tail}); + } + } + }; + } + +} diff --git a/src/app/components/app-holder/app-holder.controller.ts b/src/app/components/app-holder/app-holder.controller.ts new file mode 100644 index 0000000..0fc2906 --- /dev/null +++ b/src/app/components/app-holder/app-holder.controller.ts @@ -0,0 +1,32 @@ +import * as robot from 'post-robot'; +import {StateParams} from '@uirouter/angularjs'; +import {IController} from 'angular'; +import {NavigationCommand} from '../../shared/navigation-command.enum'; +import {CurrentWindowService} from '../../shared/current-window-service'; + +export class AppHolderController implements IController { + src: string; + uid = Math.random() * 10e6; + + constructor(private $stateParams: StateParams, + private currentWindowService: CurrentWindowService) { + 'ngInject'; + } + + $onInit() { + console.debug(`[AppHolderController] - try to load: ${this.src}`); + } + + onApplicationLoad(contentWindow: Window) { + console.debug(`[AppHolderController] - ${this.src} completely loaded`); + this.currentWindowService.currentWindow = contentWindow; + robot + .send(contentWindow, NavigationCommand.TRY_TO_NAVIGATE, {url: '/' + this.$stateParams.tail}) + .then(function (event) { + console.log('received:', event); + }) + .catch(function (err) { + console.error(`[AppHolderController] - ${NavigationCommand.TRY_TO_NAVIGATE} failed:`, err); + }); + } +} diff --git a/src/app/components/app-holder/index.ts b/src/app/components/app-holder/index.ts new file mode 100644 index 0000000..293a551 --- /dev/null +++ b/src/app/components/app-holder/index.ts @@ -0,0 +1 @@ +export * from './app-holder.component'; diff --git a/src/app/components/index.ts b/src/app/components/index.ts new file mode 100644 index 0000000..7724c51 --- /dev/null +++ b/src/app/components/index.ts @@ -0,0 +1,2 @@ +export * from './app-holder'; +export * from './portal'; diff --git a/src/app/components/portal/index.ts b/src/app/components/portal/index.ts new file mode 100644 index 0000000..29883c4 --- /dev/null +++ b/src/app/components/portal/index.ts @@ -0,0 +1 @@ +export * from './portal.component'; diff --git a/src/app/components/portal/portal.component.html b/src/app/components/portal/portal.component.html new file mode 100644 index 0000000..d3e9f98 --- /dev/null +++ b/src/app/components/portal/portal.component.html @@ -0,0 +1,25 @@ +
+ Header +
+ diff --git a/src/app/components/portal/portal.component.ts b/src/app/components/portal/portal.component.ts new file mode 100644 index 0000000..75d19b4 --- /dev/null +++ b/src/app/components/portal/portal.component.ts @@ -0,0 +1,15 @@ +import {PortalController} from './portal.controller'; +import {portal} from '../../module-portal'; +import * as template from './portal.component.html'; +import {Component} from '../../shared/component.decorator'; + +export class SiPortal { + @Component('si-portal', portal) + static create() { + return { + controller: PortalController, + controllerAs: 'vm', + template, + }; + } +} diff --git a/src/app/components/portal/portal.controller.ts b/src/app/components/portal/portal.controller.ts new file mode 100644 index 0000000..0535144 --- /dev/null +++ b/src/app/components/portal/portal.controller.ts @@ -0,0 +1,12 @@ +import {ILocationService} from 'angular'; +export class PortalController { + test = 'test'; + + constructor(private $location: ILocationService) { + 'ngInject'; + } + + changeLocation() { + this.$location.path('/app/hello/x'); + } +} diff --git a/src/app/index.ts b/src/app/index.ts new file mode 100644 index 0000000..037e1bb --- /dev/null +++ b/src/app/index.ts @@ -0,0 +1,9 @@ +// import 'si.aol.framework/dist/main.css'; +import '../styles/index.scss'; + +export * from './module-app'; +export * from './module-portal'; +export * from './components'; +export * from './sample-app'; +export * from './shared'; +export * from './portal.states'; diff --git a/src/app/module-app.ts b/src/app/module-app.ts new file mode 100644 index 0000000..1d0b94c --- /dev/null +++ b/src/app/module-app.ts @@ -0,0 +1,62 @@ +import * as angular from 'angular'; +import {ILocationProvider, ILocationService, IRootScopeService} from 'angular'; +import * as robot from 'post-robot'; +import {StateService, TransitionService, Transition} from '@uirouter/angularjs'; +import {NavigationCommand} from './shared/navigation-command.enum'; +import {NavigationMessage} from './shared/navigation-message.enum'; +robot.CONFIG.LOG_LEVEL = 'error'; + +export const appName = 'si.aol.app'; +export const app = angular.module(appName, ['ui.router']); + +// remove "!" prefix +app.config(function ($locationProvider: ILocationProvider) { + 'ngInject'; + + $locationProvider.hashPrefix(''); +}); + +app.run(function($rootScope: IRootScopeService) { + 'ngInject'; + + $rootScope.$on("$stateChangeError", console.log.bind(console)); +}); + +app.run(function ($transitions: TransitionService, + $window: angular.IWindowService, + $location: ILocationService) { + 'ngInject'; + + console.log(`[${appName}] - registering transition hooks`); + + $transitions.onSuccess({}, function (trans: Transition) { + console.log(`[${appName}] - transition: onSuccess`); + if ($window && $window.top !== $window) { + console.log(`[${appName}] - sending: ${NavigationMessage.APP_STATE_SUCCESS}`); + robot + .send($window.top, NavigationMessage.APP_STATE_SUCCESS, {state: trans.to(), absUrl: $location.absUrl(), path: $location.path(), url: $location.url()}) + .then(function (event) { + console.log('URL changed in Portal:', event); + }) + .catch(function (err) { + console.error(err); + }); + } + }); + +}); + +app.run(function ($state: StateService, $location: ILocationService, $rootScope: IRootScopeService) { + 'ngInject'; + + console.log(`[${appName}] - registering post message hooks`); + + robot.on(NavigationCommand.TRY_TO_NAVIGATE, function (event, callback) { + console.log(`[${appName}] - ${NavigationCommand.TRY_TO_NAVIGATE} occured:`, event, $location); + console.log('We are at URL:', $location.url(), 'and state:', $state.current); + console.log(`[${appName}] - trying to navigate to:`, event.data.url); + $location.url(event.data.url); + $rootScope.$apply(); + }); + +}); diff --git a/src/app/module-portal.ts b/src/app/module-portal.ts new file mode 100644 index 0000000..0ef706b --- /dev/null +++ b/src/app/module-portal.ts @@ -0,0 +1,53 @@ +import * as angular from 'angular'; +import * as robot from 'post-robot'; + +import {ILocationProvider, ILocationService, IRootScopeService} from 'angular'; +import {StateService, UIRouter} from '@uirouter/angularjs'; +import {NavigationMessage} from './shared/navigation-message.enum'; +robot.CONFIG.LOG_LEVEL = 'error'; + +export const portal = angular.module('si.aol.portal', ['ui.router']); + +portal.config(function ($sceDelegateProvider) { + 'ngInject'; + + $sceDelegateProvider.resourceUrlWhitelist([ + // Allow same origin resource loads. + 'self', + 'http://localhost:15201/**', + 'http://localhost:15202/**', + 'http://localhost:15203/**', + ]); +}); + +// remove "!" prefix +portal.config(function ($locationProvider: ILocationProvider) { + 'ngInject'; + console.log('Hello World!'); + $locationProvider.hashPrefix(''); +}); + +portal.run(function($rootScope: IRootScopeService) { + 'ngInject'; + + $rootScope.$on("$stateChangeError", console.log.bind(console)); +}); + +portal.run(function ($state: StateService, $location: ILocationService, $window: angular.IWindowService,) { + 'ngInject'; + + robot.on(NavigationMessage.APP_STATE_SUCCESS, function (event) { + //$location.path('fsadf/dfsdfsdf/fsdf'); + // const url = `${$location.protocol()}${$location.host()}${event.data.url}`; + //const url = `#/app/hello${event.data.url}`; + //$window.location.replace(url); + //$state.go('about'); + console.log('old path', $location.path()); + console.log(NavigationMessage.APP_STATE_SUCCESS, 'occured:', event.data.path); + // $location.path('/about'); + // $location.url(event.data.url); + console.log('new path', $location.path()); + // console.log('new path', event.source.location); + }); + +}); diff --git a/src/app/portal.states.ts b/src/app/portal.states.ts new file mode 100644 index 0000000..1c29e50 --- /dev/null +++ b/src/app/portal.states.ts @@ -0,0 +1,16 @@ +import {StateProvider} from '@uirouter/angularjs'; +import {portal} from './module-portal'; + +export default portal.config(function ($stateProvider: StateProvider) { + 'ngInject'; + + console.log('Configuring portal states'); + + const aboutState = { + name: 'about', + url: '/about', + template: '

This is a page of the portal!

' + }; + + $stateProvider.state(aboutState); +}); diff --git a/src/app/sample-app/app.component.html b/src/app/sample-app/app.component.html new file mode 100644 index 0000000..9a3c20e --- /dev/null +++ b/src/app/sample-app/app.component.html @@ -0,0 +1,13 @@ +
+
+ +
+
+

Hello Iframe!

+ +
+
diff --git a/src/app/sample-app/app.component.ts b/src/app/sample-app/app.component.ts new file mode 100644 index 0000000..203a485 --- /dev/null +++ b/src/app/sample-app/app.component.ts @@ -0,0 +1,15 @@ +import {AppController} from './app.controller'; +import * as template from './app.component.html'; +import {app} from '../module-app'; +import {Component} from '../shared/component.decorator'; + +export class SiApp { + @Component('si-app', app) + static create(): angular.IComponentOptions { + return { + controller: AppController, + controllerAs: 'vm', + template: template, + }; + } +} diff --git a/src/app/sample-app/app.controller.ts b/src/app/sample-app/app.controller.ts new file mode 100644 index 0000000..47bdbe0 --- /dev/null +++ b/src/app/sample-app/app.controller.ts @@ -0,0 +1,5 @@ +export class AppController { + constructor() { + 'ngInject'; + } +} diff --git a/src/app/sample-app/app.states.ts b/src/app/sample-app/app.states.ts new file mode 100644 index 0000000..830b2a5 --- /dev/null +++ b/src/app/sample-app/app.states.ts @@ -0,0 +1,29 @@ +import {app} from '../module-app'; + +export default app.config(function($stateProvider) { + 'ngInject'; + + console.log('Configuring app states'); + + const xState = { + name: 'x', + url: '/x', + template: '

X

' + }; + + const yState = { + name: 'y', + url: '/y', + template: '

Y

' + }; + + const zState = { + name: 'z', + url: '/z', + template: '

Z

' + }; + + $stateProvider.state(xState); + $stateProvider.state(yState); + $stateProvider.state(zState); +}); diff --git a/src/app/sample-app/index.ts b/src/app/sample-app/index.ts new file mode 100644 index 0000000..688e2f6 --- /dev/null +++ b/src/app/sample-app/index.ts @@ -0,0 +1,2 @@ +export * from './app.component'; +export * from './app.states'; diff --git a/src/app/shared/component.decorator.ts b/src/app/shared/component.decorator.ts new file mode 100644 index 0000000..ab6308b --- /dev/null +++ b/src/app/shared/component.decorator.ts @@ -0,0 +1,8 @@ +import {IModule} from 'angular'; +import {convertDashToCamelCase} from './convert-dash-to-camel-case'; + +export function Component(selector: string, module: IModule) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + module.component(convertDashToCamelCase(selector), descriptor.value()); + }; +} diff --git a/src/app/shared/convert-dash-to-camel-case.ts b/src/app/shared/convert-dash-to-camel-case.ts new file mode 100644 index 0000000..f388852 --- /dev/null +++ b/src/app/shared/convert-dash-to-camel-case.ts @@ -0,0 +1,11 @@ +const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; +const MOZ_HACK_REGEXP = /^moz([A-Z])/; + +/** + * Convert dash-case (kebab-case) to camelCase. + */ +export function convertDashToCamelCase(str: string) { + return str.replace(SPECIAL_CHARS_REGEXP, function (_, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }).replace(MOZ_HACK_REGEXP, 'Moz$1'); +} diff --git a/src/app/shared/current-window-service.ts b/src/app/shared/current-window-service.ts new file mode 100644 index 0000000..8be03d6 --- /dev/null +++ b/src/app/shared/current-window-service.ts @@ -0,0 +1,21 @@ +import {portal} from '../module-portal'; +import {Service} from './service.decorator'; + +@Service('currentWindowService', portal) +export class CurrentWindowService { + private window: Window; + + constructor() { + + } + + set currentWindow(window: Window) { + console.debug(`current window set changed`); + this.window = window; + } + + get currentWindow() { + return this.window; + } + +} diff --git a/src/app/shared/directive.decorator.ts b/src/app/shared/directive.decorator.ts new file mode 100644 index 0000000..2e9bfa4 --- /dev/null +++ b/src/app/shared/directive.decorator.ts @@ -0,0 +1,8 @@ +import {IModule} from 'angular'; +import {convertDashToCamelCase} from './convert-dash-to-camel-case'; + +export function Directive(selector: string, module: IModule) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + module.directive(convertDashToCamelCase(selector), descriptor.value); + }; +} diff --git a/src/app/shared/index.ts b/src/app/shared/index.ts new file mode 100644 index 0000000..37a760f --- /dev/null +++ b/src/app/shared/index.ts @@ -0,0 +1,9 @@ +export * from './onload.directive'; +export * from './navigation-command.enum'; +export * from './navigation-message.enum'; +export * from './current-window-service'; +export * from './state-config.decorator'; +export * from './directive.decorator'; +export * from './component.decorator'; +export * from './service.decorator'; +export * from './convert-dash-to-camel-case'; diff --git a/src/app/shared/navigation-command.enum.ts b/src/app/shared/navigation-command.enum.ts new file mode 100644 index 0000000..563b2e8 --- /dev/null +++ b/src/app/shared/navigation-command.enum.ts @@ -0,0 +1,3 @@ +export enum NavigationCommand { + TRY_TO_NAVIGATE = 'TRY_TO_NAVIGATE' +} diff --git a/src/app/shared/navigation-message.enum.ts b/src/app/shared/navigation-message.enum.ts new file mode 100644 index 0000000..6ca7094 --- /dev/null +++ b/src/app/shared/navigation-message.enum.ts @@ -0,0 +1,3 @@ +export enum NavigationMessage { + APP_STATE_SUCCESS = 'APP_STATE_SUCCESS' +} diff --git a/src/app/shared/onload.directive.ts b/src/app/shared/onload.directive.ts new file mode 100644 index 0000000..1e55b46 --- /dev/null +++ b/src/app/shared/onload.directive.ts @@ -0,0 +1,37 @@ +import * as angular from 'angular'; +import {IAugmentedJQuery, IScope} from 'angular'; + +import {portal} from '../module-portal'; +import {Directive} from './directive.decorator'; + +/** + * Binds a function to given HTML elements load event, for example an iframe. + * The load event is fired when a resource and its dependent resources have finished loading. + */ +export class SiOnload { + @Directive('si-onload', portal) + static create(): angular.IDirective { + 'ngInject'; + + return { + scope: { + callback: '&siOnload' + }, + link: (scope: IScope, element: IAugmentedJQuery) => { + const iframe = element[0]; + console.debug(`[${portal.name}] - linking onload directive`); + // do it only once (but each time) + element.one('load', () => { + // The contentWindow property returns the Window object of an