diff --git a/.travis.yml b/.travis.yml index cd1c1e0e7..c4ad92b87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ jobs: include: - env: "MODE=lint" - env: "MODE=aot" + - env: "MODE=prerender" - env: "MODE=closure-compiler" - env: "MODE=saucelabs_required" - env: "MODE=browserstack_required" diff --git a/package.json b/package.json index 2aca027d5..598e6224e 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,19 @@ "url": "git+https://github.com/angular/flex-layout.git" }, "scripts": { + "api": "gulp api-docs", "build": "gulp :publish:build-releases", + "closure": "./scripts/closure-compiler/build-devapp-bundle.sh", "demo-app": "gulp serve:devapp", - "test": "gulp test", - "tslint": "gulp lint", - "stylelint": "gulp lint", - "e2e": "gulp e2e", + "docs": "gulp docs", "deploy": "gulp deploy:devapp", "stage": "gulp stage-deploy:devapp", + "stylelint": "gulp lint", + "test": "gulp test", + "tslint": "gulp lint", "webdriver-manager": "webdriver-manager", - "docs": "gulp docs", - "api": "gulp api-docs" + "universal": "gulp universal:build", + "universal:test": "gulp ci:prerender" }, "version": "2.0.0-beta.8", "license": "MIT", diff --git a/scripts/ci/travis-testing.sh b/scripts/ci/travis-testing.sh index 44dae4a30..65e046768 100755 --- a/scripts/ci/travis-testing.sh +++ b/scripts/ci/travis-testing.sh @@ -39,6 +39,8 @@ elif is_aot; then $(npm bin)/gulp ci:aot elif is_unit; then $(npm bin)/gulp ci:test +elif is_prerender; then + $(npm bin)/gulp ci:prerender elif is_closure_compiler; then ./scripts/closure-compiler/build-devapp-bundle.sh fi diff --git a/scripts/closure-compiler/build-devapp-bundle.sh b/scripts/closure-compiler/build-devapp-bundle.sh index 5f511dede..b5be9b1d3 100755 --- a/scripts/closure-compiler/build-devapp-bundle.sh +++ b/scripts/closure-compiler/build-devapp-bundle.sh @@ -9,7 +9,7 @@ set -e -o pipefail cd $(dirname $0)/../.. -# Build a release of material and of the CDK package. +# Build a release of Flex-Layout library $(npm bin)/gulp flex-layout:build-release:clean # Build demo-app with ES2015 modules. Closure compiler is then able to parse imports. diff --git a/src/demo-app/app/github-issues/splitter/split-handle.directive.ts b/src/demo-app/app/github-issues/splitter/split-handle.directive.ts index 3ea6d3949..3960bb6d7 100644 --- a/src/demo-app/app/github-issues/splitter/split-handle.directive.ts +++ b/src/demo-app/app/github-issues/splitter/split-handle.directive.ts @@ -1,4 +1,6 @@ -import {Directive, ElementRef, Output} from '@angular/core'; +import {Directive, ElementRef, Inject, Output} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; + import {Observable} from 'rxjs/Observable'; import 'rxjs/add/observable/fromEvent'; @@ -17,14 +19,13 @@ export class SplitHandleDirective { @Output() drag: Observable<{ x: number, y: number }>; - constructor(ref: ElementRef) { + constructor(ref: ElementRef, @Inject(DOCUMENT) _document: any) { + const fromEvent = Observable.fromEvent; const getMouseEventPosition = (event: MouseEvent) => ({x: event.movementX, y: event.movementY}); - const mouseup$ = Observable.fromEvent(document, 'mouseup'); - const mousemove$ = Observable.fromEvent(document, 'mousemove') - .map(getMouseEventPosition); - const mousedown$ = Observable.fromEvent(ref.nativeElement, 'mousedown') - .map(getMouseEventPosition); + const mousedown$ = fromEvent(ref.nativeElement, 'mousedown').map(getMouseEventPosition); + const mousemove$ = fromEvent(_document, 'mousemove').map(getMouseEventPosition); + const mouseup$ = fromEvent(_document, 'mouseup').map(getMouseEventPosition); this.drag = mousedown$.switchMap(_ => mousemove$.takeUntil(mouseup$)); } diff --git a/src/lib/flexbox/api/base.ts b/src/lib/flexbox/api/base.ts index 710c7d314..efc1abd14 100644 --- a/src/lib/flexbox/api/base.ts +++ b/src/lib/flexbox/api/base.ts @@ -9,6 +9,7 @@ import { ElementRef, OnDestroy, SimpleChanges, OnChanges, SimpleChange, Renderer } from '@angular/core'; +import {ɵgetDOM as getDom} from '@angular/platform-browser'; import {applyCssPrefixes} from '../../utils/auto-prefixer'; import {buildLayoutCSS} from '../../utils/layout-validator'; @@ -117,27 +118,40 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { */ protected _getDisplayStyle(source?: HTMLElement): string { let element: HTMLElement = source || this._elementRef.nativeElement; - let value = (element.style as any)['display'] || getComputedStyle(element)['display']; - return value ? value.trim() : 'block'; + let value = this._lookupStyle(element, 'display'); + + return value ? value.trim() : ((element.nodeType === 1) ? 'block' : 'inline-block'); } protected _getFlowDirection(target: any, addIfMissing = false): string { - let value = ''; + let value = 'row'; + if (target) { - let directionKeys = Object.keys(applyCssPrefixes({'flex-direction': ''})); - let findDirection = (styles) => directionKeys.reduce((direction, key) => { - return direction || styles[key]; - }, null); - - let immediateValue = findDirection(target.style); - value = immediateValue || findDirection(getComputedStyle(target as Element)); - if (!immediateValue && addIfMissing) { - value = value || 'row'; + value = this._lookupStyle(target, 'flex-direction') || 'row'; + + let hasInlineValue = getDom().getStyle(target, 'flex-direction'); + if (!hasInlineValue && addIfMissing) { this._applyStyleToElements(buildLayoutCSS(value), [target]); } } - return value ? value.trim() : 'row'; + return value.trim(); + } + + /** + * Determine the inline or inherited CSS style + */ + protected _lookupStyle(element: HTMLElement, styleName: string): any { + let value = ''; + try { + if (element) { + let immediateValue = getDom().getStyle(element, styleName); + value = immediateValue || getDom().getComputedStyle(element).display; + } + } catch (e) { + // TODO: platform-server throws an exception for getComputedStyle + } + return value; } /** @@ -220,11 +234,11 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { * Special accessor to query for all child 'element' nodes regardless of type, class, etc. */ protected get childrenNodes() { - const obj = this._elementRef.nativeElement.childNodes; + const obj = this._elementRef.nativeElement.children; const buffer = []; // iterate backwards ensuring that length is an UInt32 - for ( let i = obj.length; i--; ) { + for (let i = obj.length; i--; ) { buffer[i] = obj[i]; } return buffer; diff --git a/src/lib/flexbox/api/layout-gap.ts b/src/lib/flexbox/api/layout-gap.ts index 2c43f4b37..f437c2abd 100644 --- a/src/lib/flexbox/api/layout-gap.ts +++ b/src/lib/flexbox/api/layout-gap.ts @@ -24,11 +24,13 @@ import {LayoutDirective} from './layout'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {LAYOUT_VALUES} from '../../utils/layout-validator'; + /** * 'layout-padding' styling directive * Defines padding of child elements in a layout container */ -@Directive({selector: ` +@Directive({ + selector: ` [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], @@ -42,22 +44,22 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI protected _observer: MutationObserver; /* tslint:disable */ - @Input('fxLayoutGap') set gap(val) { this._cacheInput('gap', val); } - @Input('fxLayoutGap.xs') set gapXs(val) { this._cacheInput('gapXs', val); } - @Input('fxLayoutGap.sm') set gapSm(val) { this._cacheInput('gapSm', val); }; - @Input('fxLayoutGap.md') set gapMd(val) { this._cacheInput('gapMd', val); }; - @Input('fxLayoutGap.lg') set gapLg(val) { this._cacheInput('gapLg', val); }; - @Input('fxLayoutGap.xl') set gapXl(val) { this._cacheInput('gapXl', val); }; - - @Input('fxLayoutGap.gt-xs') set gapGtXs(val) { this._cacheInput('gapGtXs', val); }; - @Input('fxLayoutGap.gt-sm') set gapGtSm(val) { this._cacheInput('gapGtSm', val); }; - @Input('fxLayoutGap.gt-md') set gapGtMd(val) { this._cacheInput('gapGtMd', val); }; - @Input('fxLayoutGap.gt-lg') set gapGtLg(val) { this._cacheInput('gapGtLg', val); }; - - @Input('fxLayoutGap.lt-sm') set gapLtSm(val) { this._cacheInput('gapLtSm', val); }; - @Input('fxLayoutGap.lt-md') set gapLtMd(val) { this._cacheInput('gapLtMd', val); }; - @Input('fxLayoutGap.lt-lg') set gapLtLg(val) { this._cacheInput('gapLtLg', val); }; - @Input('fxLayoutGap.lt-xl') set gapLtXl(val) { this._cacheInput('gapLtXl', val); }; + @Input('fxLayoutGap') set gap(val) { this._cacheInput('gap', val); } + @Input('fxLayoutGap.xs') set gapXs(val) { this._cacheInput('gapXs', val); } + @Input('fxLayoutGap.sm') set gapSm(val) { this._cacheInput('gapSm', val); }; + @Input('fxLayoutGap.md') set gapMd(val) { this._cacheInput('gapMd', val); }; + @Input('fxLayoutGap.lg') set gapLg(val) { this._cacheInput('gapLg', val); }; + @Input('fxLayoutGap.xl') set gapXl(val) { this._cacheInput('gapXl', val); }; + + @Input('fxLayoutGap.gt-xs') set gapGtXs(val) { this._cacheInput('gapGtXs', val); }; + @Input('fxLayoutGap.gt-sm') set gapGtSm(val) { this._cacheInput('gapGtSm', val); }; + @Input('fxLayoutGap.gt-md') set gapGtMd(val) { this._cacheInput('gapGtMd', val); }; + @Input('fxLayoutGap.gt-lg') set gapGtLg(val) { this._cacheInput('gapGtLg', val); }; + + @Input('fxLayoutGap.lt-sm') set gapLtSm(val) { this._cacheInput('gapLtSm', val); }; + @Input('fxLayoutGap.lt-md') set gapLtMd(val) { this._cacheInput('gapLtMd', val); }; + @Input('fxLayoutGap.lt-lg') set gapLtLg(val) { this._cacheInput('gapLtLg', val); }; + @Input('fxLayoutGap.lt-xl') set gapLtXl(val) { this._cacheInput('gapLtXl', val); }; /* tslint:enable */ constructor(monitor: MediaMonitor, @@ -124,8 +126,10 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI } }; - this._observer = new MutationObserver(onMutationCallback); - this._observer.observe(this._elementRef.nativeElement, {childList: true}); + if (typeof MutationObserver !== 'undefined') { + this._observer = new MutationObserver(onMutationCallback); + this._observer.observe(this._elementRef.nativeElement, {childList: true}); + } } /** diff --git a/src/lib/media-query/match-media.ts b/src/lib/media-query/match-media.ts index fcfd7f4e5..45857192c 100644 --- a/src/lib/media-query/match-media.ts +++ b/src/lib/media-query/match-media.ts @@ -5,8 +5,8 @@ * 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 {Injectable, NgZone} from '@angular/core'; - +import {Inject, Injectable, NgZone} from '@angular/core'; +import {ɵgetDOM as getDom, DOCUMENT} from '@angular/platform-browser'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Observable} from 'rxjs/Observable'; import {filter} from 'rxjs/operator/filter'; @@ -27,7 +27,9 @@ export interface MediaQueryListListener { export interface MediaQueryList { readonly matches: boolean; readonly media: string; + addListener(listener: MediaQueryListListener): void; + removeListener(listener: MediaQueryListListener): void; } @@ -45,7 +47,7 @@ export class MatchMedia { protected _source: BehaviorSubject; protected _observable$: Observable; - constructor(protected _zone: NgZone) { + constructor(protected _zone: NgZone, @Inject(DOCUMENT) protected _document: any) { this._registry = new Map(); this._source = new BehaviorSubject(new MediaChange(true)); this._observable$ = this._source.asObservable(); @@ -86,7 +88,7 @@ export class MatchMedia { let list = normalizeQuery(mediaQuery); if (list.length > 0) { - prepareQueryCSS(list); + prepareQueryCSS(list, this._document); list.forEach(query => { let mql = this._registry.get(query); @@ -114,8 +116,9 @@ export class MatchMedia { * Call window.matchMedia() to build a MediaQueryList; which * supports 0..n listeners for activation/deactivation */ - protected _buildMQL(query: string): MediaQueryList { - let canListen = !!(window).matchMedia('all').addListener; + protected _buildMQL(query: string): MediaQueryList { + let canListen = isBrowser() && !!(window).matchMedia('all').addListener; + return canListen ? (window).matchMedia(query) : { matches: query === 'all' || query === '', media: query, @@ -127,6 +130,13 @@ export class MatchMedia { } } +/** + * Determine if SSR or Browser rendering. + */ +export function isBrowser() { + return getDom().supportsDOMEvents(); +} + /** * Private global registry for all dynamically-created, injected style tags * @see prepare(query) @@ -140,27 +150,28 @@ const ALL_STYLES = {}; * @param query string The mediaQuery used to create a faux CSS selector * */ -function prepareQueryCSS(mediaQueries: string[]) { +function prepareQueryCSS(mediaQueries: string[], _document: any) { let list = mediaQueries.filter(it => !ALL_STYLES[it]); if (list.length > 0) { let query = list.join(', '); + try { - let style = document.createElement('style'); + let styleEl = getDom().createElement('style'); - style.setAttribute('type', 'text/css'); - if (!style['styleSheet']) { + getDom().setAttribute(styleEl, 'type', 'text/css'); + if (!styleEl['styleSheet']) { let cssText = `/* @angular/flex-layout - workaround for possible browser quirk with mediaQuery listeners see http://bit.ly/2sd4HMP */ @media ${query} {.fx-query-test{ }}`; - style.appendChild(document.createTextNode(cssText)); + getDom().appendChild(styleEl, getDom().createTextNode(cssText)); } - document.getElementsByTagName('head')[0].appendChild(style); + getDom().appendChild(_document.head, styleEl); // Store in private global registry - list.forEach(mq => ALL_STYLES[mq] = style); + list.forEach(mq => ALL_STYLES[mq] = styleEl); } catch (e) { console.error(e); diff --git a/src/lib/media-query/mock/mock-match-media.ts b/src/lib/media-query/mock/mock-match-media.ts index 681f75fd6..43440119a 100644 --- a/src/lib/media-query/mock/mock-match-media.ts +++ b/src/lib/media-query/mock/mock-match-media.ts @@ -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 {Injectable, NgZone} from '@angular/core'; +import {Inject, Injectable, NgZone} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; + import {MatchMedia} from '../match-media'; import {BreakPointRegistry} from '../breakpoints/break-point-registry'; @@ -28,8 +30,10 @@ export class MockMatchMedia extends MatchMedia { */ public useOverlaps = false; - constructor(_zone: NgZone, private _breakpoints: BreakPointRegistry) { - super(_zone); + constructor(_zone: NgZone, + @Inject(DOCUMENT) _document: any, + private _breakpoints: BreakPointRegistry) { + super(_zone, _document); this._actives = []; } @@ -83,18 +87,34 @@ export class MockMatchMedia extends MatchMedia { // Simulate activation of overlapping lt- ranges switch (alias) { - case 'lg' : this._activateByAlias('lt-xl'); break; - case 'md' : this._activateByAlias('lt-xl, lt-lg'); break; - case 'sm' : this._activateByAlias('lt-xl, lt-lg, lt-md'); break; - case 'xs' : this._activateByAlias('lt-xl, lt-lg, lt-md, lt-sm'); break; + case 'lg' : + this._activateByAlias('lt-xl'); + break; + case 'md' : + this._activateByAlias('lt-xl, lt-lg'); + break; + case 'sm' : + this._activateByAlias('lt-xl, lt-lg, lt-md'); + break; + case 'xs' : + this._activateByAlias('lt-xl, lt-lg, lt-md, lt-sm'); + break; } // Simulate activate of overlapping gt- mediaQuery ranges switch (alias) { - case 'xl' : this._activateByAlias('gt-lg, gt-md, gt-sm, gt-xs'); break; - case 'lg' : this._activateByAlias('gt-md, gt-sm, gt-xs'); break; - case 'md' : this._activateByAlias('gt-sm, gt-xs'); break; - case 'sm' : this._activateByAlias('gt-xs'); break; + case 'xl' : + this._activateByAlias('gt-lg, gt-md, gt-sm, gt-xs'); + break; + case 'lg' : + this._activateByAlias('gt-md, gt-sm, gt-xs'); + break; + case 'md' : + this._activateByAlias('gt-sm, gt-xs'); + break; + case 'sm' : + this._activateByAlias('gt-xs'); + break; } } // Activate last since the responsiveActivation is watching *this* mediaQuery @@ -195,7 +215,7 @@ export class MockMediaQueryList implements MediaQueryList { * Notify all listeners that 'matches === TRUE' */ activate(): MockMediaQueryList { - if ( !this._isActive ) { + if (!this._isActive) { this._isActive = true; this._listeners.forEach((callback) => { callback(this); diff --git a/src/universal-app/app/responsive-app.css b/src/universal-app/app/responsive-app.css new file mode 100644 index 000000000..dec9c5a8b --- /dev/null +++ b/src/universal-app/app/responsive-app.css @@ -0,0 +1,84 @@ +.night-theme { + background: #1c2029; + color: #cfcfcf; +} + +.handle { + outline: none; + -webkit-user-select: none; + user-select: none; + z-index: 100; + height: 5px; + display: block; + padding: 0; + margin: 0; + position: relative; + line-height: 0; +} + +.handle-row { + width: 15px; + top: 50%; + left: -2px; + transform: translateX(-50%) rotate(270deg); + cursor: col-resize; +} + +.handle-column { + height: 15px; + left: 50%; + top: -4px; + cursor: row-resize; +} + +.c2r1_header, .c2r2_header, .c1r1_header { + background: #13141b; + padding: 10px; + height: 50px; + margin: -8px -8px -9px -9px; + font-size: 1.2em; + font-weight: bold; + color: #ffdb86; +} + +.c1r1 { + background: #3949ab; + padding: 10px; +} + +.c1r1_header { + background: #2c3c7a; +} + +.c2r2_header { + margin-top: -9px; +} + +.c1r1_header { + margin: -7px -9px -8px -9px; +} + +.c2r1_body { + background: #009688; + padding: 10px; +} + +.c2r1_header { + background: #00695d; +} + +.c2r2 { + background: #9c27b0; + padding: 10px; +} + +.c2r2_header { + background: #5f1b6d; +} + +.c1r1 > *, .c2r1_header, .c2r1_body > *, .c2r2 > * { + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/src/universal-app/app/responsive-app.ts b/src/universal-app/app/responsive-app.ts new file mode 100644 index 000000000..044d43809 --- /dev/null +++ b/src/universal-app/app/responsive-app.ts @@ -0,0 +1,81 @@ +import {Component, NgModule} from '@angular/core'; +import {ServerModule} from '@angular/platform-server'; +import {BrowserModule} from '@angular/platform-browser'; +import {FlexLayoutModule} from '@angular/flex-layout'; + +import {SplitModule} from './splitter/split.module'; + + +@Component({ + selector: 'responsive-app', + styleUrls: ['./responsive-app.css'], + template: ` +
+
+
+
Column #1 - Row #1
+
    +
  • 2 Columns: 30% + 70%
  • +
  • 2nd Column: 2 rows
  • +
  • 2nd Column: 50% + 50%
  • +
+
+
+ +
+
+
+
+
Column #2 - Row #1
+

Layout Dashboard

+

+ Demonstrate use of ngxSplit with the Flex-Layout API + and flexbox css layouts. +

+ Haxx0r ipsum cd ctl-c Starcraft concurrently salt unix baz class bar linux + January 1, 1970 syn for mutex daemon todo mountain dew recursively. Mainframe + wannabee machine code hack the mainframe do void python bin big-endian break + tcp ddos emacs public frack.Over clock headers data private *.* pwned + fork script kiddies. +

+
+
+ +
+
+
Column #2 - Row #2
+
    +
  • List Item #1
  • +
  • List Item #2
  • +
  • List Item #3
  • +
+
+
+
+
+
+ `, +}) +export class ResponsiveApp { +} + + +@NgModule({ + imports: [ + BrowserModule.withServerTransition({appId: 'responsive-app'}), + FlexLayoutModule, + SplitModule + ], + bootstrap: [ResponsiveApp], + declarations: [ResponsiveApp], +}) +export class ResponsiveAppClientModule { +} + + +@NgModule({ + imports: [ResponsiveAppClientModule, ServerModule], + bootstrap: [ResponsiveApp], +}) +export class ResponsiveAppServerModule { +} diff --git a/src/universal-app/app/splitter/split-area.directive.ts b/src/universal-app/app/splitter/split-area.directive.ts new file mode 100644 index 000000000..d8654cbdf --- /dev/null +++ b/src/universal-app/app/splitter/split-area.directive.ts @@ -0,0 +1,12 @@ +import { Directive, Optional, Self } from '@angular/core'; +import { FlexDirective } from '@angular/flex-layout'; + +@Directive({ + selector: '[ngxSplitArea]', + host: { + style: 'overflow: auto;' + } +}) +export class SplitAreaDirective { + constructor(@Optional() @Self() public flex: FlexDirective) { } +} diff --git a/src/universal-app/app/splitter/split-handle.directive.ts b/src/universal-app/app/splitter/split-handle.directive.ts new file mode 100644 index 000000000..0155bb6b3 --- /dev/null +++ b/src/universal-app/app/splitter/split-handle.directive.ts @@ -0,0 +1,38 @@ +import {Directive, ElementRef, Inject, Output} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; +import { isBrowser } from '../util/helper'; + +import {Observable} from 'rxjs/Observable'; + +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/map'; + +@Directive({ + selector: '[ngxSplitHandle]', + host: { + class: 'ngx-split-handle', + title: 'Drag to resize' + } +}) +export class SplitHandleDirective { + + @Output() drag: Observable<{ x: number, y: number }>; + + constructor(ref: ElementRef, @Inject(DOCUMENT) _document: any) { + const getMouseEventPosition = (event: MouseEvent) => ({x: event.movementX, y: event.movementY}); + + if ( isBrowser() ) { + /* tslint:disable */ + const mousedown$ = Observable.fromEvent(ref.nativeElement, 'mousedown').map(getMouseEventPosition); + const mousemove$ = Observable.fromEvent(_document, 'mousemove').map(getMouseEventPosition); + const mouseup$ = Observable.fromEvent(_document, 'mouseup').map(getMouseEventPosition); + + /* tslint:enable*/ + this.drag = mousedown$.switchMap(_ => mousemove$.takeUntil(mouseup$)); + } + + } + +} diff --git a/src/universal-app/app/splitter/split.component.scss b/src/universal-app/app/splitter/split.component.scss new file mode 100644 index 000000000..c2292261f --- /dev/null +++ b/src/universal-app/app/splitter/split.component.scss @@ -0,0 +1,39 @@ +.ngx-split { + &.row-split { + > .ngx-split-handle .ngx-split-button { + top: 50%; + left: 50%; + cursor: col-resize; + transform: translate(-50%, -50%); + } + } + + &.column-split { + > .ngx-split-handle .ngx-split-button { + left: 50%; + cursor: row-resize; + top: -3px; + transform: translateX(-50%) rotate(270deg); + } + } + + .ngx-split-area { + overflow: auto; + } + + .ngx-split-handle { + position: relative; + + .ngx-split-button { + line-height: 0; + font-size: 32px; + position: absolute; + display: block; + padding: 0; + } + } +} + +.icon-split-handle::before { + content: ''\f1aa''; +} diff --git a/src/universal-app/app/splitter/split.directive.ts b/src/universal-app/app/splitter/split.directive.ts new file mode 100644 index 000000000..e4d766bdb --- /dev/null +++ b/src/universal-app/app/splitter/split.directive.ts @@ -0,0 +1,86 @@ +import { + Directive, Input, ContentChild, + ContentChildren, AfterContentInit, QueryList, ElementRef, OnDestroy +} from '@angular/core'; + +import {SplitAreaDirective} from './split-area.directive'; +import {SplitHandleDirective} from './split-handle.directive'; +import {FlexDirective} from '@angular/flex-layout'; +import {Subscription} from 'rxjs/Subscription'; + +import { isBrowser } from '../util/helper'; + +@Directive({ + selector: '[ngxSplit]', + host: { + class: 'ngx-split' + } +}) +export class SplitDirective implements AfterContentInit, OnDestroy { + watcher: Subscription; + + @Input('ngxSplit') + direction: string = 'row'; + + @ContentChild(SplitHandleDirective) handle: SplitHandleDirective; + @ContentChildren(SplitAreaDirective) areas: QueryList; + + constructor(private elementRef: ElementRef) { + } + + ngAfterContentInit(): void { + if (isBrowser()) { + this.watcher = this.handle.drag.subscribe(pos => this.onDrag(pos)); + } + } + + ngOnDestroy() { + if (this.watcher) { + this.watcher.unsubscribe(); + } + } + + /** + * While dragging, continually update the `flex.activatedValue` for each area + * managed by the splitter. + */ + onDrag({x, y}): void { + const dragAmount = (this.direction === 'row') ? x : y; + + this.areas.forEach((area, i) => { + // get the cur flex and the % in px + const flex = (area.flex as FlexDirective); + const delta = (i === 0) ? dragAmount : -dragAmount; + const currentValue = flex.activatedValue; + + // Update Flex-Layout value to build/inject new flexbox CSS + flex.activatedValue = this.calculateSize(currentValue, delta); + }); + } + + /** + * Use the pixel delta change to recalculate the area size (%) + * Note: flex value may be '', %, px, or ' ' + */ + calculateSize(value, delta) { + const containerSizePx = this.elementRef.nativeElement.clientWidth; + const elementSizePx = Math.round(this.valueToPixel(value, containerSizePx)); + + const elementSize = ((elementSizePx + delta) / containerSizePx) * 100; + return Math.round(elementSize * 100) / 100; + } + + /** + * Convert the pixel or percentage value to a raw + * pixel float value. + */ + valueToPixel(value: string | number, parentWidth: number): number { + let isPercent = () => String(value).indexOf('px') < 0; + let size = parseFloat(String(value)); + if (isPercent()) { + size = parentWidth * (size / 100); // Convert percentage to actual pixel float value + } + return size; + } + +} diff --git a/src/universal-app/app/splitter/split.module.ts b/src/universal-app/app/splitter/split.module.ts new file mode 100644 index 000000000..74fd60dac --- /dev/null +++ b/src/universal-app/app/splitter/split.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FlexLayoutModule } from '@angular/flex-layout'; + +import { SplitDirective } from './split.directive'; +import { SplitAreaDirective } from './split-area.directive'; +import { SplitHandleDirective } from './split-handle.directive'; + +@NgModule({ + imports: [CommonModule, FlexLayoutModule], + declarations: [SplitDirective, SplitAreaDirective, SplitHandleDirective], + exports: [SplitDirective, SplitAreaDirective, SplitHandleDirective] +}) +export class SplitModule { } diff --git a/src/universal-app/app/util/helper.ts b/src/universal-app/app/util/helper.ts new file mode 100644 index 000000000..e8e9c29ef --- /dev/null +++ b/src/universal-app/app/util/helper.ts @@ -0,0 +1,5 @@ +import {ɵgetDOM as getDom} from '@angular/platform-browser'; + +export function isBrowser() { + return getDom().supportsDOMEvents(); +} diff --git a/src/universal-app/index.html b/src/universal-app/index.html new file mode 100644 index 000000000..dd2fb782d --- /dev/null +++ b/src/universal-app/index.html @@ -0,0 +1,11 @@ + + + + + Angular Flex-Layout Universal Test + + + + Loading... + + diff --git a/src/universal-app/main.ts b/src/universal-app/main.ts new file mode 100644 index 000000000..a16a8d973 --- /dev/null +++ b/src/universal-app/main.ts @@ -0,0 +1 @@ +export * from './app/responsive-app'; diff --git a/src/universal-app/prerender.ts b/src/universal-app/prerender.ts new file mode 100644 index 000000000..0d690772c --- /dev/null +++ b/src/universal-app/prerender.ts @@ -0,0 +1,22 @@ +import 'reflect-metadata'; +import 'zone.js'; + +import {enableProdMode} from '@angular/core'; +import {renderModuleFactory} from '@angular/platform-server'; +import {join} from 'path'; +import {readFileSync} from 'fs-extra'; +import {log} from 'gulp-util'; +import {ResponsiveAppServerModuleNgFactory} from './app/responsive-app.ngfactory'; + +enableProdMode(); + +const result = renderModuleFactory(ResponsiveAppServerModuleNgFactory, { + document: readFileSync(join(__dirname, 'index.html'), 'utf-8') +}); + +result + .then(() => log('Prerender done.')) + // If rendering the module factory fails, exit the process with an error code because otherwise + // the CI task will not recognize the failure and will show as "success". The error message + // will be printed automatically by the `renderModuleFactory` method. + .catch(() => process.exit(1)); diff --git a/src/universal-app/tsconfig-build.json b/src/universal-app/tsconfig-build.json new file mode 100644 index 000000000..fce1e8acf --- /dev/null +++ b/src/universal-app/tsconfig-build.json @@ -0,0 +1,38 @@ +// TypeScript config file that is used to compile the Universal App. All sources are compiled +// inside of the output folder and therefore all paths can be relative to the output folder. +{ + "compilerOptions": { + "declaration": true, + "stripInternal": false, + "experimentalDecorators": true, + "noUnusedParameters": true, + "strictNullChecks": true, + "module": "commonjs", + "moduleResolution": "node", + "outDir": ".", + "rootDir": ".", + "sourceMap": true, + "target": "es2015", + "lib": [ + "es2015", + "dom" + ], + "skipLibCheck": true, + "types": [], + "baseUrl": ".", + "typeRoots": [ + "../../node_modules/@types/!(node)" + ], + "paths": { + "@angular/flex-layout": [ + "./flex-layout" + ] + } + }, + "files": [ + "main.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true + } +} diff --git a/src/universal-app/tsconfig-prerender.json b/src/universal-app/tsconfig-prerender.json new file mode 100644 index 000000000..a92958102 --- /dev/null +++ b/src/universal-app/tsconfig-prerender.json @@ -0,0 +1,30 @@ +// TypeScript config file that is used to compile the files that prerender the Universal app. +{ + "compilerOptions": { + "declaration": false, + "stripInternal": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noUnusedParameters": false, + "strictNullChecks": false, + "module": "commonjs", + "moduleResolution": "node", + "outDir": ".", + "rootDir": ".", + "sourceMap": true, + "target": "es5", + "lib": ["es2015", "dom"], + "skipLibCheck": true, + "types": [], + "typeRoots": [ + "../../node_modules/@types/!(node)" + ], + "baseUrl": ".", + "paths": { + "@angular/flex-layout": ["./flex-layout"] + } + }, + "files": [ + "prerender.ts" + ] +} diff --git a/test/karma-test-shim.js b/test/karma-test-shim.js index 08ea3597c..4052f92a2 100644 --- a/test/karma-test-shim.js +++ b/test/karma-test-shim.js @@ -45,6 +45,7 @@ System.config({ '@angular/platform-browser-dynamic/testing': 'node:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', '@angular/material': 'node:@angular/material/bundles/material.umd.js', + '@angular/material': 'node:@angular/material/bundles/material.umd.js', '@angular/cdk': 'node:@angular/cdk/bundles/cdk.umd.js', // Path mappings for local packages that can be imported inside of tests. diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index 831886221..979c4115e 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -3,13 +3,14 @@ import {createPackageBuildTasks} from 'lib-build-tools'; /** Create gulp tasks to build the different packages in the project. */ createPackageBuildTasks('flex-layout'); -import './tasks/ci'; +import './tasks/aot'; +import './tasks/build-release'; // release build `github.com/angular/flex-layout-builds` import './tasks/clean'; +import './tasks/ci'; import './tasks/default'; import './tasks/development'; import './tasks/lint'; +import './tasks/publish'; // publish release to npm import './tasks/unit-test'; -import './tasks/aot'; +import './tasks/universal'; import './tasks/validate-release'; -import './tasks/build-release'; // release build `github.com/angular/flex-layout-builds` -import './tasks/publish'; // publish release to npm diff --git a/tools/gulp/tasks/ci.ts b/tools/gulp/tasks/ci.ts index 1aeeed016..95d531d5b 100644 --- a/tools/gulp/tasks/ci.ts +++ b/tools/gulp/tasks/ci.ts @@ -10,13 +10,11 @@ task('ci:test', ['test:single-run'], () => process.exit(0)); task('ci:aot', ['aot:build']); /** Task that verifies if all Flex-layout components are working with platform-server.*/ -task('ci:prerender', ['universal:test-prerender']); +task('ci:prerender', ['prerender']); /** Task which reports the size of the library and stores it in a database. */ task('ci:payload', ['payload']); -/** Task that uploads the coverage results to a firebase database. */ -task('ci:coverage', ['coverage:upload']); -task('ci:e2e', ['e2e']); + diff --git a/tools/gulp/tasks/universal.ts b/tools/gulp/tasks/universal.ts new file mode 100644 index 000000000..450a59244 --- /dev/null +++ b/tools/gulp/tasks/universal.ts @@ -0,0 +1,56 @@ +import {task} from 'gulp'; +import {ngcBuildTask, tsBuildTask, copyTask, execTask} from '../util/task_helpers'; +import {join} from 'path'; +import {copySync} from 'fs-extra'; +import {buildConfig, sequenceTask} from 'lib-build-tools'; + +const {outputDir, packagesDir} = buildConfig; + +/** Path to the directory where all releases are created. */ +const releasesDir = join(outputDir, 'releases'); + +const appDir = join(packagesDir, 'universal-app'); +const outDir = join(outputDir, 'packages', 'universal-app'); + +// Paths to the different tsconfig files of the Universal app. +// Building the sources in the output directory is part of the workaround for +// https://github.com/angular/angular/issues/12249 +const tsconfigAppPath = join(outDir, 'tsconfig-build.json'); +const tsconfigPrerenderPath = join(outDir, 'tsconfig-prerender.json'); + +/** Path to the compiled prerender file. Running this file just dumps the HTML output for now. */ +const prerenderOutFile = join(outDir, 'prerender.js'); + +/** Task that builds the universal-app and runs the prerender script. */ +task('prerender', ['universal:build'], execTask( + // Runs node with the tsconfig-paths module to alias the @angular/flex-layout dependency. + 'node', ['-r', 'tsconfig-paths/register', prerenderOutFile], { + env: {TS_NODE_PROJECT: tsconfigPrerenderPath}, + // Errors in lifecycle hooks will write to STDERR, but won't exit the process with an + // error code, however we still want to catch those cases in the CI. + failOnStderr: true + } +)); + +task('universal:build', sequenceTask( + 'clean', + ['flex-layout:build-release'], + ['universal:copy-release', 'universal:copy-files'], + 'universal:build-app-ts', + 'universal:build-prerender-ts' +)); + +/** Task that builds the universal app in the output directory. */ +task('universal:build-app-ts', ngcBuildTask(tsconfigAppPath)); + +/** Task that copies all files to the output directory. */ +task('universal:copy-files', copyTask(appDir, outDir)); + +/** Task that builds the prerender script in the output directory. */ +task('universal:build-prerender-ts', tsBuildTask(tsconfigPrerenderPath)); + +// As a workaround for https://github.com/angular/angular/issues/12249, we need to +// copy the Flex-Layout and CDK ESM output inside of the universal-app output. +task('universal:copy-release', () => { + copySync(join(releasesDir, 'flex-layout'), join(outDir, 'flex-layout')); +});