Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
018750154d | |||
b19216d58b | |||
84fc1a3663 | |||
1c40be26c6 | |||
2c5cf19c6d | |||
0dacf6d5f1 | |||
e9f1d44015 | |||
1d9024ee9a | |||
6a6164ab4f | |||
7231f5e26a | |||
86415223cb | |||
269f5acc54 | |||
6e6c866de9 | |||
732ed92cb7 | |||
53a807ae09 | |||
3342a8253b | |||
630c19f52d | |||
af8c2fa4be | |||
0789601dd6 | |||
ce0ac46e42 | |||
b531d87580 | |||
23a2154817 | |||
76d2496f24 | |||
b85cb410f1 | |||
1be22df0df | |||
a805839d38 | |||
3ac61a7550 | |||
57ea33bc5c | |||
4891649d68 | |||
93aba1bb1c | |||
f983a6c615 | |||
18f1b016e5 | |||
591dcc26af | |||
4acd322128 | |||
32a814bdfa |
@ -104,7 +104,7 @@ groups:
|
||||
animations:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/animation/*"
|
||||
- "packages/animations/*"
|
||||
- "packages/platform-browser/animations/*"
|
||||
users:
|
||||
- matsko #primary
|
||||
|
@ -1,12 +1,10 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
# force trusty as Google Chrome addon is not supported on Precise
|
||||
dist: trusty
|
||||
node_js:
|
||||
- '6.9.5'
|
||||
|
||||
addons:
|
||||
chrome: stable
|
||||
# firefox: "38.0"
|
||||
apt:
|
||||
sources:
|
||||
@ -50,7 +48,8 @@ env:
|
||||
- CI_MODE=e2e_2
|
||||
- CI_MODE=js
|
||||
- CI_MODE=saucelabs_required
|
||||
- CI_MODE=browserstack_required
|
||||
# deactivated, see #19768
|
||||
# - CI_MODE=browserstack_required
|
||||
- CI_MODE=saucelabs_optional
|
||||
- CI_MODE=browserstack_optional
|
||||
- CI_MODE=aio_tools_test
|
||||
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -1,12 +1,36 @@
|
||||
<a name="4.4.7"></a>
|
||||
## [4.4.7](https://github.com/angular/angular/compare/4.4.6...4.4.7) (2018-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** use appropriate inert document strategy for Firefox & Safari ([#22077](https://github.com/angular/angular/issues/22077)) ([2c5cf19](https://github.com/angular/angular/commit/2c5cf19))
|
||||
|
||||
|
||||
|
||||
<a name="4.4.6"></a>
|
||||
## [4.4.6](https://github.com/angular/angular/compare/4.4.5...4.4.6) (2017-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** properly support boolean-based transitions and state changes ([#19672](https://github.com/angular/angular/issues/19672)) ([f983a6c](https://github.com/angular/angular/commit/f983a6c)), closes [#9396](https://github.com/angular/angular/issues/9396) [#12337](https://github.com/angular/angular/issues/12337)
|
||||
* **common:** attempt to JSON.parse errors for JSON responses ([#19773](https://github.com/angular/angular/issues/19773)) ([269f5ac](https://github.com/angular/angular/commit/269f5ac))
|
||||
* **router:** RouterLinkActive should update its state right after checking the children ([53a807a](https://github.com/angular/angular/commit/53a807a)), closes [#18983](https://github.com/angular/angular/issues/18983)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **animations:** reduce size of bundle by removing AST classes ([#19673](https://github.com/angular/angular/issues/19673)) ([76d2496](https://github.com/angular/angular/commit/76d2496))
|
||||
|
||||
|
||||
|
||||
<a name="4.4.5"></a>
|
||||
## [4.4.5](https://github.com/angular/angular/compare/4.4.4...4.4.5) (2017-10-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reformat files from previous cherry-picks ([ccc25ee](https://github.com/angular/angular/commit/ccc25ee))
|
||||
* **aio:** downgrade yarn to 1.0.2 temporarily ([#19600](https://github.com/angular/angular/issues/19600)) ([d45e3aa](https://github.com/angular/angular/commit/d45e3aa))
|
||||
* **aio:** fix SearchService to work with TypeScript 2.4 ([#19600](https://github.com/angular/angular/issues/19600)) ([cf4b4d5](https://github.com/angular/angular/commit/cf4b4d5))
|
||||
* **compiler:** `TestBed.overrideProvider` should keep imported `NgModule`s eager ([#19624](https://github.com/angular/angular/issues/19624)) ([734378c](https://github.com/angular/angular/commit/734378c))
|
||||
* **compiler:** correctly instantiate eager providers that are used via `Injector.get` ([#19558](https://github.com/angular/angular/issues/19558)) ([e292548](https://github.com/angular/angular/commit/e292548)), closes [#15501](https://github.com/angular/angular/issues/15501)
|
||||
* **compiler:** disallow references for select and index evaluation ([95f3b1d](https://github.com/angular/angular/commit/95f3b1d))
|
||||
|
@ -12,8 +12,10 @@ describe('Component Style Tests', function () {
|
||||
let componentH1 = element(by.css('hero-app > h1'));
|
||||
let externalH1 = element(by.css('body > h1'));
|
||||
|
||||
expect(componentH1.getCssValue('fontWeight')).toEqual('normal');
|
||||
expect(externalH1.getCssValue('fontWeight')).not.toEqual('normal');
|
||||
// Note: sometimes webdriver returns the fontWeight as "normal",
|
||||
// othertimes as "400", both of which are equal in CSS terms.
|
||||
expect(componentH1.getCssValue('fontWeight')).toMatch(/normal|400/);
|
||||
expect(externalH1.getCssValue('fontWeight')).not.toMatch(/normal|400/);
|
||||
});
|
||||
|
||||
|
||||
|
@ -7,3 +7,4 @@
|
||||
<p>We're sorry. The page you are looking for cannot be found.</p>
|
||||
</div>
|
||||
</div>
|
||||
<aio-file-not-found-search></aio-file-not-found-search>
|
||||
|
@ -4502,8 +4502,8 @@ helps instantly identify which members of the component serve which purpose.
|
||||
|
||||
**Why?** The property associated with `@HostBinding` or the method associated with `@HostListener`
|
||||
can be modified only in a single place—in the directive's class.
|
||||
If you use the `host` metadata property, you must modify both the property declaration inside the controller,
|
||||
and the metadata associated with the directive.
|
||||
If you use the `host` metadata property, you must modify both the property/method declaration in the
|
||||
directive's class and the metadata in the decorator associated with the directive.
|
||||
|
||||
|
||||
</div>
|
||||
|
@ -13,18 +13,6 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- ngJapan -->
|
||||
<tr>
|
||||
<th><a href="http://ngjapan.org/" title="ng-Japan">ng-Japan</a></th>
|
||||
<td>Tokyo, Japan</td>
|
||||
<td>June 17, 2017</td>
|
||||
</tr>
|
||||
<!-- AngularMix -->
|
||||
<tr>
|
||||
<th><a href="https://angularmix.com/" title="AngularMix">AngularMix</a></th>
|
||||
<td>Universal Studios, Orlando, Florida</td>
|
||||
<td>October 8, 2017</td>
|
||||
</tr>
|
||||
<!-- ReactiveConf -->
|
||||
<tr>
|
||||
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
|
||||
@ -35,7 +23,7 @@
|
||||
<tr>
|
||||
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
|
||||
<td>London, United Kingdom</td>
|
||||
<td>November 07, 2017</td>
|
||||
<td>November 7-8, 2017</td>
|
||||
</tr>
|
||||
<!-- ngAtlanta-->
|
||||
<tr>
|
||||
@ -43,11 +31,30 @@
|
||||
<td>Atlanta, Georgia</td>
|
||||
<td>January 30, 2018</td>
|
||||
</tr>
|
||||
<!-- ngVikings-->
|
||||
<tr>
|
||||
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
|
||||
<td>Helsinki, Finland</td>
|
||||
<td>March 1-2, 2018</td>
|
||||
</tr>
|
||||
<!-- ngconf 2018-->
|
||||
<tr>
|
||||
<th><a href="https://www.ng-conf.org/" title="ng-conf">ng-conf</a></th>
|
||||
<td>Salt Lake City, UT</td>
|
||||
<td>April 18-20, 2018</td>
|
||||
</tr>
|
||||
<!-- WeRDevs-->
|
||||
<tr>
|
||||
<th><a href="https://www.wearedevelopers.com/" title="WeAreDevs">WeAreDevelopers</a></th>
|
||||
<td>Vienna</td>
|
||||
<td>May 16-18, 2018</td>
|
||||
</tr>
|
||||
<!-- AngularConnect-->
|
||||
<tr>
|
||||
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
|
||||
<td>London, United Kingdom</td>
|
||||
<td>November 5-7, 2018</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { browser, element, by, promise } from 'protractor';
|
||||
import { element, by } from 'protractor';
|
||||
import { SitePage } from './app.po';
|
||||
|
||||
describe('site App', function() {
|
||||
@ -41,20 +41,20 @@ describe('site App', function() {
|
||||
|
||||
describe('scrolling to the top', () => {
|
||||
it('should scroll to the top when navigating to another page', () => {
|
||||
page.navigateTo('guide/docs');
|
||||
page.navigateTo('guide/security');
|
||||
page.scrollToBottom();
|
||||
page.getScrollTop().then(scrollTop => expect(scrollTop).toBeGreaterThan(0));
|
||||
|
||||
page.navigateTo('guide/api');
|
||||
page.navigateTo('api');
|
||||
page.getScrollTop().then(scrollTop => expect(scrollTop).toBe(0));
|
||||
});
|
||||
|
||||
it('should scroll to the top when navigating to the same page', () => {
|
||||
page.navigateTo('guide/docs');
|
||||
page.navigateTo('guide/security');
|
||||
page.scrollToBottom();
|
||||
page.getScrollTop().then(scrollTop => expect(scrollTop).toBeGreaterThan(0));
|
||||
|
||||
page.navigateTo('guide/docs');
|
||||
page.navigateTo('guide/security');
|
||||
page.getScrollTop().then(scrollTop => expect(scrollTop).toBe(0));
|
||||
});
|
||||
});
|
||||
@ -66,7 +66,9 @@ describe('site App', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('google analytics', () => {
|
||||
// TODO(https://github.com/angular/angular/issues/19785): Activate this again
|
||||
// once it is no more flaky.
|
||||
xdescribe('google analytics', () => {
|
||||
beforeEach(done => page.gaReady.then(done));
|
||||
|
||||
it('should call ga', done => {
|
||||
@ -100,4 +102,12 @@ describe('site App', function() {
|
||||
expect(page.getSearchResults().map(link => link.getText())).toContain('ControlValueAccessor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('404 page', () => {
|
||||
it('should search the index for words found in the url', () => {
|
||||
page.navigateTo('http/router');
|
||||
expect(page.getSearchResults().map(link => link.getText())).toContain('Http');
|
||||
expect(page.getSearchResults().map(link => link.getText())).toContain('Router');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -30,8 +30,14 @@ module.exports = function (config) {
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
browsers: ['CustomChrome'],
|
||||
browserNoActivityTimeout: 60000,
|
||||
singleRun: false
|
||||
singleRun: false,
|
||||
customLaunchers: {
|
||||
CustomChrome: {
|
||||
base: 'Chrome',
|
||||
flags: process.env.TRAVIS && ['--no-sandbox']
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -57,7 +57,7 @@
|
||||
"~~check-env": "node scripts/check-environment",
|
||||
"~~build": "ng build --target=production --environment=stable -sm --build-optimizer",
|
||||
"post~~build": "yarn sw-manifest && yarn sw-copy",
|
||||
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false"
|
||||
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.5 <7.0.0",
|
||||
|
@ -12,7 +12,8 @@ exports.config = {
|
||||
browserName: 'chrome',
|
||||
// For Travis
|
||||
chromeOptions: {
|
||||
binary: process.env.CHROME_BIN
|
||||
binary: process.env.CHROME_BIN,
|
||||
args: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
directConnect: true,
|
||||
|
@ -3,7 +3,7 @@
|
||||
set -u -e -o pipefail
|
||||
|
||||
declare -A limitUncompressed
|
||||
limitUncompressed=(["inline"]=1600 ["main"]=525000 ["polyfills"]=38000)
|
||||
limitUncompressed=(["inline"]=1600 ["main"]=525500 ["polyfills"]=38000)
|
||||
declare -A limitGzip7
|
||||
limitGzip7=(["inline"]=1000 ["main"]=127000 ["polyfills"]=12500)
|
||||
declare -A limitGzip9
|
||||
|
@ -67,7 +67,7 @@ case $deployEnv in
|
||||
readonly firebaseToken=$FIREBASE_TOKEN
|
||||
;;
|
||||
archive)
|
||||
readonly projectId=angular-io-${majorVersion}
|
||||
readonly projectId=v${majorVersion}-angular-io
|
||||
readonly deployedUrl=https://v${majorVersion}.angular.io/
|
||||
readonly firebaseToken=$FIREBASE_TOKEN
|
||||
;;
|
||||
|
@ -94,7 +94,7 @@ Deployment URL : https://angular.io/"
|
||||
)
|
||||
expected="Git branch : 2.4.x
|
||||
Build/deploy mode : archive
|
||||
Firebase project : angular-io-2
|
||||
Firebase project : v2-angular-io
|
||||
Deployment URL : https://v2.angular.io/"
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
|
@ -17,8 +17,16 @@ const printer = require('lighthouse/lighthouse-cli/printer');
|
||||
const config = require('lighthouse/lighthouse-core/config/default.js');
|
||||
|
||||
// Constants
|
||||
const CHROME_LAUNCH_OPTS = {};
|
||||
const VIEWER_URL = 'https://googlechrome.github.io/lighthouse/viewer/';
|
||||
|
||||
|
||||
// Specify the path and flags for Chrome on Travis
|
||||
if (process.env.TRAVIS) {
|
||||
process.env.LIGHTHOUSE_CHROMIUM_PATH = process.env.CHROME_BIN;
|
||||
CHROME_LAUNCH_OPTS.chromeFlags = ['--no-sandbox'];
|
||||
}
|
||||
|
||||
// Run
|
||||
_main(process.argv.slice(2));
|
||||
|
||||
@ -66,7 +74,7 @@ function ignoreHttpsAudits(config) {
|
||||
}
|
||||
|
||||
function launchChromeAndRunLighthouse(url, flags, config) {
|
||||
return chromeLauncher.launch().then(chrome => {
|
||||
return chromeLauncher.launch(CHROME_LAUNCH_OPTS).then(chrome => {
|
||||
flags.port = chrome.port;
|
||||
return lighthouse(url, flags, config).
|
||||
then(results => chrome.kill().then(() => results)).
|
||||
|
@ -13,7 +13,7 @@
|
||||
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
|
||||
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
|
||||
</md-toolbar>
|
||||
<aio-search-results #searchResults *ngIf="showSearchResults" (resultSelected)="hideSearchResults()"></aio-search-results>
|
||||
<aio-search-results #searchResultsView *ngIf="showSearchResults" [searchResults]="searchResults | async" (resultSelected)="hideSearchResults()"></aio-search-results>
|
||||
|
||||
<md-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { async, inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MdProgressBar, MdSidenav } from '@angular/material';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
@ -22,9 +21,9 @@ import { MockSearchService } from 'testing/search.service';
|
||||
import { NavigationNode } from 'app/navigation/navigation.service';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
||||
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { SelectComponent, Option } from 'app/shared/select/select.component';
|
||||
import { SelectComponent } from 'app/shared/select/select.component';
|
||||
import { TocComponent } from 'app/embedded/toc/toc.component';
|
||||
import { TocItem, TocService } from 'app/shared/toc.service';
|
||||
|
||||
@ -1054,11 +1053,6 @@ class TestGaService {
|
||||
locationChanged = jasmine.createSpy('locationChanged');
|
||||
}
|
||||
|
||||
class TestSearchService {
|
||||
initWorker = jasmine.createSpy('initWorker');
|
||||
loadIndex = jasmine.createSpy('loadIndex');
|
||||
}
|
||||
|
||||
class TestHttpClient {
|
||||
|
||||
static versionInfo = {
|
||||
|
@ -2,18 +2,18 @@ import { Component, ElementRef, HostBinding, HostListener, OnInit,
|
||||
QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||
import { MdSidenav } from '@angular/material';
|
||||
|
||||
import { CurrentNodes, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
||||
import { CurrentNodes, NavigationService, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
||||
import { DocumentService, DocumentContents } from 'app/documents/document.service';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
import { Deployment } from 'app/shared/deployment.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { SearchResults } from 'app/search/interfaces';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { combineLatest } from 'rxjs/observable/combineLatest';
|
||||
|
||||
@ -89,10 +89,9 @@ export class AppComponent implements OnInit {
|
||||
|
||||
// Search related properties
|
||||
showSearchResults = false;
|
||||
@ViewChildren('searchBox, searchResults', { read: ElementRef })
|
||||
searchResults: Observable<SearchResults>;
|
||||
@ViewChildren('searchBox, searchResultsView', { read: ElementRef })
|
||||
searchElements: QueryList<ElementRef>;
|
||||
@ViewChild(SearchResultsComponent)
|
||||
searchResults: SearchResultsComponent;
|
||||
@ViewChild(SearchBoxComponent)
|
||||
searchBox: SearchBoxComponent;
|
||||
|
||||
@ -332,7 +331,7 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
|
||||
doSearch(query) {
|
||||
this.searchService.search(query);
|
||||
this.searchResults = this.searchService.search(query);
|
||||
this.showSearchResults = !!query;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,6 @@ import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
|
||||
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
|
||||
import { SearchResultsComponent } from './search/search-results/search-results.component';
|
||||
import { SearchBoxComponent } from './search/search-box/search-box.component';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
|
||||
@ -95,7 +94,6 @@ export const svgIconProviders = [
|
||||
ModeBannerComponent,
|
||||
NavMenuComponent,
|
||||
NavItemComponent,
|
||||
SearchResultsComponent,
|
||||
SearchBoxComponent,
|
||||
TopMenuComponent,
|
||||
],
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
import { ApiListComponent } from './api-list.component';
|
||||
|
@ -13,7 +13,7 @@ import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import { combineLatest } from 'rxjs/observable/combineLatest';
|
||||
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { ApiItem, ApiSection, ApiService } from './api.service';
|
||||
import { ApiSection, ApiService } from './api.service';
|
||||
|
||||
import { Option } from 'app/shared/select/select.component';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { Injector } from '@angular/core';
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { Component, ElementRef, ViewChild, OnChanges, OnDestroy, Input } from '@angular/core';
|
||||
import { Component, ElementRef, ViewChild, OnChanges, Input } from '@angular/core';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { PrettyPrinter } from './pretty-printer.service';
|
||||
import { CopierService } from 'app/shared/copier.service';
|
||||
import { MdSnackBar } from '@angular/material';
|
||||
|
||||
const originalLabel = 'Copy Code';
|
||||
const copiedLabel = 'Copied!';
|
||||
const defaultLineNumsCount = 10; // by default, show linenums over this number
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,7 @@ import { Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ContributorService } from './contributor.service';
|
||||
import { Contributor, ContributorGroup } from './contributors.model';
|
||||
import { ContributorGroup } from './contributors.model';
|
||||
|
||||
describe('ContributorService', () => {
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
import { CurrentLocationComponent } from './current-location.component';
|
||||
|
@ -20,6 +20,7 @@ import { CodeTabsComponent } from './code/code-tabs.component';
|
||||
import { ContributorListComponent } from './contributor/contributor-list.component';
|
||||
import { ContributorComponent } from './contributor/contributor.component';
|
||||
import { CurrentLocationComponent } from './current-location.component';
|
||||
import { FileNotFoundSearchComponent } from './search/file-not-found-search.component';
|
||||
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
|
||||
import { ResourceListComponent } from './resource/resource-list.component';
|
||||
import { ResourceService } from './resource/resource.service';
|
||||
@ -30,7 +31,8 @@ import { TocComponent } from './toc/toc.component';
|
||||
*/
|
||||
export const embeddedComponents: any[] = [
|
||||
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
|
||||
CurrentLocationComponent, LiveExampleComponent, ResourceListComponent, TocComponent
|
||||
CurrentLocationComponent, FileNotFoundSearchComponent, LiveExampleComponent, ResourceListComponent,
|
||||
TocComponent
|
||||
];
|
||||
|
||||
/** Injectable class w/ property returning components that can be embedded in docs */
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Component, DebugElement, ElementRef } from '@angular/core';
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example.component';
|
||||
@ -71,7 +71,6 @@ describe('LiveExampleComponent', () => {
|
||||
|
||||
describe('when not embedded', () => {
|
||||
function getLiveExampleAnchor() { return getAnchors()[0]; }
|
||||
function getDownloadAnchor() { return getAnchors()[1]; }
|
||||
|
||||
it('should create LiveExampleComponent', () => {
|
||||
testComponent(() => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { PlatformLocation } from '@angular/common';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
|
||||
import { ResourceListComponent } from './resource-list.component';
|
||||
|
@ -3,7 +3,7 @@ import { Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ResourceService } from './resource.service';
|
||||
import { Category, SubCategory, Resource } from './resource.model';
|
||||
import { Category } from './resource.model';
|
||||
|
||||
describe('ResourceService', () => {
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
import { SearchResults } from 'app/search/interfaces';
|
||||
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { FileNotFoundSearchComponent } from './file-not-found-search.component';
|
||||
|
||||
|
||||
describe('FileNotFoundSearchComponent', () => {
|
||||
let element: HTMLElement;
|
||||
let fixture: ComponentFixture<FileNotFoundSearchComponent>;
|
||||
let searchService: SearchService;
|
||||
let searchResultSubject: Subject<SearchResults>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FileNotFoundSearchComponent, SearchResultsComponent ],
|
||||
providers: [
|
||||
{ provide: LocationService, useValue: new MockLocationService('base/initial-url?some-query') },
|
||||
SearchService
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(FileNotFoundSearchComponent);
|
||||
searchService = TestBed.get(SearchService);
|
||||
searchResultSubject = new Subject<SearchResults>();
|
||||
spyOn(searchService, 'search').and.callFake(() => searchResultSubject.asObservable());
|
||||
fixture.detectChanges();
|
||||
element = fixture.nativeElement;
|
||||
});
|
||||
|
||||
it('should run a search with a query built from the current url', () => {
|
||||
expect(searchService.search).toHaveBeenCalledWith('base initial url');
|
||||
});
|
||||
|
||||
it('should pass through any results to the `aio-search-results` component', () => {
|
||||
const searchResultsComponent = fixture.debugElement.query(By.directive(SearchResultsComponent)).componentInstance;
|
||||
expect(searchResultsComponent.searchResults).toBe(null);
|
||||
|
||||
const results = { query: 'base initial url', results: []};
|
||||
searchResultSubject.next(results);
|
||||
fixture.detectChanges();
|
||||
expect(searchResultsComponent.searchResults).toEqual(results);
|
||||
});
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { SearchResults } from 'app/search/interfaces';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-file-not-found-search',
|
||||
template:
|
||||
`<p>Let's see if any of these search results help...</p>
|
||||
<aio-search-results class="embedded" [searchResults]="searchResults | async"></aio-search-results>`
|
||||
})
|
||||
export class FileNotFoundSearchComponent implements OnInit {
|
||||
searchResults: Observable<SearchResults>;
|
||||
constructor(private location: LocationService, private search: SearchService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.searchResults = this.location.currentPath.switchMap(path => {
|
||||
const query = path.split(/\W+/).join(' ');
|
||||
return this.search.search(query);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By, DOCUMENT } from '@angular/platform-browser';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { asap } from 'rxjs/scheduler/asap';
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
Component, ComponentFactoryResolver, DebugElement,
|
||||
ElementRef, Injector, NgModule, OnInit, ViewChild } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component, DebugElement, ElementRef, NgModule, OnInit, ViewChild } from '@angular/core';
|
||||
import { DocViewerComponent } from './doc-viewer.component';
|
||||
import { DocumentContents } from 'app/documents/document.service';
|
||||
import { EmbeddedModule, embeddedComponents, EmbeddedComponents } from 'app/embedded/embedded.module';
|
||||
import { EmbeddedModule, EmbeddedComponents } from 'app/embedded/embedded.module';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
Component, ComponentFactory, ComponentFactoryResolver, ComponentRef,
|
||||
DoCheck, ElementRef, EventEmitter, Injector, Input, OnDestroy,
|
||||
Output, ViewEncapsulation
|
||||
Output
|
||||
} from '@angular/core';
|
||||
|
||||
import { EmbeddedComponents } from 'app/embedded/embedded.module';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SimpleChange, SimpleChanges, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
import { NavItemComponent } from './nav-item.component';
|
||||
import { NavigationNode } from 'app/navigation/navigation.model';
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
import { TopMenuComponent } from './top-menu.component';
|
||||
import { NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service';
|
||||
import { NavigationService, NavigationViews } from 'app/navigation/navigation.service';
|
||||
|
||||
describe('TopMenuComponent', () => {
|
||||
let component: TopMenuComponent;
|
||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { AsyncSubject } from 'rxjs/AsyncSubject';
|
||||
import { combineLatest } from 'rxjs/observable/combineLatest';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/publishLast';
|
||||
|
19
aio/src/app/search/interfaces.ts
Normal file
19
aio/src/app/search/interfaces.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface SearchResults {
|
||||
query: string;
|
||||
results: SearchResult[];
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
path: string;
|
||||
title: string;
|
||||
type: string;
|
||||
titleWords: string;
|
||||
keywords: string;
|
||||
}
|
||||
|
||||
export interface SearchArea {
|
||||
name: string;
|
||||
pages: SearchResult[];
|
||||
priorityPages: SearchResult[];
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { Component } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchBoxComponent } from './search-box.component';
|
||||
import { MockSearchService } from 'testing/search.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ReflectiveInjector, NgZone } from '@angular/core';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
import { SearchService } from './search.service';
|
||||
import { WebWorkerClient } from 'app/shared/web-worker';
|
||||
|
||||
@ -36,27 +37,29 @@ describe('SearchService', () => {
|
||||
|
||||
describe('search', () => {
|
||||
beforeEach(() => {
|
||||
// We must initialize the service before calling search
|
||||
service.initWorker('some/url', 100);
|
||||
// We must initialize the service before calling connectSearches
|
||||
service.initWorker('some/url', 1000);
|
||||
// Simulate the index being ready so that searches get sent to the worker
|
||||
(service as any).ready = Observable.of(true);
|
||||
});
|
||||
|
||||
it('should trigger a `loadIndex` synchronously', () => {
|
||||
service.search('some query');
|
||||
it('should trigger a `loadIndex` synchronously (not waiting for the delay)', () => {
|
||||
expect(mockWorker.sendMessage).not.toHaveBeenCalled();
|
||||
service.search('some query').subscribe();
|
||||
expect(mockWorker.sendMessage).toHaveBeenCalledWith('load-index');
|
||||
});
|
||||
|
||||
it('should send a "query-index" message to the worker', () => {
|
||||
service.search('some query');
|
||||
service.search('some query').subscribe();
|
||||
expect(mockWorker.sendMessage).toHaveBeenCalledWith('query-index', 'some query');
|
||||
});
|
||||
|
||||
it('should push the response to the `searchResults` observable', () => {
|
||||
it('should push the response to the returned observable', () => {
|
||||
const mockSearchResults = { results: ['a', 'b'] };
|
||||
let actualSearchResults;
|
||||
(mockWorker.sendMessage as jasmine.Spy).and.returnValue(Observable.of(mockSearchResults));
|
||||
let searchResults: any;
|
||||
service.searchResults.subscribe(results => searchResults = results);
|
||||
service.search('some query');
|
||||
expect(searchResults).toEqual(mockSearchResults);
|
||||
service.search('some query').subscribe(results => actualSearchResults = results);
|
||||
expect(actualSearchResults).toEqual(mockSearchResults);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,34 +4,21 @@ Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
*/
|
||||
|
||||
import { NgZone, Injectable, Type } from '@angular/core';
|
||||
import { NgZone, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import 'rxjs/add/observable/race';
|
||||
import 'rxjs/add/observable/timer';
|
||||
import 'rxjs/add/operator/concatMap';
|
||||
import 'rxjs/add/operator/publish';
|
||||
import 'rxjs/add/operator/publishReplay';
|
||||
import { WebWorkerClient } from 'app/shared/web-worker';
|
||||
|
||||
export interface SearchResults {
|
||||
query: string;
|
||||
results: SearchResult[];
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
path: string;
|
||||
title: string;
|
||||
type: string;
|
||||
titleWords: string;
|
||||
keywords: string;
|
||||
}
|
||||
|
||||
import { SearchResults } from 'app/search/interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class SearchService {
|
||||
private ready: Observable<boolean>;
|
||||
private searchesSubject = new ReplaySubject<string>(1);
|
||||
searchResults: Observable<SearchResults>;
|
||||
|
||||
private worker: WebWorkerClient;
|
||||
constructor(private zone: NgZone) {}
|
||||
|
||||
/**
|
||||
@ -43,36 +30,32 @@ export class SearchService {
|
||||
* @param initDelay the number of milliseconds to wait before we load the WebWorker and generate the search index
|
||||
*/
|
||||
initWorker(workerUrl: string, initDelay: number) {
|
||||
const searchResults = Observable
|
||||
const ready = this.ready = Observable
|
||||
// Wait for the initDelay or the first search
|
||||
.race<any>(
|
||||
Observable.timer(initDelay),
|
||||
this.searchesSubject.first()
|
||||
(this.searchesSubject as Observable<string>).first()
|
||||
)
|
||||
.concatMap(() => {
|
||||
// Create the worker and load the index
|
||||
const worker = WebWorkerClient.create(workerUrl, this.zone);
|
||||
return worker.sendMessage('load-index').concatMap(() =>
|
||||
// Once the index has loaded, switch to listening to the searches coming in
|
||||
this.searchesSubject.switchMap((query) =>
|
||||
// Each search gets switched to a web worker message, whose results are returned via an observable
|
||||
worker.sendMessage<SearchResults>('query-index', query)
|
||||
)
|
||||
);
|
||||
}).publish();
|
||||
this.worker = WebWorkerClient.create(workerUrl, this.zone);
|
||||
return this.worker.sendMessage<boolean>('load-index');
|
||||
}).publishReplay(1);
|
||||
|
||||
// Connect to the observable to kick off the timer
|
||||
searchResults.connect();
|
||||
|
||||
// Expose the connected observable to the rest of the world
|
||||
this.searchResults = searchResults;
|
||||
// Connect to the observable to kick off the timer
|
||||
ready.connect();
|
||||
return ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a search query to the index.
|
||||
* The results will appear on the `searchResults` observable.
|
||||
* Search the index using the given query and emit results on the observable that is returned.
|
||||
* @param query The query to run against the index.
|
||||
* @returns an observable collection of search results
|
||||
*/
|
||||
search(query: string) {
|
||||
search(query: string): Observable<SearchResults> {
|
||||
// Trigger the searches subject to override the init delay timer
|
||||
this.searchesSubject.next(query);
|
||||
// Once the index has loaded, switch to listening to the searches coming in.
|
||||
return this.ready.concatMap(() => this.worker.sendMessage<SearchResults>('query-index', query));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { MdIconRegistry } from '@angular/material';
|
||||
import { CustomMdIconRegistry, SVG_ICONS, SvgIconInfo } from './custom-md-icon-registry';
|
||||
import { CustomMdIconRegistry, SvgIconInfo } from './custom-md-icon-registry';
|
||||
|
||||
describe('CustomMdIconRegistry', () => {
|
||||
it('should get the SVG element for a preloaded icon from the cache', () => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Location, PlatformLocation } from '@angular/common';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import 'rxjs/add/operator/do';
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { DOCUMENT } from '@angular/platform-browser';
|
||||
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { ScrollItem, ScrollSpiedElement, ScrollSpiedElementGroup, ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-spy.service';
|
||||
import { ScrollItem, ScrollSpiedElement, ScrollSpiedElementGroup, ScrollSpyService } from 'app/shared/scroll-spy.service';
|
||||
|
||||
|
||||
describe('ScrollSpiedElement', () => {
|
||||
@ -197,6 +197,7 @@ describe('ScrollSpyService', () => {
|
||||
.and.callFake(() => actions.push('calibrate'));
|
||||
|
||||
expect(onResizeSpy).not.toHaveBeenCalled();
|
||||
expect(calibrateSpy).not.toHaveBeenCalled();
|
||||
|
||||
scrollSpyService.spyOn([]);
|
||||
expect(actions).toEqual(['onResize', 'calibrate']);
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { SearchService, SearchResult, SearchResults } from '../search.service';
|
||||
import { SearchResultsComponent, SearchArea } from './search-results.component';
|
||||
import { MockSearchService } from 'testing/search.service';
|
||||
import { SearchResult } from 'app/search/interfaces';
|
||||
import { SearchResultsComponent } from './search-results.component';
|
||||
|
||||
describe('SearchResultsComponent', () => {
|
||||
let component: SearchResultsComponent;
|
||||
let fixture: ComponentFixture<SearchResultsComponent>;
|
||||
let searchResults: Subject<SearchResults>;
|
||||
|
||||
/** Get all text from component element */
|
||||
function getText() { return fixture.debugElement.nativeElement.textContent; }
|
||||
@ -38,27 +34,26 @@ describe('SearchResultsComponent', () => {
|
||||
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
|
||||
}
|
||||
|
||||
function setSearchResults(query: string, results: SearchResult[]) {
|
||||
component.searchResults = {query, results};
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SearchResultsComponent ],
|
||||
providers: [
|
||||
{ provide: SearchService, useFactory: () => new MockSearchService() }
|
||||
]
|
||||
declarations: [ SearchResultsComponent ]
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchResultsComponent);
|
||||
component = fixture.componentInstance;
|
||||
searchResults = TestBed.get(SearchService).searchResults;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should map the search results into groups based on their containing folder', () => {
|
||||
const results = getTestResults(3);
|
||||
|
||||
searchResults.next({ query: '', results: results});
|
||||
setSearchResults('', getTestResults(3));
|
||||
expect(component.searchAreas).toEqual([
|
||||
{ name: 'api', priorityPages: [
|
||||
{ path: 'api/d', title: 'API D', type: '', keywords: '', titleWords: '' }
|
||||
@ -71,10 +66,10 @@ describe('SearchResultsComponent', () => {
|
||||
});
|
||||
|
||||
it('should special case results that are top level folders', () => {
|
||||
searchResults.next({ query: '', results: [
|
||||
setSearchResults('', [
|
||||
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
|
||||
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' },
|
||||
]});
|
||||
]);
|
||||
expect(component.searchAreas).toEqual([
|
||||
{ name: 'tutorial', priorityPages: [
|
||||
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
|
||||
@ -85,21 +80,21 @@ describe('SearchResultsComponent', () => {
|
||||
|
||||
it('should put first 5 results for each area into priorityPages', () => {
|
||||
const results = getTestResults();
|
||||
searchResults.next({ query: '', results: results });
|
||||
setSearchResults('', results);
|
||||
expect(component.searchAreas[0].priorityPages).toEqual(results.filter(p => p.path.startsWith('api')).slice(0, 5));
|
||||
expect(component.searchAreas[1].priorityPages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(0, 5));
|
||||
});
|
||||
|
||||
it('should put the nonPriorityPages into the pages array, sorted by title', () => {
|
||||
const results = getTestResults();
|
||||
searchResults.next({ query: '', results: results });
|
||||
setSearchResults('', results);
|
||||
expect(component.searchAreas[0].pages).toEqual([]);
|
||||
expect(component.searchAreas[1].pages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(5).sort(compareTitle));
|
||||
});
|
||||
|
||||
it('should put a total count in the header of each area of search results', () => {
|
||||
const results = getTestResults();
|
||||
searchResults.next({ query: '', results: results });
|
||||
setSearchResults('', results);
|
||||
fixture.detectChanges();
|
||||
const headers = fixture.debugElement.queryAll(By.css('h3'));
|
||||
expect(headers.length).toEqual(2);
|
||||
@ -112,7 +107,7 @@ describe('SearchResultsComponent', () => {
|
||||
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
|
||||
];
|
||||
|
||||
searchResults.next({ query: '', results: results });
|
||||
setSearchResults('', results);
|
||||
expect(component.searchAreas).toEqual([
|
||||
{ name: 'other', priorityPages: [
|
||||
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
|
||||
@ -125,7 +120,7 @@ describe('SearchResultsComponent', () => {
|
||||
{ path: 'news', title: undefined, type: 'marketing', keywords: '', titleWords: '' }
|
||||
];
|
||||
|
||||
searchResults.next({ query: 'something', results: results });
|
||||
setSearchResults('something', results);
|
||||
expect(component.searchAreas).toEqual([]);
|
||||
});
|
||||
|
||||
@ -144,7 +139,7 @@ describe('SearchResultsComponent', () => {
|
||||
|
||||
selected = null;
|
||||
searchResult = { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' };
|
||||
searchResults.next({ query: 'something', results: [searchResult] });
|
||||
setSearchResults('something', [searchResult]);
|
||||
|
||||
fixture.detectChanges();
|
||||
anchor = fixture.debugElement.query(By.css('a'));
|
||||
@ -179,10 +174,8 @@ describe('SearchResultsComponent', () => {
|
||||
|
||||
describe('when no query results', () => {
|
||||
it('should display "not found" message', () => {
|
||||
searchResults.next({ query: 'something', results: [] });
|
||||
fixture.detectChanges();
|
||||
setSearchResults('something', []);
|
||||
expect(getText()).toContain('No results');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -1,28 +1,20 @@
|
||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { SearchResult, SearchResults, SearchService } from '../search.service';
|
||||
|
||||
export interface SearchArea {
|
||||
name: string;
|
||||
pages: SearchResult[];
|
||||
priorityPages: SearchResult[];
|
||||
}
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { SearchResult, SearchResults, SearchArea } from 'app/search/interfaces';
|
||||
|
||||
/**
|
||||
* A component to display the search results
|
||||
* A component to display search results in groups
|
||||
*/
|
||||
@Component({
|
||||
selector: 'aio-search-results',
|
||||
templateUrl: './search-results.component.html',
|
||||
})
|
||||
export class SearchResultsComponent implements OnInit, OnDestroy {
|
||||
export class SearchResultsComponent implements OnChanges {
|
||||
|
||||
private resultsSubscription: Subscription;
|
||||
readonly defaultArea = 'other';
|
||||
notFoundMessage = 'Searching ...';
|
||||
readonly topLevelFolders = ['guide', 'tutorial'];
|
||||
/**
|
||||
* The results to display
|
||||
*/
|
||||
@Input()
|
||||
searchResults: SearchResults;
|
||||
|
||||
/**
|
||||
* Emitted when the user selects a search result
|
||||
@ -30,20 +22,13 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
|
||||
@Output()
|
||||
resultSelected = new EventEmitter<SearchResult>();
|
||||
|
||||
/**
|
||||
* A mapping of the search results grouped into areas
|
||||
*/
|
||||
readonly defaultArea = 'other';
|
||||
notFoundMessage = 'Searching ...';
|
||||
readonly topLevelFolders = ['guide', 'tutorial'];
|
||||
searchAreas: SearchArea[] = [];
|
||||
|
||||
constructor(private searchService: SearchService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.resultsSubscription = this.searchService.searchResults
|
||||
.subscribe(search => this.searchAreas = this.processSearchResults(search));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.resultsSubscription.unsubscribe();
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
this.searchAreas = this.processSearchResults(this.searchResults);
|
||||
}
|
||||
|
||||
onResultSelected(page: SearchResult, event: MouseEvent) {
|
||||
@ -55,6 +40,9 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Map the search results into groups by area
|
||||
private processSearchResults(search: SearchResults) {
|
||||
if (!search) {
|
||||
return [];
|
||||
}
|
||||
this.notFoundMessage = 'No results found.';
|
||||
const searchAreaMap = {};
|
||||
search.results.forEach(result => {
|
||||
@ -84,6 +72,6 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
function compareResults(l: {title: string}, r: {title: string}) {
|
||||
function compareResults(l: SearchResult, r: SearchResult) {
|
||||
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SelectComponent, Option } from './select.component';
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SearchResultsComponent } from './search-results/search-results.component';
|
||||
import { SelectComponent } from './select/select.component';
|
||||
|
||||
@NgModule({
|
||||
@ -7,9 +8,11 @@ import { SelectComponent } from './select/select.component';
|
||||
CommonModule
|
||||
],
|
||||
exports: [
|
||||
SearchResultsComponent,
|
||||
SelectComponent
|
||||
],
|
||||
declarations: [
|
||||
SearchResultsComponent,
|
||||
SelectComponent
|
||||
]
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector, SecurityContext } from '@angular/core';
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
@ -268,7 +268,6 @@ describe('TocService', () => {
|
||||
});
|
||||
|
||||
it('should calculate and set id of heading without an id', () => {
|
||||
const tocItem = lastTocList.find(item => item.title === 'H2 Two');
|
||||
const id = headings[2].getAttribute('id');
|
||||
expect(id).toEqual('h2-two');
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
*/
|
||||
|
||||
import {NgZone, Injectable} from '@angular/core';
|
||||
import {NgZone} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
export interface WebWorkerMessage {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable, OnDestroy } from '@angular/core';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { NgServiceWorker } from '@angular/service-worker';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
@ -29,6 +29,24 @@ aio-search-results {
|
||||
}
|
||||
}
|
||||
|
||||
aio-search-results.embedded .search-results {
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
width: auto;
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
background-color: inherit;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
.search-area a {
|
||||
color: lighten($darkgray, 10);
|
||||
&:hover {
|
||||
color: $accentblue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { SearchResults } from 'app/search/search.service';
|
||||
import { SearchResults } from 'app/search/interfaces';
|
||||
|
||||
export class MockSearchService {
|
||||
searchResults = new Subject<SearchResults>();
|
||||
|
@ -68,7 +68,7 @@
|
||||
// other libraries
|
||||
'rxjs': 'npm:rxjs@5.0.1',
|
||||
'tslib': 'npm:tslib/tslib.js',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api@0.4/bundles/in-memory-web-api.umd.js',
|
||||
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
|
||||
'typescript': 'npm:typescript@2.3.2/lib/typescript.js',
|
||||
|
||||
|
@ -54,7 +54,7 @@
|
||||
// other libraries
|
||||
'rxjs': 'npm:rxjs@5.0.1',
|
||||
'tslib': 'npm:tslib/tslib.js',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api@0.4/bundles/in-memory-web-api.umd.js',
|
||||
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
|
||||
'typescript': 'npm:typescript@2.3.2/lib/typescript.js',
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"http-server": "http-server",
|
||||
"protractor": "protractor",
|
||||
"webdriver:update": "webdriver-manager update --standalone false --gecko false",
|
||||
"webdriver:update": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG",
|
||||
"postinstall": "yarn webdriver:update"
|
||||
},
|
||||
"keywords": [],
|
||||
|
@ -20,7 +20,12 @@ exports.config = {
|
||||
|
||||
// Capabilities to be passed to the webdriver instance.
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
'browserName': 'chrome',
|
||||
// For Travis
|
||||
chromeOptions: {
|
||||
binary: process.env.CHROME_BIN,
|
||||
args: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
|
||||
// Framework to use. Jasmine is recommended.
|
||||
|
@ -76,15 +76,17 @@ class NgPackagesInstaller {
|
||||
// Prevent accidental publishing of the package, if something goes wrong.
|
||||
tmpConfig.private = true;
|
||||
|
||||
// Overwrite project dependencies to Angular packages with local files.
|
||||
const deps = tmpConfig.dependencies || {};
|
||||
Object.keys(deps).forEach(key2 => {
|
||||
const pkg2 = packages[key2];
|
||||
if (pkg2) {
|
||||
// point the core Angular packages at the distributable folder
|
||||
deps[key2] = `file:${pkg2.parentDir}/${key2.replace('@angular/', '')}`;
|
||||
this._log(`Overriding dependency of local ${key} with local package: ${key2}: ${deps[key2]}`);
|
||||
}
|
||||
// Overwrite project dependencies/devDependencies to Angular packages with local files.
|
||||
['dependencies', 'devDependencies'].forEach(prop => {
|
||||
const deps = tmpConfig[prop] || {};
|
||||
Object.keys(deps).forEach(key2 => {
|
||||
const pkg2 = packages[key2];
|
||||
if (pkg2) {
|
||||
// point the core Angular packages at the distributable folder
|
||||
deps[key2] = `file:${pkg2.parentDir}/${key2.replace('@angular/', '')}`;
|
||||
this._log(`Overriding dependency of local ${key} with local package: ${key2}: ${deps[key2]}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
fs.writeFileSync(pkg.packageJsonPath, JSON.stringify(tmpConfig));
|
||||
|
@ -81,7 +81,10 @@ describe('NgPackagesInstaller', () => {
|
||||
'@angular/tsc-wrapped': {
|
||||
parentDir: toolsDir,
|
||||
packageJsonPath: `${toolsDir}/tsc-wrapped/package.json`,
|
||||
config: { peerDependencies: { tsickle: '^1.4.0' } }
|
||||
config: {
|
||||
devDependencies: { '@angular/common': '4.4.4-1ab23cd4' },
|
||||
peerDependencies: { tsickle: '^1.4.0' }
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(installer, '_getDistPackages').and.callFake(() => copyJsonObj(dummyNgPackages));
|
||||
@ -160,7 +163,10 @@ describe('NgPackagesInstaller', () => {
|
||||
private: true,
|
||||
dependencies: { '@angular/tsc-wrapped': `file:${toolsDir}/tsc-wrapped` }
|
||||
}))],
|
||||
[pkgJsonFor('tsc-wrapped'), JSON.stringify(overwriteConfigFor('tsc-wrapped', {private: true}))],
|
||||
[pkgJsonFor('tsc-wrapped'), JSON.stringify(overwriteConfigFor('tsc-wrapped', {
|
||||
private: true,
|
||||
devDependencies: { '@angular/common': `file:${packagesDir}/common` }
|
||||
}))],
|
||||
]);
|
||||
|
||||
expect(lastFiveArgs).toEqual(['core', 'common', 'compiler', 'compiler-cli', 'tsc-wrapped']
|
||||
@ -201,7 +207,7 @@ describe('NgPackagesInstaller', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getDistPackages', () => {
|
||||
describe('_getDistPackages()', () => {
|
||||
it('should include top level Angular packages', () => {
|
||||
const ngPackages = installer._getDistPackages();
|
||||
const expectedValue = jasmine.objectContaining({
|
||||
@ -259,7 +265,7 @@ describe('NgPackagesInstaller', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_printWarning', () => {
|
||||
describe('_printWarning()', () => {
|
||||
it('should mention the message passed in the warning', () => {
|
||||
installer._printWarning();
|
||||
expect(console.warn.calls.argsFor(0)[0]).toContain('is running against the local Angular build');
|
||||
@ -282,7 +288,7 @@ describe('NgPackagesInstaller', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_installDeps', () => {
|
||||
describe('_installDeps()', () => {
|
||||
it('should run yarn install with the given options', () => {
|
||||
installer._installDeps('option-1', 'option-2');
|
||||
expect(shelljs.exec).toHaveBeenCalledWith('yarn install option-1 option-2', { cwd: absoluteRootDir });
|
||||
|
@ -8,6 +8,7 @@
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"noUnusedLocals": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
|
@ -30,7 +30,7 @@ var CIconfiguration = {
|
||||
'Safari8': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari9': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari10': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS7': {unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}},
|
||||
'iOS7': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS8': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS9': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS10': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
|
@ -23,7 +23,7 @@
|
||||
"protractor": "file:../../node_modules/protractor"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"closure": "java -jar node_modules/google-closure-compiler/compiler.jar --flagfile closure.conf",
|
||||
"test": "ngc && yarn run closure && concurrently \"yarn run serve\" \"yarn run protractor\" --kill-others --success first",
|
||||
"serve": "lite-server -c e2e/browser.config.json",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"test": "concurrently \"npm run serve\" \"npm run protractor\" --kill-others --success first",
|
||||
"serve": "lite-server -c bs-config.e2e.json",
|
||||
"preprotractor": "tsc -p e2e",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "4.4.5",
|
||||
"version": "4.4.7",
|
||||
"private": true,
|
||||
"branchPattern": "2.0.*",
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
@ -18,7 +18,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('Please use Yarn instead of NPM to install dependencies. See: https://yarnpkg.com/lang/en/docs/install/')\"",
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "yarn update-webdriver",
|
||||
"update-webdriver": "webdriver-manager update --gecko false $CHROMEDRIVER_VERSION_ARG",
|
||||
"check-env": "gulp check-env"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -84,6 +85,7 @@
|
||||
"rollup-plugin-node-resolve": "3.0.0",
|
||||
"selenium-webdriver": "3.5.0",
|
||||
"semver": "5.4.1",
|
||||
"shelljs": "^0.7.8",
|
||||
"sorcery": "0.10.0",
|
||||
"source-map": "0.5.7",
|
||||
"source-map-support": "0.4.18",
|
||||
|
@ -5,7 +5,7 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AnimationMetadata, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
import {AnimationMetadata, AnimationMetadataType, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationDriver} from '../render/animation_driver';
|
||||
import {normalizeStyles} from '../util';
|
||||
@ -17,7 +17,7 @@ import {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
export class Animation {
|
||||
private _animationAst: Ast;
|
||||
private _animationAst: Ast<AnimationMetadataType>;
|
||||
constructor(private _driver: AnimationDriver, input: AnimationMetadata|AnimationMetadata[]) {
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(input, errors);
|
||||
|
@ -5,7 +5,7 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AnimateTimings, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
import {AnimateTimings, AnimationMetadataType, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
|
||||
const EMPTY_ANIMATION_OPTIONS: AnimationOptions = {};
|
||||
|
||||
@ -23,129 +23,90 @@ export interface AstVisitor {
|
||||
visitAnimateRef(ast: AnimateRefAst, context: any): any;
|
||||
visitQuery(ast: QueryAst, context: any): any;
|
||||
visitStagger(ast: StaggerAst, context: any): any;
|
||||
visitTiming(ast: TimingAst, context: any): any;
|
||||
}
|
||||
|
||||
export abstract class Ast {
|
||||
abstract visit(ast: AstVisitor, context: any): any;
|
||||
public options: AnimationOptions = EMPTY_ANIMATION_OPTIONS;
|
||||
|
||||
get params(): {[name: string]: any}|null { return this.options['params'] || null; }
|
||||
export interface Ast<T extends AnimationMetadataType> {
|
||||
type: T;
|
||||
options: AnimationOptions|null;
|
||||
}
|
||||
|
||||
export class TriggerAst extends Ast {
|
||||
public queryCount: number = 0;
|
||||
public depCount: number = 0;
|
||||
|
||||
constructor(public name: string, public states: StateAst[], public transitions: TransitionAst[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTrigger(this, context); }
|
||||
export interface TriggerAst extends Ast<AnimationMetadataType.Trigger> {
|
||||
type: AnimationMetadataType.Trigger;
|
||||
name: string;
|
||||
states: StateAst[];
|
||||
transitions: TransitionAst[];
|
||||
queryCount: number;
|
||||
depCount: number;
|
||||
}
|
||||
|
||||
export class StateAst extends Ast {
|
||||
constructor(public name: string, public style: StyleAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitState(this, context); }
|
||||
export interface StateAst extends Ast<AnimationMetadataType.State> {
|
||||
type: AnimationMetadataType.State;
|
||||
name: string;
|
||||
style: StyleAst;
|
||||
}
|
||||
|
||||
export class TransitionAst extends Ast {
|
||||
public queryCount: number = 0;
|
||||
public depCount: number = 0;
|
||||
|
||||
constructor(
|
||||
public matchers: ((fromState: string, toState: string) => boolean)[], public animation: Ast) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTransition(this, context); }
|
||||
export interface TransitionAst extends Ast<AnimationMetadataType.Transition> {
|
||||
matchers: ((fromState: string, toState: string) => boolean)[];
|
||||
animation: Ast<AnimationMetadataType>;
|
||||
queryCount: number;
|
||||
depCount: number;
|
||||
}
|
||||
|
||||
export class SequenceAst extends Ast {
|
||||
constructor(public steps: Ast[]) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitSequence(this, context); }
|
||||
export interface SequenceAst extends Ast<AnimationMetadataType.Sequence> {
|
||||
steps: Ast<AnimationMetadataType>[];
|
||||
}
|
||||
|
||||
export class GroupAst extends Ast {
|
||||
constructor(public steps: Ast[]) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitGroup(this, context); }
|
||||
export interface GroupAst extends Ast<AnimationMetadataType.Group> {
|
||||
steps: Ast<AnimationMetadataType>[];
|
||||
}
|
||||
|
||||
export class AnimateAst extends Ast {
|
||||
constructor(public timings: TimingAst, public style: StyleAst|KeyframesAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimate(this, context); }
|
||||
export interface AnimateAst extends Ast<AnimationMetadataType.Animate> {
|
||||
timings: TimingAst;
|
||||
style: StyleAst|KeyframesAst;
|
||||
}
|
||||
|
||||
export class StyleAst extends Ast {
|
||||
public isEmptyStep = false;
|
||||
public containsDynamicStyles = false;
|
||||
|
||||
constructor(
|
||||
public styles: (ɵStyleData|string)[], public easing: string|null,
|
||||
public offset: number|null) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitStyle(this, context); }
|
||||
export interface StyleAst extends Ast<AnimationMetadataType.Style> {
|
||||
styles: (ɵStyleData|string)[];
|
||||
easing: string|null;
|
||||
offset: number|null;
|
||||
containsDynamicStyles: boolean;
|
||||
isEmptyStep?: boolean;
|
||||
}
|
||||
|
||||
export class KeyframesAst extends Ast {
|
||||
constructor(public styles: StyleAst[]) { super(); }
|
||||
export interface KeyframesAst extends Ast<AnimationMetadataType.Keyframes> { styles: StyleAst[]; }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitKeyframes(this, context); }
|
||||
export interface ReferenceAst extends Ast<AnimationMetadataType.Reference> {
|
||||
animation: Ast<AnimationMetadataType>;
|
||||
}
|
||||
|
||||
export class ReferenceAst extends Ast {
|
||||
constructor(public animation: Ast) { super(); }
|
||||
export interface AnimateChildAst extends Ast<AnimationMetadataType.AnimateChild> {}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitReference(this, context); }
|
||||
export interface AnimateRefAst extends Ast<AnimationMetadataType.AnimateRef> {
|
||||
animation: ReferenceAst;
|
||||
}
|
||||
|
||||
export class AnimateChildAst extends Ast {
|
||||
constructor() { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimateChild(this, context); }
|
||||
export interface QueryAst extends Ast<AnimationMetadataType.Query> {
|
||||
selector: string;
|
||||
limit: number;
|
||||
optional: boolean;
|
||||
includeSelf: boolean;
|
||||
animation: Ast<AnimationMetadataType>;
|
||||
originalSelector: string;
|
||||
}
|
||||
|
||||
export class AnimateRefAst extends Ast {
|
||||
constructor(public animation: ReferenceAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimateRef(this, context); }
|
||||
export interface StaggerAst extends Ast<AnimationMetadataType.Stagger> {
|
||||
timings: AnimateTimings;
|
||||
animation: Ast<AnimationMetadataType>;
|
||||
}
|
||||
|
||||
export class QueryAst extends Ast {
|
||||
public originalSelector: string;
|
||||
|
||||
constructor(
|
||||
public selector: string, public limit: number, public optional: boolean,
|
||||
public includeSelf: boolean, public animation: Ast) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitQuery(this, context); }
|
||||
export interface TimingAst {
|
||||
duration: number;
|
||||
delay: number;
|
||||
easing: string|null;
|
||||
dynamic?: boolean;
|
||||
}
|
||||
|
||||
export class StaggerAst extends Ast {
|
||||
constructor(public timings: AnimateTimings, public animation: Ast) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitStagger(this, context); }
|
||||
}
|
||||
|
||||
export class TimingAst extends Ast {
|
||||
constructor(
|
||||
public duration: number, public delay: number = 0, public easing: string|null = null) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTiming(this, context); }
|
||||
}
|
||||
|
||||
export class DynamicTimingAst extends TimingAst {
|
||||
constructor(public value: string) { super(0, 0, ''); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTiming(this, context); }
|
||||
export interface DynamicTimingAst extends TimingAst {
|
||||
strValue: string;
|
||||
dynamic: true;
|
||||
}
|
||||
|
@ -8,10 +8,10 @@
|
||||
import {AUTO_STYLE, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, style, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {getOrSetAsInMap} from '../render/shared';
|
||||
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, SUBSTITUTION_EXPR_START, copyObj, extractStyleParams, iteratorToArray, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
|
||||
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, SUBSTITUTION_EXPR_START, copyObj, extractStyleParams, iteratorToArray, normalizeAnimationEntry, resolveTiming, validateStyleParams, visitDslNode} from '../util';
|
||||
|
||||
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
import {AnimationDslVisitor} from './animation_dsl_visitor';
|
||||
import {parseTransitionExpr} from './animation_transition_expr';
|
||||
|
||||
const SELF_TOKEN = ':self';
|
||||
@ -54,7 +54,7 @@ const SELF_TOKEN_REGEX = new RegExp(`\s*${SELF_TOKEN}\s*,?`, 'g');
|
||||
* Otherwise an error will be thrown.
|
||||
*/
|
||||
export function buildAnimationAst(
|
||||
metadata: AnimationMetadata | AnimationMetadata[], errors: any[]): Ast {
|
||||
metadata: AnimationMetadata | AnimationMetadata[], errors: any[]): Ast<AnimationMetadataType> {
|
||||
return new AnimationAstBuilderVisitor().build(metadata, errors);
|
||||
}
|
||||
|
||||
@ -65,10 +65,12 @@ const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
|
||||
const ROOT_SELECTOR = '';
|
||||
|
||||
export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
build(metadata: AnimationMetadata|AnimationMetadata[], errors: any[]): Ast {
|
||||
build(metadata: AnimationMetadata|AnimationMetadata[], errors: any[]):
|
||||
Ast<AnimationMetadataType> {
|
||||
const context = new AnimationAstBuilderContext(errors);
|
||||
this._resetContextStyleTimingState(context);
|
||||
return visitAnimationNode(this, normalizeAnimationEntry(metadata), context) as Ast;
|
||||
return <Ast<AnimationMetadataType>>visitDslNode(
|
||||
this, normalizeAnimationEntry(metadata), context);
|
||||
}
|
||||
|
||||
private _resetContextStyleTimingState(context: AnimationAstBuilderContext) {
|
||||
@ -104,11 +106,12 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
'only state() and transition() definitions can sit inside of a trigger()');
|
||||
}
|
||||
});
|
||||
const ast = new TriggerAst(metadata.name, states, transitions);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
ast.queryCount = queryCount;
|
||||
ast.depCount = depCount;
|
||||
return ast;
|
||||
|
||||
return {
|
||||
type: AnimationMetadataType.Trigger,
|
||||
name: metadata.name, states, transitions, queryCount, depCount,
|
||||
options: null
|
||||
};
|
||||
}
|
||||
|
||||
visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst {
|
||||
@ -136,31 +139,38 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
const stateAst = new StateAst(metadata.name, styleAst);
|
||||
if (astParams) {
|
||||
stateAst.options = {params: astParams};
|
||||
}
|
||||
return stateAst;
|
||||
return {
|
||||
type: AnimationMetadataType.State,
|
||||
name: metadata.name,
|
||||
style: styleAst,
|
||||
options: astParams ? {params: astParams} : null
|
||||
};
|
||||
}
|
||||
|
||||
visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext):
|
||||
TransitionAst {
|
||||
context.queryCount = 0;
|
||||
context.depCount = 0;
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
const animation = visitDslNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
const matchers = parseTransitionExpr(metadata.expr, context.errors);
|
||||
const ast = new TransitionAst(matchers, entry);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
ast.queryCount = context.queryCount;
|
||||
ast.depCount = context.depCount;
|
||||
return ast;
|
||||
|
||||
return {
|
||||
type: AnimationMetadataType.Transition,
|
||||
matchers,
|
||||
animation,
|
||||
queryCount: context.queryCount,
|
||||
depCount: context.depCount,
|
||||
options: normalizeAnimationOptions(metadata.options)
|
||||
};
|
||||
}
|
||||
|
||||
visitSequence(metadata: AnimationSequenceMetadata, context: AnimationAstBuilderContext):
|
||||
SequenceAst {
|
||||
const ast = new SequenceAst(metadata.steps.map(s => visitAnimationNode(this, s, context)));
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
return {
|
||||
type: AnimationMetadataType.Sequence,
|
||||
steps: metadata.steps.map(s => visitDslNode(this, s, context)),
|
||||
options: normalizeAnimationOptions(metadata.options)
|
||||
};
|
||||
}
|
||||
|
||||
visitGroup(metadata: AnimationGroupMetadata, context: AnimationAstBuilderContext): GroupAst {
|
||||
@ -168,15 +178,17 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
let furthestTime = 0;
|
||||
const steps = metadata.steps.map(step => {
|
||||
context.currentTime = currentTime;
|
||||
const innerAst = visitAnimationNode(this, step, context);
|
||||
const innerAst = visitDslNode(this, step, context);
|
||||
furthestTime = Math.max(furthestTime, context.currentTime);
|
||||
return innerAst;
|
||||
});
|
||||
|
||||
context.currentTime = furthestTime;
|
||||
const ast = new GroupAst(steps);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
return {
|
||||
type: AnimationMetadataType.Group,
|
||||
steps,
|
||||
options: normalizeAnimationOptions(metadata.options)
|
||||
};
|
||||
}
|
||||
|
||||
visitAnimate(metadata: AnimationAnimateMetadata, context: AnimationAstBuilderContext):
|
||||
@ -184,10 +196,10 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
const timingAst = constructTimingAst(metadata.timings, context.errors);
|
||||
context.currentAnimateTimings = timingAst;
|
||||
|
||||
let styles: StyleAst|KeyframesAst;
|
||||
let styleAst: StyleAst|KeyframesAst;
|
||||
let styleMetadata: AnimationMetadata = metadata.styles ? metadata.styles : style({});
|
||||
if (styleMetadata.type == AnimationMetadataType.Keyframes) {
|
||||
styles = this.visitKeyframes(styleMetadata as AnimationKeyframesSequenceMetadata, context);
|
||||
styleAst = this.visitKeyframes(styleMetadata as AnimationKeyframesSequenceMetadata, context);
|
||||
} else {
|
||||
let styleMetadata = metadata.styles as AnimationStyleMetadata;
|
||||
let isEmpty = false;
|
||||
@ -200,13 +212,18 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
styleMetadata = style(newStyleData);
|
||||
}
|
||||
context.currentTime += timingAst.duration + timingAst.delay;
|
||||
const styleAst = this.visitStyle(styleMetadata, context);
|
||||
styleAst.isEmptyStep = isEmpty;
|
||||
styles = styleAst;
|
||||
const _styleAst = this.visitStyle(styleMetadata, context);
|
||||
_styleAst.isEmptyStep = isEmpty;
|
||||
styleAst = _styleAst;
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
return new AnimateAst(timingAst, styles);
|
||||
return {
|
||||
type: AnimationMetadataType.Animate,
|
||||
timings: timingAst,
|
||||
style: styleAst,
|
||||
options: null
|
||||
};
|
||||
}
|
||||
|
||||
visitStyle(metadata: AnimationStyleMetadata, context: AnimationAstBuilderContext): StyleAst {
|
||||
@ -256,9 +273,13 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
}
|
||||
});
|
||||
|
||||
const ast = new StyleAst(styles, collectedEasing, metadata.offset);
|
||||
ast.containsDynamicStyles = containsDynamicStyles;
|
||||
return ast;
|
||||
return {
|
||||
type: AnimationMetadataType.Style,
|
||||
styles,
|
||||
easing: collectedEasing,
|
||||
offset: metadata.offset, containsDynamicStyles,
|
||||
options: null
|
||||
};
|
||||
}
|
||||
|
||||
private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void {
|
||||
@ -303,9 +324,10 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
|
||||
visitKeyframes(metadata: AnimationKeyframesSequenceMetadata, context: AnimationAstBuilderContext):
|
||||
KeyframesAst {
|
||||
const ast: KeyframesAst = {type: AnimationMetadataType.Keyframes, styles: [], options: null};
|
||||
if (!context.currentAnimateTimings) {
|
||||
context.errors.push(`keyframes() must be placed inside of a call to animate()`);
|
||||
return new KeyframesAst([]);
|
||||
return ast;
|
||||
}
|
||||
|
||||
const MAX_KEYFRAME_OFFSET = 1;
|
||||
@ -359,33 +381,38 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
currentAnimateTimings.duration = durationUpToThisFrame;
|
||||
this._validateStyleAst(kf, context);
|
||||
kf.offset = offset;
|
||||
|
||||
ast.styles.push(kf);
|
||||
});
|
||||
|
||||
return new KeyframesAst(keyframes);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitReference(metadata: AnimationReferenceMetadata, context: AnimationAstBuilderContext):
|
||||
ReferenceAst {
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
const ast = new ReferenceAst(entry);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
return {
|
||||
type: AnimationMetadataType.Reference,
|
||||
animation: visitDslNode(this, normalizeAnimationEntry(metadata.animation), context),
|
||||
options: normalizeAnimationOptions(metadata.options)
|
||||
};
|
||||
}
|
||||
|
||||
visitAnimateChild(metadata: AnimationAnimateChildMetadata, context: AnimationAstBuilderContext):
|
||||
AnimateChildAst {
|
||||
context.depCount++;
|
||||
const ast = new AnimateChildAst();
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
return {
|
||||
type: AnimationMetadataType.AnimateChild,
|
||||
options: normalizeAnimationOptions(metadata.options)
|
||||
};
|
||||
}
|
||||
|
||||
visitAnimateRef(metadata: AnimationAnimateRefMetadata, context: AnimationAstBuilderContext):
|
||||
AnimateRefAst {
|
||||
const animation = this.visitReference(metadata.animation, context);
|
||||
const ast = new AnimateRefAst(animation);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
return {
|
||||
type: AnimationMetadataType.AnimateRef,
|
||||
animation: this.visitReference(metadata.animation, context),
|
||||
options: normalizeAnimationOptions(metadata.options)
|
||||
};
|
||||
}
|
||||
|
||||
visitQuery(metadata: AnimationQueryMetadata, context: AnimationAstBuilderContext): QueryAst {
|
||||
@ -399,14 +426,18 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
parentSelector.length ? (parentSelector + ' ' + selector) : selector;
|
||||
getOrSetAsInMap(context.collectedStyles, context.currentQuerySelector, {});
|
||||
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
const animation = visitDslNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
context.currentQuery = null;
|
||||
context.currentQuerySelector = parentSelector;
|
||||
|
||||
const ast = new QueryAst(selector, options.limit || 0, !!options.optional, includeSelf, entry);
|
||||
ast.originalSelector = metadata.selector;
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
return {
|
||||
type: AnimationMetadataType.Query,
|
||||
selector,
|
||||
limit: options.limit || 0,
|
||||
optional: !!options.optional, includeSelf, animation,
|
||||
originalSelector: metadata.selector,
|
||||
options: normalizeAnimationOptions(metadata.options)
|
||||
};
|
||||
}
|
||||
|
||||
visitStagger(metadata: AnimationStaggerMetadata, context: AnimationAstBuilderContext):
|
||||
@ -417,9 +448,12 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
const timings = metadata.timings === 'full' ?
|
||||
{duration: 0, delay: 0, easing: 'full'} :
|
||||
resolveTiming(metadata.timings, context.errors, true);
|
||||
const animation =
|
||||
visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
return new StaggerAst(timings, animation);
|
||||
|
||||
return {
|
||||
type: AnimationMetadataType.Stagger,
|
||||
animation: visitDslNode(this, normalizeAnimationEntry(metadata.animation), context), timings,
|
||||
options: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,17 +525,20 @@ function constructTimingAst(value: string | number | AnimateTimings, errors: any
|
||||
timings = value as AnimateTimings;
|
||||
} else if (typeof value == 'number') {
|
||||
const duration = resolveTiming(value as number, errors).duration;
|
||||
return new TimingAst(value as number, 0, '');
|
||||
return makeTimingAst(duration as number, 0, '');
|
||||
}
|
||||
|
||||
const strValue = value as string;
|
||||
const isDynamic = strValue.split(/\s+/).some(v => v.charAt(0) == '{' && v.charAt(1) == '{');
|
||||
if (isDynamic) {
|
||||
return new DynamicTimingAst(strValue);
|
||||
const ast = makeTimingAst(0, 0, '') as any;
|
||||
ast.dynamic = true;
|
||||
ast.strValue = strValue;
|
||||
return ast as DynamicTimingAst;
|
||||
}
|
||||
|
||||
timings = timings || resolveTiming(strValue, errors);
|
||||
return new TimingAst(timings.duration, timings.delay, timings.easing);
|
||||
return makeTimingAst(timings.duration, timings.delay, timings.easing);
|
||||
}
|
||||
|
||||
function normalizeAnimationOptions(options: AnimationOptions | null): AnimationOptions {
|
||||
@ -515,3 +552,7 @@ function normalizeAnimationOptions(options: AnimationOptions | null): AnimationO
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function makeTimingAst(duration: number, delay: number, easing: string | null): TimingAst {
|
||||
return {duration, delay, easing};
|
||||
}
|
||||
|
@ -5,54 +5,20 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationQueryMetadata, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata} from '@angular/animations';
|
||||
import {AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationQueryMetadata, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata} from '@angular/animations';
|
||||
|
||||
export interface AnimationDslVisitor {
|
||||
visitTrigger(ast: AnimationTriggerMetadata, context: any): any;
|
||||
visitState(ast: AnimationStateMetadata, context: any): any;
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any;
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: any): any;
|
||||
visitGroup(ast: AnimationGroupMetadata, context: any): any;
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: any): any;
|
||||
visitStyle(ast: AnimationStyleMetadata, context: any): any;
|
||||
visitKeyframes(ast: AnimationKeyframesSequenceMetadata, context: any): any;
|
||||
visitReference(ast: AnimationReferenceMetadata, context: any): any;
|
||||
visitAnimateChild(ast: AnimationAnimateChildMetadata, context: any): any;
|
||||
visitAnimateRef(ast: AnimationAnimateRefMetadata, context: any): any;
|
||||
visitQuery(ast: AnimationQueryMetadata, context: any): any;
|
||||
visitStagger(ast: AnimationStaggerMetadata, context: any): any;
|
||||
}
|
||||
|
||||
export function visitAnimationNode(
|
||||
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any) {
|
||||
switch (node.type) {
|
||||
case AnimationMetadataType.Trigger:
|
||||
return visitor.visitTrigger(node as AnimationTriggerMetadata, context);
|
||||
case AnimationMetadataType.State:
|
||||
return visitor.visitState(node as AnimationStateMetadata, context);
|
||||
case AnimationMetadataType.Transition:
|
||||
return visitor.visitTransition(node as AnimationTransitionMetadata, context);
|
||||
case AnimationMetadataType.Sequence:
|
||||
return visitor.visitSequence(node as AnimationSequenceMetadata, context);
|
||||
case AnimationMetadataType.Group:
|
||||
return visitor.visitGroup(node as AnimationGroupMetadata, context);
|
||||
case AnimationMetadataType.Animate:
|
||||
return visitor.visitAnimate(node as AnimationAnimateMetadata, context);
|
||||
case AnimationMetadataType.Keyframes:
|
||||
return visitor.visitKeyframes(node as AnimationKeyframesSequenceMetadata, context);
|
||||
case AnimationMetadataType.Style:
|
||||
return visitor.visitStyle(node as AnimationStyleMetadata, context);
|
||||
case AnimationMetadataType.Reference:
|
||||
return visitor.visitReference(node as AnimationReferenceMetadata, context);
|
||||
case AnimationMetadataType.AnimateChild:
|
||||
return visitor.visitAnimateChild(node as AnimationAnimateChildMetadata, context);
|
||||
case AnimationMetadataType.AnimateRef:
|
||||
return visitor.visitAnimateRef(node as AnimationAnimateRefMetadata, context);
|
||||
case AnimationMetadataType.Query:
|
||||
return visitor.visitQuery(node as AnimationQueryMetadata, context);
|
||||
case AnimationMetadataType.Stagger:
|
||||
return visitor.visitStagger(node as AnimationStaggerMetadata, context);
|
||||
default:
|
||||
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
|
||||
}
|
||||
visitTrigger(node: AnimationTriggerMetadata, context: any): any;
|
||||
visitState(node: AnimationStateMetadata, context: any): any;
|
||||
visitTransition(node: AnimationTransitionMetadata, context: any): any;
|
||||
visitSequence(node: AnimationSequenceMetadata, context: any): any;
|
||||
visitGroup(node: AnimationGroupMetadata, context: any): any;
|
||||
visitAnimate(node: AnimationAnimateMetadata, context: any): any;
|
||||
visitStyle(node: AnimationStyleMetadata, context: any): any;
|
||||
visitKeyframes(node: AnimationKeyframesSequenceMetadata, context: any): any;
|
||||
visitReference(node: AnimationReferenceMetadata, context: any): any;
|
||||
visitAnimateChild(node: AnimationAnimateChildMetadata, context: any): any;
|
||||
visitAnimateRef(node: AnimationAnimateRefMetadata, context: any): any;
|
||||
visitQuery(node: AnimationQueryMetadata, context: any): any;
|
||||
visitStagger(node: AnimationStaggerMetadata, context: any): any;
|
||||
}
|
||||
|
@ -5,10 +5,10 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AUTO_STYLE, AnimateChildOptions, AnimateTimings, AnimationOptions, AnimationQueryOptions, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
|
||||
import {AUTO_STYLE, AnimateChildOptions, AnimateTimings, AnimationMetadataType, AnimationOptions, AnimationQueryOptions, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationDriver} from '../render/animation_driver';
|
||||
import {copyObj, copyStyles, interpolateParams, iteratorToArray, resolveTiming, resolveTimingValue} from '../util';
|
||||
import {copyObj, copyStyles, interpolateParams, iteratorToArray, resolveTiming, resolveTimingValue, visitDslNode} from '../util';
|
||||
|
||||
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, AstVisitor, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationTimelineInstruction, createTimelineInstruction} from './animation_timeline_instruction';
|
||||
@ -101,8 +101,8 @@ const ONE_FRAME_IN_MILLISECONDS = 1;
|
||||
* the `AnimationValidatorVisitor` code.
|
||||
*/
|
||||
export function buildAnimationTimelines(
|
||||
driver: AnimationDriver, rootElement: any, ast: Ast, startingStyles: ɵStyleData = {},
|
||||
finalStyles: ɵStyleData = {}, options: AnimationOptions,
|
||||
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>,
|
||||
startingStyles: ɵStyleData = {}, finalStyles: ɵStyleData = {}, options: AnimationOptions,
|
||||
subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
|
||||
return new AnimationTimelineBuilderVisitor().buildKeyframes(
|
||||
driver, rootElement, ast, startingStyles, finalStyles, options, subInstructions, errors);
|
||||
@ -110,15 +110,15 @@ export function buildAnimationTimelines(
|
||||
|
||||
export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
buildKeyframes(
|
||||
driver: AnimationDriver, rootElement: any, ast: Ast, startingStyles: ɵStyleData,
|
||||
finalStyles: ɵStyleData, options: AnimationOptions, subInstructions?: ElementInstructionMap,
|
||||
errors: any[] = []): AnimationTimelineInstruction[] {
|
||||
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>,
|
||||
startingStyles: ɵStyleData, finalStyles: ɵStyleData, options: AnimationOptions,
|
||||
subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
|
||||
subInstructions = subInstructions || new ElementInstructionMap();
|
||||
const context = new AnimationTimelineContext(driver, rootElement, subInstructions, errors, []);
|
||||
context.options = options;
|
||||
context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
|
||||
|
||||
ast.visit(this, context);
|
||||
visitDslNode(this, ast, context);
|
||||
|
||||
// this checks to see if an actual animation happened
|
||||
const timelines = context.timelines.filter(timeline => timeline.containsAnimation());
|
||||
@ -193,7 +193,7 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
|
||||
visitReference(ast: ReferenceAst, context: AnimationTimelineContext) {
|
||||
context.updateOptions(ast.options, true);
|
||||
ast.animation.visit(this, context);
|
||||
visitDslNode(this, ast.animation, context);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
@ -207,7 +207,7 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
ctx.transformIntoNewTimeline();
|
||||
|
||||
if (options.delay != null) {
|
||||
if (ctx.previousNode instanceof StyleAst) {
|
||||
if (ctx.previousNode.type == AnimationMetadataType.Style) {
|
||||
ctx.currentTimeline.snapshotCurrentStyles();
|
||||
ctx.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
}
|
||||
@ -218,7 +218,7 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
}
|
||||
|
||||
if (ast.steps.length) {
|
||||
ast.steps.forEach(s => s.visit(this, ctx));
|
||||
ast.steps.forEach(s => visitDslNode(this, s, ctx));
|
||||
|
||||
// this is here just incase the inner steps only contain or end with a style() call
|
||||
ctx.currentTimeline.applyStylesToKeyframe();
|
||||
@ -245,7 +245,7 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
innerContext.delayNextStep(delay);
|
||||
}
|
||||
|
||||
s.visit(this, innerContext);
|
||||
visitDslNode(this, s, innerContext);
|
||||
furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime);
|
||||
innerTimelines.push(innerContext.currentTimeline);
|
||||
});
|
||||
@ -259,19 +259,19 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitTiming(ast: TimingAst, context: AnimationTimelineContext): AnimateTimings {
|
||||
if (ast instanceof DynamicTimingAst) {
|
||||
const strValue = context.params ?
|
||||
interpolateParams(ast.value, context.params, context.errors) :
|
||||
ast.value.toString();
|
||||
return resolveTiming(strValue, context.errors);
|
||||
private _visitTiming(ast: TimingAst, context: AnimationTimelineContext): AnimateTimings {
|
||||
if ((ast as DynamicTimingAst).dynamic) {
|
||||
const strValue = (ast as DynamicTimingAst).strValue;
|
||||
const timingValue =
|
||||
context.params ? interpolateParams(strValue, context.params, context.errors) : strValue;
|
||||
return resolveTiming(timingValue, context.errors);
|
||||
} else {
|
||||
return {duration: ast.duration, delay: ast.delay, easing: ast.easing};
|
||||
}
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimateAst, context: AnimationTimelineContext) {
|
||||
const timings = context.currentAnimateTimings = this.visitTiming(ast.timings, context);
|
||||
const timings = context.currentAnimateTimings = this._visitTiming(ast.timings, context);
|
||||
const timeline = context.currentTimeline;
|
||||
if (timings.delay) {
|
||||
context.incrementTime(timings.delay);
|
||||
@ -279,7 +279,7 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
}
|
||||
|
||||
const style = ast.style;
|
||||
if (style instanceof KeyframesAst) {
|
||||
if (style.type == AnimationMetadataType.Keyframes) {
|
||||
this.visitKeyframes(style, context);
|
||||
} else {
|
||||
context.incrementTime(timings.duration);
|
||||
@ -343,7 +343,7 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
const options = (ast.options || {}) as AnimationQueryOptions;
|
||||
const delay = options.delay ? resolveTimingValue(options.delay) : 0;
|
||||
|
||||
if (delay && (context.previousNode instanceof StyleAst ||
|
||||
if (delay && (context.previousNode.type === AnimationMetadataType.Style ||
|
||||
(startTime == 0 && context.currentTimeline.getCurrentStyleProperties().length))) {
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
@ -368,7 +368,7 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
sameElementTimeline = innerContext.currentTimeline;
|
||||
}
|
||||
|
||||
ast.animation.visit(this, innerContext);
|
||||
visitDslNode(this, ast.animation, innerContext);
|
||||
|
||||
// this is here just incase the inner steps only contain or end
|
||||
// with a style() call (which is here to signal that this is a preparatory
|
||||
@ -415,7 +415,7 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
}
|
||||
|
||||
const startingTime = timeline.currentTime;
|
||||
ast.animation.visit(this, context);
|
||||
visitDslNode(this, ast.animation, context);
|
||||
context.previousNode = ast;
|
||||
|
||||
// time = duration + delay
|
||||
@ -431,12 +431,12 @@ export declare type StyleAtTime = {
|
||||
time: number; value: string | number;
|
||||
};
|
||||
|
||||
const DEFAULT_NOOP_PREVIOUS_NODE = <Ast>{};
|
||||
const DEFAULT_NOOP_PREVIOUS_NODE = <Ast<AnimationMetadataType>>{};
|
||||
export class AnimationTimelineContext {
|
||||
public parentContext: AnimationTimelineContext|null = null;
|
||||
public currentTimeline: TimelineBuilder;
|
||||
public currentAnimateTimings: AnimateTimings|null = null;
|
||||
public previousNode: Ast = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
public previousNode: Ast<AnimationMetadataType> = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
public subContextCount = 0;
|
||||
public options: AnimationOptions = {};
|
||||
public currentQueryIndex: number = 0;
|
||||
@ -489,7 +489,7 @@ export class AnimationTimelineContext {
|
||||
const oldParams = this.options.params;
|
||||
if (oldParams) {
|
||||
const params: {[name: string]: any} = options['params'] = {};
|
||||
Object.keys(this.options.params).forEach(name => { params[name] = oldParams[name]; });
|
||||
Object.keys(oldParams).forEach(name => { params[name] = oldParams[name]; });
|
||||
}
|
||||
}
|
||||
return options;
|
||||
|
@ -55,16 +55,27 @@ function parseAnimationAlias(alias: string, errors: string[]): string {
|
||||
}
|
||||
}
|
||||
|
||||
const TRUE_BOOLEAN_VALUES = new Set<string>();
|
||||
TRUE_BOOLEAN_VALUES.add('true');
|
||||
TRUE_BOOLEAN_VALUES.add('1');
|
||||
|
||||
const FALSE_BOOLEAN_VALUES = new Set<string>();
|
||||
FALSE_BOOLEAN_VALUES.add('false');
|
||||
FALSE_BOOLEAN_VALUES.add('0');
|
||||
|
||||
function makeLambdaFromStates(lhs: string, rhs: string): TransitionMatcherFn {
|
||||
const LHS_MATCH_BOOLEAN = TRUE_BOOLEAN_VALUES.has(lhs) || FALSE_BOOLEAN_VALUES.has(lhs);
|
||||
const RHS_MATCH_BOOLEAN = TRUE_BOOLEAN_VALUES.has(rhs) || FALSE_BOOLEAN_VALUES.has(rhs);
|
||||
|
||||
return (fromState: any, toState: any): boolean => {
|
||||
let lhsMatch = lhs == ANY_STATE || lhs == fromState;
|
||||
let rhsMatch = rhs == ANY_STATE || rhs == toState;
|
||||
|
||||
if (!lhsMatch && typeof fromState === 'boolean') {
|
||||
lhsMatch = fromState ? lhs === 'true' : lhs === 'false';
|
||||
if (!lhsMatch && LHS_MATCH_BOOLEAN && typeof fromState === 'boolean') {
|
||||
lhsMatch = fromState ? TRUE_BOOLEAN_VALUES.has(lhs) : FALSE_BOOLEAN_VALUES.has(lhs);
|
||||
}
|
||||
if (!rhsMatch && typeof toState === 'boolean') {
|
||||
rhsMatch = toState ? rhs === 'true' : rhs === 'false';
|
||||
if (!rhsMatch && RHS_MATCH_BOOLEAN && typeof toState === 'boolean') {
|
||||
rhsMatch = toState ? TRUE_BOOLEAN_VALUES.has(rhs) : FALSE_BOOLEAN_VALUES.has(rhs);
|
||||
}
|
||||
|
||||
return lhsMatch && rhsMatch;
|
||||
|
@ -5,7 +5,7 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {ɵStyleData} from '@angular/animations';
|
||||
import {AnimationMetadataType, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyStyles, interpolateParams} from '../util';
|
||||
|
||||
@ -13,6 +13,7 @@ import {SequenceAst, StyleAst, TransitionAst, TriggerAst} from './animation_ast'
|
||||
import {AnimationStateStyles, AnimationTransitionFactory} from './animation_transition_factory';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
@ -60,8 +61,15 @@ function createFallbackTransition(
|
||||
triggerName: string,
|
||||
states: {[stateName: string]: AnimationStateStyles}): AnimationTransitionFactory {
|
||||
const matchers = [(fromState: any, toState: any) => true];
|
||||
const animation = new SequenceAst([]);
|
||||
const transition = new TransitionAst(matchers, animation);
|
||||
const animation: SequenceAst = {type: AnimationMetadataType.Sequence, steps: [], options: null};
|
||||
const transition: TransitionAst = {
|
||||
type: AnimationMetadataType.Transition,
|
||||
animation,
|
||||
matchers,
|
||||
options: null,
|
||||
queryCount: 0,
|
||||
depCount: 0
|
||||
};
|
||||
return new AnimationTransitionFactory(triggerName, transition, states);
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AUTO_STYLE, AnimationMetadata, AnimationOptions, AnimationPlayer, ɵStyleData} from '@angular/animations';
|
||||
import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationPlayer, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {Ast} from '../dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../dsl/animation_ast_builder';
|
||||
@ -20,7 +20,7 @@ import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes,
|
||||
const EMPTY_INSTRUCTION_MAP = new ElementInstructionMap();
|
||||
|
||||
export class TimelineAnimationEngine {
|
||||
private _animations: {[id: string]: Ast} = {};
|
||||
private _animations: {[id: string]: Ast<AnimationMetadataType>} = {};
|
||||
private _playersById: {[id: string]: AnimationPlayer} = {};
|
||||
public players: AnimationPlayer[] = [];
|
||||
|
||||
|
@ -119,18 +119,18 @@ export class AnimationTransitionNamespace {
|
||||
|
||||
listen(element: any, name: string, phase: string, callback: (event: any) => boolean): () => any {
|
||||
if (!this._triggers.hasOwnProperty(name)) {
|
||||
throw new Error(
|
||||
`Unable to listen on the animation trigger event "${phase}" because the animation trigger "${name}" doesn\'t exist!`);
|
||||
throw new Error(`Unable to listen on the animation trigger event "${
|
||||
phase}" because the animation trigger "${name}" doesn\'t exist!`);
|
||||
}
|
||||
|
||||
if (phase == null || phase.length == 0) {
|
||||
throw new Error(
|
||||
`Unable to listen on the animation trigger "${name}" because the provided event is undefined!`);
|
||||
throw new Error(`Unable to listen on the animation trigger "${
|
||||
name}" because the provided event is undefined!`);
|
||||
}
|
||||
|
||||
if (!isTriggerEventValid(phase)) {
|
||||
throw new Error(
|
||||
`The provided animation trigger event "${phase}" for the animation trigger "${name}" is not supported!`);
|
||||
throw new Error(`The provided animation trigger event "${phase}" for the animation trigger "${
|
||||
name}" is not supported!`);
|
||||
}
|
||||
|
||||
const listeners = getOrSetAsInMap(this._elementListeners, element, []);
|
||||
@ -802,7 +802,8 @@ export class TransitionAnimationEngine {
|
||||
|
||||
reportError(errors: string[]) {
|
||||
throw new Error(
|
||||
`Unable to process animations due to the following failed trigger transitions\n ${errors.join("\n")}`);
|
||||
`Unable to process animations due to the following failed trigger transitions\n ${
|
||||
errors.join('\n')}`);
|
||||
}
|
||||
|
||||
private _flushAnimations(cleanupFns: Function[], microtaskId: number):
|
||||
@ -1413,13 +1414,11 @@ function deleteOrUnsetInMap(map: Map<any, any[]>| {[key: string]: any}, key: any
|
||||
return currentValues;
|
||||
}
|
||||
|
||||
function normalizeTriggerValue(value: any): string {
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
return value ? '1' : '0';
|
||||
default:
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
function normalizeTriggerValue(value: any): any {
|
||||
// we use `!= null` here because it's the most simple
|
||||
// way to test against a "falsy" value without mixing
|
||||
// in empty strings or a zero value. DO NOT OPTIMIZE.
|
||||
return value != null ? value : null;
|
||||
}
|
||||
|
||||
function isElementNode(node: any) {
|
||||
|
@ -5,7 +5,9 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleData} from '@angular/animations';
|
||||
import {AnimateTimings, AnimationMetadata, AnimationMetadataType, AnimationOptions, sequence, ɵStyleData} from '@angular/animations';
|
||||
import {Ast as AnimationAst, AstVisitor as AnimationAstVisitor} from './dsl/animation_ast';
|
||||
import {AnimationDslVisitor} from './dsl/animation_dsl_visitor';
|
||||
|
||||
export const ONE_SECOND = 1000;
|
||||
|
||||
@ -232,3 +234,40 @@ export function dashCaseToCamelCase(input: string): string {
|
||||
export function allowPreviousPlayerStylesMerge(duration: number, delay: number) {
|
||||
return duration === 0 || delay === 0;
|
||||
}
|
||||
|
||||
export function visitDslNode(
|
||||
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any): any;
|
||||
export function visitDslNode(
|
||||
visitor: AnimationAstVisitor, node: AnimationAst<AnimationMetadataType>, context: any): any;
|
||||
export function visitDslNode(visitor: any, node: any, context: any): any {
|
||||
switch (node.type) {
|
||||
case AnimationMetadataType.Trigger:
|
||||
return visitor.visitTrigger(node, context);
|
||||
case AnimationMetadataType.State:
|
||||
return visitor.visitState(node, context);
|
||||
case AnimationMetadataType.Transition:
|
||||
return visitor.visitTransition(node, context);
|
||||
case AnimationMetadataType.Sequence:
|
||||
return visitor.visitSequence(node, context);
|
||||
case AnimationMetadataType.Group:
|
||||
return visitor.visitGroup(node, context);
|
||||
case AnimationMetadataType.Animate:
|
||||
return visitor.visitAnimate(node, context);
|
||||
case AnimationMetadataType.Keyframes:
|
||||
return visitor.visitKeyframes(node, context);
|
||||
case AnimationMetadataType.Style:
|
||||
return visitor.visitStyle(node, context);
|
||||
case AnimationMetadataType.Reference:
|
||||
return visitor.visitReference(node, context);
|
||||
case AnimationMetadataType.AnimateChild:
|
||||
return visitor.visitAnimateChild(node, context);
|
||||
case AnimationMetadataType.AnimateRef:
|
||||
return visitor.visitAnimateRef(node, context);
|
||||
case AnimationMetadataType.Query:
|
||||
return visitor.visitQuery(node, context);
|
||||
case AnimationMetadataType.Stagger:
|
||||
return visitor.visitStagger(node, context);
|
||||
default:
|
||||
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
|
||||
}
|
||||
}
|
||||
|
@ -257,6 +257,11 @@ export interface AnimationStaggerMetadata extends AnimationMetadata {
|
||||
the
|
||||
* trigger is bound to (in the form of `[@triggerName]="expression"`.
|
||||
*
|
||||
* Animation trigger bindings strigify values and then match the previous and current values against
|
||||
* any linked transitions. If a boolean value is provided into the trigger binding then it will both
|
||||
* be represented as `1` or `true` and `0` or `false` for a true and false boolean values
|
||||
* respectively.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* `trigger` will create an animation trigger reference based on the provided `name` value. The
|
||||
@ -734,6 +739,21 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* ### Boolean values
|
||||
* if a trigger binding value is a boolean value then it can be matched using a transition
|
||||
* expression that compares `true` and `false` or `1` and `0`.
|
||||
*
|
||||
* ```
|
||||
* // in the template
|
||||
* <div [@openClose]="open ? true : false">...</div>
|
||||
*
|
||||
* // in the component metadata
|
||||
* trigger('openClose', [
|
||||
* state('true', style({ height: '*' })),
|
||||
* state('false', style({ height: '0px' })),
|
||||
* transition('false <=> true', animate(500))
|
||||
* ])
|
||||
* ```
|
||||
* {@example core/animation/ts/dsl/animation_example.ts region='Component'}
|
||||
*
|
||||
* @experimental Animation support is experimental.
|
||||
@ -756,7 +776,7 @@ export function transition(
|
||||
* var fadeAnimation = animation([
|
||||
* style({ opacity: '{{ start }}' }),
|
||||
* animate('{{ time }}',
|
||||
* style({ opacity: '{{ end }}'))
|
||||
* style({ opacity: '{{ end }}'}))
|
||||
* ], { params: { time: '1000ms', start: 0, end: 1 }});
|
||||
* ```
|
||||
*
|
||||
|
@ -191,6 +191,14 @@ export class HttpXhrBackend implements HttpBackend {
|
||||
// The parse error contains the text of the body that failed to parse.
|
||||
body = { error, text: body } as HttpJsonParseError;
|
||||
}
|
||||
} else if (!ok && req.responseType === 'json' && typeof body === 'string') {
|
||||
try {
|
||||
// Attempt to parse the body as JSON.
|
||||
body = JSON.parse(body);
|
||||
} catch (error) {
|
||||
// Cannot be certain that the body was meant to be parsed as JSON.
|
||||
// Leave the body as a string.
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
|
@ -17,7 +17,7 @@ import {MockXhrFactory} from './xhr_mock';
|
||||
|
||||
function trackEvents(obs: Observable<HttpEvent<any>>): HttpEvent<any>[] {
|
||||
const events: HttpEvent<any>[] = [];
|
||||
obs.subscribe(event => events.push(event));
|
||||
obs.subscribe(event => events.push(event), err => events.push(err));
|
||||
return events;
|
||||
}
|
||||
|
||||
@ -92,6 +92,13 @@ export function main() {
|
||||
const res = events[1] as HttpResponse<{data: string}>;
|
||||
expect(res.body !.data).toBe('some data');
|
||||
});
|
||||
it('handles a json error response', () => {
|
||||
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
||||
factory.mock.mockFlush(500, 'Error', JSON.stringify({data: 'some data'}));
|
||||
expect(events.length).toBe(2);
|
||||
const res = events[1] as any as HttpErrorResponse;
|
||||
expect(res.error !.data).toBe('some data');
|
||||
});
|
||||
it('handles a json string response', () => {
|
||||
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
||||
expect(factory.mock.responseType).toEqual('text');
|
||||
|
@ -14,12 +14,12 @@
|
||||
*/
|
||||
export enum ChangeDetectionStrategy {
|
||||
/**
|
||||
* `OnPush` means that the change detector's mode will be set to `CheckOnce` during hydration.
|
||||
* `OnPush` means that the change detector's mode will be initially set to `CheckOnce`.
|
||||
*/
|
||||
OnPush,
|
||||
|
||||
/**
|
||||
* `Default` means that the change detector's mode will be set to `CheckAlways` during hydration.
|
||||
* `Default` means that the change detector's mode will be initially set to `CheckAlways`.
|
||||
*/
|
||||
Default,
|
||||
}
|
||||
|
@ -340,6 +340,42 @@ export function main() {
|
||||
expect(completed).toBe(true);
|
||||
}));
|
||||
|
||||
it('should always fire inner callbacks even if no animation is fired when a view is inserted',
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
<div *ngIf="exp">
|
||||
<div @myAnimation (@myAnimation.start)="track($event)" (@myAnimation.done)="track($event)"></div>
|
||||
</div>
|
||||
`,
|
||||
animations: [
|
||||
trigger('myAnimation', []),
|
||||
]
|
||||
})
|
||||
class Cmp {
|
||||
exp: any = false;
|
||||
log: string[] = [];
|
||||
track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); }
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.log).toEqual([]);
|
||||
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.log).toEqual(['myAnimation-start', 'myAnimation-done']);
|
||||
}));
|
||||
|
||||
it('should only turn a view removal as into `void` state transition', () => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
@ -475,6 +511,103 @@ export function main() {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should understand boolean values as `true` and `false` for transition animations', () => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
<div [@myAnimation]="exp"></div>
|
||||
`,
|
||||
animations: [
|
||||
trigger(
|
||||
'myAnimation',
|
||||
[
|
||||
transition(
|
||||
'true => false',
|
||||
[
|
||||
style({opacity: 0}),
|
||||
animate(1234, style({opacity: 1})),
|
||||
]),
|
||||
transition(
|
||||
'false => true',
|
||||
[
|
||||
style({opacity: 1}),
|
||||
animate(4567, style({opacity: 0})),
|
||||
])
|
||||
]),
|
||||
]
|
||||
})
|
||||
class Cmp {
|
||||
exp: any = false;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
let players = getLog();
|
||||
expect(players.length).toEqual(1);
|
||||
let [player] = players;
|
||||
|
||||
expect(player.duration).toEqual(1234);
|
||||
});
|
||||
|
||||
it('should understand boolean values as `true` and `false` for transition animations and apply the corresponding state() value',
|
||||
() => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
<div [@myAnimation]="exp"></div>
|
||||
`,
|
||||
animations: [
|
||||
trigger(
|
||||
'myAnimation',
|
||||
[
|
||||
state('true', style({color: 'red'})),
|
||||
state('false', style({color: 'blue'})),
|
||||
transition(
|
||||
'true <=> false',
|
||||
[
|
||||
animate(1000, style({color: 'gold'})),
|
||||
animate(1000),
|
||||
]),
|
||||
]),
|
||||
]
|
||||
})
|
||||
class Cmp {
|
||||
exp: any = false;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
let players = getLog();
|
||||
expect(players.length).toEqual(1);
|
||||
let [player] = players;
|
||||
|
||||
expect(player.keyframes).toEqual([
|
||||
{color: 'blue', offset: 0},
|
||||
{color: 'gold', offset: 0.5},
|
||||
{color: 'red', offset: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not throw an error if a trigger with the same name exists in separate components',
|
||||
() => {
|
||||
@Component({selector: 'cmp1', template: '...', animations: [trigger('trig', [])]})
|
||||
|
@ -10,35 +10,9 @@ import {isDevMode} from '@angular/core';
|
||||
|
||||
import {DomAdapter, getDOM} from '../dom/dom_adapter';
|
||||
|
||||
import {InertBodyHelper} from './inert_body';
|
||||
import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer';
|
||||
|
||||
/** A <body> element that can be safely used to parse untrusted HTML. Lazily initialized below. */
|
||||
let inertElement: HTMLElement|null = null;
|
||||
/** Lazily initialized to make sure the DOM adapter gets set before use. */
|
||||
let DOM: DomAdapter = null !;
|
||||
|
||||
/** Returns an HTML element that is guaranteed to not execute code when creating elements in it. */
|
||||
function getInertElement() {
|
||||
if (inertElement) return inertElement;
|
||||
DOM = getDOM();
|
||||
|
||||
// Prefer using <template> element if supported.
|
||||
const templateEl = DOM.createElement('template');
|
||||
if ('content' in templateEl) return templateEl;
|
||||
|
||||
const doc = DOM.createHtmlDocument();
|
||||
inertElement = DOM.querySelector(doc, 'body');
|
||||
if (inertElement == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so we
|
||||
// need to create one.
|
||||
const html = DOM.createElement('html', doc);
|
||||
inertElement = DOM.createElement('body', doc);
|
||||
DOM.appendChild(html, inertElement);
|
||||
DOM.appendChild(doc, html);
|
||||
}
|
||||
return inertElement;
|
||||
}
|
||||
|
||||
function tagSet(tags: string): {[k: string]: boolean} {
|
||||
const res: {[k: string]: boolean} = {};
|
||||
for (const t of tags.split(',')) res[t] = true;
|
||||
@ -121,53 +95,54 @@ class SanitizingHtmlSerializer {
|
||||
// because characters were re-encoded.
|
||||
public sanitizedSomething = false;
|
||||
private buf: string[] = [];
|
||||
private DOM = getDOM();
|
||||
|
||||
sanitizeChildren(el: Element): string {
|
||||
// This cannot use a TreeWalker, as it has to run on Angular's various DOM adapters.
|
||||
// However this code never accesses properties off of `document` before deleting its contents
|
||||
// again, so it shouldn't be vulnerable to DOM clobbering.
|
||||
let current: Node = el.firstChild !;
|
||||
let current: Node = this.DOM.firstChild(el) !;
|
||||
while (current) {
|
||||
if (DOM.isElementNode(current)) {
|
||||
if (this.DOM.isElementNode(current)) {
|
||||
this.startElement(current as Element);
|
||||
} else if (DOM.isTextNode(current)) {
|
||||
this.chars(DOM.nodeValue(current) !);
|
||||
} else if (this.DOM.isTextNode(current)) {
|
||||
this.chars(this.DOM.nodeValue(current) !);
|
||||
} else {
|
||||
// Strip non-element, non-text nodes.
|
||||
this.sanitizedSomething = true;
|
||||
}
|
||||
if (DOM.firstChild(current)) {
|
||||
current = DOM.firstChild(current) !;
|
||||
if (this.DOM.firstChild(current)) {
|
||||
current = this.DOM.firstChild(current) !;
|
||||
continue;
|
||||
}
|
||||
while (current) {
|
||||
// Leaving the element. Walk up and to the right, closing tags as we go.
|
||||
if (DOM.isElementNode(current)) {
|
||||
if (this.DOM.isElementNode(current)) {
|
||||
this.endElement(current as Element);
|
||||
}
|
||||
|
||||
let next = checkClobberedElement(current, DOM.nextSibling(current) !);
|
||||
let next = this.checkClobberedElement(current, this.DOM.nextSibling(current) !);
|
||||
|
||||
if (next) {
|
||||
current = next;
|
||||
break;
|
||||
}
|
||||
|
||||
current = checkClobberedElement(current, DOM.parentElement(current) !);
|
||||
current = this.checkClobberedElement(current, this.DOM.parentElement(current) !);
|
||||
}
|
||||
}
|
||||
return this.buf.join('');
|
||||
}
|
||||
|
||||
private startElement(element: Element) {
|
||||
const tagName = DOM.nodeName(element).toLowerCase();
|
||||
const tagName = this.DOM.nodeName(element).toLowerCase();
|
||||
if (!VALID_ELEMENTS.hasOwnProperty(tagName)) {
|
||||
this.sanitizedSomething = true;
|
||||
return;
|
||||
}
|
||||
this.buf.push('<');
|
||||
this.buf.push(tagName);
|
||||
DOM.attributeMap(element).forEach((value: string, attrName: string) => {
|
||||
this.DOM.attributeMap(element).forEach((value: string, attrName: string) => {
|
||||
const lower = attrName.toLowerCase();
|
||||
if (!VALID_ATTRS.hasOwnProperty(lower)) {
|
||||
this.sanitizedSomething = true;
|
||||
@ -186,7 +161,7 @@ class SanitizingHtmlSerializer {
|
||||
}
|
||||
|
||||
private endElement(current: Element) {
|
||||
const tagName = DOM.nodeName(current).toLowerCase();
|
||||
const tagName = this.DOM.nodeName(current).toLowerCase();
|
||||
if (VALID_ELEMENTS.hasOwnProperty(tagName) && !VOID_ELEMENTS.hasOwnProperty(tagName)) {
|
||||
this.buf.push('</');
|
||||
this.buf.push(tagName);
|
||||
@ -195,14 +170,14 @@ class SanitizingHtmlSerializer {
|
||||
}
|
||||
|
||||
private chars(chars: string) { this.buf.push(encodeEntities(chars)); }
|
||||
}
|
||||
|
||||
function checkClobberedElement(node: Node, nextNode: Node): Node {
|
||||
if (nextNode && DOM.contains(node, nextNode)) {
|
||||
throw new Error(
|
||||
`Failed to sanitize html because the element is clobbered: ${DOM.getOuterHTML(node)}`);
|
||||
checkClobberedElement(node: Node, nextNode: Node): Node {
|
||||
if (nextNode && this.DOM.contains(node, nextNode)) {
|
||||
throw new Error(
|
||||
`Failed to sanitize html because the element is clobbered: ${this.DOM.getOuterHTML(node)}`);
|
||||
}
|
||||
return nextNode;
|
||||
}
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
// Regular Expressions for parsing tags and attributes
|
||||
@ -233,33 +208,20 @@ function encodeEntities(value: string) {
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1'
|
||||
* attribute to declare ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo').
|
||||
*
|
||||
* This is undesirable since we don't want to allow any of these custom attributes. This method
|
||||
* strips them all.
|
||||
*/
|
||||
function stripCustomNsAttrs(el: Element) {
|
||||
DOM.attributeMap(el).forEach((_, attrName) => {
|
||||
if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
|
||||
DOM.removeAttribute(el, attrName);
|
||||
}
|
||||
});
|
||||
for (const n of DOM.childNodesAsList(el)) {
|
||||
if (DOM.isElementNode(n)) stripCustomNsAttrs(n as Element);
|
||||
}
|
||||
}
|
||||
let inertBodyHelper: InertBodyHelper;
|
||||
|
||||
/**
|
||||
* Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to
|
||||
* the DOM in a browser environment.
|
||||
*/
|
||||
export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
const DOM = getDOM();
|
||||
let inertBodyElement: HTMLElement|null = null;
|
||||
try {
|
||||
const containerEl = getInertElement();
|
||||
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc, DOM);
|
||||
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
|
||||
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
|
||||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
|
||||
// mXSS protection. Repeatedly parse the document to make sure it stabilizes, so that a browser
|
||||
// trying to auto-correct incorrect HTML cannot cause formerly inert HTML to become dangerous.
|
||||
@ -273,31 +235,25 @@ export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
mXSSAttempts--;
|
||||
|
||||
unsafeHtml = parsedHtml;
|
||||
DOM.setInnerHTML(containerEl, unsafeHtml);
|
||||
if (defaultDoc.documentMode) {
|
||||
// strip custom-namespaced attributes on IE<=11
|
||||
stripCustomNsAttrs(containerEl);
|
||||
}
|
||||
parsedHtml = DOM.getInnerHTML(containerEl);
|
||||
parsedHtml = DOM.getInnerHTML(inertBodyElement);
|
||||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
} while (unsafeHtml !== parsedHtml);
|
||||
|
||||
const sanitizer = new SanitizingHtmlSerializer();
|
||||
const safeHtml = sanitizer.sanitizeChildren(DOM.getTemplateContent(containerEl) || containerEl);
|
||||
|
||||
// Clear out the body element.
|
||||
const parent = DOM.getTemplateContent(containerEl) || containerEl;
|
||||
for (const child of DOM.childNodesAsList(parent)) {
|
||||
DOM.removeChild(parent, child);
|
||||
}
|
||||
|
||||
const safeHtml =
|
||||
sanitizer.sanitizeChildren(DOM.getTemplateContent(inertBodyElement) || inertBodyElement);
|
||||
if (isDevMode() && sanitizer.sanitizedSomething) {
|
||||
DOM.log('WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).');
|
||||
}
|
||||
|
||||
return safeHtml;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
// In case anything goes wrong, clear out inertElement to reset the entire DOM structure.
|
||||
inertElement = null;
|
||||
throw e;
|
||||
if (inertBodyElement) {
|
||||
const parent = DOM.getTemplateContent(inertBodyElement) || inertBodyElement;
|
||||
for (const child of DOM.childNodesAsList(parent)) {
|
||||
DOM.removeChild(parent, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
171
packages/platform-browser/src/security/inert_body.ts
Normal file
171
packages/platform-browser/src/security/inert_body.ts
Normal file
@ -0,0 +1,171 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DomAdapter, getDOM} from '../dom/dom_adapter';
|
||||
|
||||
/**
|
||||
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
|
||||
* that needs sanitizing.
|
||||
* Depending upon browser support we must use one of three strategies for doing this.
|
||||
* Support: Safari 10.x -> XHR strategy
|
||||
* Support: Firefox -> DomParser strategy
|
||||
* Default: InertDocument strategy
|
||||
*/
|
||||
export class InertBodyHelper {
|
||||
private inertBodyElement: HTMLElement;
|
||||
|
||||
constructor(private defaultDoc: any, private DOM: DomAdapter) {
|
||||
const inertDocument = this.DOM.createHtmlDocument();
|
||||
this.inertBodyElement = inertDocument.body;
|
||||
|
||||
if (this.inertBodyElement == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so
|
||||
// we need to create one.
|
||||
const inertHtml = this.DOM.createElement('html', inertDocument);
|
||||
this.inertBodyElement = this.DOM.createElement('body', inertDocument);
|
||||
this.DOM.appendChild(inertHtml, this.inertBodyElement);
|
||||
this.DOM.appendChild(inertDocument, inertHtml);
|
||||
}
|
||||
|
||||
this.DOM.setInnerHTML(
|
||||
this.inertBodyElement, '<svg><g onload="this.parentNode.remove()"></g></svg>');
|
||||
if (this.inertBodyElement.querySelector && !this.inertBodyElement.querySelector('svg')) {
|
||||
// We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element
|
||||
// so use the XHR strategy.
|
||||
this.getInertBodyElement = this.getInertBodyElement_XHR;
|
||||
return;
|
||||
}
|
||||
|
||||
this.DOM.setInnerHTML(
|
||||
this.inertBodyElement, '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">');
|
||||
if (this.inertBodyElement.querySelector && this.inertBodyElement.querySelector('svg img')) {
|
||||
// We just hit the Firefox bug - which prevents the inner img JS from being sanitized
|
||||
// so use the DOMParser strategy, if it is available.
|
||||
// If the DOMParser is not available then we are not in Firefox (Server/WebWorker?) so we
|
||||
// fall through to the default strategy below.
|
||||
if (isDOMParserAvailable()) {
|
||||
this.getInertBodyElement = this.getInertBodyElement_DOMParser;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// None of the bugs were hit so it is safe for us to use the default InertDocument strategy
|
||||
this.getInertBodyElement = this.getInertBodyElement_InertDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
|
||||
* The implementation of this is determined in the constructor, when the class is instantiated.
|
||||
*/
|
||||
getInertBodyElement: (html: string) => HTMLElement | null;
|
||||
|
||||
/**
|
||||
* Use XHR to create and fill an inert body element (on Safari 10.1)
|
||||
* See
|
||||
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
*/
|
||||
private getInertBodyElement_XHR(html: string) {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
try {
|
||||
html = encodeURI(html);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'document';
|
||||
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
|
||||
xhr.send(null);
|
||||
const body: HTMLBodyElement = xhr.response.body;
|
||||
body.removeChild(body.firstChild !);
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use DOMParser to create and fill an inert body element (on Firefox)
|
||||
* See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
*
|
||||
*/
|
||||
private getInertBodyElement_DOMParser(html: string) {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
try {
|
||||
const body = new (window as any)
|
||||
.DOMParser()
|
||||
.parseFromString(html, 'text/html')
|
||||
.body as HTMLBodyElement;
|
||||
body.removeChild(body.firstChild !);
|
||||
return body;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
||||
* This is the default sane strategy to use if the browser does not require one of the specialised
|
||||
* strategies above.
|
||||
*/
|
||||
private getInertBodyElement_InertDocument(html: string) {
|
||||
// Prefer using <template> element if supported.
|
||||
const templateEl = this.DOM.createElement('template');
|
||||
if ('content' in templateEl) {
|
||||
this.DOM.setInnerHTML(templateEl, html);
|
||||
return templateEl;
|
||||
}
|
||||
|
||||
this.DOM.setInnerHTML(this.inertBodyElement, html);
|
||||
|
||||
// Support: IE 9-11 only
|
||||
// strip custom-namespaced attributes on IE<=11
|
||||
if (this.defaultDoc.documentMode) {
|
||||
this.stripCustomNsAttrs(this.inertBodyElement);
|
||||
}
|
||||
|
||||
return this.inertBodyElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1'
|
||||
* attribute to declare ns1 namespace and prefixes the attribute with 'ns1' (e.g.
|
||||
* 'ns1:xlink:foo').
|
||||
*
|
||||
* This is undesirable since we don't want to allow any of these custom attributes. This method
|
||||
* strips them all.
|
||||
*/
|
||||
private stripCustomNsAttrs(el: Element) {
|
||||
this.DOM.attributeMap(el).forEach((_, attrName) => {
|
||||
if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
|
||||
this.DOM.removeAttribute(el, attrName);
|
||||
}
|
||||
});
|
||||
for (const n of this.DOM.childNodesAsList(el)) {
|
||||
if (this.DOM.isElementNode(n)) this.stripCustomNsAttrs(n as Element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to determine whether the DOMParser exists in the global context.
|
||||
* The try-catch is because, on some browsers, trying to access this property
|
||||
* on window can actually throw an error.
|
||||
*
|
||||
* @suppress {uselessCode}
|
||||
*/
|
||||
function isDOMParserAvailable() {
|
||||
try {
|
||||
return !!(window as any).DOMParser;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -134,6 +134,32 @@ export function main() {
|
||||
}
|
||||
});
|
||||
|
||||
// See
|
||||
// https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
it('should not allow JavaScript execution when creating inert document', () => {
|
||||
const output = sanitizeHtml(defaultDoc, '<svg><g onload="window.xxx = 100"></g></svg>');
|
||||
const window = defaultDoc.defaultView;
|
||||
if (window) {
|
||||
expect(window.xxx).toBe(undefined);
|
||||
window.xxx = undefined;
|
||||
}
|
||||
expect(output).toEqual('');
|
||||
});
|
||||
|
||||
// See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)',
|
||||
() => {
|
||||
debugger;
|
||||
expect(sanitizeHtml(
|
||||
defaultDoc, '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">'))
|
||||
.toEqual(
|
||||
isDOMParserAvailable() ?
|
||||
// PlatformBrowser output
|
||||
'<p><img src="<img src="x"></p>' :
|
||||
// PlatformServer output
|
||||
'<p><img src="</style><img src=x onerror=alert(1)//"></p>');
|
||||
});
|
||||
|
||||
if (browserDetection.isWebkit) {
|
||||
it('should prevent mXSS attacks', function() {
|
||||
expect(sanitizeHtml(defaultDoc, '<a href=" javascript:alert(1)">CLICKME</a>'))
|
||||
@ -142,3 +168,18 @@ export function main() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to determine whether the DOMParser exists in the global context.
|
||||
* The try-catch is because, on some browsers, trying to access this property
|
||||
* on window can actually throw an error.
|
||||
*
|
||||
* @suppress {uselessCode}
|
||||
*/
|
||||
function isDOMParserAvailable() {
|
||||
try {
|
||||
return !!(window as any).DOMParser;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -34,10 +34,11 @@
|
||||
"webpack": "^2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"build": "./build.sh",
|
||||
"test": "npm run build && concurrently \"npm run serve\" \"npm run protractor\" --kill-others --success first",
|
||||
"serve": "node built/server-bundle.js",
|
||||
"preprotractor": "webdriver-manager update --gecko false && tsc -p e2e",
|
||||
"preprotractor": "tsc -p e2e",
|
||||
"protractor": "protractor e2e/protractor.config.js"
|
||||
}
|
||||
}
|
||||
|
@ -119,19 +119,19 @@ export class RouterLinkActive implements OnChanges,
|
||||
|
||||
private update(): void {
|
||||
if (!this.links || !this.linksWithHrefs || !this.router.navigated) return;
|
||||
const hasActiveLinks = this.hasActiveLinks();
|
||||
|
||||
// react only when status has changed to prevent unnecessary dom updates
|
||||
if (this.active !== hasActiveLinks) {
|
||||
this.classes.forEach((c) => {
|
||||
if (hasActiveLinks) {
|
||||
this.renderer.addClass(this.element.nativeElement, c);
|
||||
} else {
|
||||
this.renderer.removeClass(this.element.nativeElement, c);
|
||||
}
|
||||
});
|
||||
Promise.resolve(hasActiveLinks).then(active => this.active = active);
|
||||
}
|
||||
Promise.resolve().then(() => {
|
||||
const hasActiveLinks = this.hasActiveLinks();
|
||||
if (this.active !== hasActiveLinks) {
|
||||
this.active = hasActiveLinks;
|
||||
this.classes.forEach((c) => {
|
||||
if (hasActiveLinks) {
|
||||
this.renderer.addClass(this.element.nativeElement, c);
|
||||
} else {
|
||||
this.renderer.removeClass(this.element.nativeElement, c);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isLinkActive(router: Router): (link: (RouterLink|RouterLinkWithHref)) => boolean {
|
||||
|
@ -2572,6 +2572,7 @@ describe('Integration', () => {
|
||||
|
||||
router.navigateByUrl('/team/22/link;exact=true');
|
||||
advance(fixture);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link;exact=true');
|
||||
|
||||
const nativeLink = fixture.nativeElement.querySelector('a');
|
||||
@ -2628,6 +2629,7 @@ describe('Integration', () => {
|
||||
|
||||
router.navigateByUrl('/team/22/link;exact=true');
|
||||
advance(fixture);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link;exact=true');
|
||||
|
||||
const native = fixture.nativeElement.querySelector('#link-parent');
|
||||
@ -2656,6 +2658,7 @@ describe('Integration', () => {
|
||||
|
||||
router.navigateByUrl('/team/22/link');
|
||||
advance(fixture);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link');
|
||||
|
||||
const native = fixture.nativeElement.querySelector('a');
|
||||
|
@ -7,8 +7,9 @@
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component, NgModule, Type} from '@angular/core';
|
||||
import {Component, ContentChild, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {Router} from '@angular/router';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
|
||||
@ -56,6 +57,56 @@ describe('Integration', () => {
|
||||
instance.show = true;
|
||||
expect(() => advance(fixture)).not.toThrow();
|
||||
}));
|
||||
|
||||
it('should set isActive right after looking at its children -- #18983', fakeAsync(() => {
|
||||
@Component({
|
||||
template: `
|
||||
<div #rla="routerLinkActive" routerLinkActive>
|
||||
isActive: {{rla.isActive}}
|
||||
|
||||
<ng-template let-data>
|
||||
<a [routerLink]="data">link</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-container #container></ng-container>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
class ComponentWithRouterLink {
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
@ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
|
||||
|
||||
addLink() {
|
||||
this.container.createEmbeddedView(this.templateRef, {$implicit: '/simple'});
|
||||
}
|
||||
|
||||
removeLink() { this.container.clear(); }
|
||||
}
|
||||
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule.withRoutes([{path: 'simple', component: SimpleCmp}])],
|
||||
declarations: [ComponentWithRouterLink, SimpleCmp]
|
||||
});
|
||||
|
||||
const router: Router = TestBed.get(Router);
|
||||
const fixture = createRoot(router, ComponentWithRouterLink);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
fixture.componentInstance.addLink();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.removeLink();
|
||||
advance(fixture);
|
||||
advance(fixture);
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toContain('isActive: false');
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ function setEnvVar() {
|
||||
if [[ ${print} == "print" ]]; then
|
||||
echo ${name}=${value}
|
||||
fi
|
||||
export ${name}=${value}
|
||||
export ${name}="${value}"
|
||||
}
|
||||
|
||||
# use BASH_SOURCE so that we get the right path when this script is called AND source-d
|
||||
@ -36,6 +36,10 @@ fi
|
||||
|
||||
setEnvVar NODE_VERSION 6.9.5
|
||||
setEnvVar YARN_VERSION 1.0.2
|
||||
# Pin to a Chromium version that does not cause the aio e2e tests to flake. (See https://github.com/angular/angular/pull/20403.)
|
||||
# Revision 494239 (which was part of Chrome 62.0.3186.0) is the last version that does not cause flakes. (Latest revision checked: 508578)
|
||||
setEnvVar CHROMIUM_VERSION 494239 # Chrome 62 linux stable, see https://www.chromium.org/developers/calendar
|
||||
setEnvVar CHROMEDRIVER_VERSION_ARG "--versions.chrome 2.33"
|
||||
setEnvVar SAUCE_CONNECT_VERSION 4.4.9
|
||||
setEnvVar PROJECT_ROOT $(cd ${thisDir}/../..; pwd)
|
||||
|
||||
@ -101,6 +105,7 @@ if [[ ${TRAVIS:-} ]]; then
|
||||
setEnvVar BROWSER_STACK_USERNAME angularteam1
|
||||
# not using use setEnvVar so that we don't print the key
|
||||
export BROWSER_STACK_ACCESS_KEY=BWCd4SynLzdDcv8xtzsB
|
||||
setEnvVar CHROME_BIN ${HOME}/.chrome/chromium/chrome-linux/chrome
|
||||
setEnvVar BROWSER_PROVIDER_READY_FILE /tmp/angular-build/browser-provider-tunnel-init.lock
|
||||
fi
|
||||
|
||||
|
84
scripts/ci/install-chromium.sh
Executable file
84
scripts/ci/install-chromium.sh
Executable file
@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -u -e -o pipefail
|
||||
|
||||
# Setup environment
|
||||
readonly thisDir=$(cd $(dirname $0); pwd)
|
||||
source ${thisDir}/_travis-fold.sh
|
||||
|
||||
|
||||
# This script basically follows the instructions to download an old version of Chromium: https://www.chromium.org/getting-involved/download-chromium
|
||||
# 1) It retrieves the current stable version number from https://www.chromium.org/developers/calendar (via the https://omahaproxy.appspot.com/all file), e.g. 359700 for Chromium 48.
|
||||
# 2) It checks the Travis cache for this specific version
|
||||
# 3) If not available, it downloads and caches it, using the "decrement commit number" trick.
|
||||
|
||||
#Build version read from the OmahaProxy CSV Viewer at https://www.chromium.org/developers/calendar
|
||||
#Let's use the following version of Chromium, and inform about availability of newer build from https://omahaproxy.appspot.com/all
|
||||
#
|
||||
# CHROMIUM_VERSION <<< this variable is now set via env.sh
|
||||
|
||||
PLATFORM="$(uname -s)"
|
||||
case "$PLATFORM" in
|
||||
(Darwin)
|
||||
ARCHITECTURE=Mac
|
||||
DIST_FILE=chrome-mac.zip
|
||||
;;
|
||||
(Linux)
|
||||
ARCHITECTURE=Linux_x64
|
||||
DIST_FILE=chrome-linux.zip
|
||||
;;
|
||||
(*)
|
||||
echo Unsupported platform $PLATFORM. Exiting ... >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
|
||||
TMP=$(curl -s "https://omahaproxy.appspot.com/all") || true
|
||||
oldIFS="$IFS"
|
||||
IFS='
|
||||
'
|
||||
IFS=${IFS:0:1}
|
||||
lines=( $TMP )
|
||||
IFS=','
|
||||
for line in "${lines[@]}"
|
||||
do
|
||||
lineArray=($line);
|
||||
if [ "${lineArray[0]}" = "linux" ] && [ "${lineArray[1]}" = "stable" ] ; then
|
||||
LATEST_CHROMIUM_VERSION="${lineArray[7]}"
|
||||
fi
|
||||
done
|
||||
IFS="$oldIFS"
|
||||
|
||||
CHROMIUM_DIR=$HOME/.chrome/chromium
|
||||
CHROMIUM_BIN=$CHROMIUM_DIR/chrome-linux/chrome
|
||||
CHROMIUM_VERSION_FILE=$CHROMIUM_DIR/VERSION
|
||||
|
||||
EXISTING_VERSION=""
|
||||
if [[ -f $CHROMIUM_VERSION_FILE && -x $CHROMIUM_BIN ]]; then
|
||||
EXISTING_VERSION=`cat $CHROMIUM_VERSION_FILE`
|
||||
echo Found cached Chromium version: ${EXISTING_VERSION}
|
||||
fi
|
||||
|
||||
if [[ "$EXISTING_VERSION" != "$CHROMIUM_VERSION" ]]; then
|
||||
echo Downloading Chromium version: ${CHROMIUM_VERSION}
|
||||
rm -fR $CHROMIUM_DIR
|
||||
mkdir -p $CHROMIUM_DIR
|
||||
|
||||
NEXT=$CHROMIUM_VERSION
|
||||
FILE="chrome-linux.zip"
|
||||
STATUS=404
|
||||
while [[ $STATUS == 404 && $NEXT -ge 0 ]]
|
||||
do
|
||||
echo Fetch Chromium version: ${NEXT}
|
||||
STATUS=$(curl "https://storage.googleapis.com/chromium-browser-snapshots/${ARCHITECTURE}/${NEXT}/${DIST_FILE}" -s -w %{http_code} --create-dirs -o $FILE) || true
|
||||
NEXT=$[$NEXT-1]
|
||||
done
|
||||
|
||||
unzip $FILE -d $CHROMIUM_DIR
|
||||
rm $FILE
|
||||
echo $CHROMIUM_VERSION > $CHROMIUM_VERSION_FILE
|
||||
fi
|
||||
|
||||
if [[ "$CHROMIUM_VERSION" != "$LATEST_CHROMIUM_VERSION" ]]; then
|
||||
echo "New version of Chromium available. Update install-chromium.sh with build number: ${LATEST_CHROMIUM_VERSION}"
|
||||
fi
|
@ -35,7 +35,7 @@ travisFoldEnd "install-yarn"
|
||||
|
||||
# Install all npm dependencies according to yarn.lock
|
||||
travisFoldStart "yarn-install"
|
||||
node tools/npm/check-node-modules --purge || yarn install --freeze-lockfile --non-interactive
|
||||
(node tools/npm/check-node-modules --purge && yarn update-webdriver) || yarn install --frozen-lockfile --non-interactive
|
||||
travisFoldEnd "yarn-install"
|
||||
|
||||
|
||||
@ -64,11 +64,21 @@ if [[ ${TRAVIS} && ${CI_MODE} == "bazel" ]]; then
|
||||
travisFoldEnd "bazel-install"
|
||||
fi
|
||||
|
||||
# Start xvfb for local Chrome testing
|
||||
if [[ ${TRAVIS} && (${CI_MODE} == "js" || ${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e") ]]; then
|
||||
travisFoldStart "xvfb-start"
|
||||
sh -e /etc/init.d/xvfb start
|
||||
travisFoldEnd "xvfb-start"
|
||||
|
||||
# Install Chromium
|
||||
if [[ ${TRAVIS} && ${CI_MODE} == "js" || ${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e" ]]; then
|
||||
travisFoldStart "install-chromium"
|
||||
(
|
||||
${thisDir}/install-chromium.sh
|
||||
|
||||
# Start xvfb for local Chrome used for testing
|
||||
if [[ ${TRAVIS} ]]; then
|
||||
travisFoldStart "install-chromium.xvfb-start"
|
||||
sh -e /etc/init.d/xvfb start
|
||||
travisFoldEnd "install-chromium.xvfb-start"
|
||||
fi
|
||||
)
|
||||
travisFoldEnd "install-chromium"
|
||||
fi
|
||||
|
||||
|
||||
@ -92,12 +102,6 @@ if [[ ${TRAVIS} && (${CI_MODE} == "browserstack_required" || ${CI_MODE} == "brow
|
||||
fi
|
||||
|
||||
|
||||
# Install Selenium WebDriver
|
||||
travisFoldStart "webdriver-manager-update"
|
||||
$(npm bin)/webdriver-manager update
|
||||
travisFoldEnd "webdriver-manager-update"
|
||||
|
||||
|
||||
# Install bower packages
|
||||
travisFoldStart "bower-install"
|
||||
$(npm bin)/bower install
|
||||
|
@ -6,37 +6,48 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
// tslint:disable:no-console
|
||||
module.exports = (gulp) => () => {
|
||||
const validateCommitMessage = require('../validate-commit-message');
|
||||
const childProcess = require('child_process');
|
||||
const shelljs = require('shelljs');
|
||||
|
||||
let baseBranch = 'master';
|
||||
const currentVersion = require('semver').parse(require('../../package.json').version);
|
||||
const baseHead =
|
||||
shelljs.exec(`git ls-remote --heads origin ${currentVersion.major}.${currentVersion.minor}.*`)
|
||||
.trim()
|
||||
.split('\n')
|
||||
.pop();
|
||||
if (baseHead) {
|
||||
const match = /refs\/heads\/(.+)/.exec(baseHead);
|
||||
baseBranch = match && match[1] || baseBranch;
|
||||
}
|
||||
|
||||
// We need to fetch origin explicitly because it might be stale.
|
||||
// I couldn't find a reliable way to do this without fetch.
|
||||
childProcess.exec(
|
||||
'git fetch origin master && git log --reverse --format=%s HEAD ^origin/master',
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(stderr);
|
||||
process.exit(1);
|
||||
}
|
||||
result = shelljs.exec(
|
||||
`git fetch origin ${baseBranch} && git log --reverse --format=%s HEAD ^origin/${baseBranch}`);
|
||||
|
||||
let someCommitsInvalid = false;
|
||||
let commitsByLine = stdout.trim().split(/\n/).filter(line => line != '');
|
||||
if (result.code) {
|
||||
console.log(result.stderr);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Examining ${commitsByLine.length} commits between HEAD and master`);
|
||||
const commitsByLine = result.trim().split(/\n/).filter(line => line != '');
|
||||
|
||||
if (commitsByLine.length == 0) {
|
||||
console.log('There are zero new commits between this HEAD and master');
|
||||
}
|
||||
console.log(`Examining ${commitsByLine.length} commits between HEAD and ${baseBranch}`);
|
||||
|
||||
someCommitsInvalid = !commitsByLine.every(validateCommitMessage);
|
||||
if (commitsByLine.length == 0) {
|
||||
console.log(`There are zero new commits between this HEAD and ${baseBranch}`);
|
||||
}
|
||||
|
||||
if (someCommitsInvalid) {
|
||||
console.log('Please fix the failing commit messages before continuing...');
|
||||
console.log(
|
||||
'Commit message guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines');
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
const someCommitsInvalid = !commitsByLine.every(validateCommitMessage);
|
||||
|
||||
if (someCommitsInvalid) {
|
||||
console.log('Please fix the failing commit messages before continuing...');
|
||||
console.log(
|
||||
'Commit message guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines');
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
3
tools/npm/check-node-modules.js
Executable file → Normal file
3
tools/npm/check-node-modules.js
Executable file → Normal file
@ -17,8 +17,7 @@ var PROJECT_ROOT = path.join(__dirname, '../../');
|
||||
// tslint:disable:no-console
|
||||
function checkNodeModules(logOutput, purgeIfStale) {
|
||||
var yarnCheck = childProcess.spawnSync(
|
||||
'yarn check --integrity',
|
||||
{shell: true, cwd: path.resolve(__dirname, '../..')});
|
||||
'yarn check --integrity', {shell: true, cwd: path.resolve(__dirname, '../..')});
|
||||
|
||||
var nodeModulesOK = yarnCheck.status === 0;
|
||||
if (nodeModulesOK) {
|
||||
|
@ -5011,6 +5011,14 @@ shell-quote@1.6.1:
|
||||
array-reduce "~0.0.0"
|
||||
jsonify "~0.0.0"
|
||||
|
||||
shelljs@^0.7.8:
|
||||
version "0.7.8"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
|
||||
dependencies:
|
||||
glob "^7.0.0"
|
||||
interpret "^1.0.0"
|
||||
rechoir "^0.6.2"
|
||||
|
||||
sigmund@~1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
|
||||
|
Reference in New Issue
Block a user