├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── build └── autoprefixer-options.js ├── config ├── environment.dev.ts ├── environment.js └── environment.prod.ts ├── firebase.json ├── gulpfile.js ├── package-lock.json ├── package.json ├── public └── empty-placeholder ├── scripts ├── browserstack │ ├── start_tunnel.js │ ├── start_tunnel.sh │ ├── teardown_tunnel.sh │ └── waitfor_tunnel.sh ├── ci │ ├── README.md │ ├── build-and-test.sh │ ├── forbidden-identifiers.js │ └── sources │ │ ├── mode.sh │ │ └── tunnel.sh ├── e2e.sh ├── release │ ├── changelog.js │ ├── enact-release.sh │ └── inline-resources.js └── sauce │ ├── sauce_config.js │ ├── sauce_connect_block.sh │ ├── sauce_connect_setup.sh │ └── sauce_connect_teardown.sh ├── src └── lib │ ├── calendar │ ├── README.md │ ├── calendar.html │ ├── calendar.scss │ ├── calendar.spec.ts │ ├── calendar.ts │ ├── package.json │ └── public_api.ts │ ├── masonry │ ├── masonry.scss │ ├── masonry.ts │ ├── package.json │ └── public_api.ts │ ├── module.d.ts │ ├── system-config-spec.ts │ ├── tsconfig-spec.json │ └── tsconfig.json ├── stylelint-config.json ├── test ├── browser-providers.ts ├── karma-test-shim.js ├── karma.conf.js ├── karma.config.ts └── protractor.conf.js ├── tools ├── gulp │ ├── constants.ts │ ├── gulpfile.ts │ ├── task_helpers.ts │ ├── tasks │ │ ├── ci.ts │ │ ├── clean.ts │ │ ├── components.ts │ │ ├── default.ts │ │ ├── development.ts │ │ ├── e2e.ts │ │ ├── lint.ts │ │ ├── release.ts │ │ ├── serve.ts │ │ └── unit-test.ts │ └── tsconfig.json └── travis │ └── fold.ts └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /typings 7 | /deploy 8 | 9 | # dependencies 10 | /node_modules 11 | /bower_components 12 | 13 | # Dart 14 | /.pub 15 | /.packages 16 | /packages 17 | /pubspec.lock 18 | /src/.pub 19 | /src/.packages 20 | /src/packages 21 | /src/pubspec.lock 22 | 23 | 24 | # IDEs 25 | /.idea 26 | /.vscode 27 | 28 | # misc 29 | .DS_Store 30 | /.sass-cache 31 | /connect.lock 32 | /coverage/* 33 | /libpeerconnection.log 34 | npm-debug.log 35 | testem.log 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | node_js: 5 | - '5.6.0' 6 | 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - libstdc++6 13 | 14 | branches: 15 | only: 16 | - master 17 | 18 | env: 19 | global: 20 | - LOGS_DIR=/tmp/ionic2-extra-build/logs 21 | - SAUCE_USERNAME=angular-ci 22 | - SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987 23 | - BROWSER_STACK_USERNAME=angularteam1 24 | - BROWSER_STACK_ACCESS_KEY=BWCd4SynLzdDcv8xtzsB 25 | - ARCH=linux-x64 26 | - BROWSER_PROVIDER_READY_FILE=/tmp/ionic2-extra-build/readyfile 27 | # GITHUB_TOKEN_ANGULAR 28 | - secure: "fq/U7VDMWO8O8SnAQkdbkoSe2X92PVqg4d044HmRYVmcf6YbO48+xeGJ8yOk0pCBwl3ISO4Q2ot0x546kxfiYBuHkZetlngZxZCtQiFT9kyId8ZKcYdXaIW9OVdw3Gh3tQyUwDucfkVhqcs52D6NZjyE2aWZ4/d1V4kWRO/LMgo=" 29 | matrix: 30 | # Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete. 31 | - MODE=lint 32 | - MODE=extract_metadata 33 | - MODE=e2e 34 | - MODE=saucelabs_required 35 | - MODE=browserstack_required 36 | - MODE=saucelabs_optional 37 | - MODE=browserstack_optional 38 | 39 | matrix: 40 | fast_finish: true 41 | allow_failures: 42 | - env: "MODE=saucelabs_optional" 43 | - env: "MODE=browserstack_optional" 44 | 45 | install: 46 | - npm install 47 | 48 | before_script: 49 | - mkdir -p $LOGS_DIR 50 | 51 | script: 52 | - ./scripts/ci/build-and-test.sh 53 | 54 | cache: 55 | directories: 56 | - node_modules 57 | - $HOME/.pub-cache 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Google, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /build/autoprefixer-options.js: -------------------------------------------------------------------------------- 1 | // Options for autoprefixer. See https://github.com/postcss/autoprefixer#options 2 | module.exports = { 3 | // To see the full list of supported browers, you can direcly use `browserslist` 4 | browsers: [ 5 | 'last 2 versions', 6 | 'not ie <= 10', 7 | 'not ie_mob <= 10', 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /config/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | }; 4 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | return { 5 | environment: environment, 6 | baseURL: '/', 7 | locationType: 'auto' 8 | }; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /config/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": "ionic2-extra-test", 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**", 8 | "tmp", 9 | "deploy", 10 | "typings" 11 | ], 12 | "headers": [{ 13 | "source": "*", 14 | "headers": [{ 15 | "key": "Cache-Control", 16 | "value": "no-cache" 17 | }] 18 | }], 19 | "rewrites": [{ 20 | "source": "/**/!(*.@(js|ts|html|css|json|svg|png|jpg|jpeg))", 21 | "destination": "/index.html" 22 | }] 23 | } 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Load the TypeScript compiler, then load the TypeScript gulpfile which simply loads all 4 | * the tasks. The tasks are really inside tools/gulp/tasks. 5 | */ 6 | 7 | const path = require('path'); 8 | 9 | // Register TS compilation. 10 | require('ts-node').register({ 11 | project: path.join(__dirname, 'tools/gulp') 12 | }); 13 | 14 | require('./tools/gulp/gulpfile'); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic2-extra", 3 | "description": "Extra widgets for Ionic 2", 4 | "homepage": "https://github.com/gnucoop/ionic2-extra", 5 | "bugs": "https://github.com/gnucoop/ionic2-extra/issues", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/gnucoop/ionic2-extra.git" 9 | }, 10 | "scripts": { 11 | "build": "gulp build:components", 12 | "build:prod": "gulp build:release", 13 | "demo-app": "gulp serve:devapp", 14 | "test": "gulp test", 15 | "tslint": "gulp lint", 16 | "stylelint": "gulp lint", 17 | "e2e": "gulp e2e", 18 | "deploy": "firebase deploy", 19 | "webdriver-manager": "webdriver-manager" 20 | }, 21 | "version": "0.2.3", 22 | "license": "MIT", 23 | "engines": { 24 | "node": ">= 4.2.1 < 5" 25 | }, 26 | "dependencies": { 27 | "@angular/animations": "5.0.1", 28 | "@angular/common": "5.0.1", 29 | "@angular/compiler": "5.0.1", 30 | "@angular/core": "5.0.1", 31 | "@angular/forms": "5.0.1", 32 | "@angular/http": "5.0.1", 33 | "@angular/platform-browser": "5.0.1", 34 | "@angular/platform-browser-dynamic": "5.0.1", 35 | "@angular/platform-server": "5.0.1", 36 | "@angular/router": "5.0.1", 37 | "@ngx-translate/core": "^9.1.1", 38 | "core-js": "^2.4.1", 39 | "hammerjs": "^2.0.8", 40 | "ionic-angular": "^3.9.2", 41 | "moment": "^2.22.1", 42 | "rxjs": "5.5.11", 43 | "systemjs": "~0.19.47", 44 | "zone.js": "0.8.18" 45 | }, 46 | "devDependencies": { 47 | "@angular/compiler-cli": "5.0.1", 48 | "@biesbjerg/ngx-translate-extract": "^2.3.4", 49 | "@ngx-translate/http-loader": "^2.0.1", 50 | "@types/glob": "^5.0.33", 51 | "@types/gulp": "^3.8.33", 52 | "@types/hammerjs": "^2.0.35", 53 | "@types/jasmine": "^2.6.0", 54 | "@types/merge2": "^1.1.2", 55 | "@types/minimist": "^1.2.0", 56 | "@types/node": "^8.0.28", 57 | "@types/run-sequence": "^0.0.29", 58 | "browserstacktunnel-wrapper": "^2.0.1", 59 | "conventional-changelog": "^1.1.6", 60 | "express": "^4.16.1", 61 | "firebase-tools": "^3.12.0", 62 | "fs-extra": "^4.0.2", 63 | "glob": "^7.1.2", 64 | "gulp": "^3.9.1", 65 | "gulp-clean": "^0.3.2", 66 | "gulp-sass": "^3.1.0", 67 | "gulp-server-livereload": "^1.9.2", 68 | "gulp-shell": "^0.6.3", 69 | "gulp-sourcemaps": "^2.6.1", 70 | "gulp-typescript": "^3.2.2", 71 | "jasmine-core": "^2.8.0", 72 | "karma": "^1.7.1", 73 | "karma-browserstack-launcher": "^1.3.0", 74 | "karma-chrome-launcher": "^2.2.0", 75 | "karma-firefox-launcher": "^1.0.1", 76 | "karma-jasmine": "^1.1.0", 77 | "karma-sauce-launcher": "^1.2.0", 78 | "madge": "^2.2.0", 79 | "merge2": "^1.2.0", 80 | "minimist": "^1.2.0", 81 | "node-sass": "^4.5.3", 82 | "protractor": "^5.1.2", 83 | "protractor-accessibility-plugin": "^0.3.0", 84 | "resolve-bin": "^0.4.0", 85 | "rollup": "^0.50.0", 86 | "run-sequence": "^2.2.0", 87 | "sass": "^1.0.0-beta.2", 88 | "selenium-webdriver": "^3.5.0", 89 | "strip-ansi": "^4.0.0", 90 | "stylelint": "^8.1.1", 91 | "symlink-or-copy": "^1.1.8", 92 | "ts-node": "^3.0.2", 93 | "tslint": "^5.7.0", 94 | "typescript": "~2.4.2", 95 | "which": "^1.2.14" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /public/empty-placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnucoop/ionic2-extra/ee57c24e8f986740947ca6f7614c1da799071761/public/empty-placeholder -------------------------------------------------------------------------------- /scripts/browserstack/start_tunnel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Start the BrowserStack tunnel. Once its started it creates a file so the callee can wait 4 | * for the tunnel to be started. 5 | */ 6 | 7 | var fs = require('fs'); 8 | var http = require('http'); 9 | var BrowserStackTunnel = require('browserstacktunnel-wrapper'); 10 | 11 | var HOSTNAME = 'localhost'; 12 | var PORTS = [9876, 9877]; 13 | var ACCESS_KEY = process.env.BROWSER_STACK_ACCESS_KEY; 14 | var READY_FILE = process.env.BROWSER_PROVIDER_READY_FILE; 15 | var TUNNEL_IDENTIFIER = process.env.TRAVIS_JOB_NUMBER; 16 | 17 | // We need to start fake servers, otherwise the tunnel does not start. 18 | var fakeServers = []; 19 | var hosts = []; 20 | 21 | PORTS.forEach(function(port) { 22 | fakeServers.push(http.createServer(function() {}).listen(port)); 23 | hosts.push({ 24 | name: HOSTNAME, 25 | port: port, 26 | sslFlag: 0 27 | }); 28 | }); 29 | 30 | var tunnel = new BrowserStackTunnel({ 31 | key: ACCESS_KEY, 32 | localIdentifier: TUNNEL_IDENTIFIER, 33 | hosts: hosts 34 | }); 35 | 36 | console.log('Starting tunnel on ports', PORTS.join(', ')); 37 | tunnel.start(function(error) { 38 | if (error) { 39 | console.error('Can not establish the tunnel', error); 40 | } else { 41 | console.log('Tunnel established.'); 42 | fakeServers.forEach(function(server) { 43 | server.close(); 44 | }); 45 | 46 | if (READY_FILE) { 47 | fs.writeFile(READY_FILE, ''); 48 | } 49 | } 50 | }); 51 | 52 | tunnel.on('error', function(error) { 53 | console.error(error); 54 | }); 55 | -------------------------------------------------------------------------------- /scripts/browserstack/start_tunnel.sh: -------------------------------------------------------------------------------- 1 | export BROWSER_STACK_ACCESS_KEY=`echo $BROWSER_STACK_ACCESS_KEY | rev` 2 | 3 | node ./scripts/browserstack/start_tunnel.js & 4 | -------------------------------------------------------------------------------- /scripts/browserstack/teardown_tunnel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | 6 | echo "Shutting down Browserstack tunnel" 7 | echo "TODO: implement me" 8 | exit 1 -------------------------------------------------------------------------------- /scripts/browserstack/waitfor_tunnel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # Wait for Connect to be ready before exiting 5 | # Time out if we wait for more than 2 minutes, so that we can print logs. 6 | let "counter=0" 7 | 8 | while [ ! -f $BROWSER_PROVIDER_READY_FILE ]; do 9 | let "counter++" 10 | if [ $counter -gt 240 ]; then 11 | echo "Timed out after 2 minutes waiting for browser provider ready file" 12 | # We must manually print logs here because travis will not run 13 | # after_script commands if the failure occurs before the script 14 | # phase. 15 | ./scripts/ci/print-logs.sh 16 | exit 5 17 | fi 18 | sleep .5 19 | done 20 | -------------------------------------------------------------------------------- /scripts/ci/README.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration Scripts 2 | 3 | This directory contains scripts that are related to CI only. 4 | 5 | ### `build-and-test.sh` 6 | 7 | The main script. Setup the tests environment, build the files using the `angular-cli` tool and run the tests. 8 | -------------------------------------------------------------------------------- /scripts/ci/build-and-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | echo "======= Starting build-and-test.sh ========================================" 5 | 6 | # Go to project dir 7 | cd $(dirname $0)/../.. 8 | 9 | # Include sources. 10 | source scripts/ci/sources/mode.sh 11 | source scripts/ci/sources/tunnel.sh 12 | 13 | start_tunnel 14 | 15 | wait_for_tunnel 16 | if is_lint; then 17 | $(npm bin)/gulp ci:lint 18 | elif is_e2e; then 19 | $(npm bin)/gulp ci:e2e 20 | elif is_extract_metadata; then 21 | $(npm bin)/gulp ci:extract-metadata 22 | else 23 | $(npm bin)/gulp ci:test 24 | fi 25 | teardown_tunnel 26 | -------------------------------------------------------------------------------- /scripts/ci/forbidden-identifiers.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /* 6 | * The forbidden identifiers script will check for blocked statements and also detect invalid 7 | * imports of other scope packages. 8 | * 9 | * When running against a PR, the script will only analyze the specific amount of commits inside 10 | * of the Pull Request. 11 | * 12 | * By default it checks all source files and fail if any errors were found. 13 | */ 14 | 15 | const child_process = require('child_process'); 16 | const fs = require('fs'); 17 | const path = require('path'); 18 | const glob = require('glob').sync; 19 | 20 | const blocked_statements = [ 21 | '\\bddescribe\\(', 22 | '\\bfdescribe\\(', 23 | '\\biit\\(', 24 | '\\bfit\\(', 25 | '\\bxdescribe\\(', 26 | '\\bxit\\(', 27 | '\\bdebugger;', 28 | 'from \\\'rxjs/Rx\\\'', 29 | ]; 30 | 31 | const sourceFolders = ['./src', './e2e']; 32 | const scopePackages = glob('src/lib/*'); 33 | const blockedRegex = new RegExp(blocked_statements.join('|'), 'g'); 34 | const importRegex = /from\s+'(.*)';/g; 35 | 36 | /* 37 | * Verify that the current PR is not adding any forbidden identifiers. 38 | * Run the forbidden identifiers check against all sources when not verifying a PR. 39 | */ 40 | 41 | findTestFiles() 42 | 43 | /* Only match .js or .ts, and remove .d.ts files. */ 44 | .then(files => files.filter(name => /\.(js|ts)$/.test(name) && !/\.d\.ts$/.test(name))) 45 | 46 | /* Read content of the filtered files */ 47 | .then(files => files.map(name => ({ name, content: fs.readFileSync(name, 'utf-8') }))) 48 | 49 | /* Run checks against content of the filtered files. */ 50 | .then(diffList => { 51 | 52 | return diffList.reduce((errors, diffFile) => { 53 | let fileName = diffFile.name; 54 | let content = diffFile.content.split('\n'); 55 | let lineCount = 0; 56 | 57 | content.forEach(line => { 58 | lineCount++; 59 | 60 | let matches = line.match(blockedRegex); 61 | let scopeImport = isRelativeScopeImport(fileName, line); 62 | 63 | if (matches || scopeImport) { 64 | 65 | let error = { 66 | fileName: fileName, 67 | lineNumber: lineCount, 68 | statement: matches && matches[0] || scopeImport.invalidStatement 69 | }; 70 | 71 | if (scopeImport) { 72 | error.messages = [ 73 | 'You are using an import statement, which imports a file from another scope package.', 74 | `Please import the file by using the following path: ${scopeImport.scopeImportPath}` 75 | ]; 76 | } 77 | 78 | errors.push(error); 79 | } 80 | }); 81 | 82 | return errors; 83 | 84 | }, []); 85 | }) 86 | 87 | /* Print the resolved errors to the console */ 88 | .then(errors => { 89 | if (errors.length > 0) { 90 | console.error('Error: You are using one or more blocked statements:\n'); 91 | 92 | errors.forEach(entry => { 93 | if (entry.messages) { 94 | entry.messages.forEach(message => console.error(` ${message}`)) 95 | } 96 | 97 | console.error(` ${entry.fileName}@${entry.lineNumber}, Statement: ${entry.statement}.\n`); 98 | }); 99 | 100 | process.exit(1); 101 | } 102 | }) 103 | 104 | .catch(err => { 105 | // An error occurred in this script. Output the error and the stack. 106 | console.error('An error occurred during execution:'); 107 | console.error(err.stack || err); 108 | process.exit(2); 109 | }); 110 | 111 | 112 | /** 113 | * Resolves all files, which should run against the forbidden identifiers check. 114 | * @return {Promise.>} Files to be checked. 115 | */ 116 | function findTestFiles() { 117 | if (process.env['TRAVIS_PULL_REQUEST']) { 118 | return findChangedFiles(); 119 | } 120 | 121 | var files = sourceFolders.map(folder => { 122 | return glob(`${folder}/**/*.ts`); 123 | }).reduce((files, fileSet) => files.concat(fileSet), []); 124 | 125 | return Promise.resolve(files); 126 | } 127 | 128 | /** 129 | * List all the files that have been changed or added in the last commit range. 130 | * @returns {Promise.>} Resolves with a list of files that are added or changed. 131 | */ 132 | function findChangedFiles() { 133 | let commitRange = process.env['TRAVIS_COMMIT_RANGE']; 134 | 135 | return exec(`git diff --name-status ${commitRange} ${sourceFolders.join(' ')}`) 136 | .then(rawDiff => { 137 | return rawDiff 138 | .split('\n') 139 | .filter(line => { 140 | // Status: C## => Copied (##% confident) 141 | // R## => Renamed (##% confident) 142 | // D => Deleted 143 | // M => Modified 144 | // A => Added 145 | return line.match(/([CR][0-9]*|[AM])\s+/); 146 | }) 147 | .map(line => line.split(/\s+/, 2)[1]); 148 | }); 149 | } 150 | 151 | /** 152 | * Checks the line for any relative imports of a scope package, which should be imported by using 153 | * the scope package name instead of the relative path. 154 | * @param fileName Filename to validate the path 155 | * @param line Line to be checked. 156 | */ 157 | function isRelativeScopeImport(fileName, line) { 158 | let importMatch = importRegex.exec(line); 159 | 160 | // We have to reset the last index of the import regex, otherwise we 161 | // would have incorrect matches in the next execution. 162 | importRegex.lastIndex = 0; 163 | 164 | // Skip the check if the current line doesn't match any imports. 165 | if (!importMatch) { 166 | return false; 167 | } 168 | 169 | let importPath = importMatch[1]; 170 | 171 | // Skip the check when the import doesn't start with a dot, because the line 172 | // isn't importing any file relatively. Also applies to scope packages starting 173 | // with `@`. 174 | if (!importPath.startsWith('.')) { 175 | return false; 176 | } 177 | 178 | // Transform the relative import path into an absolute path. 179 | importPath = path.resolve(path.dirname(fileName), importPath); 180 | 181 | let fileScope = findScope(fileName); 182 | let importScope = findScope(importPath); 183 | 184 | if (fileScope.path !== importScope.path) { 185 | 186 | // Creates a valid import statement, which uses the correct scope package. 187 | let importFilePath = path.relative(importScope.path, importPath); 188 | let validImportPath = `@ionic2-extra/${importScope.name}/${importFilePath}`; 189 | 190 | return { 191 | fileScope: fileScope.name, 192 | importScope: importScope.name, 193 | invalidStatement: importMatch[0], 194 | scopeImportPath: validImportPath 195 | } 196 | } 197 | 198 | function findScope(filePath) { 199 | // Normalize the filePath, to avoid issues with the different environments path delimiter. 200 | filePath = path.normalize(filePath); 201 | 202 | // Iterate through all scope paths and try to find them inside of the file path. 203 | var scopePath = scopePackages 204 | .filter(scope => filePath.indexOf(path.normalize(scope)) !== -1) 205 | .pop(); 206 | 207 | // Return an object containing the name of the scope and the associated path. 208 | return { 209 | name: path.basename(scopePath), 210 | path: scopePath 211 | } 212 | } 213 | 214 | } 215 | 216 | /** 217 | * Executes a process command and wraps it inside of a promise. 218 | * @returns {Promise.} 219 | */ 220 | function exec(cmd) { 221 | return new Promise(function(resolve, reject) { 222 | child_process.exec(cmd, function(err, stdout /*, stderr */) { 223 | if (err) { 224 | reject(err); 225 | } else { 226 | resolve(stdout); 227 | } 228 | }); 229 | }); 230 | } 231 | -------------------------------------------------------------------------------- /scripts/ci/sources/mode.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source ./scripts/ci/sources/tunnel.sh 3 | 4 | is_e2e() { 5 | [[ "$MODE" = e2e ]] 6 | } 7 | 8 | is_lint() { 9 | [[ "$MODE" = lint ]] 10 | } 11 | 12 | is_extract_metadata() { 13 | [[ "$MODE" = extract_metadata ]] 14 | } 15 | -------------------------------------------------------------------------------- /scripts/ci/sources/tunnel.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | start_tunnel() { 5 | case "$MODE" in 6 | e2e*|saucelabs*) 7 | ./scripts/sauce/sauce_connect_setup.sh 8 | ;; 9 | browserstack*) 10 | ./scripts/browserstack/start_tunnel.sh 11 | ;; 12 | *) 13 | ;; 14 | esac 15 | } 16 | 17 | wait_for_tunnel() { 18 | case "$MODE" in 19 | e2e*|saucelabs*) 20 | ./scripts/sauce/sauce_connect_block.sh 21 | ;; 22 | browserstack*) 23 | ./scripts/browserstack/waitfor_tunnel.sh 24 | ;; 25 | *) 26 | ;; 27 | esac 28 | sleep 10 29 | } 30 | 31 | teardown_tunnel() { 32 | case "$MODE" in 33 | e2e*|saucelabs*) 34 | ./scripts/sauce/sauce_connect_teardown.sh 35 | ;; 36 | browserstack*) 37 | # ./scripts/browserstack/teardown_tunnel.sh 38 | ;; 39 | *) 40 | ;; 41 | esac 42 | } 43 | 44 | -------------------------------------------------------------------------------- /scripts/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export MODE=e2e 4 | export LOGS_DIR=/tmp/angular-material2-build/logs 5 | export SAUCE_USERNAME=angular-ci 6 | export SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987 7 | export TRAVIS_JOB_NUMBER=12345 8 | export BROWSER_PROVIDER_READY_FILE=/tmp/angular-material2-build/readyfile 9 | 10 | 11 | mkdir -p $LOGS_DIR 12 | rm -f $BROWSER_PROVIDER_READY_FILE 13 | 14 | # Force cleanup (shouldn't be necessary) 15 | killall angular-cli 16 | ./scripts/sauce/sauce_connect_teardown.sh 17 | # Run the script 18 | ./scripts/ci/build-and-test.sh 19 | # Actual cleanup 20 | ./scripts/sauce/sauce_connect_teardown.sh 21 | killall angular-cli 22 | -------------------------------------------------------------------------------- /scripts/release/changelog.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /* 6 | * Creates a conventional changelog from the current git repository / metadata. 7 | */ 8 | 9 | const fs = require('fs'); 10 | const addStream = require('add-stream'); 11 | const changelog = require('conventional-changelog'); 12 | const spawnSync = require('child_process').spawnSync; 13 | const npmVersion = require('../../package.json').version; 14 | 15 | /** 16 | * When the command line argument `--force` is provided, then the full changelog will created and overwritten. 17 | * By default, it will only create the changelog from the latest tag to head and prepends it to the changelog. 18 | */ 19 | const isForce = process.argv.indexOf('--force') !== -1; 20 | const inStream = fs.createReadStream('CHANGELOG.md'); 21 | const gitTags = getAvailableTags(); 22 | 23 | // Whether the npm version is later than the most recent tag. 24 | const isNpmLatest = npmVersion !== gitTags[0]; 25 | // When the npm version is the latest, use the npm version, otherwise use the latest tag. 26 | const currentTag = isNpmLatest ? npmVersion : gitTags[0]; 27 | // When the npm version is the latest use the most recent tag. Otherwise use the previous tag. 28 | const previousTag = isNpmLatest ? gitTags[0] : gitTags[1]; 29 | 30 | inStream.on('error', function(err) { 31 | console.error('An error occurred, while reading the previous changelog file.\n' + 32 | 'If there is no previous changelog, then you should create an empty file or use the `--force` flag.\n' + err); 33 | 34 | process.exit(1); 35 | }); 36 | 37 | var config = { 38 | preset: 'angular', 39 | releaseCount: isForce ? 0 : 1 40 | }; 41 | 42 | var context = { 43 | currentTag: currentTag, 44 | previousTag: previousTag 45 | }; 46 | 47 | var stream = changelog(config, context) 48 | .on('error', function(err) { 49 | console.error('An error occurred while generating the changelog: ' + err); 50 | }) 51 | .pipe(!isForce && addStream(inStream) || getOutputStream()); 52 | 53 | // When we are pre-pending the new changelog, then we need to wait for the input stream to be ending, 54 | // otherwise we can't write into the same destination. 55 | if (!isForce) { 56 | inStream.on('end', function() { 57 | stream.pipe(getOutputStream()); 58 | }); 59 | } 60 | 61 | function getOutputStream() { 62 | return fs.createWriteStream('CHANGELOG.md'); 63 | } 64 | 65 | /** 66 | * Resolves available tags over all branches from the repository metadata. 67 | * @returns {Array.} Array of available tags. 68 | */ 69 | function getAvailableTags() { 70 | return spawnSync('git', ['tag']).stdout.toString().trim().split('\n').reverse(); 71 | } 72 | -------------------------------------------------------------------------------- /scripts/release/enact-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run this script after running `stage-release.sh` to publish the packages staged to deploy/ 4 | # Optionally uses the first argument as the tag for the release (such as "next"). 5 | # This script should be run from the root of the ionic2-extra repo. 6 | 7 | 8 | # `npm whoami` errors and dies if you're not logged in, 9 | # so we redirect the stderr output to /dev/null since we don't care. 10 | NPM_USER=$(npm whoami 2> /dev/null) 11 | 12 | if [ "${NPM_USER}" != "ionic2-extra" ]; then 13 | echo "You must be logged in as 'ionic2-extra' to publish. Use 'npm login'." 14 | exit 15 | fi 16 | 17 | NPM_TAG="latest" 18 | if [ "$1" ] ; then 19 | NPM_TAG=${1} 20 | fi 21 | 22 | set -ex 23 | 24 | for package in ./deploy/* ; do 25 | npm publish --access public ${package} --tag ${NPM_TAG} 26 | done 27 | 28 | # Always log out of npm when publish is complete. 29 | npm logout 30 | -------------------------------------------------------------------------------- /scripts/release/inline-resources.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const glob = require('glob'); 7 | 8 | /** 9 | * Simple Promiseify function that takes a Node API and return a version that supports promises. 10 | * We use promises instead of synchronized functions to make the process less I/O bound and 11 | * faster. It also simplify the code. 12 | */ 13 | function promiseify(fn) { 14 | return function() { 15 | const args = [].slice.call(arguments, 0); 16 | return new Promise((resolve, reject) => { 17 | fn.apply(this, args.concat([function (err, value) { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | resolve(value); 22 | } 23 | }])); 24 | }); 25 | }; 26 | } 27 | 28 | const readFile = promiseify(fs.readFile); 29 | const writeFile = promiseify(fs.writeFile); 30 | 31 | 32 | function inlineResources(globs) { 33 | /** 34 | * For every argument, inline the templates and styles under it and write the new file. 35 | */ 36 | for (let pattern of globs) { 37 | if (pattern.indexOf('*') < 0) { 38 | // Argument is a directory target, add glob patterns to include every files. 39 | pattern = path.join(pattern, '**', '*'); 40 | } 41 | 42 | const files = glob.sync(pattern, {}) 43 | .filter(name => /\.js$/.test(name)); // Matches only JavaScript files. 44 | // Generate all files content with inlined templates. 45 | files.forEach(filePath => { 46 | readFile(filePath, 'utf-8') 47 | .then(content => inlineTemplate(filePath, content)) 48 | .then(content => inlineStyle(filePath, content)) 49 | .then(content => removeModuleId(filePath, content)) 50 | .then(content => writeFile(filePath, content)) 51 | .catch(err => { 52 | console.error('An error occured: ', err); 53 | }); 54 | }); 55 | } 56 | } 57 | 58 | if (require.main === module) { 59 | inlineResources(process.argv.slice(2)); 60 | } 61 | 62 | 63 | /** 64 | * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and 65 | * replace with `template: ...` (with the content of the file included). 66 | * @param filePath {string} The path of the source file. 67 | * @param content {string} The source file's content. 68 | * @return {string} The content with all templates inlined. 69 | */ 70 | function inlineTemplate(filePath, content) { 71 | return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function(m, templateUrl) { 72 | const templateFile = path.join(path.dirname(filePath), templateUrl); 73 | const templateContent = fs.readFileSync(templateFile, 'utf-8'); 74 | const shortenedTemplate = templateContent 75 | .replace(/([\n\r]\s*)+/gm, ' ') 76 | .replace(/"/g, '\\"'); 77 | return `template: "${shortenedTemplate}"`; 78 | }); 79 | } 80 | 81 | 82 | /** 83 | * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and 84 | * replace with `styles: [...]` (with the content of the file included). 85 | * @param filePath {string} The path of the source file. 86 | * @param content {string} The source file's content. 87 | * @return {string} The content with all styles inlined. 88 | */ 89 | function inlineStyle(filePath, content) { 90 | return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function(m, styleUrls) { 91 | const urls = eval(styleUrls); 92 | return 'styles: [' 93 | + urls.map(styleUrl => { 94 | const styleFile = path.join(path.dirname(filePath), styleUrl); 95 | const styleContent = fs.readFileSync(styleFile, 'utf-8'); 96 | const shortenedStyle = styleContent 97 | .replace(/([\n\r]\s*)+/gm, ' ') 98 | .replace(/"/g, '\\"'); 99 | return `"${shortenedStyle}"`; 100 | }) 101 | .join(',\n') 102 | + ']'; 103 | }); 104 | } 105 | 106 | 107 | /** 108 | * Remove every mention of `moduleId: module.id`. 109 | * @param _ {string} The file path of the source file, currently ignored. 110 | * @param content {string} The source file's content. 111 | * @returns {string} The content with all moduleId: mentions removed. 112 | */ 113 | function removeModuleId(_, content) { 114 | return content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, ''); 115 | } 116 | 117 | 118 | module.exports = inlineResources; 119 | -------------------------------------------------------------------------------- /scripts/sauce/sauce_config.js: -------------------------------------------------------------------------------- 1 | module.exports = process.env.SAUCE_ACCESS_KEY.split('').reverse().join(''); 2 | -------------------------------------------------------------------------------- /scripts/sauce/sauce_connect_block.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Wait for Connect to be ready before exiting 4 | echo "Connecting to Sauce Labs" 5 | while [ ! -f $BROWSER_PROVIDER_READY_FILE ]; do 6 | printf "." 7 | sleep .5 8 | done 9 | echo 10 | echo "Connected" 11 | -------------------------------------------------------------------------------- /scripts/sauce/sauce_connect_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | # Setup and start Sauce Connect for your TravisCI build 6 | # This script requires your .travis.yml to include the following two private env variables: 7 | # SAUCE_USERNAME 8 | # SAUCE_ACCESS_KEY 9 | # Follow the steps at https://saucelabs.com/opensource/travis to set that up. 10 | # 11 | # Curl and run this script as part of your .travis.yml before_script section: 12 | # before_script: 13 | # - curl https://gist.github.com/santiycr/5139565/raw/sauce_connect_setup.sh | bash 14 | 15 | CONNECT_DIR="/tmp/sauce-connect-$RANDOM" 16 | CONNECT_DOWNLOAD="sc-latest-linux.tar.gz" 17 | 18 | CONNECT_LOG="$LOGS_DIR/sauce-connect" 19 | CONNECT_STDOUT="$LOGS_DIR/sauce-connect.stdout" 20 | CONNECT_STDERR="$LOGS_DIR/sauce-connect.stderr" 21 | 22 | # Get the appropriate URL for downloading Sauce Connect 23 | if [ `uname -s` = "Darwin" ]; then 24 | # If the user is running Mac, download the OSX version 25 | # https://en.wikipedia.org/wiki/Darwin_(operating_system) 26 | CONNECT_URL="https://saucelabs.com/downloads/sc-4.3.11-osx.zip" 27 | else 28 | # Otherwise, default to Linux for Travis-CI 29 | CONNECT_URL="https://saucelabs.com/downloads/sc-4.3.11-linux.tar.gz" 30 | fi 31 | mkdir -p $CONNECT_DIR 32 | cd $CONNECT_DIR 33 | curl $CONNECT_URL -o $CONNECT_DOWNLOAD 2> /dev/null 1> /dev/null 34 | mkdir sauce-connect 35 | tar --extract --file=$CONNECT_DOWNLOAD --strip-components=1 --directory=sauce-connect > /dev/null 36 | rm $CONNECT_DOWNLOAD 37 | 38 | 39 | SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY | rev` 40 | 41 | ARGS="" 42 | 43 | # Set tunnel-id only on Travis, to make local testing easier. 44 | if [ ! -z "$TRAVIS_JOB_NUMBER" ]; then 45 | ARGS="$ARGS --tunnel-identifier $TRAVIS_JOB_NUMBER" 46 | fi 47 | if [ ! -z "$BROWSER_PROVIDER_READY_FILE" ]; then 48 | ARGS="$ARGS --readyfile $BROWSER_PROVIDER_READY_FILE" 49 | fi 50 | 51 | 52 | echo "Starting Sauce Connect in the background, logging into:" 53 | echo " $CONNECT_LOG" 54 | echo " $CONNECT_STDOUT" 55 | echo " $CONNECT_STDERR" 56 | echo " ---" 57 | echo " $ARGS" 58 | sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY $ARGS \ 59 | --logfile $CONNECT_LOG 2> $CONNECT_STDERR 1> $CONNECT_STDOUT & 60 | -------------------------------------------------------------------------------- /scripts/sauce/sauce_connect_teardown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | 6 | echo "Shutting down Sauce Connect tunnel" 7 | 8 | killall sc 9 | 10 | while [[ -n `ps -ef | grep "sauce-connect-" | grep -v "grep"` ]]; do 11 | printf "." 12 | sleep .5 13 | done 14 | 15 | echo "" 16 | echo "Sauce Connect tunnel has been shut down" 17 | -------------------------------------------------------------------------------- /src/lib/calendar/README.md: -------------------------------------------------------------------------------- 1 | # ion-calendar 2 | 3 | `ion-calendar` is a simple calendar widget that allows users to 4 | select pick single date or date ranges (weeks, months, years) 5 | 6 | 7 | ## Notes 8 | 9 | The `ion-calendar` component fully support two-way binding of 10 | `ngModel` 11 | 12 | 13 | ## Usage 14 | 15 | ### Basic Usage 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | 22 | ### Usage within Forms 23 | 24 | `ion-calendar` supports `[(ngModel)]` and `ngControl` for use within 25 | forms. 26 | 27 | ```html 28 |
29 | 30 |
31 | 32 |
33 |
34 | ``` 35 | 36 | 37 | ### API Summary 38 | 39 | Properties: 40 | 41 | | Name | Type | Description | 42 | | --- | --- | --- | 43 | | `selection-mode` | `"day" | "week" | "month" | "year"` | The range of dates selected when user clicks on a date 44 | | `view-date` | Date | The initial date displayed. Default to current date 45 | | `view-mode` | `"month" | "year" | "decade"` | The initial view mode. Default to `"month"` 46 | | `start-of-week-day` | `"monday" | "tuesday" | "wednesday" | "thursday" | "friday"| "satudary" | "sunday"` | The first day of week. Default to `"monday"` 47 | | `disabled` | boolean | Whether or not the button is disabled -------------------------------------------------------------------------------- /src/lib/calendar/calendar.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 |
8 |
9 |
10 |
{{ weekDay | translate }}
11 |
12 |
13 |
14 | 23 |
24 | -------------------------------------------------------------------------------- /src/lib/calendar/calendar.scss: -------------------------------------------------------------------------------- 1 | $ion-calendar-row-height: 40px; 2 | $ion-calendar-height: $ion-calendar-row-height * 8; 3 | 4 | ion-calendar { 5 | display: flex; 6 | 7 | box-sizing: border-box; 8 | 9 | width: 100%; 10 | height: $ion-calendar-height; 11 | 12 | flex-direction: column; 13 | 14 | .ion-calendar-header, .ion-calendar-row { 15 | display: flex; 16 | box-sizing: border-box; 17 | 18 | width: 100%; 19 | 20 | flex-direction: row; 21 | } 22 | 23 | .ion-calendar-header { 24 | height: $ion-calendar-row-height; 25 | 26 | button[ion-fab] { 27 | width: $ion-calendar-row-height; 28 | height: $ion-calendar-row-height; 29 | margin: 0; 30 | position: relative; 31 | left: 0; 32 | right: 0; 33 | } 34 | .ion-calendar-header-title { 35 | flex: 1; 36 | margin: 0 10px; 37 | } 38 | } 39 | 40 | .ion-calendar-row { 41 | flex: 1; 42 | 43 | button, div { 44 | flex: 1; 45 | margin: 3px; 46 | height: auto; 47 | } 48 | 49 | div { 50 | line-height: $ion-calendar-row-height; 51 | text-align: center; 52 | } 53 | 54 | .ion-calendar-partial-selection { 55 | ::before { 56 | content: ''; 57 | position: absolute; 58 | top: 0; 59 | right: 0; 60 | bottom: 0; 61 | left: 0; 62 | background-color: rgba(255, 255, 255, 0.5); 63 | } 64 | } 65 | 66 | .ion-calendar-highlight { 67 | background-color: #fcd739; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/calendar/calendar.spec.ts: -------------------------------------------------------------------------------- 1 | /*import { 2 | async, 3 | beforeEachProviders, 4 | describe, 5 | it, 6 | expect, 7 | beforeEach, 8 | fakeAsync, 9 | inject 10 | } from '@angular/core/testing'; 11 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 12 | import { Component } from '@angular/core'; 13 | import { By } from '@angular/platform-browser'; 14 | import { 15 | disableDeprecatedForms, 16 | REACTIVE_FORM_DIRECTIVES, 17 | provideForms 18 | } from '@angular/forms'; 19 | import { 20 | IonCalendar, 21 | IonCalendarPeriod, 22 | ION_CALENDAR_DIRECTIVES 23 | } from './calendar'; 24 | 25 | import * as moment from 'moment'; 26 | 27 | describe('IonCalendar', () => { 28 | let builder: TestComponentBuilder; 29 | 30 | beforeEachProviders(() => [ 31 | disableDeprecatedForms(), 32 | provideForms() 33 | ]); 34 | 35 | describe('basic behaviors', () => { 36 | let fixture: ComponentFixture; 37 | 38 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 39 | builder = tcb; 40 | })); 41 | 42 | beforeEach(async(() => { 43 | builder.createAsync(IonCalendarBasicTestComponent).then(f => { 44 | fixture = f; 45 | fixture.detectChanges(); 46 | }); 47 | })); 48 | 49 | it('creates an action header and entry rows', () => { 50 | let calendarItem = fixture.debugElement.query(By.directive(IonCalendar)); 51 | expect(calendarItem.query(By.css('.ion-calendar-header'))).toBeTruthy(); 52 | expect(calendarItem.query(By.css('.ion-calendar-row'))).toBeTruthy(); 53 | }); 54 | 55 | it('should display the current month', () => { 56 | let calendarItem = fixture.debugElement.query(By.directive(IonCalendar)); 57 | let calendarRows = calendarItem.queryAll(By.css('.ion-calendar-row')); 58 | 59 | let startDate = moment().startOf('month').startOf('week'); 60 | let endDate = moment().endOf('month').endOf('week').add(1, 'day'); 61 | expect(calendarRows.length).toBe(endDate.diff(startDate, 'weeks') + 1); 62 | 63 | let curDate = startDate.clone(); 64 | let rowIdx: number = 1; 65 | let colIdx: number = 0; 66 | let curRow = calendarRows[rowIdx]; 67 | while (curRow != null && curDate.isBefore(endDate)) { 68 | if (colIdx >= curRow.children.length) { 69 | curRow = calendarRows[++rowIdx]; 70 | colIdx = 0; 71 | } 72 | if (curRow != null) { 73 | expect(parseInt(curRow.children[colIdx] 74 | .query(By.css('.md-button-wrapper')).nativeElement.innerHTML, 10)) 75 | .toBe(curDate.date()); 76 | colIdx++; 77 | curDate.add(1, 'day'); 78 | } 79 | } 80 | }); 81 | 82 | it('supports ngModel', fakeAsync(() => { 83 | let instance = fixture.componentInstance; 84 | let component = fixture.debugElement.query(By.directive(IonCalendar)).componentInstance; 85 | 86 | let curDate = new Date(); 87 | instance.model = { 88 | type: 'day', 89 | startDate: curDate, 90 | endDate: curDate 91 | }; 92 | 93 | fixture.detectChanges(); 94 | 95 | let selected = fixture.debugElement.queryAll(By.css('button.md-warn')); 96 | expect(selected.length).toEqual(1); 97 | expect(parseInt(selected[0].nativeElement.children[0].innerHTML, 10)) 98 | .toEqual(curDate.getDate()); 99 | 100 | curDate = moment(curDate).add(1, 'day').toDate(); 101 | component.value = { 102 | type: 'day', 103 | startDate: curDate, 104 | endDate: curDate 105 | }; 106 | 107 | fixture.detectChanges(); 108 | 109 | selected = fixture.debugElement.queryAll(By.css('button.md-warn')); 110 | expect(selected.length).toEqual(1); 111 | expect(parseInt(selected[0].nativeElement.children[0].innerHTML, 10)) 112 | .toEqual(curDate.getDate()); 113 | })); 114 | }); 115 | 116 | describe('year view', () => { 117 | let fixture: ComponentFixture; 118 | 119 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 120 | builder = tcb; 121 | })); 122 | 123 | beforeEach(async(() => { 124 | builder.createAsync(IonCalendarMonthViewTestComponent).then(f => { 125 | fixture = f; 126 | fixture.detectChanges(); 127 | }); 128 | })); 129 | 130 | it('should display the current year', () => { 131 | let calendarItem = fixture.debugElement.query(By.directive(IonCalendar)); 132 | let calendarRows = calendarItem.queryAll(By.css('.ion-calendar-row')); 133 | 134 | let startDate = moment().startOf('year'); 135 | let endDate = moment().endOf('year').add(1, 'day'); 136 | let totalEntries: number = 0; 137 | for (let i = 0 ; i < calendarRows.length ; i++) { 138 | totalEntries += calendarRows[i].queryAll(By.css('button')).length; 139 | } 140 | expect(totalEntries).toBe(12); 141 | 142 | let curDate = startDate.clone(); 143 | let rowIdx: number = 0; 144 | let colIdx: number = 0; 145 | let curRow = calendarRows[rowIdx]; 146 | while (curRow != null && curDate.isBefore(endDate)) { 147 | if (colIdx >= curRow.children.length) { 148 | curRow = calendarRows[++rowIdx]; 149 | colIdx = 0; 150 | } 151 | if (curRow != null) { 152 | expect(curRow.children[colIdx] 153 | .query(By.css('.md-button-wrapper')).nativeElement.innerHTML.trim()) 154 | .toBe(curDate.format('MMM')); 155 | colIdx++; 156 | curDate.add(1, 'month'); 157 | } 158 | } 159 | }); 160 | }); 161 | }); 162 | 163 | @Component({ 164 | selector: 'ion-calendar-test-component', 165 | directives: [REACTIVE_FORM_DIRECTIVES, ION_CALENDAR_DIRECTIVES], 166 | template: `` 167 | }) 168 | class IonCalendarBasicTestComponent { 169 | model: IonCalendarPeriod; 170 | } 171 | 172 | @Component({ 173 | selector: 'ion-calendar-test-component', 174 | directives: [ION_CALENDAR_DIRECTIVES], 175 | template: `` 176 | }) 177 | class IonCalendarMonthViewTestComponent { 178 | } 179 | */ 180 | -------------------------------------------------------------------------------- /src/lib/calendar/calendar.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | AfterContentInit, Component, EventEmitter, forwardRef, Input, 4 | NgModule, OnInit, Output, ViewEncapsulation 5 | } from '@angular/core'; 6 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 7 | 8 | import { Observable } from 'rxjs/Observable'; 9 | 10 | import { IonicModule } from 'ionic-angular'; 11 | 12 | import { TranslateModule, TranslateService } from '@ngx-translate/core'; 13 | 14 | import * as moment from 'moment'; 15 | const momentConstructor: (value?: any) => moment.Moment = (moment).default || moment; 16 | 17 | export const ION_CALENDAR_CONTROL_VALUE_ACCESSOR: any = { 18 | provide: NG_VALUE_ACCESSOR, 19 | useExisting: forwardRef(() => IonCalendar), 20 | multi: true 21 | }; 22 | 23 | const weekDays: string[] = [ 24 | '', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' 25 | ]; 26 | 27 | export type IonCalendarViewMode = ('month' | 'year' | 'decade'); 28 | export type IonCalendarPeriodType = ('day' | 'week' | 'month' | 'year'); 29 | export type IonCalendarEntryType = ('day' | 'month' | 'year'); 30 | export type IonCalendarWeekDay = ( 31 | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday' 32 | ); 33 | export type IonCalendarEntrySelectedState = ('none' | 'partial' | 'full'); 34 | 35 | export class IonCalendarPeriod { 36 | type: IonCalendarPeriodType; 37 | startDate: Date; 38 | endDate: Date; 39 | } 40 | 41 | export class IonCalendarChange { 42 | source: IonCalendar; 43 | period: IonCalendarPeriod | null; 44 | } 45 | 46 | export class IonCalendarEntry { 47 | type: IonCalendarEntryType; 48 | date: Date; 49 | selected: IonCalendarEntrySelectedState; 50 | disabled = false; 51 | highlight = false; 52 | 53 | constructor(params: { 54 | type: IonCalendarEntryType, 55 | date: Date, 56 | selected: IonCalendarEntrySelectedState, 57 | highlight?: boolean, 58 | disabled?: boolean 59 | }) { 60 | this.type = params.type; 61 | this.date = params.date; 62 | this.selected = params.selected; 63 | if (params.highlight != null) { 64 | this.highlight = params.highlight; 65 | } 66 | if (params.disabled != null) { 67 | this.disabled = params.disabled; 68 | } 69 | } 70 | 71 | toString(): string { 72 | if (this.type === 'day') { 73 | return `${this.date.getDate()}`; 74 | } 75 | if (this.type === 'month') { 76 | return momentConstructor(this.date).format('MMM'); 77 | } 78 | return `${this.date.getFullYear()}`; 79 | } 80 | 81 | getRange(): { start: moment.Moment, end: moment.Moment } { 82 | if (this.type === 'day') { 83 | let day: moment.Moment = momentConstructor(this.date); 84 | return { start: day, end: day }; 85 | } else { 86 | let curMoment: moment.Moment = momentConstructor(this.date); 87 | return { 88 | start: curMoment.clone().startOf(this.type), 89 | end: curMoment.clone().endOf(this.type) 90 | }; 91 | } 92 | } 93 | } 94 | 95 | @Component({ 96 | moduleId: module.id, 97 | selector: 'ion-calendar', 98 | templateUrl: 'calendar.html', 99 | styleUrls: ['calendar.css'], 100 | providers: [ION_CALENDAR_CONTROL_VALUE_ACCESSOR], 101 | encapsulation: ViewEncapsulation.None, 102 | }) 103 | export class IonCalendar implements AfterContentInit, ControlValueAccessor, OnInit { 104 | @Output() ionChange: EventEmitter = new EventEmitter(); 105 | 106 | get viewDate(): Date { return this._viewDate; } 107 | @Input('view-date') 108 | set viewDate(viewDate: Date) { this._setViewDate(viewDate); } 109 | 110 | private _disabled = false; 111 | get disabled(): boolean { return this._disabled; } 112 | @Input() 113 | set disabled(disabled: boolean) { 114 | this._disabled = disabled != null && `${disabled}` !== 'false'; 115 | } 116 | 117 | private _dateOnlyForDay = false; 118 | get dateOnlyForDay(): boolean { return this._disabled; } 119 | @Input() 120 | set dateOnlyForDay(dateOnlyForDay: boolean) { 121 | this._dateOnlyForDay = dateOnlyForDay != null && `${dateOnlyForDay}` !== 'false'; 122 | } 123 | 124 | private _viewMode: IonCalendarViewMode = 'month'; 125 | get viewMode(): IonCalendarViewMode { return this._viewMode; } 126 | @Input('view-mode') 127 | set viewMode(viewMode: IonCalendarViewMode) { 128 | this._viewMode = viewMode; 129 | this._buildCalendar(); 130 | } 131 | 132 | private _selectionMode: IonCalendarPeriodType = 'day'; 133 | get selectionMode(): IonCalendarPeriodType { return this._selectionMode; } 134 | @Input('selection-mode') 135 | set selectionMode(selectionMode: IonCalendarPeriodType) { 136 | this._selectionMode = selectionMode; 137 | } 138 | 139 | private _startOfWeekDay = 1; 140 | get startOfWeekDay(): IonCalendarWeekDay { 141 | return weekDays[this._startOfWeekDay]; 142 | } 143 | @Input('start-of-week-day') 144 | set startOfWeekDay(weekDay: IonCalendarWeekDay) { 145 | this._startOfWeekDay = weekDays.indexOf(weekDay); 146 | 147 | (moment).updateLocale(moment.locale(), { week: { dow: this._startOfWeekDay } }); 148 | 149 | if (this._viewMode === 'month') { 150 | this._buildCalendar(); 151 | } 152 | } 153 | 154 | private _isoMode: boolean = false; 155 | 156 | get isoMode(): boolean { 157 | return this._isoMode; 158 | } 159 | @Input('iso-mode') 160 | set isoMode(isoMode: boolean) { 161 | this._isoMode = isoMode; 162 | } 163 | 164 | private _minDate: Date; 165 | get minDate(): Date { 166 | return this._minDate; 167 | } 168 | @Input() 169 | set minDate(minDate: Date | null) { 170 | this._minDate = minDate != null ? new Date(minDate.valueOf()) : null; 171 | } 172 | 173 | private _maxDate: Date | null; 174 | get maxDate(): Date | null { 175 | return this._maxDate; 176 | } 177 | @Input() 178 | set maxDate(maxDate: Date | null) { 179 | this._maxDate = maxDate != null ? new Date(maxDate.valueOf()) : null; 180 | } 181 | 182 | private _change: EventEmitter = new EventEmitter(); 183 | @Output() 184 | get change(): Observable { 185 | this._buildCalendar(); 186 | return this._change.asObservable(); 187 | } 188 | 189 | private _selectedPeriod: IonCalendarPeriod | null; 190 | private set selectedPeriod(period: IonCalendarPeriod | null) { 191 | this._selectedPeriod = period; 192 | this._change.emit({ 193 | source: this, 194 | period: period 195 | }); 196 | this._refreshSelection(); 197 | } 198 | 199 | get value(): IonCalendarPeriod | Date | null { 200 | if (this._dateOnlyForDay && this.selectionMode === 'day') { 201 | return this._selectedPeriod != null ? this._selectedPeriod.startDate : null; 202 | } 203 | return this._selectedPeriod; 204 | } 205 | set value(period: IonCalendarPeriod | Date | null) { 206 | if (this._dateOnlyForDay && this.selectionMode === 'day') { 207 | if (period instanceof Date && 208 | (this._selectedPeriod == null || period !== this._selectedPeriod.startDate)) { 209 | this.selectedPeriod = { 210 | type: 'day', 211 | startDate: period, 212 | endDate: period 213 | }; 214 | if (this._init) { 215 | this.ionChange.emit(this); 216 | } 217 | this._onChangeCallback(period); 218 | } 219 | } else if (period instanceof Object && period !== this._selectedPeriod) { 220 | this.selectedPeriod = period; 221 | if (this._init) { 222 | this.ionChange.emit(this); 223 | } 224 | this._onChangeCallback(period); 225 | } 226 | } 227 | 228 | public get calendarRows(): IonCalendarEntry[][] { return this._calendarRows; } 229 | public get viewHeader(): string { return this._viewHeader; } 230 | public get weekDays(): string[] { return this._weekDays; } 231 | 232 | private _viewDate: Date = new Date(); 233 | private _viewHeader = ''; 234 | 235 | private _calendarRows: IonCalendarEntry[][] = []; 236 | private _weekDays: string[] = []; 237 | private _init: boolean; 238 | 239 | constructor(private _ts: TranslateService) { } 240 | 241 | prevPage(): void { 242 | if (this._viewMode === 'month') { 243 | this.viewDate = momentConstructor(this.viewDate).subtract(1, 'M').toDate(); 244 | } else if (this._viewMode === 'year') { 245 | this.viewDate = momentConstructor(this.viewDate).subtract(1, 'y').toDate(); 246 | } 247 | this._buildCalendar(); 248 | } 249 | 250 | nextPage(): void { 251 | if (this._viewMode === 'month') { 252 | this.viewDate = momentConstructor(this.viewDate).add(1, 'M').toDate(); 253 | } else if (this._viewMode === 'year') { 254 | this.viewDate = momentConstructor(this.viewDate).add(1, 'y').toDate(); 255 | } 256 | this._buildCalendar(); 257 | } 258 | 259 | previousViewMode(): void { 260 | if (this._viewMode === 'decade') { 261 | return; 262 | } else if (this._viewMode === 'year') { 263 | this._viewMode = 'decade'; 264 | } else if (this._viewMode === 'month') { 265 | this._viewMode = 'year'; 266 | } 267 | this._buildCalendar(); 268 | } 269 | 270 | selectEntry(entry: IonCalendarEntry): void { 271 | if (!this._canSelectEntry(entry)) { 272 | return this._nextViewMode(entry); 273 | } 274 | 275 | let newPeriod: IonCalendarPeriod | null = null; 276 | if (this._isEntrySelected(entry) === 'full') { 277 | newPeriod = null; 278 | } else if (this._selectionMode === 'day') { 279 | newPeriod = { 280 | type: 'day', 281 | startDate: entry.date, 282 | endDate: entry.date 283 | }; 284 | } else if (this._selectionMode === 'week') { 285 | newPeriod = { 286 | type: 'week', 287 | startDate: new Date( 288 | momentConstructor(entry.date).startOf(this._isoMode ? 'isoWeek' : 'week') 289 | .toDate().valueOf() 290 | ), 291 | endDate: new Date( 292 | momentConstructor(entry.date).endOf(this._isoMode ? 'isoWeek' : 'week') 293 | .toDate().valueOf() 294 | ) 295 | }; 296 | } else if (this._selectionMode === 'month') { 297 | const monthBounds = this._getMonthStartEnd(entry.date); 298 | newPeriod = { 299 | type: 'month', 300 | startDate: new Date(monthBounds.start.toDate().valueOf()), 301 | endDate: new Date(monthBounds.end.toDate().valueOf()) 302 | }; 303 | } else if (this._selectionMode === 'year') { 304 | newPeriod = { 305 | type: 'year', 306 | startDate: new Date(momentConstructor(entry.date).startOf('year').toDate().valueOf()), 307 | endDate: new Date(momentConstructor(entry.date).endOf('year').toDate().valueOf()) 308 | }; 309 | } 310 | this.value = newPeriod; 311 | } 312 | 313 | registerOnChange(fn: (value: any) => void) { 314 | this._onChangeCallback = fn; 315 | } 316 | 317 | registerOnTouched(fn: any) { 318 | this._onTouchedCallback = fn; 319 | } 320 | 321 | writeValue(value: any) { 322 | if (typeof value === 'string') { 323 | value = momentConstructor(value).toDate(); 324 | } 325 | this.value = value; 326 | } 327 | 328 | ngOnInit(): void { 329 | this._buildCalendar(); 330 | } 331 | 332 | ngAfterContentInit(): void { 333 | this._init = true; 334 | this._refreshSelection(); 335 | } 336 | 337 | private _onChangeCallback: (_: any) => void = (_: any) => { }; 338 | private _onTouchedCallback: () => void = () => { }; 339 | 340 | private _setViewDate(date: Date): void { 341 | this._viewDate = date; 342 | } 343 | 344 | private _getMonthStartEnd(date: Date): { start: moment.Moment, end: moment.Moment } { 345 | let startDate = momentConstructor(date).startOf('month'); 346 | let endDate = momentConstructor(date).endOf('month'); 347 | if (this._isoMode) { 348 | const startWeekDay = startDate.day(); 349 | const endWeekDay = endDate.day(); 350 | if (startWeekDay === 0 || startWeekDay > 4) { 351 | startDate = startDate.add(7, 'd'); 352 | } 353 | if (endWeekDay > 0 && endWeekDay < 4) { 354 | endDate = endDate.subtract(7, 'd'); 355 | } 356 | startDate = startDate.startOf('isoWeek'); 357 | endDate = endDate.endOf('isoWeek'); 358 | } 359 | return { start: startDate, end: endDate }; 360 | } 361 | 362 | private _buildCalendar(): void { 363 | if (this._viewMode === 'month') { 364 | this._buildMonthView(); 365 | } else if (this._viewMode === 'year') { 366 | this._buildYearView(); 367 | } else if (this._viewMode === 'decade') { 368 | this._buildDecadeView(); 369 | } 370 | } 371 | 372 | private _buildDecadeView(): void { 373 | let curYear: number = this._viewDate.getFullYear(); 374 | let firstYear = curYear - (curYear % 10) + 1; 375 | let lastYear = firstYear + 11; 376 | 377 | this._viewHeader = `${firstYear} - ${lastYear}`; 378 | 379 | let curDate: moment.Moment = momentConstructor(this.viewDate) 380 | .startOf('year') 381 | .year(firstYear); 382 | 383 | let rows: IonCalendarEntry[][] = []; 384 | for (let i = 0; i < 4; i++) { 385 | let row: IonCalendarEntry[] = []; 386 | for (let j = 0; j < 3; j++) { 387 | let date = new Date(curDate.toDate().valueOf()); 388 | let newEntry = new IonCalendarEntry({ 389 | type: 'year', 390 | date: date, 391 | selected: 'none' 392 | }); 393 | newEntry.selected = this._isEntrySelected(newEntry); 394 | row.push(newEntry); 395 | curDate.add(1, 'y'); 396 | } 397 | rows.push(row); 398 | } 399 | this._calendarRows = rows; 400 | } 401 | 402 | private _buildYearView(): void { 403 | this._viewHeader = `${this._viewDate.getFullYear()}`; 404 | 405 | let curDate: moment.Moment = momentConstructor(this.viewDate) 406 | .startOf('year'); 407 | 408 | let rows: IonCalendarEntry[][] = []; 409 | for (let i = 0; i < 4; i++) { 410 | let row: IonCalendarEntry[] = []; 411 | for (let j = 0; j < 3; j++) { 412 | let date = new Date(curDate.toDate().valueOf()); 413 | let newEntry = new IonCalendarEntry({ 414 | type: 'month', 415 | date: date, 416 | selected: 'none' 417 | }); 418 | newEntry.selected = this._isEntrySelected(newEntry); 419 | row.push(newEntry); 420 | curDate.add(1, 'M'); 421 | } 422 | rows.push(row); 423 | } 424 | this._calendarRows = rows; 425 | } 426 | 427 | private _buildMonthView(): void { 428 | this._viewHeader = 429 | this._ts.instant(momentConstructor(this._viewDate).format('MMM')) 430 | + ' ' 431 | + momentConstructor(this._viewDate).format('YYYY'); 432 | 433 | this._buildMonthViewWeekDays(); 434 | const monthBounds = this._getMonthStartEnd(this._viewDate); 435 | let viewStartDate: moment.Moment = monthBounds.start; 436 | let viewEndDate: moment.Moment = monthBounds.end; 437 | if (!this._isoMode) { 438 | viewStartDate = viewStartDate.startOf('week'); 439 | viewEndDate = viewEndDate.endOf('week'); 440 | } 441 | 442 | let rows: IonCalendarEntry[][] = []; 443 | let todayDate = momentConstructor(); 444 | let curDate = momentConstructor(viewStartDate); 445 | let minDate = this.minDate == null ? null : momentConstructor(this.minDate); 446 | let maxDate = this.maxDate == null ? null : momentConstructor(this.maxDate); 447 | while (curDate < viewEndDate) { 448 | let row: IonCalendarEntry[] = []; 449 | for (let i = 0; i < 7; i++) { 450 | let disabled = (minDate != null && curDate.isBefore(minDate)) || 451 | (maxDate != null && curDate.isAfter(maxDate)); 452 | let date = new Date(curDate.toDate().valueOf()); 453 | let newEntry: IonCalendarEntry = new IonCalendarEntry({ 454 | type: 'day', 455 | date: date, 456 | selected: 'none', 457 | highlight: todayDate.format('YYYY-MM-DD') === curDate.format('YYYY-MM-DD'), 458 | disabled: disabled 459 | }); 460 | newEntry.selected = this._isEntrySelected(newEntry); 461 | row.push(newEntry); 462 | curDate.add(1, 'd'); 463 | } 464 | rows.push(row); 465 | } 466 | 467 | this._calendarRows = rows; 468 | } 469 | 470 | private _buildMonthViewWeekDays(): void { 471 | let curMoment; 472 | if (this._isoMode) { 473 | curMoment = momentConstructor(this._viewDate).startOf('week').isoWeekday(1); 474 | } else { 475 | curMoment = momentConstructor(this._viewDate).startOf('week'); 476 | } 477 | let weekDayNames: string[] = []; 478 | for (let i = 0; i < 7; i++) { 479 | weekDayNames.push(curMoment.format('dddd')); 480 | curMoment.add(1, 'd'); 481 | } 482 | this._weekDays = weekDayNames; 483 | } 484 | 485 | private _periodOrder(entryType: IonCalendarPeriodType): number { 486 | return ['day', 'week', 'month', 'year'].indexOf(entryType); 487 | } 488 | 489 | private _isEntrySelected(entry: IonCalendarEntry): IonCalendarEntrySelectedState { 490 | if ( 491 | this._selectedPeriod != null && this._selectedPeriod.startDate != null && 492 | this._selectedPeriod.endDate != null 493 | ) { 494 | let selectionStart: moment.Moment = momentConstructor(this._selectedPeriod.startDate) 495 | .startOf('day'); 496 | let selectionEnd: moment.Moment = momentConstructor(this._selectedPeriod.endDate) 497 | .endOf('day'); 498 | let selectionPeriodOrder: number = this._periodOrder(this._selectedPeriod.type); 499 | 500 | let entryPeriodOrder: number = this._periodOrder(entry.type); 501 | let entryRange: { start: moment.Moment, end: moment.Moment } = entry.getRange(); 502 | 503 | if (entryPeriodOrder <= selectionPeriodOrder && 504 | entryRange.start.isBetween(selectionStart, selectionEnd, null, '[]') && 505 | entryRange.end.isBetween(selectionStart, selectionEnd, null, '[]') 506 | ) { 507 | return 'full'; 508 | } else if (entryPeriodOrder > selectionPeriodOrder && 509 | selectionStart.isBetween(entryRange.start, entryRange.end, null, '[]') && 510 | selectionEnd.isBetween(entryRange.start, entryRange.end, null, '[]') 511 | ) { 512 | return 'partial'; 513 | } 514 | } 515 | 516 | return 'none'; 517 | } 518 | 519 | private _refreshSelection(): void { 520 | for (let row of this._calendarRows) { 521 | for (let entry of row) { 522 | entry.selected = this._isEntrySelected(entry); 523 | } 524 | } 525 | } 526 | 527 | private _canSelectEntry(entry: IonCalendarEntry): boolean { 528 | if (['day', 'week'].indexOf(this._selectionMode) >= 0 && entry.type != 'day') { 529 | return false; 530 | } 531 | if (this._selectionMode === 'month' && entry.type === 'year') { 532 | return false; 533 | } 534 | return true; 535 | } 536 | 537 | private _nextViewMode(entry: IonCalendarEntry): void { 538 | if (this._viewMode === 'decade') { 539 | this._viewMode = 'year'; 540 | } else if (this._viewMode === 'year') { 541 | this._viewMode = 'month'; 542 | } else if (this._viewMode === 'month') { 543 | return; 544 | } 545 | this._viewDate = entry.date; 546 | this._buildCalendar(); 547 | } 548 | } 549 | 550 | @NgModule({ 551 | imports: [CommonModule, IonicModule, TranslateModule], 552 | exports: [IonCalendar], 553 | declarations: [IonCalendar] 554 | }) 555 | export class IonCalendarModule { } 556 | -------------------------------------------------------------------------------- /src/lib/calendar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ionic2-extra/calendar", 3 | "version": "0.2.3", 4 | "description": "Calendar widget built on Ionic 2", 5 | "main": "./calendar.umd.js", 6 | "module": "./public_api.js", 7 | "typings": "./public_api.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/gnucoop/ionic2-extra.git" 11 | }, 12 | "keywords": [ 13 | "ionic", 14 | "components", 15 | "calendar", 16 | "datepicker" 17 | ], 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/gnucoop/ionic2-extra/issues" 21 | }, 22 | "homepage": "https://github.com/gnucoop/ionic2-extra#readme", 23 | "peerDependencies": { 24 | "moment": "^2.18.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/calendar/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './calendar'; 2 | -------------------------------------------------------------------------------- /src/lib/masonry/masonry.scss: -------------------------------------------------------------------------------- 1 | md-masonry { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | md-masonry-row { 6 | display: flex; 7 | flex-direction: row; 8 | 9 | md-masonry-item { 10 | display: flex; 11 | flex: 1; 12 | 13 | &::after { 14 | content: ''; 15 | display: block; 16 | padding-bottom: 100%; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/masonry/masonry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ModuleWithProviders, 4 | NgModule, 5 | ViewEncapsulation 6 | } from '@angular/core'; 7 | import { CommonModule } from '@angular/common'; 8 | 9 | @Component({ 10 | moduleId: module.id, 11 | selector: 'ion-masonry', 12 | host: {'role': 'list'}, 13 | template: '', 14 | styleUrls: ['masonry.css'], 15 | encapsulation: ViewEncapsulation.None 16 | }) 17 | export class IonMasonry { } 18 | 19 | @Component({ 20 | moduleId: module.id, 21 | selector: 'ion-masonry-row', 22 | template: '' 23 | }) 24 | export class IonMasonryRow { } 25 | 26 | @Component({ 27 | moduleId: module.id, 28 | selector: 'ion-masonry-item', 29 | host: { 'role': 'listitem' }, 30 | template: '', 31 | encapsulation: ViewEncapsulation.None 32 | }) 33 | export class IonMasonryItem { 34 | } 35 | 36 | export const ION_MASONRY_DIRECTIVES = [IonMasonry, IonMasonryRow, IonMasonryItem]; 37 | 38 | @NgModule({ 39 | imports: [CommonModule], 40 | exports: ION_MASONRY_DIRECTIVES, 41 | declarations: ION_MASONRY_DIRECTIVES, 42 | providers: [] 43 | }) 44 | export class IonMasonryModule { 45 | static forRoot(): ModuleWithProviders { 46 | return { 47 | ngModule: IonMasonryModule, 48 | providers: [] 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/masonry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ionic2-extra/masonry", 3 | "version": "0.2.3", 4 | "description": "Masonry layout built on Ionic 2", 5 | "main": "./masonry.umd.js", 6 | "module": "./public_api.js", 7 | "typings": "./public_api.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/gnucoop/ionic2-extra.git" 11 | }, 12 | "keywords": [ 13 | "angular", 14 | "ionic", 15 | "components", 16 | "masonry", 17 | "layout" 18 | ], 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/gnucoop/ionic2-extra/issues" 22 | }, 23 | "homepage": "https://github.com/gnucoop/ionic2-extra#readme", 24 | "peerDependencies": { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/masonry/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './masonry'; 2 | -------------------------------------------------------------------------------- /src/lib/module.d.ts: -------------------------------------------------------------------------------- 1 | declare var module: {id: string}; 2 | -------------------------------------------------------------------------------- /src/lib/system-config-spec.ts: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | * User Configuration. 3 | **********************************************************************************************/ 4 | 5 | const components = [ 6 | 'calendar', 7 | 'masonry', 8 | ]; 9 | 10 | 11 | /** User packages configuration. */ 12 | const packages: any = { 13 | // Set the default extension for the root package, because otherwise the demo-app can't 14 | // be built within the production mode. Due to missing file extensions. 15 | '.': { 16 | defaultExtension: 'js' 17 | } 18 | }; 19 | components.forEach(name => { 20 | packages[`@ionic2-extra/${name}`] = { 21 | format: 'cjs', 22 | defaultExtension: 'js', 23 | main: 'index' 24 | }; 25 | }); 26 | 27 | 28 | //////////////////////////////////////////////////////////////////////////////////////////////// 29 | /*********************************************************************************************** 30 | * Everything underneath this line is managed by the CLI. 31 | **********************************************************************************************/ 32 | const angularPackages = { 33 | // Angular specific barrels. 34 | '@angular/core': { main: 'bundles/core.umd.js'}, 35 | '@angular/core/testing': { main: '../bundles/core-testing.umd.js'}, 36 | '@angular/common': { main: 'bundles/common.umd.js'}, 37 | '@angular/compiler': { main: 'bundles/compiler.umd.js'}, 38 | '@angular/compiler/testing': { main: '../bundles/compiler-testing.umd.js'}, 39 | '@angular/http': { main: 'bundles/http.umd.js'}, 40 | '@angular/http/testing': { main: '../bundles/http-testing.umd.js'}, 41 | '@angular/forms': { main: 'bundles/forms.umd.js'}, 42 | '@angular/router': { main: 'bundles/router.umd.js'}, 43 | '@angular/platform-browser': { main: 'bundles/platform-browser.umd.js'}, 44 | '@angular/platform-browser/testing': { main: '../bundles/platform-browser-testing.umd.js'}, 45 | '@angular/platform-browser-dynamic': { main: 'bundles/platform-browser-dynamic.umd.js'}, 46 | '@angular/platform-browser-dynamic/testing': { 47 | main: '../bundles/platform-browser-dynamic-testing.umd.js' 48 | }, 49 | }; 50 | 51 | const barrels: string[] = [ 52 | // Thirdparty barrels. 53 | 'rxjs', 54 | 55 | // App specific barrels. 56 | ...components 57 | /** @cli-barrel */ 58 | ]; 59 | 60 | const _cliSystemConfig = angularPackages; 61 | barrels.forEach((barrelName: string) => { 62 | ( _cliSystemConfig)[barrelName] = { main: 'index' }; 63 | }); 64 | 65 | /** Type declaration for ambient System. */ 66 | declare var System: any; 67 | 68 | // Apply the CLI SystemJS configuration. 69 | System.config({ 70 | map: { 71 | '@angular': 'vendor/@angular', 72 | 'moment': 'vendor/moment', 73 | 'rxjs': 'vendor/rxjs', 74 | 'main': 'main.js' 75 | }, 76 | packages: _cliSystemConfig 77 | }); 78 | 79 | // Apply the user's configuration. 80 | System.config({ packages }); 81 | -------------------------------------------------------------------------------- /src/lib/tsconfig-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es6", "es2015", "dom"], 7 | "mapRoot": "", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noImplicitAny": true, 12 | "outDir": "../../dist/@ionic2-extra", 13 | "rootDir": ".", 14 | "sourceMap": true, 15 | "target": "es5", 16 | "inlineSources": true, 17 | "stripInternal": true, 18 | "skipLibCheck": true, 19 | "baseUrl": "", 20 | "paths": { 21 | "@ionic2-extra/*": [ 22 | "./*" 23 | ] 24 | }, 25 | "typeRoots": [ 26 | "../../node_modules/@types" 27 | ], 28 | "types": [ 29 | "moment" 30 | ] 31 | }, 32 | "angularCompilerOptions": { 33 | "genDir": "../../dist", 34 | "skipTemplateCodegen": true, 35 | "debug": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es6", "es2015", "dom"], 7 | "mapRoot": "", 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noImplicitAny": true, 12 | "outDir": "../../dist/@ionic2-extra", 13 | "rootDir": ".", 14 | "sourceMap": true, 15 | "target": "es2015", 16 | "inlineSources": true, 17 | "stripInternal": true, 18 | "skipLibCheck": true, 19 | "baseUrl": "", 20 | "paths": { 21 | "@ionic2-extra/*": [ 22 | "./*" 23 | ] 24 | }, 25 | "typeRoots": [ 26 | "../../node_modules/@types" 27 | ], 28 | "types": [ 29 | "moment" 30 | ] 31 | }, 32 | "exclude": [ 33 | "**/*.spec.*", 34 | "system-config-spec" 35 | ], 36 | "angularCompilerOptions": { 37 | "genDir": "../../dist", 38 | "skipTemplateCodegen": true, 39 | "debug": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /stylelint-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "color-hex-case": "lower", 4 | "color-no-invalid-hex": true, 5 | 6 | "function-calc-no-unspaced-operator": true, 7 | "function-comma-space-after": "always-single-line", 8 | "function-comma-space-before": "never", 9 | "function-name-case": "lower", 10 | "function-url-quotes": "always", 11 | "function-whitespace-after": "always", 12 | 13 | "number-leading-zero": "always", 14 | "number-no-trailing-zeros": true, 15 | "length-zero-no-unit": true, 16 | 17 | "string-no-newline": true, 18 | "string-quotes": "single", 19 | 20 | "unit-case": "lower", 21 | "unit-no-unknown": true, 22 | "unit-whitelist": ["px", "%", "deg", "ms", "em", "vh"], 23 | 24 | "value-list-comma-space-after": "always-single-line", 25 | "value-list-comma-space-before": "never", 26 | 27 | "shorthand-property-no-redundant-values": true, 28 | 29 | "property-case": "lower", 30 | 31 | "declaration-block-no-duplicate-properties": true, 32 | "declaration-block-no-ignored-properties": true, 33 | "declaration-block-trailing-semicolon": "always", 34 | "declaration-block-single-line-max-declarations": 1, 35 | "declaration-block-semicolon-space-before": "never", 36 | "declaration-block-semicolon-space-after": "always-single-line", 37 | "declaration-block-semicolon-newline-before": "never-multi-line", 38 | "declaration-block-semicolon-newline-after": "always-multi-line", 39 | 40 | "block-closing-brace-newline-after": "always", 41 | "block-closing-brace-newline-before": "always-multi-line", 42 | "block-opening-brace-newline-after": "always-multi-line", 43 | "block-opening-brace-space-before": "always-multi-line", 44 | 45 | "selector-attribute-brackets-space-inside": "never", 46 | "selector-attribute-operator-space-after": "never", 47 | "selector-attribute-operator-space-before": "never", 48 | "selector-combinator-space-after": "always", 49 | "selector-combinator-space-before": "always", 50 | "selector-pseudo-class-case": "lower", 51 | "selector-pseudo-class-parentheses-space-inside": "never", 52 | "selector-pseudo-element-case": "lower", 53 | "selector-pseudo-element-colon-notation": "double", 54 | "selector-pseudo-element-no-unknown": true, 55 | "selector-type-case": "lower", 56 | "selector-no-id": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/browser-providers.ts: -------------------------------------------------------------------------------- 1 | type ContextConfigurationInfo = { target: string, required: boolean }; 2 | export interface ConfigurationInfo { 3 | unitTest: ContextConfigurationInfo; 4 | e2e: ContextConfigurationInfo; 5 | }; 6 | 7 | export interface BrowserLauncherInfo { 8 | base: string; 9 | flags?: string[]; 10 | version?: string; 11 | platform?: string; 12 | device?: string; 13 | browser?: string; 14 | browser_version?: string; 15 | os?: string; 16 | os_version?: string; 17 | }; 18 | 19 | export type AliasMap = { [name: string]: string[] }; 20 | 21 | 22 | // Unique place to configure the browsers which are used in the different CI jobs in Sauce Labs (SL) 23 | // and BrowserStack (BS). 24 | // If the target is set to null, then the browser is not run anywhere during CI. 25 | // If a category becomes empty (e.g. BS and required), then the corresponding job must be commented 26 | // out in Travis configuration. 27 | const configuration: { [name: string]: ConfigurationInfo } = { 28 | 'Chrome': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, 29 | 'Firefox': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, 30 | 'ChromeBeta': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, 31 | 'FirefoxBeta': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}}, 32 | 'ChromeDev': { unitTest: {target: null, required: true}, e2e: {target: null, required: true}}, 33 | 'FirefoxDev': { unitTest: {target: null, required: true}, e2e: {target: null, required: true}}, 34 | 'IE9': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}}, 35 | 'IE10': { unitTest: {target: null, required: true}, e2e: {target: null, required: true}}, 36 | 'IE11': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, 37 | 'Edge': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, 38 | 'Android4.1': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, 39 | 'Android4.2': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, 40 | 'Android4.3': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, 41 | 'Android4.4': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, 42 | 'Android5': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, 43 | 'Safari7': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}}, 44 | 'Safari8': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}, 45 | 'Safari9': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}, 46 | 'iOS7': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}, 47 | 'iOS8': { unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}}, 48 | // TODO(mlaval): iOS9 deactivated as not reliable, reactivate after 49 | // https://github.com/angular/angular/issues/5408 50 | 'iOS9': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}, 51 | 'WindowsPhone': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}} 52 | }; 53 | 54 | export const customLaunchers: { [name: string]: BrowserLauncherInfo } = { 55 | 'ChromeNoSandbox': { 56 | base: 'Chrome', 57 | flags: ['--no-sandbox'] 58 | }, 59 | // Chrome set to 1024x768 resolution for *local testing only*. 60 | // On CI, both SauceLabs and Browserstack already default all browser window sizes to 1024x768. 61 | 'Chrome_1024x768': { 62 | base : 'Chrome', 63 | flags: ['--window-size=1024,768'] 64 | }, 65 | 'SL_CHROME': { 66 | base: 'SauceLabs', 67 | browserName: 'chrome', 68 | version: '46' 69 | }, 70 | 'SL_CHROMEBETA': { 71 | base: 'SauceLabs', 72 | browserName: 'chrome', 73 | version: 'beta' 74 | }, 75 | 'SL_CHROMEDEV': { 76 | base: 'SauceLabs', 77 | browserName: 'chrome', 78 | version: 'dev' 79 | }, 80 | 'SL_FIREFOX': { 81 | base: 'SauceLabs', 82 | browserName: 'firefox', 83 | version: '42' 84 | }, 85 | 'SL_FIREFOXBETA': { 86 | base: 'SauceLabs', 87 | browserName: 'firefox', 88 | version: 'beta' 89 | }, 90 | 'SL_FIREFOXDEV': { 91 | base: 'SauceLabs', 92 | browserName: 'firefox', 93 | version: 'dev' 94 | }, 95 | 'SL_SAFARI7': { 96 | base: 'SauceLabs', 97 | browserName: 'safari', 98 | platform: 'OS X 10.9', 99 | version: '7' 100 | }, 101 | 'SL_SAFARI8': { 102 | base: 'SauceLabs', 103 | browserName: 'safari', 104 | platform: 'OS X 10.10', 105 | version: '8' 106 | }, 107 | 'SL_SAFARI9': { 108 | base: 'SauceLabs', 109 | browserName: 'safari', 110 | platform: 'OS X 10.11', 111 | version: '9.0' 112 | }, 113 | 'SL_IOS7': { 114 | base: 'SauceLabs', 115 | browserName: 'iphone', 116 | platform: 'OS X 10.10', 117 | version: '7.1' 118 | }, 119 | 'SL_IOS8': { 120 | base: 'SauceLabs', 121 | browserName: 'iphone', 122 | platform: 'OS X 10.10', 123 | version: '8.4' 124 | }, 125 | 'SL_IOS9': { 126 | base: 'SauceLabs', 127 | browserName: 'iphone', 128 | platform: 'OS X 10.10', 129 | version: '9.1' 130 | }, 131 | 'SL_IE9': { 132 | base: 'SauceLabs', 133 | browserName: 'internet explorer', 134 | platform: 'Windows 2008', 135 | version: '9' 136 | }, 137 | 'SL_IE10': { 138 | base: 'SauceLabs', 139 | browserName: 'internet explorer', 140 | platform: 'Windows 2012', 141 | version: '10' 142 | }, 143 | 'SL_IE11': { 144 | base: 'SauceLabs', 145 | browserName: 'internet explorer', 146 | platform: 'Windows 8.1', 147 | version: '11' 148 | }, 149 | 'SL_EDGE': { 150 | base: 'SauceLabs', 151 | browserName: 'microsoftedge', 152 | platform: 'Windows 10', 153 | version: '20.10240' 154 | }, 155 | 'SL_ANDROID4.1': { 156 | base: 'SauceLabs', 157 | browserName: 'android', 158 | platform: 'Linux', 159 | version: '4.1' 160 | }, 161 | 'SL_ANDROID4.2': { 162 | base: 'SauceLabs', 163 | browserName: 'android', 164 | platform: 'Linux', 165 | version: '4.2' 166 | }, 167 | 'SL_ANDROID4.3': { 168 | base: 'SauceLabs', 169 | browserName: 'android', 170 | platform: 'Linux', 171 | version: '4.3' 172 | }, 173 | 'SL_ANDROID4.4': { 174 | base: 'SauceLabs', 175 | browserName: 'android', 176 | platform: 'Linux', 177 | version: '4.4' 178 | }, 179 | 'SL_ANDROID5': { 180 | base: 'SauceLabs', 181 | browserName: 'android', 182 | platform: 'Linux', 183 | version: '5.1' 184 | }, 185 | 186 | 'BS_CHROME': { 187 | base: 'BrowserStack', 188 | browser: 'chrome', 189 | os: 'OS X', 190 | os_version: 'Yosemite' 191 | }, 192 | 'BS_FIREFOX': { 193 | base: 'BrowserStack', 194 | browser: 'firefox', 195 | os: 'Windows', 196 | os_version: '10' 197 | }, 198 | 'BS_SAFARI7': { 199 | base: 'BrowserStack', 200 | browser: 'safari', 201 | os: 'OS X', 202 | os_version: 'Mavericks' 203 | }, 204 | 'BS_SAFARI8': { 205 | base: 'BrowserStack', 206 | browser: 'safari', 207 | os: 'OS X', 208 | os_version: 'Yosemite' 209 | }, 210 | 'BS_SAFARI9': { 211 | base: 'BrowserStack', 212 | browser: 'safari', 213 | os: 'OS X', 214 | os_version: 'El Capitan' 215 | }, 216 | 'BS_IOS7': { 217 | base: 'BrowserStack', 218 | device: 'iPhone 5S', 219 | os: 'ios', 220 | os_version: '7.0', 221 | resolution: '1024x768' 222 | }, 223 | 'BS_IOS8': { 224 | base: 'BrowserStack', 225 | device: 'iPhone 6', 226 | os: 'ios', 227 | os_version: '8.3', 228 | resolution: '1024x768' 229 | }, 230 | 'BS_IOS9': { 231 | base: 'BrowserStack', 232 | device: 'iPhone 6S', 233 | os: 'ios', 234 | os_version: '9.0', 235 | resolution: '1024x768' 236 | }, 237 | 'BS_IE9': { 238 | base: 'BrowserStack', 239 | browser: 'ie', 240 | browser_version: '9.0', 241 | os: 'Windows', 242 | os_version: '7' 243 | }, 244 | 'BS_IE10': { 245 | base: 'BrowserStack', 246 | browser: 'ie', 247 | browser_version: '10.0', 248 | os: 'Windows', 249 | os_version: '8' 250 | }, 251 | 'BS_IE11': { 252 | base: 'BrowserStack', 253 | browser: 'ie', 254 | browser_version: '11.0', 255 | os: 'Windows', 256 | os_version: '10' 257 | }, 258 | 'BS_EDGE': { 259 | base: 'BrowserStack', 260 | browser: 'edge', 261 | os: 'Windows', 262 | os_version: '10' 263 | }, 264 | 'BS_WINDOWSPHONE' : { 265 | base: 'BrowserStack', 266 | device: 'Nokia Lumia 930', 267 | os: 'winphone', 268 | os_version: '8.1' 269 | }, 270 | 'BS_ANDROID5': { 271 | base: 'BrowserStack', 272 | device: 'Google Nexus 5', 273 | os: 'android', 274 | os_version: '5.0' 275 | }, 276 | 'BS_ANDROID4.4': { 277 | base: 'BrowserStack', 278 | device: 'HTC One M8', 279 | os: 'android', 280 | os_version: '4.4' 281 | }, 282 | 'BS_ANDROID4.3': { 283 | base: 'BrowserStack', 284 | device: 'Samsung Galaxy S4', 285 | os: 'android', 286 | os_version: '4.3' 287 | }, 288 | 'BS_ANDROID4.2': { 289 | base: 'BrowserStack', 290 | device: 'Google Nexus 4', 291 | os: 'android', 292 | os_version: '4.2' 293 | }, 294 | 'BS_ANDROID4.1': { 295 | base: 'BrowserStack', 296 | device: 'Google Nexus 7', 297 | os: 'android', 298 | os_version: '4.1' 299 | } 300 | }; 301 | 302 | const sauceAliases: AliasMap = { 303 | 'ALL': Object.keys(customLaunchers).filter(function(item) { 304 | return customLaunchers[item].base == 'SauceLabs'; 305 | }), 306 | 'DESKTOP': ['SL_CHROME', 'SL_FIREFOX', 'SL_IE9', 'SL_IE10', 'SL_IE11', 'SL_EDGE', 'SL_SAFARI7', 307 | 'SL_SAFARI8', 'SL_SAFARI9'], 308 | 'MOBILE': ['SL_ANDROID4.1', 'SL_ANDROID4.2', 'SL_ANDROID4.3', 'SL_ANDROID4.4', 'SL_ANDROID5', 309 | 'SL_IOS7', 'SL_IOS8', 'SL_IOS9'], 310 | 'ANDROID': ['SL_ANDROID4.1', 'SL_ANDROID4.2', 'SL_ANDROID4.3', 'SL_ANDROID4.4', 'SL_ANDROID5'], 311 | 'IE': ['SL_IE9', 'SL_IE10', 'SL_IE11'], 312 | 'IOS': ['SL_IOS7', 'SL_IOS8', 'SL_IOS9'], 313 | 'SAFARI': ['SL_SAFARI7', 'SL_SAFARI8', 'SL_SAFARI9'], 314 | 'BETA': ['SL_CHROMEBETA', 'SL_FIREFOXBETA'], 315 | 'DEV': ['SL_CHROMEDEV', 'SL_FIREFOXDEV'], 316 | 'REQUIRED': buildConfiguration('unitTest', 'SL', true), 317 | 'OPTIONAL': buildConfiguration('unitTest', 'SL', false) 318 | }; 319 | 320 | const browserstackAliases: AliasMap = { 321 | 'ALL': Object.keys(customLaunchers).filter(function(item) { 322 | return customLaunchers[item].base == 'BrowserStack'; 323 | }), 324 | 'DESKTOP': ['BS_CHROME', 'BS_FIREFOX', 'BS_IE9', 'BS_IE10', 'BS_IE11', 'BS_EDGE', 'BS_SAFARI7', 325 | 'BS_SAFARI8', 'BS_SAFARI9'], 326 | 'MOBILE': ['BS_ANDROID4.3', 'BS_ANDROID4.4', 'BS_IOS7', 'BS_IOS8', 'BS_IOS9', 'BS_WINDOWSPHONE'], 327 | 'ANDROID': ['BS_ANDROID4.3', 'BS_ANDROID4.4'], 328 | 'IE': ['BS_IE9', 'BS_IE10', 'BS_IE11'], 329 | 'IOS': ['BS_IOS7', 'BS_IOS8', 'BS_IOS9'], 330 | 'SAFARI': ['BS_SAFARI7', 'BS_SAFARI8', 'BS_SAFARI9'], 331 | 'REQUIRED': buildConfiguration('unitTest', 'BS', true), 332 | 'OPTIONAL': buildConfiguration('unitTest', 'BS', false) 333 | }; 334 | 335 | export const platformMap: { [name: string]: AliasMap } = { 336 | 'saucelabs': sauceAliases, 337 | 'browserstack': browserstackAliases, 338 | }; 339 | 340 | 341 | /** Decode the token for Travis to use. */ 342 | function decode(str: string): string { 343 | return (str || '').split('').reverse().join(''); 344 | } 345 | 346 | 347 | /** Setup the access keys */ 348 | if (process.env.TRAVIS) { 349 | process.env.SAUCE_ACCESS_KEY = decode(process.env.SAUCE_ACCESS_KEY); 350 | process.env.BROWSER_STACK_ACCESS_KEY = decode(process.env.BROWSER_STACK_ACCESS_KEY); 351 | } 352 | 353 | /** Build a list of configuration (custom launcher names). */ 354 | function buildConfiguration(type: string, target: string, required: boolean): string[] { 355 | return Object.keys(configuration) 356 | .map(item => [item, configuration[item][type]]) 357 | .filter(([item, conf]) => conf.required == required && conf.target == target) 358 | .map(([item, conf]) => `${target}_${item.toUpperCase()}`); 359 | } 360 | -------------------------------------------------------------------------------- /test/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | /*global jasmine, __karma__, window*/ 2 | Error.stackTraceLimit = Infinity; 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; 4 | 5 | __karma__.loaded = function () { 6 | }; 7 | 8 | var distPath = '/base/dist/'; 9 | 10 | function isJsFile(path) { 11 | return path.slice(-3) == '.js'; 12 | } 13 | 14 | function isSpecFile(path) { 15 | return path.slice(-8) == '.spec.js'; 16 | } 17 | 18 | function isIonic2ExtraFile(path) { 19 | return isJsFile(path) && path.indexOf('vendor') == -1; 20 | } 21 | 22 | var allSpecFiles = Object.keys(window.__karma__.files) 23 | .filter(isSpecFile) 24 | .filter(isIonic2ExtraFile); 25 | 26 | // Load our SystemJS configuration. 27 | System.config({ 28 | baseURL: distPath 29 | }); 30 | 31 | System.import(distPath + '@ionic2-extra/system-config-spec.js').then(function() { 32 | // Load and configure the TestComponentBuilder. 33 | return Promise.all([ 34 | System.import('@angular/core/testing'), 35 | System.import('@angular/platform-browser-dynamic/testing') 36 | ]).then(function (providers) { 37 | var testing = providers[0]; 38 | var testingBrowser = providers[1]; 39 | 40 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; 41 | 42 | testing.TestBed.initTestEnvironment( 43 | testingBrowser.BrowserDynamicTestingModule, 44 | testingBrowser.platformBrowserDynamicTesting()); 45 | }); 46 | }).then(function() { 47 | // Finally, load all spec files. 48 | // This will run the tests directly. 49 | return Promise.all( 50 | allSpecFiles.map(function (moduleName) { 51 | return System.import(moduleName).then(function(module) { 52 | return module; 53 | }); 54 | })); 55 | }).then(__karma__.start, __karma__.error); 56 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This file only hook up on require calls to transpile the TypeScript. 2 | // If you're looking at this file to see Karma configuration, you should look at 3 | // karma.config.ts instead. 4 | 5 | const fs = require('fs'); 6 | const ts = require('typescript'); 7 | 8 | const old = require.extensions['.ts']; 9 | 10 | require.extensions['.ts'] = function(m, filename) { 11 | // If we're in node module, either call the old hook or simply compile the 12 | // file without transpilation. We do not touch node_modules/**. 13 | if (filename.match(/node_modules/)) { 14 | if (old) { 15 | return old(m, filename); 16 | } 17 | return m._compile(fs.readFileSync(filename), filename); 18 | } 19 | 20 | // Node requires all require hooks to be sync. 21 | const source = fs.readFileSync(filename).toString(); 22 | const result = ts.transpile(source, { 23 | target: ts.ScriptTarget.ES5, 24 | module: ts.ModuleKind.CommonJs, 25 | }); 26 | 27 | // Send it to node to execute. 28 | return m._compile(result, filename); 29 | }; 30 | 31 | // Import the TS once we know it's safe to require. 32 | module.exports = require('./karma.config.ts').config; 33 | -------------------------------------------------------------------------------- /test/karma.config.ts: -------------------------------------------------------------------------------- 1 | // This file is named differently than its JS bootstrapper to avoid the ts compiler to overwrite it. 2 | 3 | import path = require('path'); 4 | import { 5 | customLaunchers, 6 | platformMap, 7 | } from './browser-providers.ts'; 8 | 9 | 10 | export function config(config) { 11 | config.set({ 12 | basePath: path.join(__dirname, '..'), 13 | frameworks: ['jasmine'], 14 | plugins: [ 15 | require('karma-jasmine'), 16 | require('karma-browserstack-launcher'), 17 | require('karma-sauce-launcher'), 18 | require('karma-chrome-launcher'), 19 | require('karma-firefox-launcher'), 20 | ], 21 | files: [ 22 | {pattern: 'dist/vendor/core-js/client/core.js', included: true, watched: false}, 23 | {pattern: 'dist/vendor/systemjs/dist/system-polyfills.js', included: true, watched: false}, 24 | {pattern: 'dist/vendor/systemjs/dist/system.src.js', included: true, watched: false}, 25 | {pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false}, 26 | {pattern: 'dist/vendor/zone.js/dist/proxy.js', included: true, watched: false}, 27 | {pattern: 'dist/vendor/zone.js/dist/sync-test.js', included: true, watched: false}, 28 | {pattern: 'dist/vendor/zone.js/dist/jasmine-patch.js', included: true, watched: false}, 29 | {pattern: 'dist/vendor/zone.js/dist/async-test.js', included: true, watched: false}, 30 | {pattern: 'dist/vendor/zone.js/dist/fake-async-test.js', included: true, watched: false}, 31 | {pattern: 'dist/vendor/hammerjs/hammer.min.js', included: true, watched: false}, 32 | 33 | {pattern: 'test/karma-test-shim.js', included: true, watched: false}, 34 | 35 | // paths loaded via module imports 36 | {pattern: 'dist/**/*.js', included: false, watched: true}, 37 | 38 | // paths loaded via Angular's component compiler 39 | // (these paths need to be rewritten, see proxies section) 40 | {pattern: 'dist/**/*.html', included: false, watched: true}, 41 | {pattern: 'dist/**/*.css', included: false, watched: true}, 42 | 43 | // paths to support debugging with source maps in dev tools 44 | {pattern: 'dist/**/*.ts', included: false, watched: false}, 45 | {pattern: 'dist/**/*.js.map', included: false, watched: false} 46 | ], 47 | proxies: { 48 | // required for component assets fetched by Angular's compiler 49 | '/components/': '/base/dist/components/', 50 | '/core/': '/base/dist/core/', 51 | }, 52 | 53 | customLaunchers: customLaunchers, 54 | 55 | exclude: [], 56 | preprocessors: {}, 57 | reporters: ['dots'], 58 | port: 9876, 59 | colors: true, 60 | logLevel: config.LOG_INFO, 61 | autoWatch: true, 62 | 63 | sauceLabs: { 64 | testName: 'ionic2-extra', 65 | startConnect: false, 66 | recordVideo: false, 67 | recordScreenshots: false, 68 | options: { 69 | 'selenium-version': '2.48.2', 70 | 'command-timeout': 600, 71 | 'idle-timeout': 600, 72 | 'max-duration': 5400 73 | } 74 | }, 75 | 76 | browserStack: { 77 | project: 'ionic2-extra', 78 | startTunnel: false, 79 | retryLimit: 1, 80 | timeout: 600, 81 | pollingTimeout: 20000 82 | }, 83 | 84 | browserDisconnectTimeout: 20000, 85 | browserNoActivityTimeout: 240000, 86 | captureTimeout: 120000, 87 | browsers: ['Chrome_1024x768'], 88 | 89 | singleRun: false 90 | }); 91 | 92 | if (process.env['TRAVIS']) { 93 | var buildId = `TRAVIS #${process.env.TRAVIS_BUILD_NUMBER} (${process.env.TRAVIS_BUILD_ID})`; 94 | 95 | // The MODE variable is the indicator of what row in the test matrix we're running. 96 | // It will look like _, where platform is one of 'saucelabs' or 'browserstack', 97 | // and alias is one of the keys in the CIconfiguration variable declared in 98 | // browser-providers.ts. 99 | let [platform, alias] = process.env.MODE.split('_'); 100 | 101 | if (platform == 'saucelabs') { 102 | config.sauceLabs.build = buildId; 103 | config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; 104 | 105 | // TODO(mlaval): remove once SauceLabs supports websockets. 106 | // This speeds up the capturing a bit, as browsers don't even try to use websocket. 107 | console.log('>>>> setting socket.io transport to polling <<<<'); 108 | config.transports = ['polling']; 109 | } else if (platform == 'browserstack') { 110 | config.browserStack.build = buildId; 111 | config.browserStack.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; 112 | } else { 113 | throw new Error(`Platform "${platform}" unknown, but Travis specified. Exiting.`); 114 | } 115 | 116 | config.browsers = platformMap[platform][alias.toUpperCase()]; 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /test/protractor.conf.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // Load ts-node to be able to execute TypeScript files with protractor. 5 | require('ts-node').register({ 6 | project: path.join(__dirname, '../e2e/') 7 | }); 8 | 9 | 10 | const E2E_BASE_URL = process.env['E2E_BASE_URL'] || 'http://localhost:4200'; 11 | const config = { 12 | // TODO(jelbourn): add back plugin for a11y assersions once it supports specifying AXS options. 13 | useAllAngular2AppRoots: true, 14 | specs: [ path.join(__dirname, '../e2e/**/*.e2e.ts') ], 15 | baseUrl: E2E_BASE_URL, 16 | allScriptsTimeout: 120000, 17 | getPageTimeout: 120000, 18 | jasmineNodeOpts: { 19 | defaultTimeoutInterval: 120000, 20 | } 21 | }; 22 | 23 | if (process.env['TRAVIS']) { 24 | const key = require('../scripts/sauce/sauce_config'); 25 | config.sauceUser = process.env['SAUCE_USERNAME']; 26 | config.sauceKey = key; 27 | config.capabilities = { 28 | 'browserName': 'chrome', 29 | 'tunnel-identifier': process.env['TRAVIS_JOB_NUMBER'], 30 | 'build': process.env['TRAVIS_JOB_NUMBER'], 31 | 'name': 'Ionic 2 extra E2E Tests' 32 | }; 33 | } 34 | 35 | 36 | exports.config = config; 37 | -------------------------------------------------------------------------------- /tools/gulp/constants.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | 3 | export const PROJECT_ROOT = join(__dirname, '../..'); 4 | export const SOURCE_ROOT = join(PROJECT_ROOT, 'src'); 5 | 6 | export const DIST_ROOT = join(PROJECT_ROOT, 'dist'); 7 | export const DIST_COMPONENTS_ROOT = join(DIST_ROOT, '@ionic2-extra'); 8 | 9 | 10 | export const NPM_VENDOR_FILES = [ 11 | '@angular', 12 | 'core-js/client', 13 | 'hammerjs', 14 | 'ionic-angular', 15 | 'moment', 16 | 'rxjs', 17 | 'systemjs/dist', 18 | 'zone.js/dist' 19 | ]; 20 | -------------------------------------------------------------------------------- /tools/gulp/gulpfile.ts: -------------------------------------------------------------------------------- 1 | import './tasks/ci'; 2 | import './tasks/clean'; 3 | import './tasks/components'; 4 | import './tasks/default'; 5 | import './tasks/development'; 6 | import './tasks/e2e'; 7 | import './tasks/lint'; 8 | import './tasks/release'; 9 | import './tasks/serve'; 10 | import './tasks/unit-test'; 11 | -------------------------------------------------------------------------------- /tools/gulp/task_helpers.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as gulp from 'gulp'; 4 | import * as gulpTs from 'gulp-typescript'; 5 | import * as path from 'path'; 6 | 7 | import {NPM_VENDOR_FILES, PROJECT_ROOT, DIST_ROOT} from './constants'; 8 | 9 | 10 | /** Those imports lack typings. */ 11 | const gulpClean = require('gulp-clean'); 12 | const gulpMerge = require('merge2'); 13 | const gulpRunSequence = require('run-sequence'); 14 | const gulpSass = require('gulp-sass'); 15 | const gulpServer = require('gulp-server-livereload'); 16 | const gulpSourcemaps = require('gulp-sourcemaps'); 17 | const resolveBin = require('resolve-bin'); 18 | 19 | 20 | /** If the string passed in is a glob, returns it, otherwise append '**\/*' to it. */ 21 | function _globify(maybeGlob: string, suffix = '**/*') { 22 | return maybeGlob.indexOf('*') != -1 ? maybeGlob : path.join(maybeGlob, suffix); 23 | } 24 | 25 | 26 | /** Create a TS Build Task, based on the options. */ 27 | export function tsBuildTask(tsConfigPath: string) { 28 | let tsConfigDir = tsConfigPath; 29 | if (fs.existsSync(path.join(tsConfigDir, 'tsconfig.json'))) { 30 | // Append tsconfig.json 31 | tsConfigPath = path.join(tsConfigDir, 'tsconfig.json'); 32 | } else { 33 | tsConfigDir = path.dirname(tsConfigDir); 34 | } 35 | 36 | return () => { 37 | const tsConfig: any = JSON.parse(fs.readFileSync(tsConfigPath, 'utf-8')); 38 | const dest: string = path.join(tsConfigDir, tsConfig['compilerOptions']['outDir']); 39 | 40 | const tsProject = gulpTs.createProject(tsConfigPath, { 41 | typescript: require('typescript') 42 | }); 43 | 44 | let pipe = tsProject.src() 45 | .pipe(gulpSourcemaps.init()) 46 | .pipe(tsProject()); 47 | let dts = pipe.dts.pipe(gulp.dest(dest)); 48 | 49 | return gulpMerge([ 50 | dts, 51 | pipe 52 | .pipe(gulpSourcemaps.write('.')) 53 | .pipe(gulp.dest(dest)) 54 | ]); 55 | }; 56 | } 57 | 58 | 59 | /** Create a SASS Build Task. */ 60 | export function sassBuildTask(dest: string, root: string, includePaths: string[]) { 61 | const sassOptions = { includePaths }; 62 | 63 | return () => { 64 | return gulp.src(_globify(root, '**/*.scss')) 65 | .pipe(gulpSourcemaps.init()) 66 | .pipe(gulpSass(sassOptions).on('error', gulpSass.logError)) 67 | .pipe(gulpSourcemaps.write('.')) 68 | .pipe(gulp.dest(dest)); 69 | }; 70 | } 71 | 72 | 73 | /** Options that can be passed to execTask or execNodeTask. */ 74 | export interface ExecTaskOptions { 75 | // Whether to output to STDERR and STDOUT. 76 | silent?: boolean; 77 | // If an error happens, this will replace the standard error. 78 | errMessage?: string; 79 | } 80 | 81 | /** Create a task that executes a binary as if from the command line. */ 82 | export function execTask(binPath: string, args: string[], options: ExecTaskOptions = {}) { 83 | return (done: (err?: string) => void) => { 84 | const childProcess = child_process.spawn(binPath, args); 85 | 86 | if (!options.silent) { 87 | childProcess.stdout.on('data', (data: string) => { 88 | process.stdout.write(data); 89 | }); 90 | 91 | childProcess.stderr.on('data', (data: string) => { 92 | process.stderr.write(data); 93 | }); 94 | } 95 | 96 | childProcess.on('close', (code: number) => { 97 | if (code != 0) { 98 | if (options.errMessage === undefined) { 99 | done('Process failed with code ' + code); 100 | } else { 101 | done(options.errMessage); 102 | } 103 | } else { 104 | done(); 105 | } 106 | }); 107 | } 108 | } 109 | 110 | /** 111 | * Create a task that executes an NPM Bin, by resolving the binary path then executing it. These are 112 | * binaries that are normally in the `./node_modules/.bin` directory, but their name might differ 113 | * from the package. Examples are typescript, ngc and gulp itself. 114 | */ 115 | export function execNodeTask(packageName: string, executable: string | string[], args?: string[], 116 | options: ExecTaskOptions = {}) { 117 | if (!args) { 118 | args = executable; 119 | executable = undefined; 120 | } 121 | 122 | return (done: (err: any) => void) => { 123 | resolveBin(packageName, { executable: executable }, (err: any, binPath: string) => { 124 | if (err) { 125 | done(err); 126 | } else { 127 | // Forward to execTask. 128 | execTask(binPath, args, options)(done); 129 | } 130 | }); 131 | } 132 | } 133 | 134 | 135 | /** Copy files from a glob to a destination. */ 136 | export function copyTask(srcGlobOrDir: string, outRoot: string) { 137 | return () => { 138 | return gulp.src(_globify(srcGlobOrDir)).pipe(gulp.dest(outRoot)); 139 | } 140 | } 141 | 142 | 143 | /** Delete files. */ 144 | export function cleanTask(glob: string) { 145 | return () => gulp.src(glob, { read: false }).pipe(gulpClean(null)); 146 | } 147 | 148 | 149 | /** Build an task that depends on all application build tasks. */ 150 | export function buildAppTask(appName: string) { 151 | const buildTasks = ['vendor', 'ts', 'scss', 'assets'] 152 | .map(taskName => `:build:${appName}:${taskName}`); 153 | 154 | return (done: () => void) => { 155 | gulpRunSequence( 156 | 'clean', 157 | ['build:components', ...buildTasks], 158 | done 159 | ); 160 | }; 161 | } 162 | 163 | 164 | /** Create a task that copies vendor files in the proper destination. */ 165 | export function vendorTask() { 166 | return () => gulpMerge( 167 | NPM_VENDOR_FILES.map(root => { 168 | const glob = path.join(PROJECT_ROOT, 'node_modules', root, '**/*.+(js|js.map)'); 169 | return gulp.src(glob).pipe(gulp.dest(path.join(DIST_ROOT, 'vendor', root))); 170 | })); 171 | } 172 | 173 | 174 | /** Create a task that serves the dist folder. */ 175 | export function serverTask(liveReload: boolean = true, 176 | streamCallback: (stream: NodeJS.ReadWriteStream) => void = null) { 177 | return () => { 178 | const stream = gulp.src('dist').pipe(gulpServer({ 179 | livereload: liveReload, 180 | fallback: 'index.html', 181 | port: 4200 182 | })); 183 | 184 | if (streamCallback) { 185 | streamCallback(stream); 186 | } 187 | return stream; 188 | } 189 | } 190 | 191 | 192 | /** Create a task that's a sequence of other tasks. */ 193 | export function sequenceTask(...args: any[]) { 194 | return (done: any) => { 195 | gulpRunSequence( 196 | ...args, 197 | done 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /tools/gulp/tasks/ci.ts: -------------------------------------------------------------------------------- 1 | import {task} from 'gulp'; 2 | 3 | 4 | task('ci:lint', ['ci:forbidden-identifiers', 'lint']); 5 | 6 | task('ci:extract-metadata', [':build:components:ngc']); 7 | task('ci:forbidden-identifiers', function() { 8 | require('../../../scripts/ci/forbidden-identifiers.js'); 9 | }); 10 | 11 | // Travis sometimes does not exit the process and times out. This is to prevent that. 12 | task('ci:test', ['test:single-run'], () => process.exit(0)); 13 | // Travis sometimes does not exit the process and times out. This is to prevent that. 14 | task('ci:e2e', ['e2e'], () => process.exit(0)); 15 | -------------------------------------------------------------------------------- /tools/gulp/tasks/clean.ts: -------------------------------------------------------------------------------- 1 | import {task} from 'gulp'; 2 | import {cleanTask} from '../task_helpers'; 3 | 4 | 5 | task('clean', cleanTask('dist')); 6 | -------------------------------------------------------------------------------- /tools/gulp/tasks/components.ts: -------------------------------------------------------------------------------- 1 | import {task, watch} from 'gulp'; 2 | import {readdirSync, statSync, writeFileSync} from 'fs'; 3 | import * as path from 'path'; 4 | 5 | import {SOURCE_ROOT, DIST_COMPONENTS_ROOT, PROJECT_ROOT} from '../constants'; 6 | import {sassBuildTask, tsBuildTask, execNodeTask, copyTask, sequenceTask} from '../task_helpers'; 7 | 8 | // No typings for these. 9 | const inlineResources = require('../../../scripts/release/inline-resources'); 10 | const rollup = require('rollup').rollup; 11 | 12 | const componentsDir = path.join(SOURCE_ROOT, 'lib'); 13 | 14 | 15 | function camelCase(str: string) { 16 | return str.replace(/-(\w)/g, (_: any, letter: string) => { 17 | return letter.toUpperCase(); 18 | }); 19 | } 20 | 21 | 22 | task(':watch:components', () => { 23 | watch(path.join(componentsDir, '**/*.ts'), [':build:components:ts']); 24 | watch(path.join(componentsDir, '**/*.scss'), [':build:components:scss']); 25 | watch(path.join(componentsDir, '**/*.html'), [':build:components:assets']); 26 | }); 27 | 28 | task(':watch:components:spec', () => { 29 | watch(path.join(componentsDir, '**/*.ts'), [':build:components:spec']); 30 | watch(path.join(componentsDir, '**/*.scss'), [':build:components:scss']); 31 | watch(path.join(componentsDir, '**/*.html'), [':build:components:assets']); 32 | }); 33 | 34 | 35 | task(':build:components:ts', tsBuildTask(componentsDir)); 36 | task(':build:components:spec', tsBuildTask(path.join(componentsDir, 'tsconfig-spec.json'))); 37 | task(':build:components:assets', 38 | copyTask(path.join(componentsDir, '*/**/*.!(ts|spec.ts)'), DIST_COMPONENTS_ROOT)); 39 | task(':build:components:scss', sassBuildTask( 40 | DIST_COMPONENTS_ROOT, componentsDir, [path.join(componentsDir, 'core/style')] 41 | )); 42 | task(':build:components:rollup', [':build:components:ts'], () => { 43 | const components = readdirSync(componentsDir) 44 | .filter(componentName => (statSync(path.join(componentsDir, componentName))).isDirectory()); 45 | 46 | const globals: {[name: string]: string} = { 47 | // Angular dependencies 48 | '@angular/core': 'ng.core', 49 | '@angular/common': 'ng.common', 50 | '@angular/forms': 'ng.forms', 51 | '@angular/http': 'ng.http', 52 | '@angular/platform-browser': 'ng.platformBrowser', 53 | '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic', 54 | 55 | // Rxjs dependencies 56 | 'rxjs/Subject': 'Rx', 57 | 'rxjs/add/observable/forkJoin': 'Rx.Observable', 58 | 'rxjs/add/observable/of': 'Rx.Observable', 59 | 'rxjs/add/operator/toPromise': 'Rx.Observable.prototype', 60 | 'rxjs/add/operator/map': 'Rx.Observable.prototype', 61 | 'rxjs/add/operator/filter': 'Rx.Observable.prototype', 62 | 'rxjs/add/operator/do': 'Rx.Observable.prototype', 63 | 'rxjs/add/operator/share': 'Rx.Observable.prototype', 64 | 'rxjs/add/operator/finally': 'Rx.Observable.prototype', 65 | 'rxjs/add/operator/catch': 'Rx.Observable.prototype', 66 | 'rxjs/Observable': 'Rx', 67 | 68 | 'ionic-angular': 'ing', 69 | 'moment': 'moment', 70 | '@ngx-translate/core': 'ngxt.core' 71 | }; 72 | components.forEach(name => { 73 | globals[`@ionic2-extra/${name}`] = `i2e.${camelCase(name)}`; 74 | }); 75 | 76 | // Build all of them asynchronously. 77 | return components.reduce((previous, name) => { 78 | return previous 79 | .then(() => { 80 | return rollup({ 81 | input: path.join(DIST_COMPONENTS_ROOT, name, 'public_api.js'), 82 | context: 'window', 83 | external: [ 84 | ...Object.keys(globals), 85 | ...components.map(n => `@ionic2-extra/${n}`) 86 | ] 87 | }); 88 | }) 89 | .then((bundle: any) => { 90 | bundle.generate({ 91 | moduleId: '', 92 | name: `md.${camelCase(name)}`, 93 | format: 'umd', 94 | globals, 95 | sourcemap: true 96 | }) 97 | .then((result: any) => { 98 | const outputPath = path.join(DIST_COMPONENTS_ROOT, name, `${name}.umd.js`); 99 | writeFileSync( outputPath, result.code ); 100 | }); 101 | }); 102 | }, Promise.resolve()); 103 | }); 104 | 105 | task('build:components', sequenceTask( 106 | ':build:components:rollup', 107 | ':build:components:assets', 108 | ':build:components:scss', 109 | ':inline-resources', 110 | )); 111 | 112 | task(':build:components:ngc', ['build:components'], execNodeTask( 113 | '@angular/compiler-cli', 'ngc', 114 | ['-p', path.relative(PROJECT_ROOT, path.join(componentsDir, 'tsconfig.json'))] 115 | )); 116 | 117 | task(':inline-resources', () => { 118 | inlineResources([DIST_COMPONENTS_ROOT]); 119 | }); 120 | -------------------------------------------------------------------------------- /tools/gulp/tasks/default.ts: -------------------------------------------------------------------------------- 1 | import {task} from 'gulp'; 2 | const gulp = require('gulp'); 3 | 4 | task('default', ['help']); 5 | 6 | task('help', function() { 7 | const taskList = Object.keys(gulp.tasks) 8 | .filter(taskName => !taskName.startsWith(':')) 9 | .filter(taskName => !taskName.startsWith('ci:')) 10 | .filter(taskName => taskName != 'default') 11 | .sort(); 12 | 13 | console.log(`\nHere's a list of supported tasks:\n `, taskList.join('\n ')); 14 | console.log(`\nYou're probably looking for "test" or "serve:devapp".\n\n`); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /tools/gulp/tasks/development.ts: -------------------------------------------------------------------------------- 1 | import {task, watch} from 'gulp'; 2 | import * as path from 'path'; 3 | 4 | import {DIST_ROOT, SOURCE_ROOT} from '../constants'; 5 | import { 6 | sassBuildTask, tsBuildTask, copyTask, buildAppTask, vendorTask, 7 | serverTask, sequenceTask 8 | } from '../task_helpers'; 9 | 10 | 11 | const appDir = path.join(SOURCE_ROOT, 'demo-app'); 12 | const outDir = DIST_ROOT; 13 | 14 | 15 | task(':watch:devapp', () => { 16 | watch(path.join(appDir, '**/*.ts'), [':build:devapp:ts']); 17 | watch(path.join(appDir, '**/*.scss'), [':build:devapp:scss']); 18 | watch(path.join(appDir, '**/*.html'), [':build:devapp:assets']); 19 | }); 20 | 21 | 22 | task(':build:devapp:vendor', vendorTask()); 23 | task(':build:devapp:ts', [':build:components:rollup'], tsBuildTask(appDir)); 24 | task(':build:devapp:scss', [':build:components:scss'], sassBuildTask(outDir, appDir, [])); 25 | task(':build:devapp:assets', copyTask(appDir, outDir)); 26 | task('build:devapp', buildAppTask('devapp')); 27 | 28 | task(':serve:devapp', serverTask()); 29 | task('serve:devapp', ['build:devapp'], sequenceTask( 30 | [':serve:devapp', ':watch:components', ':watch:devapp'] 31 | )); 32 | -------------------------------------------------------------------------------- /tools/gulp/tasks/e2e.ts: -------------------------------------------------------------------------------- 1 | import {task, watch} from 'gulp'; 2 | import * as path from 'path'; 3 | import gulpMerge = require('merge2'); 4 | import gulpRunSequence = require('run-sequence'); 5 | 6 | import {SOURCE_ROOT, DIST_ROOT, PROJECT_ROOT} from '../constants'; 7 | import { 8 | tsBuildTask, sassBuildTask, copyTask, buildAppTask, execNodeTask, 9 | vendorTask, sequenceTask, serverTask 10 | } from '../task_helpers'; 11 | 12 | 13 | const appDir = path.join(SOURCE_ROOT, 'e2e-app'); 14 | const outDir = DIST_ROOT; 15 | const PROTRACTOR_CONFIG_PATH = path.join(PROJECT_ROOT, 'test/protractor.conf.js'); 16 | 17 | 18 | task(':watch:e2eapp', () => { 19 | watch(path.join(appDir, '**/*.ts'), [':build:e2eapp:ts']); 20 | watch(path.join(appDir, '**/*.scss'), [':build:e2eapp:scss']); 21 | watch(path.join(appDir, '**/*.html'), [':build:e2eapp:assets']); 22 | }); 23 | 24 | 25 | task(':build:e2eapp:vendor', vendorTask()); 26 | task(':build:e2eapp:ts', [':build:components:ts'], tsBuildTask(appDir)); 27 | task(':build:e2eapp:scss', [':build:components:scss'], sassBuildTask(outDir, appDir, [])); 28 | task(':build:e2eapp:assets', copyTask(appDir, outDir)); 29 | 30 | task('build:e2eapp', buildAppTask('e2eapp')); 31 | 32 | 33 | task(':test:protractor:setup', execNodeTask('protractor', 'webdriver-manager', ['update'])); 34 | task(':test:protractor', execNodeTask('protractor', [PROTRACTOR_CONFIG_PATH])); 35 | // This task is used because, in some cases, protractor will block and not exit the process, 36 | // causing Travis to timeout. This task should always be used in a synchronous sequence as 37 | // the last step. 38 | task(':e2e:done', () => process.exit(0)); 39 | 40 | let stopE2eServer: () => void = null; 41 | task(':serve:e2eapp', serverTask(false, (stream) => { stopE2eServer = () => stream.emit('kill') })); 42 | task(':serve:e2eapp:stop', () => stopE2eServer()); 43 | task('serve:e2eapp', ['build:e2eapp'], sequenceTask([ 44 | ':inline-resources', 45 | ':serve:e2eapp', 46 | ':watch:components', 47 | ])); 48 | 49 | 50 | task('e2e', sequenceTask( 51 | ':test:protractor:setup', 52 | 'serve:e2eapp', 53 | ':test:protractor', 54 | ':serve:e2eapp:stop', 55 | ':e2e:done', 56 | )); 57 | -------------------------------------------------------------------------------- /tools/gulp/tasks/lint.ts: -------------------------------------------------------------------------------- 1 | import gulp = require('gulp'); 2 | import {execNodeTask} from '../task_helpers'; 3 | 4 | 5 | gulp.task('lint', ['tslint', 'stylelint', 'madge']); 6 | gulp.task('madge', ['build:release'], execNodeTask('madge', ['--circular', './dist'])); 7 | gulp.task('stylelint', execNodeTask( 8 | 'stylelint', ['src/**/*.scss', '--config', 'stylelint-config.json', '--syntax', 'scss'] 9 | )); 10 | gulp.task('tslint', execNodeTask('tslint', ['-c', 'tslint.json', 'src/**/*.ts'])); 11 | -------------------------------------------------------------------------------- /tools/gulp/tasks/release.ts: -------------------------------------------------------------------------------- 1 | import {spawn} from 'child_process'; 2 | import {existsSync, readdirSync, statSync} from 'fs'; 3 | import {task} from 'gulp'; 4 | import gulpRunSequence = require('run-sequence'); 5 | import path = require('path'); 6 | import minimist = require('minimist'); 7 | 8 | import {execTask, cleanTask} from '../task_helpers'; 9 | import {DIST_COMPONENTS_ROOT} from '../constants'; 10 | 11 | const argv = minimist(process.argv.slice(3)); 12 | 13 | 14 | task(':build:release:clean-spec', cleanTask('dist/**/*.spec.*')); 15 | 16 | 17 | task('build:release', function(done: () => void) { 18 | // Synchronously run those tasks. 19 | gulpRunSequence( 20 | 'clean', 21 | ':build:components:ngc', 22 | ':inline-resources', 23 | ':build:release:clean-spec', 24 | done 25 | ); 26 | }); 27 | 28 | 29 | /** Make sure we're logged in. */ 30 | task(':publish:whoami', execTask('npm', ['whoami'], { 31 | silent: true, 32 | errMessage: 'You must be logged in to publish.' 33 | })); 34 | 35 | task(':publish:logout', execTask('npm', ['logout'])); 36 | 37 | 38 | function _execNpmPublish(componentName: string, label: string): Promise { 39 | const componentPath = path.join(DIST_COMPONENTS_ROOT, componentName); 40 | const stat = statSync(componentPath); 41 | 42 | if (!stat.isDirectory()) { 43 | return; 44 | } 45 | 46 | if (!existsSync(path.join(componentPath, 'package.json'))) { 47 | console.log(`Skipping ${componentPath} as it does not have a package.json.`); 48 | return; 49 | } 50 | 51 | process.chdir(componentPath); 52 | console.log(`Publishing ${componentName}...`); 53 | 54 | const command = 'npm'; 55 | const args = ['publish', '--access', 'public', label ? `--tag` : undefined, label || undefined]; 56 | return new Promise((resolve, reject) => { 57 | console.log(`Executing "${command} ${args.join(' ')}"...`); 58 | 59 | const childProcess = spawn(command, args); 60 | childProcess.stdout.on('data', (data: Buffer) => { 61 | console.log(`stdout: ${data.toString().split(/[\n\r]/g).join('\n ')}`); 62 | }); 63 | childProcess.stderr.on('data', (data: Buffer) => { 64 | console.error(`stderr: ${data.toString().split(/[\n\r]/g).join('\n ')}`); 65 | }); 66 | 67 | childProcess.on('close', (code: number) => { 68 | if (code == 0) { 69 | resolve(); 70 | } else { 71 | reject(new Error(`Component ${componentName} did not publish, status: ${code}.`)); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | task(':publish', function(done: (err?: any) => void) { 78 | const label = argv['tag']; 79 | const currentDir = process.cwd(); 80 | 81 | if (!label) { 82 | console.log('You can use a label with --tag=labelName.'); 83 | console.log('Publishing using the latest tag.'); 84 | } else { 85 | console.log(`Publishing using the ${label} tag.`); 86 | } 87 | console.log('\n\n'); 88 | 89 | // Build a promise chain that publish each component. 90 | readdirSync(DIST_COMPONENTS_ROOT) 91 | .reduce((prev, dirName) => prev.then(() => _execNpmPublish(dirName, label)), Promise.resolve()) 92 | .then(() => done()) 93 | .catch((err: Error) => done(err)) 94 | .then(() => process.chdir(currentDir)); 95 | }); 96 | 97 | task('publish', function(done: () => void) { 98 | gulpRunSequence( 99 | ':publish:whoami', 100 | 'build:release', 101 | ':publish', 102 | ':publish:logout', 103 | done 104 | ); 105 | }); 106 | -------------------------------------------------------------------------------- /tools/gulp/tasks/serve.ts: -------------------------------------------------------------------------------- 1 | import {task} from 'gulp'; 2 | import {serverTask} from '../task_helpers'; 3 | 4 | 5 | task('serve', serverTask()); 6 | -------------------------------------------------------------------------------- /tools/gulp/tasks/unit-test.ts: -------------------------------------------------------------------------------- 1 | import gulp = require('gulp'); 2 | import path = require('path'); 3 | import gulpMerge = require('merge2'); 4 | 5 | import {PROJECT_ROOT, DIST_COMPONENTS_ROOT} from '../constants'; 6 | import {sequenceTask} from '../task_helpers'; 7 | 8 | const karma = require('karma'); 9 | const runSequence = require('run-sequence'); 10 | 11 | gulp.task(':build:test:vendor', function() { 12 | const npmVendorFiles = [ 13 | '@angular', 14 | 'core-js/client', 15 | 'hammerjs', 16 | 'ionic-angular', 17 | 'moment', 18 | 'rxjs', 19 | 'systemjs/dist', 20 | 'zone.js/dist' 21 | ]; 22 | 23 | return gulpMerge( 24 | npmVendorFiles.map(function(root) { 25 | const glob = path.join(root, '**/*.+(js|js.map)'); 26 | return gulp.src(path.join('node_modules', glob)) 27 | .pipe(gulp.dest(path.join('dist/vendor', root))); 28 | })); 29 | }); 30 | 31 | gulp.task(':test:deps', sequenceTask( 32 | 'clean', 33 | [ 34 | ':build:test:vendor', 35 | ':build:components:assets', 36 | ':build:components:scss', 37 | ':build:components:spec', 38 | ':watch:components:spec', 39 | ] 40 | )); 41 | 42 | gulp.task('test', [':test:deps'], (done: () => void) => { 43 | new karma.Server({ 44 | configFile: path.join(PROJECT_ROOT, 'test/karma.conf.js') 45 | }, done).start(); 46 | }); 47 | 48 | gulp.task('test:single-run', [':test:deps'], (done: () => void) => { 49 | runSequence( 50 | ':inline-resources', 51 | () => { 52 | new karma.Server({ 53 | configFile: path.join(PROJECT_ROOT, 'test/karma.conf.js'), 54 | singleRun: true 55 | }, done).start(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tools/gulp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es6", "es2015"], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "noEmitOnError": true, 10 | "noImplicitAny": true, 11 | "outDir": "../../dist/gulp", 12 | "rootDir": ".", 13 | "sourceMap": true, 14 | "target": "es5", 15 | "inlineSources": true, 16 | "stripInternal": true, 17 | "baseUrl": "", 18 | "typeRoots": [ 19 | "../../node_modules/@types" 20 | ], 21 | "types": [ 22 | "node" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tools/travis/fold.ts: -------------------------------------------------------------------------------- 1 | 2 | function encode(str: string) { 3 | return str.replace(/\W/g, '-').replace(/-$/, ''); 4 | } 5 | 6 | export function travisFoldStart(name: string): () => void { 7 | if (process.env['TRAVIS']) { 8 | console.log('travis_fold:start:' + encode(name)); 9 | } 10 | 11 | return () => { 12 | if (process.env['TRAVIS']) { 13 | console.log('travis_fold:end:' + encode(name)); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "max-line-length": [true, 100], 4 | "no-inferrable-types": true, 5 | "class-name": true, 6 | "comment-format": [ 7 | true, 8 | "check-space" 9 | ], 10 | "indent": [ 11 | true, 12 | "spaces" 13 | ], 14 | "eofline": true, 15 | "no-duplicate-variable": true, 16 | "no-eval": true, 17 | "no-arg": true, 18 | "no-internal-module": true, 19 | "no-trailing-whitespace": true, 20 | "no-bitwise": true, 21 | "no-shadowed-variable": true, 22 | "no-unused-expression": true, 23 | "no-unused-variable": [true, {"ignore-pattern": "^(_.*)$"}], 24 | "one-line": [ 25 | true, 26 | "check-catch", 27 | "check-else", 28 | "check-open-brace", 29 | "check-whitespace" 30 | ], 31 | "quotemark": [ 32 | true, 33 | "single", 34 | "avoid-escape" 35 | ], 36 | "semicolon": true, 37 | "typedef-whitespace": [ 38 | true, 39 | { 40 | "call-signature": "nospace", 41 | "index-signature": "nospace", 42 | "parameter": "nospace", 43 | "property-declaration": "nospace", 44 | "variable-declaration": "nospace" 45 | } 46 | ], 47 | "curly": true, 48 | "variable-name": [ 49 | true, 50 | "ban-keywords", 51 | "check-format", 52 | "allow-leading-underscore" 53 | ], 54 | "whitespace": [ 55 | true, 56 | "check-branch", 57 | "check-decl", 58 | "check-operator", 59 | "check-separator", 60 | "check-type" 61 | ] 62 | } 63 | } 64 | --------------------------------------------------------------------------------