Compare commits

...

35 Commits
4.4.5 ... 4.4.x

Author SHA1 Message Date
018750154d test: fix firebase deployment script test
When I fixed the project id in 2c4850dc58,
I didn't realize we had a test that verified the wrong behavior.
2018-05-04 15:10:17 -07:00
b19216d58b fix(aio): correct project id for deployment of archive sites 2018-05-03 15:09:17 -07:00
84fc1a3663 docs: add changelog for the 4.4.7 release 2018-04-16 02:00:31 -06:00
1c40be26c6 release: cut the 4.4.7 release 2018-04-16 02:00:02 -06:00
2c5cf19c6d fix(core): use appropriate inert document strategy for Firefox & Safari (#22077)
Both Firefox and Safari are vulnerable to XSS if we use an inert document
created via `document.implementation.createHTMLDocument()`.

Now we check for those vulnerabilities and then use a DOMParser or XHR
strategy if needed.

Further the platform-server has its own library for parsing HTML, so we
sniff for that (by checking whether DOMParser exists) and fall back to
the standard strategy.

Thanks to @cure53 for the heads up on this issue.
2018-02-13 10:05:14 -08:00
0dacf6d5f1 ci: use sudo: false on Travis (#21641)
Related to #21422.

PR Close #21641
2018-02-11 21:18:03 +02:00
e9f1d44015 ci: downgrade Chromium to a version that does not cause flakes
There seems to be some issue that causes Chrome/ChromeDriver to
unexpectedly reload during the aio e2e tests, causing flakes. It is not
clear what exactly is causing the reloading, but to the best of my
knowledge it is something inside Chrome or ChromeDriver.

Pinning Chrome to r494239 (between 62.0.3185.0 and 62.0.3186.0) fixes
the flakes.

Fixes #20159
2018-02-11 21:18:03 +02:00
1d9024ee9a build: pin ChromeDriver version (#20940)
Since our version of Chromium is also pinned, a new ChromeDriver (that
drops support for our Chromium version) can cause random (and unrelated
to the corresponding changes) errors on CI.
This commit pins the version of ChromeDriver and it should now be
manually upgraded to a vrsion that is compatible with th currently used
Chromium version.

PR Close #20940
2018-02-11 21:18:03 +02:00
6a6164ab4f revert: ci: use chrome stable (#18307)
This reverts commit 8bcb268140.
2018-02-11 19:39:55 +02:00
7231f5e26a docs: add changelog for 4.4.6 2017-10-18 16:14:53 -07:00
86415223cb release: cut the 4.4.6 release 2017-10-18 16:12:10 -07:00
269f5acc54 fix(common): attempt to JSON.parse errors for JSON responses (#19773)
Prior behavior for HttpClient was to parse the body as JSON when
responseType was set to 'json', even if the response was
unsuccessful. This changed due to a recent bugfix, and
unsuccessful responses had their bodies treated as strings.

There is no guarantee that if a service returns JSON in the
successful case that it will do so for errors. However, users
indicate that most APIs in the wild do work this way. Therefore,
this commit changes the error case behavior to attempt a JSON
parsing of the response body, and falls back on returning it as
a string if that fails.

PR Close #19773
2017-10-18 12:58:49 -07:00
6e6c866de9 docs(aio): changed confusing term (#19762)
Controller should be decorator I believe

PR Close #19762
2017-10-18 12:58:40 -07:00
732ed92cb7 test(animations): ensure :enter callbacks fire on container insertion (#19674)
PR Close #19674
2017-10-18 12:58:26 -07:00
53a807ae09 fix(router): RouterLinkActive should update its state right after checking the children
Closes #18983
2017-10-18 12:57:53 -07:00
3342a8253b fix(aio): make tests less flaky (#19784)
PR Close #19784
2017-10-18 10:19:48 -07:00
630c19f52d build: remove required BrowserStack run as it fails with “Access denied” (#19769)
See #19768
PR Close #19769
2017-10-17 15:53:05 -07:00
af8c2fa4be build: don’t make BrowserStack required as it fails with “Access denied”
See #19768
2017-10-17 14:56:41 -07:00
0789601dd6 build: fix broken path for animations in .pullapprove (#19453)
PR Close #19453
2017-10-17 10:44:43 -07:00
ce0ac46e42 style: fix formatting of check-node-modules (#19720)
PR Close #19720
2017-10-17 10:41:18 -07:00
b531d87580 ci: validate commit messages correctly when not on master (#19720)
PR Close #19720
2017-10-17 10:41:18 -07:00
23a2154817 docs(aio): update 2018 events (#19706)
update ac 2017 dates

PR Close #19706
2017-10-17 10:41:09 -07:00
76d2496f24 perf(animations): reduce size of bundle by removing AST classes (#19673)
This CL refactors the animation AST code to make use of interfaces
instead of classes. Given that interfaces are not persisted during
runtime the removal of classes should nicely cut down on size for the
animations-browser bundle.

PR Close #19673
2017-10-17 10:41:01 -07:00
b85cb410f1 docs(aio): change in-mem-web-api version for examples (#19668)
PR Close #19668
2017-10-17 10:40:52 -07:00
1be22df0df ci(aio): raise payload limit to accommodate the new search feature (#19704)
PR Close #19704
2017-10-13 15:20:23 -07:00
a805839d38 feat(aio): add search to 404 page (#19704)
The 404 page will now run a search based on the given URL to offer
suggestions for the page that the user really wanted.

PR Close #19704
2017-10-13 15:20:23 -07:00
3ac61a7550 refactor(aio): move SearchResultsComponent into shared module (#19704)
This will allow it to be used by an embedded component.

PR Close #19704
2017-10-13 15:20:23 -07:00
57ea33bc5c feat(aio): allow SearchService to have multiple clients (#19704)
PR Close #19704
2017-10-13 15:20:23 -07:00
4891649d68 test(aio): tidy up e2e tests that used invalid URLs (#19704)
PR Close #19704
2017-10-13 15:20:23 -07:00
93aba1bb1c build(aio): remove unused imports and local variables (#19704)
PR Close #19704
2017-10-13 15:20:23 -07:00
f983a6c615 fix(animations): properly support boolean-based transitions and state changes (#19672)
Closes #9396
Closes #12337

PR Close #19672
2017-10-13 09:40:29 -07:00
18f1b016e5 build(aio): consider devDependencies when overwriting dependencies of local Angular packages (#19687)
Previously, only `dependencies` were taken into account.

PR Close #19687
2017-10-13 09:30:10 -07:00
591dcc26af docs(animations): add missing bracket to fadeAnimation
Closes #18899
2017-10-13 09:14:29 -07:00
4acd322128 fix(core): don't refer to hydration in docs anymore.
Closes #18458
2017-10-13 09:05:30 -07:00
32a814bdfa docs: removing unnecessary commits from change log 2017-10-12 13:32:23 -07:00
94 changed files with 1275 additions and 615 deletions

View File

@ -104,7 +104,7 @@ groups:
animations:
conditions:
files:
- "packages/animation/*"
- "packages/animations/*"
- "packages/platform-browser/animations/*"
users:
- matsko #primary

View File

@ -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

View File

@ -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))

View File

@ -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/);
});

View File

@ -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>

View File

@ -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&mdash;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>

View File

@ -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>

View File

@ -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');
});
});
});

View File

@ -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']
}
}
});
};

View File

@ -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",

View File

@ -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,

View File

@ -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

View File

@ -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
;;

View File

@ -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"
)

View File

@ -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)).

View File

@ -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">

View File

@ -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 = {

View File

@ -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;
}

View File

@ -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,
],

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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
/**

View File

@ -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', () => {

View File

@ -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';

View File

@ -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 */

View File

@ -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(() => {

View File

@ -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';

View File

@ -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', () => {

View File

@ -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);
});
});

View File

@ -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);
});
}
}

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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;

View File

@ -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';

View 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[];
}

View File

@ -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';

View File

@ -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);
});
});
});

View File

@ -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));
}
}

View File

@ -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', () => {

View File

@ -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';

View File

@ -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']);

View File

@ -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');
});
});
});

View File

@ -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;
}

View File

@ -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';

View File

@ -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
]
})

View File

@ -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');
});

View File

@ -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 {

View File

@ -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';

View File

@ -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;

View File

@ -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>();

View File

@ -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',

View File

@ -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',

View File

@ -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": [],

View File

@ -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.

View File

@ -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));

View File

@ -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 });

View File

@ -8,6 +8,7 @@
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noUnusedLocals": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"

View File

@ -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}},

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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);

View File

@ -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;
}

View File

@ -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};
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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[] = [];

View File

@ -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) {

View File

@ -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}`);
}
}

View File

@ -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 }});
* ```
*

View File

@ -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) {

View File

@ -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');

View File

@ -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,
}

View File

@ -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', [])]})

View File

@ -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, '&gt;');
}
/**
* 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);
}
}
}
}

View 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;
}
}

View File

@ -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>&lt;img src=&#34;<img src="x"></p>' :
// PlatformServer output
'<p><img src="&lt;/style&gt;&lt;img src=x onerror=alert(1)//"></p>');
});
if (browserDetection.isWebkit) {
it('should prevent mXSS attacks', function() {
expect(sanitizeHtml(defaultDoc, '<a href="&#x3000;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;
}
}

View File

@ -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"
}
}

View File

@ -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 {

View File

@ -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');

View File

@ -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');
}));
});
});

View File

@ -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
View 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

View File

@ -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

View File

@ -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
View 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) {

View File

@ -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"