import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<split-container>
<div class="navigation" split-behaviour="fixed">
Navigation<br>
Navigation<br>
Navigation<br>
Navigation<br>
Navigation<br>
Navigation<br>
Navigation<br>
Navigation<br>
Navigation<br>
Navigation<br>
</div>
<div class="content" split-behaviour="dynamic">
Content<br>
Content<br>
Content<br>
Content<br>
Content<br>
Content<br>
Content<br>
Content<br>
Content<br>
Content<br>
</div>
</split-container>
`,
})
export class AppComponent { name = 'Angular'; }
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { SplitContainerComponent } from './splitContainer.component';
import { SplitBehaviourDirective } from './splitBehaviour.directive';
import { SplitterComponent } from './splitter.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [
AppComponent,
SplitBehaviourDirective,
SplitterComponent,
SplitContainerComponent
],
entryComponents: [
SplitterComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
<!DOCTYPE html>
<html>
<head>
<title>Angular Quickstart</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
.navigation {
overflow:auto;
color:#fff;
background-color:#404040;
padding:4px;
border: 1px solid black;
}
.content {
color:#000;
background-color:#f0f0f0;
padding:4px 4px 4px 20px;
border: 1px solid black;
margin-top: 25px;
}
.splitter {
background-color:#d0d0d0;
}
</style>
<!-- Polyfills -->
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>
<script src="https://unpkg.com/zone.js@0.7.4?main=browser"></script>
<script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading AppComponent content here ...</my-app>
</body>
</html>
(function (global) {
System.config({
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
transpiler: 'ts',
typescriptOptions: {
// Copy of compiler options in standard tsconfig.json
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2015", "dom"],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
meta: {
'typescript': {
"exports": "ts"
}
},
paths: {
// paths serve as alias
'npm:': 'https://unpkg.com/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.0.10/lib/typescript.js',
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
import { Component, EventEmitter, Output, ViewChild, ElementRef,HostListener, NgZone } from '@angular/core';
import { SplitBehaviourDirective, Position } from './splitBehaviour.directive';
/**
* A horizontal, draggable divider element between resizable content areas within a split container.
* This component and its creation is managed by a split container.
**/
@Component({
selector: 'splitter',
template: `
<div #splitter class="splitter"
></div>
`,
host: { 'style': 'position:relative' },
styles: [`
.splitter {
flex: 0 0 auto;
width: 100%;
height: 25px;
cursor: row-resize;
background-position:50% 50%;
background-repeat:no-repeat;
/* Needed for height:100% without having an explicit height given to the parent */
position:absolute;
}
`]
})
export class SplitterComponent {
private startX: number;
private startWidth: number;
private dragging: boolean;
@ViewChild('splitter')
private element: ElementRef;
private _splitBehaviour: SplitBehaviourDirective;
@Output()
public positionChanged: EventEmitter<Position> = new EventEmitter();
constructor(private zone: NgZone) {
this.dragging = false;
}
public set splitBehaviour(value: SplitBehaviourDirective) {
this._splitBehaviour = value;
}
public get splitBehaviour() {
return this._splitBehaviour;
}
@HostListener('mousedown', ['$event'])
@HostListener('touchstart', ['$event'])
private onMouseDown(e: MouseEvent): void {
if (((<any>window).TouchEvent && e instanceof TouchEvent) || (e.touches || e.changedTouches)) {
e = e.touches.length > 0 ? e.touches[0] : e.changedTouches[0];
}
this.dragging = true;
this.startY = e.clientY;
this.startWidth = this.splitBehaviour.getElementHeight();
this.zone.runOutsideAngular(() => {
window.document.addEventListener('mousemove', this.onMouseMove.bind(this));
window.document.addEventListener('touchmove', this.onMouseMove.bind(this));
});
}
@HostListener('document:mouseup', ['$event'])
@HostListener('document:touchend', ['$event'])
private onMouseUp(e): void {
this.dragging = false;
}
private onMouseMove(e: MouseEvent): void {
if (((<any>window).TouchEvent && e instanceof TouchEvent) || (e.touches || e.changedTouches)) {
e = e.touches.length > 0 ? e.touches[0] : e.changedTouches[0];
}
if(this.dragging) {
this.positionChanged.emit(new Position(this.startWidth + e.clientY - this.startY, e.pageX));
}
}
private onMouseLeave(e: MouseEvent): void {
this.dragging = false;
}
}
import { Directive, ElementRef, Renderer, Input, OnInit } from '@angular/core';
export class Position {
constructor(public x: number, public y: number) {
}
}
export enum SplitBehaviour {
fixed,
dynamic
}
/**
* Marks an element as content area inside a resizable split container.
* The input value of the directive can either be 'fixed' or 'dynamic' and describes the way
* the content area is expected to resize within the split container space.
* - A fixed element is expected to have an explicitly defined size, which can be resized by the user through a split element
* - A dynamic elment is expected to fill up the space next to fixed elements.
*
* Initially, the directive will set CSS flexbox attributes to stack them horizontally.
* It also serves as a data provider to expose the element width of the hosting content area, which is most likely a div.
* Last but not least, the directive takes care of resizing the host element.
*
* @example
* <div style="width:100px" split-behaviour="fixed">
* </div>
**/
@Directive({
selector: '[split-behaviour]'
})
export class SplitBehaviourDirective implements OnInit {
private _behaviour: SplitBehaviour;
constructor(private el: ElementRef, private renderer: Renderer) {
}
@Input('split-behaviour')
public set behaviour(value: string) {
this._behaviour = SplitBehaviour[value];
}
public get behaviour(): string {
return SplitBehaviour[this._behaviour];
}
public resize(vector: Position) {
this.renderer.setElementStyle(this.el.nativeElement, 'height', `${vector.x}px`);
}
public getElementHeight(): number {
let paddingT = parseInt(window.getComputedStyle(this.el.nativeElement, null).getPropertyValue("padding-top"));
let paddingB = parseInt(window.getComputedStyle(this.el.nativeElement, null).getPropertyValue("padding-bottom"));
return <number>this.el.nativeElement.offsetHeight - paddingT - paddingB;
}
public ngOnInit() {
if(this._behaviour.valueOf() == SplitBehaviour.fixed.valueOf()) {
this.renderer.setElementStyle(this.el.nativeElement, 'flex', '0 0 auto');
}
else if(this._behaviour.valueOf() == SplitBehaviour.dynamic.valueOf()) {
this.renderer.setElementStyle(this.el.nativeElement, 'flex', '1 1 auto');
}
this.renderer.setElementStyle(this.el.nativeElement, 'overflow', 'auto');
}
}
import { Component, ContentChildren, QueryList, ComponentFactoryResolver, ViewContainerRef, AfterContentInit } from '@angular/core';
import { SplitterComponent } from './splitter.component';
import { SplitBehaviourDirective, SplitBehaviour, Position } from './splitBehaviour.directive';
/**
* Hosts resizable content areas divided by a draggable border (splitter).
*
* The split container defined flex attributes to allow the horizontal arrangement of child content areas.
* On initialization, it will query all child elements in the light DOM annotated by the split-behaviour directive
* and separate them by splitters.
* As the splitBehaviour directive manages concrete content area resizing, dragging events of the splitter (positionChanged) are subscribed
* and propagated to the directive.
**/
@Component({
selector: 'split-container',
template: `
<div class="split-container">
<ng-content></ng-content>
</div>
`,
styles: [`
.split-container {
display: flex;
flex-direction: column;
flex-wrap: no-wrap;
flex-grow: 1;
height:500px;
}
`]
})
export class SplitContainerComponent implements AfterContentInit {
// Workaround: We want to query all child elements hosting a SplitBehaviourDirective instance,
// but we both need the respective ViewContainerRef (for splitter element creation)
// as well as the respective Directive implementation instance.
// There might be a better way to achive this...
@ContentChildren(SplitBehaviourDirective, {read: ViewContainerRef})
private panesVcr: QueryList<ViewContainerRef>;
@ContentChildren(SplitBehaviourDirective)
private panes: QueryList<SplitBehaviourDirective>;
constructor(private resolver: ComponentFactoryResolver) {
}
public ngAfterContentInit(): void {
let splitterFactory = this.resolver.resolveComponentFactory(SplitterComponent);
let paneDirectives = this.panes.toArray();
this.panesVcr.map((vcr, idx) => {
if(paneDirectives[idx].behaviour == SplitBehaviour[SplitBehaviour.fixed]) {
let splitter = vcr.createComponent(splitterFactory);
splitter.instance.splitBehaviour = paneDirectives[idx];
splitter.instance.positionChanged.subscribe((pos: Position) => {
paneDirectives[idx].resize(pos);
});
}
});
}
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="2mm"
height="7mm"
viewBox="0 0 7.086614 24.803149"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="split-horizontal.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="11.262352"
inkscape:cy="21.820265"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1005"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.5591)">
<circle
style="opacity:1;fill:#909090;fill-opacity:1;stroke:none;stroke-width:8;stroke-opacity:1"
id="path4136"
cx="3.6556082"
cy="1030.9225"
r="3.0174825" />
<circle
r="3.0174825"
cy="1039.877"
cx="3.6556082"
id="circle4138"
style="opacity:1;fill:#909090;fill-opacity:1;stroke:none;stroke-width:8;stroke-opacity:1" />
<circle
style="opacity:1;fill:#909090;fill-opacity:1;stroke:none;stroke-width:8;stroke-opacity:1"
id="circle4140"
cx="3.6556082"
cy="1048.8313"
r="3.0174825" />
</g>
</svg>