diff --git a/CHANGELOG.md b/CHANGELOG.md index 834140fb71f..a4ee7998ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,21 @@ All notable changes for each version of this project will be documented in this ### New Features +- `igxSplitter` component added. + - Allows rendering a vertical or horizontal splitter with multiple splitter panes with templatable content. + Panes can be resized or collapsed/expanded via the UI. Splitter orientation is defined via the `type` input. + + ```html + + + ... + + + ... + + + ``` + - `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid` - Added ability to pin rows to top or bottom depending on the new `pinning` input. And new API methods `pinRow` and `unpinRow`. diff --git a/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-component.scss new file mode 100644 index 00000000000..75da58e80a8 --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-component.scss @@ -0,0 +1,52 @@ +/// @group components +/// @author Simeon Simeonoff +/// @author Maya Kirova +/// @requires {mixin} bem-block +/// @requires {mixin} bem-elem +/// @requires {mixin} bem-mod +//// + +@include b(igx-splitter) { + // Register the component in the component registry + $this: str-slice(bem--selector-to-string(&), 2, -1); + @include register-component($this); + + @include b(#{$this}-bar) { + @extend %igx-splitter-bar !optional; + + @include e(handle) { + @extend %igx-splitter-handle !optional; + @extend %igx-splitter-handle--horizontal !optional; + } + + @include e(expander, 'start') { + @extend %igx-splitter-expander !optional; + @extend %igx-splitter-expander--start !optional; + } + + @include e(expander, 'end') { + @extend %igx-splitter-expander !optional; + @extend %igx-splitter-expander--end !optional; + } + + @include m('vertical') { + @extend %igx-splitter-bar--vertical !optional; + + @include e(handle) { + @extend %igx-splitter-handle !optional; + @extend %igx-splitter-handle--vertical !optional; + } + + @include e(expander, 'start') { + @extend %igx-splitter-expander !optional; + @extend %igx-splitter-expander--start-vertical !optional; + } + + @include e(expander, 'end') { + @extend %igx-splitter-expander !optional; + @extend %igx-splitter-expander--end-vertical !optional; + } + } + } +} + diff --git a/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-theme.scss new file mode 100644 index 00000000000..b0733eb6575 --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-theme.scss @@ -0,0 +1,206 @@ +//// +/// @group themes +@function igx-splitter-theme( + $palette: $default-palette, + $schema: $light-schema, + $elevations: $elevations, + + $bar-color: null, + $handle-color: null, + $expander-color: null, + $border-radius: null, + $size: null +) { + $name: 'igx-splitter'; + $splitter-schema: (); + + @if map-has-key($schema, $name) { + $splitter-schema: map-get($schema, $name); + } @else { + $splitter-schema: $schema; + } + + $border-radius-handle: round-borders( + if($border-radius, $border-radius, map-get($splitter-schema, 'border-radius')), 0, 2px + ); + + $theme: apply-palette($splitter-schema, $palette); + + @if not($handle-color) and $bar-color { + $handle-color: text-contrast($bar-color); + } + + @if not($expander-color) and $bar-color { + $expander-color: text-contrast($bar-color); + } + + @return extend($theme, ( + name: $name, + palette: $palette, + bar-color: $bar-color, + handle-color: $handle-color, + expander-color: $expander-color, + border-radius: $border-radius-handle, + size: $size + )); +} + +/// @param {Map} $theme - The theme used to style the component. +/// @requires {mixin} igx-root-css-vars +/// @requires rem +/// @requires --var +@mixin igx-splitter($theme) { + @include igx-root-css-vars($theme); + $splitter-color: --var($theme, 'bar-color'); + $hitbox-size: 4px; + $debug-hitbox: false; + $hitbox-debug-color: rgba(coral, .24); + + %handle-area { + position: absolute; + content: ''; + width: 100%; + height: $hitbox-size; + background: if($debug-hitbox, $hitbox-debug-color, transparent); + } + + %handle-area--vertical { + width: $hitbox-size; + height: 100%; + } + + %igx-splitter-bar { + position: relative; + display: flex; + flex-grow: 1; + justify-content: center; + align-items: center; + background: $splitter-color; + border: 1px solid $splitter-color; + cursor: row-resize; + z-index: 99; + opacity: .68; + transition: opacity .15s $ease-out-quad !important; + + &::before { + @extend %handle-area; + top: 100%; + } + + &::after { + @extend %handle-area; + bottom: 100%; + } + + &:hover { + transition: all .25s ease-out; + opacity: 1; + } + } + + %igx-splitter-bar--vertical { + flex-direction: column; + height: 100%; + cursor: col-resize; + + &::before { + @extend %handle-area--vertical; + top: 0; + right: 100%; + } + + &::after { + @extend %handle-area--vertical; + top: 0; + left: 100%; + } + } + + %igx-splitter-handle { + background: --var($theme, 'handle-color'); + border-radius: --var($theme, 'border-radius'); + } + + %igx-splitter-handle--horizontal { + width: 25%; + height: --var($theme, 'size'); + margin: 0 rem(48px); + } + + %igx-splitter-handle--vertical { + width: --var($theme, 'size'); + height: 25%; + margin: rem(48px) 0; + } + + %igx-splitter-hitbox { + position: absolute; + content: ''; + background: if($debug-hitbox, $hitbox-debug-color, transparent); + } + + %igx-splitter-expander { + position: relative; + width: 0; + height: 0; + border-right: --var($theme, 'size') solid transparent; + border-left: --var($theme, 'size') solid transparent; + cursor: pointer; + z-index: 1; + } + + %igx-splitter-expander--start { + border-bottom: --var($theme, 'size') solid --var($theme, 'expander-color'); + + &::before { + @extend %igx-splitter-hitbox; + top: calc(100% - #{map-get($theme, 'size')}); + left: calc(100% - #{map-get($theme, 'size') * 2}); + width: #{map-get($theme, 'size') * 4}; + height: #{map-get($theme, 'size') * 3}; + } + } + + %igx-splitter-expander--end { + border-bottom: unset; + border-top: --var($theme, 'size') solid --var($theme, 'expander-color'); + + &::before { + @extend %igx-splitter-hitbox; + top: calc(100% - #{map-get($theme, 'size') * 2}); + left: calc(100% - #{map-get($theme, 'size') * 2}); + width: #{map-get($theme, 'size') * 4}; + height: #{map-get($theme, 'size') * 3}; + } + } + + %igx-splitter-expander--start-vertical { + border-top: --var($theme, 'size') solid transparent; + border-right: --var($theme, 'size') solid --var($theme, 'expander-color'); + border-bottom: --var($theme, 'size') solid transparent; + border-left: unset; + + &::before { + @extend %igx-splitter-hitbox; + top: calc(100% - #{map-get($theme, 'size') * 2}); + left: calc(100% - #{map-get($theme, 'size')}); + width: #{map-get($theme, 'size') * 3}; + height: #{map-get($theme, 'size') * 4}; + } + } + + %igx-splitter-expander--end-vertical { + border-top: --var($theme, 'size') solid transparent; + border-right: unset; + border-bottom: --var($theme, 'size') solid transparent; + border-left: --var($theme, 'size') solid --var($theme, 'expander-color'); + + &::before { + @extend %igx-splitter-hitbox; + left: calc(100% - #{map-get($theme, 'size') * 2}); + top: calc(100% - #{map-get($theme, 'size') * 2}); + height: #{map-get($theme, 'size') * 4}; + width: #{map-get($theme, 'size') * 3}; + } + } +} diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss b/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss index 8fc3548ad05..8e335642c48 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss @@ -53,6 +53,7 @@ @import '../components/radio/radio-component'; @import '../components/scrollbar/scrollbar-component'; @import '../components/slider/slider-component'; +@import '../components/splitter/splitter-component'; @import '../components/snackbar/snackbar-component'; @import '../components/switch/switch-component'; @import '../components/tabs/tabs-component'; diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss index b8eb967d04d..bdacac534b4 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss @@ -38,6 +38,7 @@ @import '../components/switch/switch-theme'; @import '../components/snackbar/snackbar-theme'; @import '../components/slider/slider-theme'; +@import '../components/splitter/splitter-theme'; @import '../components/ripple/ripple-theme'; @import '../components/radio/radio-theme'; @import '../components/progress/progress-theme'; @@ -198,6 +199,14 @@ )); } + @if not(index($exclude, 'igx-splitter')) { + @include igx-splitter(igx-splitter-theme( + $palette, + $schema, + $border-radius: $roundness, + )); + } + @if not(index($exclude, 'igx-checkbox')) { @include igx-checkbox(igx-checkbox-theme( $palette, diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss index c7cdbec9843..a16012b71f8 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss @@ -38,6 +38,7 @@ @import './scrollbar'; @import './slider'; @import './snackbar'; +@import './splitter'; @import './switch'; @import './tabs'; @import './time-picker'; @@ -82,6 +83,7 @@ /// @property {Map} igx-scrollbar [$_dark-scrollbar] /// @property {Map} igx-slider [$_dark-slider] /// @property {Map} igx-snackbar [$_dark-snackbar] +/// @property {Map} igx-splitter [$_dark-splitter] /// @property {Map} igx-switch [$_dark-switch] /// @property {Map} igx-tabs [$_dark-tabs] /// @property {Map} igx-time-picker [$_dark-time-picker] @@ -124,6 +126,7 @@ $dark-schema: ( igx-scrollbar: $_dark-scrollbar, igx-slider: $_dark-slider, igx-snackbar: $_dark-snackbar, + igx-splitter: $_dark-splitter, igx-switch: $_dark-switch, igx-tabs: $_dark-tabs, igx-time-picker: $_dark-time-picker, @@ -169,6 +172,7 @@ $dark-schema: ( /// @property {map} igx-scrollbar [$_dark-fluent-scrollbar], /// @property {map} igx-slider [$_dark-fluent-slider], /// @property {map} igx-snackbar [$_dark-fluent-snackbar], +/// @property {map} igx-splitter [$_dark-fluent-splitter], /// @property {map} igx-switch [$_dark-fluent-switch], /// @property {map} igx-tabs [$_dark-fluent-tabs], /// @property {map} igx-time-picker [$_dark-fluent-time-picker], @@ -211,6 +215,7 @@ $dark-fluent-schema: ( igx-scrollbar: $_dark-fluent-scrollbar, igx-slider: $_dark-fluent-slider, igx-snackbar: $_dark-fluent-snackbar, + igx-splitter: $_dark-fluent-splitter, igx-switch: $_dark-fluent-switch, igx-tabs: $_dark-fluent-tabs, igx-time-picker: $_dark-fluent-time-picker, @@ -256,6 +261,7 @@ $dark-fluent-schema: ( /// @property {map} igx-scrollbar [$_dark-bootstrap-scrollbar], /// @property {map} igx-slider [$_dark-bootstrap-slider], /// @property {map} igx-snackbar [$_dark-bootstrap-snackbar], +/// @property {map} igx-splitter [$_dark-bootstrap-splitter], /// @property {map} igx-switch [$_dark-bootstrap-switch], /// @property {map} igx-tabs [$_dark-bootstrap-tabs], /// @property {map} igx-time-picker [$_dark-bootstrap-time-picker], @@ -298,6 +304,7 @@ $dark-bootstrap-schema: ( igx-scrollbar: $_dark-bootstrap-scrollbar, igx-slider: $_dark-bootstrap-slider, igx-snackbar: $_dark-bootstrap-snackbar, + igx-splitter: $_dark-bootstrap-splitter, igx-switch: $_dark-bootstrap-switch, igx-tabs: $_dark-bootstrap-tabs, igx-time-picker: $_dark-bootstrap-time-picker, diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_scrollbar.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_scrollbar.scss index 1ae63442f09..daa82c7bf96 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_scrollbar.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_scrollbar.scss @@ -20,11 +20,6 @@ $_dark-scrollbar: extend( igx-color: 'surface', lighten: 20% ), - - track-background: ( - igx-color: 'surface', - lighten: 5% - ), ) ); diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_splitter.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_splitter.scss new file mode 100644 index 00000000000..27f81fe897c --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_splitter.scss @@ -0,0 +1,39 @@ +@import '../light/splitter'; + +//// +/// @group schemas +/// @access private +/// @author Simeon Simeonoff +//// + +/// Generates a dark splitter schema. +/// @type {Map} +/// @requires {function} extend +/// @requires $_light-splitter +$_dark-splitter: extend( + $_light-splitter, + ( + handle-color: ( + igx-color: 'surface', + lighten: 20% + ), + + expander-color: ( + igx-color: 'surface', + lighten: 20% + ), + ) +); + +/// Generates a fluent splitter schema. +/// @type {Map} +/// @requires {function} extend +/// @requires $_fluent-splitter +$_dark-fluent-splitter: extend($_fluent-splitter); + +/// Generates a bootstrap splitter schema. +/// @type {Map} +/// @requires {function} extend +/// @requires $_bootstrap-splitter +$_dark-bootstrap-splitter: extend($_bootstrap-splitter); + diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss index c91b32b9d8a..4f67c8e036e 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss @@ -39,6 +39,7 @@ @import './scrollbar'; @import './slider'; @import './snackbar'; +@import './splitter'; @import './switch'; @import './tabs'; @import './time-picker'; @@ -83,6 +84,7 @@ /// @property {Map} igx-scrollbar [$_light-scrollbar] /// @property {Map} igx-slider [$_light-slider] /// @property {Map} igx-snackbar [$_light-snackbar] +/// @property {Map} igx-splitter [$_light-splitter] /// @property {Map} igx-switch [$_light-switch] /// @property {Map} igx-tabs [$_light-tabs] /// @property {Map} igx-time-picker [$_light-time-picker] @@ -125,6 +127,7 @@ $light-schema: ( igx-scrollbar: $_light-scrollbar, igx-slider: $_light-slider, igx-snackbar: $_light-snackbar, + igx-splitter: $_light-splitter, igx-switch: $_light-switch, igx-tabs: $_light-tabs, igx-time-picker: $_light-time-picker, @@ -169,6 +172,7 @@ $light-fluent-schema: ( igx-scrollbar: $_fluent-scrollbar, igx-slider: $_fluent-slider, igx-snackbar: $_fluent-snackbar, + igx-splitter: $_fluent-splitter, igx-switch: $_fluent-switch, igx-tabs: $_fluent-tabs, igx-time-picker: $_fluent-time-picker, @@ -213,6 +217,7 @@ $light-bootstrap-schema: ( igx-scrollbar: $_bootstrap-scrollbar, igx-slider: $_bootstrap-slider, igx-snackbar: $_bootstrap-snackbar, + igx-splitter: $_bootstrap-splitter, igx-switch: $_bootstrap-switch, igx-tabs: $_bootstrap-tabs, igx-time-picker: $_bootstrap-time-picker, diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_splitter.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_splitter.scss new file mode 100644 index 00000000000..08b0b2e9777 --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_splitter.scss @@ -0,0 +1,57 @@ +@import '../shape/splitter'; + +//// +/// @group schemas +/// @access private +/// @author Simeon Simeonoff +//// + +/// Generates a light splitter schema. +/// @type {Map} +/// @property {Map} icon-background [igx-color: ('grays', 400)]- The background color used for splitters of type icon. +/// @property {Map} icon-color [igx-color: ('grays', 800)] - The icon color used for splitters of type icon. +/// @property {Map} initials-background [igx-color: ('grays', 400)] - The background color used for splitters of type initials. +/// @property {Map} initials-color [igx-color: ('grays', 800)] - The text color used for splitters of type initials. +/// @property {Color} image-background [transparent] - The background color used for splitters of type image. +/// @see $default-palette +$_light-splitter: extend( + $_default-shape-splitter, + ( + bar-color: ( + igx-color: 'surface', + darken: 5% + ), + + handle-color: ( + igx-color: 'surface', + darken: 20% + ), + + expander-color: ( + igx-color: 'surface', + darken: 20% + ), + + size: 4px + ) +); + +/// Generates a fluent splitter schema. +/// @type {Map} +/// @requires {function} extend +/// @requires $_light-splitter +$_fluent-splitter: extend($_light-splitter); + +/// Generates a bootstrap splitter schema. +/// @type {Map} +/// @requires {function} extend +/// @requires $_light-splitter +/// @requires $_bootstrap-shape-splitter +$_bootstrap-splitter: extend( + $_light-splitter, + $_bootstrap-shape-splitter, + ( + variant: 'bootstrap', + ) +); + diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_splitter.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_splitter.scss new file mode 100644 index 00000000000..6d82113c37e --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_splitter.scss @@ -0,0 +1,23 @@ +//// +/// @group schemas +/// @access private +/// @author Simeon Simeonoff +//// +$_default-shape-splitter: ( + border-radius: 1, +); + +/// @type Map +$_round-shape-splitter: ( + border-radius: 1, +); + +/// @type Map +$_square-shape-splitter: ( + border-radius: 0, +); + +/// @type Map +$_bootstrap-shape-splitter: ( + border-radius: 1, +); diff --git a/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.html b/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.html new file mode 100644 index 00000000000..3cda47ff07c --- /dev/null +++ b/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.html @@ -0,0 +1,12 @@ +
+
+
+
+
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.ts b/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.ts new file mode 100644 index 00000000000..f730f0ea018 --- /dev/null +++ b/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.ts @@ -0,0 +1,214 @@ +import { Component, Input, HostBinding, EventEmitter, Output, HostListener } from '@angular/core'; +import { SplitterType } from '../splitter.component'; +import { IgxSplitterPaneComponent } from '../splitter-pane/splitter-pane.component'; +import { IDragMoveEventArgs, IDragStartEventArgs, DragDirection } from '../../directives/drag-drop/drag-drop.directive'; + + +export const SPLITTER_INTERACTION_KEYS = new Set('right down left up arrowright arrowdown arrowleft arrowup'.split(' ')); + +/** + * Provides reference to `SplitBarComponent` component. + * Represents the draggable gripper that visually separates panes and allows for changing their sizes. + * @export + * @class SplitBarComponent + */ +@Component({ + selector: 'igx-splitter-bar', + templateUrl: './splitter-bar.component.html' +}) +export class IgxSplitBarComponent { + + /** + * Sets/gets `IgxSplitBarComponent` orientation. + * @type SplitterType + */ + @Input() + public type: SplitterType = SplitterType.Vertical; + + /** + * Sets/gets `IgxSplitBarComponent` element order. + * @type SplitterType + */ + @HostBinding('style.order') + @Input() + public order!: number; + + /** + * @hidden + * @internal + */ + @HostBinding('attr.tabindex') + public tabindex = 0; + + /** + * Sets/gets the `SplitPaneComponent` associated with the current `SplitBarComponent`. + * @memberof SplitBarComponent + */ + @Input() + public pane!: IgxSplitterPaneComponent; + + /** + * Sets/Gets the `SplitPaneComponent` sibling components associated with the current `SplitBarComponent`. + */ + @Input() + public siblings!: Array; + + /** + * An event that is emitted whenever we start dragging the current `SplitBarComponent`. + * @memberof SplitBarComponent + */ + @Output() + public moveStart = new EventEmitter(); + + /** + * An event that is emitted while we are dragging the current `SplitBarComponent`. + * @memberof SplitBarComponent + */ + @Output() + public moving = new EventEmitter(); + + /** + * An event that is emitted when collapsing the pane + */ + @Output() + public togglePane = new EventEmitter(); + /** + * A temporary holder for the pointer coordinates. + * @private + * @memberof SplitBarComponent + */ + private startPoint!: number; + + /** + * @hidden @internal + */ + public get prevButtonHidden() { + return this.siblings[0].hidden && !this.siblings[1].hidden; + } + + /** + * @hidden @internal + */ + @HostListener('keydown', ['$event']) + keyEvent(event: KeyboardEvent) { + const key = event.key.toLowerCase(); + const ctrl = event.ctrlKey; + event.stopPropagation(); + if (SPLITTER_INTERACTION_KEYS.has(key)) { + event.preventDefault(); + } + switch (key) { + case 'arrowup': + case 'up': + if (this.type === SplitterType.Vertical) { + if (ctrl) { + this.onCollapsing(false); + break; + } + if (!this.resizeDisallowed) { + event.preventDefault(); + this.moveStart.emit(this.pane); + this.moving.emit(10); + } + } + break; + case 'arrowdown': + case 'down': + if (this.type === SplitterType.Vertical) { + if (ctrl) { + this.onCollapsing(true); + break; + } + if (!this.resizeDisallowed) { + event.preventDefault(); + this.moveStart.emit(this.pane); + this.moving.emit(-10); + } + } + break; + case 'arrowleft': + case 'left': + if (this.type === SplitterType.Horizontal) { + if (ctrl) { + this.onCollapsing(false); + break; + } + if (!this.resizeDisallowed) { + event.preventDefault(); + this.moveStart.emit(this.pane); + this.moving.emit(10); + } + } + break; + case 'arrowright': + case 'right': + if (this.type === SplitterType.Horizontal) { + if (ctrl) { + this.onCollapsing(true); + break; + } + if (!this.resizeDisallowed) { + event.preventDefault(); + this.moveStart.emit(this.pane); + this.moving.emit(-10); + } + } + break; + default: + break; + } + } + + /** + * @hidden @internal + */ + public get dragDir() { + return this.type === SplitterType.Horizontal ? DragDirection.VERTICAL : DragDirection.HORIZONTAL; + } + + /** + * @hidden @internal + */ + public get nextButtonHidden() { + return this.siblings[1].hidden && !this.siblings[0].hidden; + } + + public onDragStart(event: IDragStartEventArgs) { + if (this.resizeDisallowed) { + event.cancel = true; + return; + } + this.startPoint = this.type === SplitterType.Horizontal ? event.startX : event.startY; + this.moveStart.emit(this.pane); + } + + public onDragMove(event: IDragMoveEventArgs) { + const isHorizontal = this.type === SplitterType.Horizontal; + const curr = isHorizontal ? event.pageX : event.pageY; + const delta = this.startPoint - curr; + if (delta !== 0) { + this.moving.emit(delta); + event.cancel = true; + event.owner.element.nativeElement.style.transform = ''; + } + } + + protected get resizeDisallowed() { + const relatedTabs = this.siblings; + return !!relatedTabs.find(x => x.resizable === false); + } + + public onCollapsing(next: boolean) { + const prevSibling = this.siblings[0]; + const nextSibling = this.siblings[1]; + let target; + if (next) { + // if next is clicked when prev pane is hidden, show prev pane, else hide next pane. + target = prevSibling.hidden ? prevSibling : nextSibling; + } else { + // if prev is clicked when next pane is hidden, show next pane, else hide prev pane. + target = nextSibling.hidden ? nextSibling : prevSibling; + } + this.togglePane.emit(target); + } +} diff --git a/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.html b/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.html new file mode 100644 index 00000000000..95a0b70bdc7 --- /dev/null +++ b/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.ts b/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.ts new file mode 100644 index 00000000000..6921ac02016 --- /dev/null +++ b/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.ts @@ -0,0 +1,131 @@ +import { Component, HostBinding, Input, ElementRef, Output, EventEmitter } from '@angular/core'; + +/** + * Provides reference to `SplitPaneComponent` component. + * Represents individual resizable panes. Users can control the resize behavior via the min and max size properties. + * @export + * @class SplitPaneComponent + */ +@Component({ + selector: 'igx-splitter-pane', + templateUrl: './splitter-pane.component.html' +}) +export class IgxSplitterPaneComponent { + + public _size = 'auto'; + /** + * Sets/gets the size of the current `IgxSplitterPaneComponent`. + */ + @Input() + get size() { + return this._size; + } + + set size(value) { + this._size = value; + this.el.nativeElement.style.flex = this.flex; + this.sizeChange.emit(value); + } + + /** + * @hidden @internal + */ + @Output() + public sizeChange = new EventEmitter(); + + /** + * Sets/gets the minimum allowable size of the current `IgxSplitterPaneComponent`. + */ + @Input() + public minSize!: string; + + /** + * Sets/gets the maximum allowable size of the current `IgxSplitterPaneComponent`. + */ + @Input() + public maxSize!: string; + + /** + * Sets/Gets whether pane is resizable. + */ + @Input() + public resizable = true; + + + /** + * Sets/gets the `order` property of the current `IgxSplitterPaneComponent`. + */ + @Input() + @HostBinding('style.order') + public order!: number; + + /** + * Gets the host native element. + * @readonly + * @type * + */ + public get element(): any { + return this.el.nativeElement; + } + + /** + * Sets/gets the `overflow` property of the current `IgxSplitterPaneComponent`. + */ + @HostBinding('style.overflow') + public overflow = 'auto'; + + /** + * Sets/gets the `minHeight` and `minWidth` propertis of the current `IgxSplitterPaneComponent`. + */ + @HostBinding('style.min-height') + @HostBinding('style.min-width') + public minHeight = 0; + + /** + * Sets/gets the `maxHeight` and `maxWidth` propertis of the current `IgxSplitterPaneComponent`. + */ + @HostBinding('style.max-height') + @HostBinding('style.max-width') + public maxHeight = '100%'; + + /** + * Gets the `flex` property of the current `IgxSplitterPaneComponent`. + * @readonly + */ + @HostBinding('style.flex') + public get flex() { + const grow = this.size !== 'auto' ? 0 : 1; + const shrink = this.size !== 'auto' ? 0 : 1; + + return `${grow} ${shrink} ${this.size}`; + } + + /** + * Sets/gets the 'display' property of the current `IgxSplitterPaneComponent` + */ + @HostBinding('style.display') + public display = 'flex'; + + private _hidden = false; + + /** + * Sets/gets whether current `IgxSplitterPanecomponent` is hidden + */ + @Input() + public set hidden(value) { + this._hidden = value; + this.display = this._hidden ? 'none' : 'flex' ; + } + + public get hidden() { + return this._hidden; + } + + /** + * Event fired when collapsing and changing the hidden state of the current pane + */ + @Output() + public onPaneToggle = new EventEmitter(); + + constructor(private el: ElementRef) { } +} diff --git a/projects/igniteui-angular/src/lib/splitter/splitter.component.html b/projects/igniteui-angular/src/lib/splitter/splitter.component.html new file mode 100644 index 00000000000..087c7895f52 --- /dev/null +++ b/projects/igniteui-angular/src/lib/splitter/splitter.component.html @@ -0,0 +1,11 @@ + + + + + diff --git a/projects/igniteui-angular/src/lib/splitter/splitter.component.spec.ts b/projects/igniteui-angular/src/lib/splitter/splitter.component.spec.ts new file mode 100644 index 00000000000..b43fbf13c88 --- /dev/null +++ b/projects/igniteui-angular/src/lib/splitter/splitter.component.spec.ts @@ -0,0 +1,357 @@ +import { IgxSplitterModule } from './splitter.module'; +import { configureTestSuite } from '../test-utils/configure-suite'; +import { TestBed, async } from '@angular/core/testing'; +import { Component, ViewChild, DebugElement } from '@angular/core'; +import { SplitterType, IgxSplitterComponent } from './splitter.component'; +import { By } from '@angular/platform-browser'; +import { UIInteractions } from '../test-utils/ui-interactions.spec'; + + +const SPLITTERBAR_CLASS = 'igx-splitter-bar'; +const SPLITTERBAR_DIV_CLASS = '.igx-splitter-bar'; +const SPLITTER_BAR_VERTICAL_CLASS = 'igx-splitter-bar--vertical'; + +describe('IgxSplitter', () => { + configureTestSuite(); + beforeAll(async(() => { + + return TestBed.configureTestingModule({ + declarations: [ SplitterTestComponent ], + imports: [ + IgxSplitterModule + ] + }).compileComponents(); + })); + + let fixture, splitter; + beforeEach(async(() => { + fixture = TestBed.createComponent(SplitterTestComponent); + fixture.detectChanges(); + splitter = fixture.componentInstance.splitter; + })); + + it('should render pane content correctly in splitter.', () => { + expect(splitter.panes.length).toBe(2); + const firstPane = splitter.panes.toArray()[0].element; + const secondPane = splitter.panes.toArray()[1].element; + expect(firstPane.textContent.trim()).toBe('Pane 1'); + expect(secondPane.textContent.trim()).toBe('Pane 2'); + + const splitterBar = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).nativeElement; + expect(firstPane.style.order).toBe('0'); + expect(splitterBar.style.order).toBe('1'); + expect(secondPane.style.order).toBe('2'); + }); + + it('should render vertical splitter.', () => { + fixture.componentInstance.type = SplitterType.Vertical; + fixture.detectChanges(); + + const splitterBarDIV = fixture.debugElement.query(By.css(SPLITTERBAR_DIV_CLASS)); + const hasVerticalClass = splitterBarDIV.nativeElement.classList.contains(SPLITTER_BAR_VERTICAL_CLASS); + expect(hasVerticalClass).toBeFalsy(); + }); + it('should render horizontal splitter.', () => { + const splitterBarDIV = fixture.debugElement.query(By.css(SPLITTERBAR_DIV_CLASS)); + const hasVerticalClass = splitterBarDIV.nativeElement.classList.contains(SPLITTER_BAR_VERTICAL_CLASS); + expect(hasVerticalClass).toBeTruthy(); + }); + it('should allow resizing vertical splitter', () => { + fixture.componentInstance.type = SplitterType.Vertical; + fixture.detectChanges(); + const pane1 = splitter.panes.toArray()[0]; + const pane2 = splitter.panes.toArray()[1]; + expect(pane1.size).toBe('auto'); + expect(pane2.size).toBe('auto'); + const pane1_originalSize = pane1.element.offsetHeight; + const pane2_originalSize = pane2.element.offsetHeight; + const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context; + splitterBarComponent.moveStart.emit(pane1); + splitterBarComponent.moving.emit(-100); + fixture.detectChanges(); + expect(pane1.size).toBe(pane1_originalSize + 100 + 'px'); + expect(pane2.size).toBe(pane2_originalSize - 100 + 'px'); + + splitterBarComponent.moving.emit(100); + fixture.detectChanges(); + expect(pane1.size).toBe(pane1_originalSize - 100 + 'px'); + expect(pane2.size).toBe(pane2_originalSize + 100 + 'px'); + }); + it('should allow resizing horizontal splitter', () => { + const pane1 = splitter.panes.toArray()[0]; + const pane2 = splitter.panes.toArray()[1]; + expect(pane1.size).toBe('auto'); + expect(pane2.size).toBe('auto'); + const pane1_originalSize = pane1.element.offsetWidth; + const pane2_originalSize = pane2.element.offsetWidth; + const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context; + splitterBarComponent.moveStart.emit(pane1); + splitterBarComponent.moving.emit(-100); + fixture.detectChanges(); + + expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize + 100, 0); + expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize - 100, 0); + + splitterBarComponent.moving.emit(100); + fixture.detectChanges(); + expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize - 100, 0); + expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize + 100, 0); + }); + it('should honor minSize/maxSize when resizing.', () => { + fixture.componentInstance.type = SplitterType.Vertical; + fixture.detectChanges(); + const pane1 = splitter.panes.toArray()[0]; + const pane2 = splitter.panes.toArray()[1]; + pane1.minSize = '100px'; + pane1.maxSize = '300px'; + fixture.detectChanges(); + + const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context; + splitterBarComponent.moveStart.emit(pane1); + splitterBarComponent.moving.emit(100); + splitterBarComponent.moveStart.emit(pane1); + splitterBarComponent.moving.emit(100); + fixture.detectChanges(); + expect(pane1.size).toBe('100px'); + expect(pane2.size).toBe('300px'); + + splitterBarComponent.moveStart.emit(pane1); + splitterBarComponent.moving.emit(-200); + splitterBarComponent.moveStart.emit(pane1); + splitterBarComponent.moving.emit(-50); + fixture.detectChanges(); + expect(pane1.size).toBe('300px'); + expect(pane2.size).toBe('100px'); + }); + + it('should not allow drag resize if resizable is set to false.', () => { + const pane1 = splitter.panes.toArray()[0]; + pane1.resizable = false; + const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context; + const args = {cancel: false}; + splitterBarComponent.onDragStart(args); + expect(args.cancel).toBeTruthy(); + }); + + it('should allow resizing with up/down arrow keys', () => { + fixture.componentInstance.type = SplitterType.Vertical; + fixture.detectChanges(); + const pane1 = splitter.panes.toArray()[0]; + const pane2 = splitter.panes.toArray()[1]; + expect(pane1.size).toBe('auto'); + expect(pane2.size).toBe('auto'); + const pane1_originalSize = pane1.element.offsetHeight; + const pane2_originalSize = pane2.element.offsetHeight; + const splitterBarComponent: DebugElement = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)); + splitterBarComponent.nativeElement.focus(); + UIInteractions.triggerEventHandlerKeyDown('ArrowUp', splitterBarComponent); + fixture.detectChanges(); + expect(pane1.size).toBe(pane1_originalSize - 10 + 'px'); + expect(pane2.size).toBe(pane2_originalSize + 10 + 'px'); + + UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent); + UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent); + fixture.detectChanges(); + expect(pane1.size).toBe(pane1_originalSize + 10 + 'px'); + expect(pane2.size).toBe(pane2_originalSize - 10 + 'px'); + + pane2.resizable = false; + UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent); + fixture.detectChanges(); + expect(pane1.size).toBe(pane1_originalSize + 10 + 'px'); + expect(pane2.size).toBe(pane2_originalSize - 10 + 'px'); + }); + + it('should allow resizing with left/right arrow keys', () => { + fixture.componentInstance.type = SplitterType.Horizontal; + fixture.detectChanges(); + const pane1 = splitter.panes.toArray()[0]; + const pane2 = splitter.panes.toArray()[1]; + expect(pane1.size).toBe('auto'); + expect(pane2.size).toBe('auto'); + const pane1_originalSize = pane1.element.offsetWidth; + const pane2_originalSize = pane2.element.offsetWidth; + const splitterBarComponent: DebugElement = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)); + splitterBarComponent.nativeElement.focus(); + UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', splitterBarComponent); + fixture.detectChanges(); + expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize - 10, 0); + expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize + 10, 0); + + UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent); + UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent); + fixture.detectChanges(); + expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize + 10, 0); + expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize - 10, 0); + + pane1.resizable = false; + UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent); + fixture.detectChanges(); + expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize + 10, 0); + expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize - 10, 0); + }); + + it('should allow expand/collapse with Ctrl + up/down arrow keys', () => { + fixture.componentInstance.type = SplitterType.Vertical; + fixture.detectChanges(); + const pane1 = splitter.panes.toArray()[0]; + const pane2 = splitter.panes.toArray()[1]; + expect(pane1.size).toBe('auto'); + expect(pane2.size).toBe('auto'); + const splitterBarComponent: DebugElement = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)); + splitterBarComponent.nativeElement.focus(); + UIInteractions.triggerEventHandlerKeyDown('ArrowUp', splitterBarComponent, false, false, true); + fixture.detectChanges(); + expect(pane1.hidden).toBeTruthy(); + UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent, false, false, true); + fixture.detectChanges(); + expect(pane1.hidden).toBeFalsy(); + UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent, false, false, true); + fixture.detectChanges(); + expect(pane2.hidden).toBeTruthy(); + UIInteractions.triggerEventHandlerKeyDown('ArrowUp', splitterBarComponent, false, false, true); + fixture.detectChanges(); + expect(pane2.hidden).toBeFalsy(); + }); + + it('should allow expand/collapse with Ctrl + left/right arrow keys', () => { + fixture.componentInstance.type = SplitterType.Horizontal; + fixture.detectChanges(); + const pane1 = splitter.panes.toArray()[0]; + const pane2 = splitter.panes.toArray()[1]; + expect(pane1.size).toBe('auto'); + expect(pane2.size).toBe('auto'); + const splitterBarComponent: DebugElement = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)); + splitterBarComponent.nativeElement.focus(); + UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', splitterBarComponent, false, false, true); + fixture.detectChanges(); + expect(pane1.hidden).toBeTruthy(); + UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent, false, false, true); + fixture.detectChanges(); + expect(pane1.hidden).toBeFalsy(); + UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent, false, false, true); + fixture.detectChanges(); + expect(pane2.hidden).toBeTruthy(); + UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', splitterBarComponent, false, false, true); + fixture.detectChanges(); + expect(pane2.hidden).toBeFalsy(); + }); + +}); + +describe('IgxSplitter pane toggle', () => { + configureTestSuite(); + beforeAll(async(() => { + + return TestBed.configureTestingModule({ + declarations: [ SplitterTogglePaneComponent ], + imports: [ + IgxSplitterModule + ] + }).compileComponents(); + })); + + let fixture, splitter; + beforeEach(async(() => { + fixture = TestBed.createComponent(SplitterTogglePaneComponent); + fixture.detectChanges(); + splitter = fixture.componentInstance.splitter; + })); + + it('should collapse/expand panes', () => { + const pane1 = splitter.panes.toArray()[0]; + const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context; + + // collapse left sibling pane + splitterBarComponent.onCollapsing(0); + fixture.detectChanges(); + expect(pane1.hidden).toBeTruthy(); + + // expand left sibling pane + splitterBarComponent.onCollapsing(1); + fixture.detectChanges(); + expect(pane1.hidden).toBeFalsy(); + }); + + it('should be able to expand both siblings when they are collapsed', () => { + const panes = splitter.panes.toArray(); + const pane1 = panes[0]; + const pane2 = panes[1]; + const splitterBarComponents = fixture.debugElement.queryAll(By.css(SPLITTERBAR_CLASS)); + const splitterBar1 = splitterBarComponents[0].context; + const splitterBar2 = splitterBarComponents[1].context; + + splitterBar1.onCollapsing(0); + splitterBar2.onCollapsing(0); + fixture.detectChanges(); + + expect(pane1.hidden).toBeTruthy(); + expect(pane2.hidden).toBeTruthy(); + + splitterBar1.onCollapsing(1); + fixture.detectChanges(); + expect(pane1.hidden).toBeFalsy(); + }); + + it('should not be able to resize a pane when it is hidden', () => { + const pane1 = splitter.panes.toArray()[0]; + const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context; + + // collapse left sibling pane + splitterBarComponent.onCollapsing(0); + fixture.detectChanges(); + expect(pane1.hidden).toBeTruthy(); + expect(pane1.resizable).toBeFalsy(); + + splitterBarComponent.onCollapsing(1); + fixture.detectChanges(); + expect(pane1.hidden).toBeFalsy(); + expect(pane1.resizable).toBeTruthy(); + }); +}); +@Component({ + template: ` + + +
+ Pane 1 +
+
+ +
+ Pane 2 +
+
+
+ `, +}) +export class SplitterTestComponent { + type = SplitterType.Horizontal; + @ViewChild(IgxSplitterComponent, { static: true }) + public splitter: IgxSplitterComponent; +} + +@Component({ + template: ` + + +
+ Pane 1 +
+
+ +
+ Pane 2 +
+
+ +
+ Pane 3 +
+
+
+ `, +}) + +export class SplitterTogglePaneComponent extends SplitterTestComponent { +} diff --git a/projects/igniteui-angular/src/lib/splitter/splitter.component.ts b/projects/igniteui-angular/src/lib/splitter/splitter.component.ts new file mode 100644 index 00000000000..e011f7181e9 --- /dev/null +++ b/projects/igniteui-angular/src/lib/splitter/splitter.component.ts @@ -0,0 +1,217 @@ +import { Component, QueryList, Input, ContentChildren, AfterContentInit, HostBinding, Output, EventEmitter } from '@angular/core'; +import { IgxSplitterPaneComponent } from './splitter-pane/splitter-pane.component'; + +/** + * An enumeration that defines the `SplitterComponent` panes orientation. + * @export + * @enum {number} + */ +export enum SplitterType { + Horizontal, + Vertical +} + +/** + * Provides reference to `SplitterComponent` component. + * The splitter consists of resizable panes that can be arranged either vertically or horizontally. + * There is a gripper between each couple of panes that helps widen or shrink them. + * @export + * @class SplitterComponent + * @implements AfterContentInit + */ +@Component({ + selector: 'igx-splitter', + templateUrl: './splitter.component.html' +}) +export class IgxSplitterComponent implements AfterContentInit { + private _type: SplitterType = SplitterType.Vertical; + /** + * Sets/gets `SplitterComponent` orientation. + * @type SplitterType + * @memberof SplitterComponent + */ + @Input() + get type() { + return this._type; + } + set type(value) { + this._type = value; + if (this.panes) { + // if type is changed runtime, should reset sizes. + this.panes.forEach(x => x.size = 'auto'); + } + } + + /** + * Event emitted when panes collection is changed. + */ + @Output() + public panesChange = new EventEmitter(); + + /** + * A list of all `IgxSplitterPaneComponent` items. + * @memberof SplitterComponent + */ + @ContentChildren(IgxSplitterPaneComponent, { read: IgxSplitterPaneComponent }) + public panes!: QueryList; + + /** + * Gets the `flex-direction` property of the current `SplitterComponent`. + * @readonly + * @type string + * @memberof SplitterComponent + */ + @HostBinding('style.flex-direction') + public get direction(): string { + return this.type === SplitterType.Horizontal ? 'row' : 'column'; + } + + /** + * Sets/gets the `overflow` property of the current `SplitterComponent`. + * @memberof SplitterComponent + */ + @HostBinding('style.overflow') + public overflow = 'hidden'; + + /** + * Sets/gets the `display` property of the current `SplitterComponent`. + * @memberof SplitterComponent + */ + @HostBinding('style.display') + public display = 'flex'; + + /** + * A field that holds the initial size of the main `IgxSplitterPaneComponent` in each couple of panes devided by a gripper. + * @private + * @memberof SplitterComponent + */ + private initialPaneSize!: number; + + /** + * A field that holds the initial size of the sibling `IgxSplitterPaneComponent` in each couple of panes devided by a gripper. + * @private + * @memberof SplitterComponent + */ + private initialSiblingSize!: number; + + /** + * The main `IgxSplitterPaneComponent` in each couple of panes devided by a gripper. + * @private + * @memberof SplitterComponent + */ + private pane!: IgxSplitterPaneComponent; + + /** + * The sibling `IgxSplitterPaneComponent` in each couple of panes devided by a gripper. + * @private + * @memberof SplitterComponent + */ + private sibling!: IgxSplitterPaneComponent; + + /** @hidden @internal */ + public ngAfterContentInit(): void { + this.assignFlexOrder(); + this.panes.changes.subscribe(() => { + requestAnimationFrame(() => { + this.panesChange.emit(this.panes.toArray()); + this.assignFlexOrder(); + }); + }); + } + + /** + * @hidden @internal + * This method performs some initialization logic when the user starts dragging the gripper between each couple of panes. + * @param {IgxSplitterPaneComponent} pane + * The main `IgxSplitterPaneComponent` associated with the currently dragged `SplitBarComponent`. + */ + public onMoveStart(pane: IgxSplitterPaneComponent) { + const panes = this.panes.toArray(); + this.pane = pane; + this.sibling = panes[panes.indexOf(this.pane) + 1]; + + const paneRect = this.pane.element.getBoundingClientRect(); + this.initialPaneSize = this.type === SplitterType.Horizontal ? paneRect.width : paneRect.height; + if (this.pane.size === 'auto') { + this.pane.size = this.type === SplitterType.Horizontal ? paneRect.width : paneRect.height; + } + + const siblingRect = this.sibling.element.getBoundingClientRect(); + this.initialSiblingSize = this.type === SplitterType.Horizontal ? siblingRect.width : siblingRect.height; + if (this.sibling.size === 'auto') { + this.sibling.size = this.type === SplitterType.Horizontal ? siblingRect.width : siblingRect.height; + } + } + + /** + * @hidden @internal + * This method performs some calculations concerning the sizes each couple of panes while the gripper between them is being dragged. + * @param {number} delta The differnce along the X (or Y) axis between the initial and the current point while dragging the gripper. + */ + public onMoving(delta: number) { + const min = parseInt(this.pane.minSize, 10) || 0; + const max = parseInt(this.pane.maxSize, 10) || this.initialPaneSize + this.initialSiblingSize; + const minSibling = parseInt(this.sibling.minSize, 10) || 0; + const maxSibling = parseInt(this.sibling.maxSize, 10) || this.initialPaneSize + this.initialSiblingSize; + + const paneSize = this.initialPaneSize - delta; + const siblingSize = this.initialSiblingSize + delta; + if (paneSize < min || paneSize > max || siblingSize < minSibling || siblingSize > maxSibling) { + return; + } + + this.pane.size = paneSize + 'px'; + this.sibling.size = siblingSize + 'px'; + } + + /** + * Toggles pane visibility. + * @param pane - The pane to be hidden/shown. + */ + public togglePane(pane: IgxSplitterPaneComponent) { + if (!pane) { + return; + } + // reset sibling sizes when pane is collapsed. + this._getSiblings(pane).forEach(sibling => sibling.size = 'auto'); + pane.hidden = !pane.hidden; + pane.resizable = !pane.hidden; + pane.onPaneToggle.emit(pane); + } + + /** + * @hidden @internal + * This method takes care for assigning an `order` property on each `IgxSplitterPaneComponent`. + */ + private assignFlexOrder() { + let k = 0; + this.panes.forEach((pane: IgxSplitterPaneComponent) => { + pane.order = k; + k += 2; + }); + } + + /** @hidden @internal */ + public getPaneSiblingsByOrder(order: number, barIndex: number): Array { + const panes = this.panes.toArray(); + const prevPane = panes[order - barIndex - 1]; + const nextPane = panes[order - barIndex]; + const siblings = [prevPane, nextPane]; + return siblings; + } + + + /** @hidden @internal */ + private _getSiblings(pane: IgxSplitterPaneComponent) { + const panes = this.panes.toArray(); + const index = panes.indexOf(pane); + const siblings = []; + if (index !== 0) { + siblings.push(panes[index - 1]); + } + if (index !== panes.length - 1) { + siblings.push(panes[index + 1]); + } + return siblings; + } +} diff --git a/projects/igniteui-angular/src/lib/splitter/splitter.module.ts b/projects/igniteui-angular/src/lib/splitter/splitter.module.ts new file mode 100644 index 00000000000..a36c897d2ac --- /dev/null +++ b/projects/igniteui-angular/src/lib/splitter/splitter.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IgxSplitBarComponent } from './splitter-bar/splitter-bar.component'; +import { IgxSplitterPaneComponent } from './splitter-pane/splitter-pane.component'; +import { IgxSplitterComponent } from './splitter.component'; +import { IgxIconModule } from '../icon/index'; +import { IgxDragDropModule } from '../directives/drag-drop/drag-drop.directive'; + +@NgModule({ + imports: [ + CommonModule, IgxIconModule, IgxDragDropModule + ], + declarations: [ + IgxSplitterComponent, + IgxSplitterPaneComponent, + IgxSplitBarComponent + ], + exports: [ + IgxSplitterComponent, + IgxSplitterPaneComponent, + IgxSplitBarComponent + ] +}) +export class IgxSplitterModule { } diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index 720ee06e21b..120c2568817 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -91,6 +91,7 @@ export * from './lib/tabs/index'; export * from './lib/time-picker/time-picker.component'; export * from './lib/toast/toast.component'; export * from './lib/select/index'; +export * from './lib/splitter/splitter.module'; /** diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e76c796b9f3..d8e106a95c3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -354,9 +354,14 @@ export class AppComponent implements OnInit { }, { link: '/slider', - icon: 'linear_scale', + icon: 'tab', name: 'Slider' }, + { + link: '/splitter', + icon: 'linear_scale', + name: 'Splitter' + }, { link: '/snackbar', icon: 'feedback', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 436538a5c65..d1e60b83431 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -38,6 +38,7 @@ import { NavdrawerSampleComponent } from './navdrawer/navdrawer.sample'; import { ProgressbarSampleComponent } from './progressbar/progressbar.sample'; import { RippleSampleComponent } from './ripple/ripple.sample'; import { SliderSampleComponent } from './slider/slider.sample'; +import { SplitterSampleComponent } from './splitter/slitter.sample'; import { SnackbarSampleComponent } from './snackbar/snackbar.sample'; import { ColorsSampleComponent } from './styleguide/colors/color.sample'; import { ShadowsSampleComponent } from './styleguide/shadows/shadows.sample'; @@ -160,6 +161,7 @@ const components = [ RippleSampleComponent, SelectSampleComponent, SliderSampleComponent, + SplitterSampleComponent, SnackbarSampleComponent, BottomNavSampleComponent, BottomNavRoutingSampleComponent, diff --git a/src/app/routing.ts b/src/app/routing.ts index 2745466630d..3a506d660d2 100644 --- a/src/app/routing.ts +++ b/src/app/routing.ts @@ -23,6 +23,7 @@ import { NavdrawerSampleComponent } from './navdrawer/navdrawer.sample'; import { ProgressbarSampleComponent } from './progressbar/progressbar.sample'; import { RippleSampleComponent } from './ripple/ripple.sample'; import { SliderSampleComponent } from './slider/slider.sample'; +import { SplitterSampleComponent } from './splitter/slitter.sample'; import { SnackbarSampleComponent } from './snackbar/snackbar.sample'; import { ColorsSampleComponent } from './styleguide/colors/color.sample'; import { ShadowsSampleComponent } from './styleguide/shadows/shadows.sample'; @@ -250,6 +251,10 @@ const appRoutes = [ path: 'slider', component: SliderSampleComponent }, + { + path: 'splitter', + component: SplitterSampleComponent + }, { path: 'snackbar', component: SnackbarSampleComponent diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index abfa6badabb..990f2026a79 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -32,6 +32,7 @@ import { IgxSliderModule, IgxSnackbarModule, IgxSwitchModule, + IgxSplitterModule, IgxTabsModule, IgxTimePickerModule, IgxToastModule, @@ -74,6 +75,7 @@ const igniteModules = [ IgxSliderModule, IgxSnackbarModule, IgxSwitchModule, + IgxSplitterModule, IgxTabsModule, IgxTimePickerModule, IgxToastModule, diff --git a/src/app/splitter/slitter.sample.ts b/src/app/splitter/slitter.sample.ts new file mode 100644 index 00000000000..6e209b779ce --- /dev/null +++ b/src/app/splitter/slitter.sample.ts @@ -0,0 +1,94 @@ +import { Component, ViewChild } from '@angular/core'; +import { SplitterType } from 'projects/igniteui-angular/src/lib/splitter/splitter.component'; +import { RemoteService } from '../shared/remote.service'; +import { IgxGridComponent } from 'igniteui-angular'; + +@Component({ + selector: 'app-splitter-sample', + styleUrls: ['splitter.sample.scss'], + templateUrl: 'splitter.sample.html' +}) +export class SplitterSampleComponent { + type = SplitterType.Horizontal; + data1 = []; + data2 = []; + data3 = []; + primaryKeys = [ + { name: 'CustomerID', type: 'string', level: 0 }, + { name: 'OrderID', type: 'number', level: 1 }, + { name: 'EmployeeID', type: 'number', level: 2 }, + { name: 'ProductID', type: 'number', level: 2 } + ]; + + @ViewChild('grid1', { static: true }) + grid1: IgxGridComponent; + + @ViewChild('grid2', { static: true }) + grid2: IgxGridComponent; + + @ViewChild('grid3', { static: true }) + grid3: IgxGridComponent; + + constructor(private remoteService: RemoteService) { + remoteService.url = 'https://services.odata.org/V4/Northwind/Northwind.svc/'; + + this.remoteService.urlBuilder = (dataState) => this.buildUrl(dataState); + } + + public buildUrl(dataState) { + let qS = ''; + if (dataState) { + qS += `${dataState.key}?`; + + const level = dataState.level; + if (level > 0) { + const parentKey = this.primaryKeys.find((key) => key.level === level - 1); + const parentID = typeof dataState.parentID !== 'object' ? dataState.parentID : dataState.parentID[parentKey.name]; + + if (parentKey.type === 'string') { + qS += `$filter=${parentKey.name} eq '${parentID}'`; + } else { + qS += `$filter=${parentKey.name} eq ${parentID}`; + } + } + } + return `${this.remoteService.url}${qS}`; + } + + changeType() { + if (this.type === SplitterType.Horizontal) { + this.type = SplitterType.Vertical; + } else { + this.type = SplitterType.Horizontal; + } + } + + public ngAfterViewInit() { + this.remoteService.getData({ parentID: null, level: 0, key: 'Customers' }, (data) => { + this.data1 = data['value']; + this.grid1.isLoading = false; + this.grid1.selectRows([this.data1[0].CustomerID], true); + const evt = { newSelection: [this.data1[0].CustomerID]}; + this.onCustomerSelection(evt); + }); + } + + public onCustomerSelection(evt) { + const newSelection = evt.newSelection[0]; + this.remoteService.getData({ parentID: newSelection, level: 1, key: 'Orders' }, (data) => { + this.data2 = data['value']; + this.grid2.isLoading = false; + this.grid2.selectRows([this.data2[0].OrderID], true); + const evt = { newSelection: [this.data2[0].OrderID]}; + this.onOrderSelection(evt); + }); + } + + public onOrderSelection(evt) { + const newSelection = evt.newSelection[0]; + this.remoteService.getData({ parentID: newSelection, level: 2, key: 'Order_Details' }, (data) => { + this.data3 = data['value']; + this.grid3.isLoading = false; + }); + } +} diff --git a/src/app/splitter/splitter.sample.html b/src/app/splitter/splitter.sample.html new file mode 100644 index 00000000000..f1712652efe --- /dev/null +++ b/src/app/splitter/splitter.sample.html @@ -0,0 +1,76 @@ +
+ Toggle Splitter Direction +
Simple sample
+ + +
+ Pane 1 +
+
+ +
+ Pane 2 +
+
+ +
+ Pane 3 +
+
+
+
Nested splitters sample
+ + + + + Pane1.1 + + + Pane1.2 + + + + + + + Pane2.1 + + + Pane2.2 + + + + + +
Sample with Grids
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/app/splitter/splitter.sample.scss b/src/app/splitter/splitter.sample.scss new file mode 100644 index 00000000000..ef4134bb18c --- /dev/null +++ b/src/app/splitter/splitter.sample.scss @@ -0,0 +1 @@ +@import '../../../projects/igniteui-angular/src/lib/core/styles/themes/utilities'; diff --git a/src/styles/igniteui-theme.scss b/src/styles/igniteui-theme.scss index 43b370ee527..6c938f4f0a3 100644 --- a/src/styles/igniteui-theme.scss +++ b/src/styles/igniteui-theme.scss @@ -14,7 +14,7 @@ $igx-foreground-color: text-contrast($igx-background-color); body { background: $igx-background-color; - color: #222; + color: rgba($igx-foreground-color, .87); } @include igx-core($direction: ltr);