UNPKG

ng-split-areas

Version:

Splitting views in Angular horizontally / vertically with configurable size-restrictions

1,205 lines (1,202 loc) 72.1 kB
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgModule, NgZone, Output, Renderer2, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; /** * Calculation logic for area sizes */ class AreaSizeCalculation { /** * Modifies sizes of areas in the list by totalSize * * @param areas list of areas to be modified * @param sourceAreaIndex index of area that triggered modification (will not be modified [a second time]) * @param totalSize size to be added / subtracted to / from areas within the list * @param containerSizePixel size of the container in pixel * @param gutterSizePxPerVisibleComponent size of the gutter in pixel per visible component * @param direction direction of modification starting from sourceAreaIndex * @returns size left, that couldnt be added / subtracted to / from the areas within the list */ static modifyAreaSizes(areas, sourceAreaIndex, totalSize, containerSizePixel, gutterSizePxPerVisibleComponent, direction) { let result = totalSize; // if there is nothing to modify (-> totalSize is zero) if (result === 0) { // there's nothing to do return result; } // if the area-list is empty if (!areas || areas.length === 0) { // there's nothing to do return result; } // if there is only 1 area in the area list if (areas.length <= 1) { // there's nothing to do, because the list contains the source area only return result; } // modifying direction = to the right if (direction === 'right') { // sourceArea is rightmost area if (sourceAreaIndex + 1 === areas.length) { // there's nothing to do return result; } // iterate the area-list to the right (starting with the component next to sourceAreaIndex) for (let i = sourceAreaIndex + 1; i < areas.length; i++) { // area at current index const currentArea = areas[i]; // left space to be subtracted result = AreaSizeCalculation.modifyAreaSize(currentArea, result, containerSizePixel, gutterSizePxPerVisibleComponent); // if there is no space left to modify if (result === 0) { // stop iterating break; } } } else if (direction === 'left') { // modifying direction = to the left // sourceArea is leftmost area if (sourceAreaIndex === 0) { // there's nothing to do return result; } // iterate the area-list to the left (starting with the component next to sourceAreaIndex) for (let i = sourceAreaIndex - 1; i >= 0; i--) { // area at current index const currentArea = areas[i]; // left space to be subtracted result = AreaSizeCalculation.modifyAreaSize(currentArea, result, containerSizePixel, gutterSizePxPerVisibleComponent); // if there is no space left to modify if (result === 0) { // stop iterating break; } } } return result; } /** * Modifies size of the area by given size * * @param area Area to be modified * @param sizeToBeModified Size to be added / subtracted to / from the area * @param containerSize Size of the container * @param gutterSizePxPerVisibleComponent gutter size in px per visible component * * @returns left size that couldn't be added / subtracted to / from * the area (through min / max restrictions) */ static modifyAreaSize(area, sizeToBeModified, containerSize, gutterSizePxPerVisibleComponent) { // size left to be modified let result = sizeToBeModified; // is current action a subtraction const isSubtract = sizeToBeModified < 0; // there's size to be taken away (subtraction if (isSubtract) { // adding size taken away from the area result += this.subtractSizeFromArea(area, Math.abs(sizeToBeModified), containerSize, gutterSizePxPerVisibleComponent); } else { // subtracting size taken away from the area result -= this.addSizeToArea(area, sizeToBeModified, containerSize, gutterSizePxPerVisibleComponent); } return result; } /** * Subtracts size of the area by given size * * @param area Area to be subtracted it's size * @param sizeToBeSubtracted Size to be subtracted from the area * @param containerSize Size of the container * @param gutterSizePxPerVisibleComponent size of the gutter in px per visible component * @returns size that was subtracted */ static subtractSizeFromArea(area, sizeToBeSubtracted, containerSize, gutterSizePxPerVisibleComponent) { // size that was subtracted let result = 0; // if the areas size is already 0 if (area.size === 0) { // there's nothing to do return result; } // maximum size to be taken away from the area let maxSizeToBeTakenAway = area.size; // area has a minimum pixel restriction if (area.minSizePx) { // minimum size of area (in pcnt of container size) const minSizeInPcntOfCurrentContainer = Math.ceil(area.minSizePx + gutterSizePxPerVisibleComponent) / containerSize; // maximum size to be taken away from area = current-size - min-size maxSizeToBeTakenAway = area.size - minSizeInPcntOfCurrentContainer; } // if there is no size available to be taken away if (maxSizeToBeTakenAway <= 0) { // there's nothing to do return result; } // size to be actually taken away from the area let sizeToBeTakenAway = sizeToBeSubtracted; // if the size to be taken away is > than the max size that can be taken away if (sizeToBeTakenAway > maxSizeToBeTakenAway) { // size to be taken away = max sizeToBeTakenAway = maxSizeToBeTakenAway; } // subtract size from current area's size area.size -= sizeToBeTakenAway; // set the result to the size taken away; result = sizeToBeTakenAway; // return what was actually taken away from the area return result; } /** * Adds size to the area by given size * * @param area Area to be added something to it's size * @param sizeToBeAdded Size to be added to the area * @param containerSize Size of the container * @param gutterSizePxPerVisibleComponent size of the gutter in px per visible component * @returns size that was subtracted */ static addSizeToArea(area, sizeToBeAdded, containerSize, gutterSizePxPerVisibleComponent) { // size that was added let result = 0; // if the areas size is 0 if (area.size === 0) { // there's nothing to do return result; } //maximum size to be added (default: Number.MAX_VALUE) let maxSizeToBeAdded = Number.MAX_VALUE; // area has a maximum pixel restriction if (area.maxSizePx) { // maximum size of area (in pcnt of container size) const maxSizePcnt = Math.ceil(area.maxSizePx + gutterSizePxPerVisibleComponent) / containerSize; // if area size is already greater or equal max-size if (area.size >= maxSizePcnt) { // we cannot add any space to this area maxSizeToBeAdded = 0; } else { // we can add the amount until the area reaches max size maxSizeToBeAdded = maxSizePcnt - area.size; } } // if there is no size available to be added if (maxSizeToBeAdded === 0) { // there's nothing to do return result; } // size to be actually added to the area let sizeToBeActuallyAdded = sizeToBeAdded; // if the size to be added is > than the max size that can be added if (sizeToBeActuallyAdded > maxSizeToBeAdded) { // size to be added = max sizeToBeAdded = maxSizeToBeAdded; } // add size to current area's size area.size += sizeToBeAdded; // set the result to the size added result = sizeToBeAdded; // return what was actually added to the area return result; } /** * Calculates area sizes according to given IAreaSizeCalculationOptions */ calculate(opts) { // if calculate was triggered by a drag-and-drop action if (opts.calculationSource && opts.calculationSource.isDragAndDrop) { // handle dragging this.handleDragAndDrop(opts); } else if (opts.calculationSource && opts.calculationSource.isWindowResize) { // handle window-resize this.handleWindowResize(opts); } else { // handle normal layout calculations this.handleNormalLayout(opts); } } /** * Calculates area sizes when gutters are dragged * (according to given AreaDragAndDrop inside IAreaSizeCalculationOptions) */ handleDragAndDrop(opts) { // area-drag-and-drop object inside calculationSource const areaDragAndDrop = opts.calculationSource; // put dragged areas from drag-and-drop into a 2 element list const draggedAreas = [areaDragAndDrop.areaA, areaDragAndDrop.areaB]; // calculate complete size in px of both dragged areas const draggedAreasSizePx = (areaDragAndDrop.areaA.size + areaDragAndDrop.areaB.size) * (opts.containerSizePx ? opts.containerSizePx : 0); // both areas are visible after drag (size > 0) and the gutter was moved from it's original position if (areaDragAndDrop.areaA.size > 0 && areaDragAndDrop.areaB.size > 0 && areaDragAndDrop.offsetPixel) { // both areas (left side and right side) have min size px configured if (areaDragAndDrop.areaA.minSizePx && areaDragAndDrop.areaB.minSizePx) { // sum of min-size of both areas const totalMinSizePx = areaDragAndDrop.areaA.minSizePx + areaDragAndDrop.areaB.minSizePx; // size in px that can be used by the components (=> without gutter size) const draggedAreasSizePxWithoutGutter = draggedAreasSizePx - (opts.gutterSizePx ? opts.gutterSizePx : 0); // both components won't fit into given size (because the sum of their min-sizes is > total available space) if (totalMinSizePx > draggedAreasSizePxWithoutGutter) { // gutter was moved left (-> offsetPixel = start - end = a positive number) if (areaDragAndDrop.offsetPixel > 0) { //left side wins areaDragAndDrop.areaA.size += areaDragAndDrop.areaB.size; //right side is reset to 0 areaDragAndDrop.areaB.size = 0; } else if (areaDragAndDrop.offsetPixel < 0) { // gutter was moved right (-> offsetPixel = start - end = a negative number) //right side wins areaDragAndDrop.areaB.size += areaDragAndDrop.areaA.size; //left side is reset to 0 areaDragAndDrop.areaA.size = 0; } } } } // check & fix min sizes of dragged areas this.checkAndFixSizePxRestrictions({ // use complete size in px of both dragged areas as containerSizePx containerSizePx: draggedAreasSizePx, // use dragged areas as list of displayed areas displayedAreas: draggedAreas, // copy gutter size of original options object, because it's the same for dragging areas gutterSizePx: opts.gutterSizePx }); } /** * Calculates area sizes when the window:resize-event was triggered */ handleWindowResize(opts) { this.checkAndFixSizePxRestrictions(opts); } /** * Calculates area sizes for normal layouting (first display, area-list changes, ...) */ handleNormalLayout(opts) { this.checkAndFixSizePxRestrictions(opts); } checkAndFixSizePxRestrictions(opts) { //check and fix min-sizes this.checkAndFixSizePxRestrictedAreas(opts, 'min'); //check and fix max-sizes this.checkAndFixSizePxRestrictedAreas(opts, 'max'); } /** * Checks and fixes <split-area>-Components that have minSizePx / maxSizePx configured * * @throws E-00001 - area sizes could not be calculated fixing minSizePx / maxSizePx */ checkAndFixSizePxRestrictedAreas(opts, restrictionProperty) { // if we have one or zero displayed area(s) if (opts.displayedAreas.length <= 1) { // there is nothing to check and fix return; } // size of the container in pixel (= available space in px) const containerSizePixel = opts.containerSizePx ? opts.containerSizePx : 0; // areas with a size > 0 const displayedAreasWithSizeGreaterZero = opts.displayedAreas.filter((a) => a.size !== 0); // total size of all gutters in px const totalGutterSizePx = (opts.displayedAreas.length - 1) * (opts.gutterSizePx ? opts.gutterSizePx : 0); // size of the gutter in px per component const gutterSizePxPerVisibleComponent = displayedAreasWithSizeGreaterZero.length > 1 ? totalGutterSizePx / displayedAreasWithSizeGreaterZero.length : totalGutterSizePx; // iterate all displayed areas (with size > 0) displayedAreasWithSizeGreaterZero.forEach((area, index) => { // new size of the current area (default: current size) let newAreaSize = area.size; //switch min-width / max-width calculation by parameter (restrictionProperty) switch (restrictionProperty) { case 'min': // if the (calculated) area size in pixels is smaller than its configured minSize if (area.size * containerSizePixel < Math.ceil(area.minSizePx + gutterSizePxPerVisibleComponent)) { // new size of the current area (-> min size in percent) = // it's configured min size in pixels / container size in pixels newAreaSize = Math.ceil(area.minSizePx + gutterSizePxPerVisibleComponent) / containerSizePixel; } break; case 'max': // if the (calculated) area size in pixels is greater than its configured maxSize if (area.size * containerSizePixel > Math.ceil(area.maxSizePx + gutterSizePxPerVisibleComponent)) { // new size of the current area (-> max size in percent) = // it's configured max size in pixels / container size in pixels newAreaSize = Math.ceil(area.maxSizePx + gutterSizePxPerVisibleComponent) / containerSizePixel; } break; } // if new size of the area differs from current size if (newAreaSize !== area.size) { // space that needs to be taken away / added from/to other areas const sizeDiffToBeModifiedOnOtherAreas = area.size - newAreaSize; // current area size = size respecting the minimum pixel size area.size = newAreaSize; if (sizeDiffToBeModifiedOnOtherAreas !== 0) { // remove/add space-diff from to/all available areas const diffAfterRemovingSpaceFromAreas = this.addAvailableSpaceToAreas(displayedAreasWithSizeGreaterZero, index, sizeDiffToBeModifiedOnOtherAreas, containerSizePixel, gutterSizePxPerVisibleComponent); // if there is still space left after checking all areas if (diffAfterRemovingSpaceFromAreas !== 0) { // we add the diff to our current area to keep our layout consistent area.size += sizeDiffToBeModifiedOnOtherAreas; } } } }); } /** * Modifies areas adding available space * * @param areas list of areas to be modified * @param sourceAreaIndex index of area that triggered modification (will not be modified [a second time]) * @param availableSpace size to be added / subtracted to / from areas within the list * @param containerSizePixel size of the container in pixel * @param gutterSizePxPerVisibleComponent size of the gutter in pixel per visible component * * @returns space left after adding/removing from/to all available areas */ addAvailableSpaceToAreas(areas, sourceAreaIndex, availableSpace, containerSizePixel, gutterSizePxPerVisibleComponent) { // add/remove space from/to areas on the right side of source-area(-index) let result = AreaSizeCalculation.modifyAreaSizes(areas, sourceAreaIndex, availableSpace, containerSizePixel, gutterSizePxPerVisibleComponent, 'right'); // if there is still some space to be added/removed left after checking right side areas if (result !== 0) { // add/remove space from/to areas on the left side of source-area(-index) result = AreaSizeCalculation.modifyAreaSizes(areas, sourceAreaIndex, result, containerSizePixel, gutterSizePxPerVisibleComponent, 'left'); } return result; } } /** * Handles and notifies size changes */ class Area { constructor(area) { this.sizeChanged = new EventEmitter(); if (!area) { return; } this._size = area.size; this.comp = area.comp; this.maxSizePx = area.maxSizePx; this.minSizePx = area.minSizePx; this.order = area.order; } get size() { return this._size; } set size(value) { let previousValue = this._size; this._size = value; if (previousValue !== value) { this.sizeChanged.next(value); } } } var __decorate$1 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (undefined && undefined.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; /** * angular-split * * Areas size are set in percentage of the split container. * Gutters size are set in pixels. * * So we set css 'flex-basis' property like this (where 0 <= area.size <= 1): * calc( { area.size * 100 }% - { area.size * nbGutter * gutterSize }px ); * * Examples with 3 visible areas and 2 gutters: * * | 10px 10px | * |---------------------[]---------------------[]------------------------------------| * | calc(20% - 4px) calc(20% - 4px) calc(60% - 12px) | * * * | 10px 10px | * |--------------------------[]--------------------------[]--------------------------| * | calc(33.33% - 6.667px) calc(33.33% - 6.667px) calc(33.33% - 6.667px) | * * * |10px 10px | * |[]----------------------------------------------------[]--------------------------| * |0 calc(66.66% - 13.333px) calc(33%% - 6.667px) | * * * 10px 10px | * |[][]------------------------------------------------------------------------------| * |0 0 calc(100% - 20px) | * */ let SplitComponent = class SplitComponent { constructor(ngZone, elRef, cdRef, renderer) { this.ngZone = ngZone; this.elRef = elRef; this.cdRef = cdRef; this.renderer = renderer; this._direction = 'horizontal'; //// this._useTransition = false; //// this._disabled = false; //// this._width = null; //// this._height = null; //// this._gutterSize = 11; //// this._gutterColor = ''; //// this._gutterImageH = ''; //// this._gutterImageV = ''; //// this._dir = 'ltr'; //// this.dragStart = new EventEmitter(false); this.dragProgress = new EventEmitter(false); this.dragEnd = new EventEmitter(false); this.gutterClick = new EventEmitter(false); /** * notifies size changes on an area */ this.currentSizeChange = new EventEmitter(); this.transitionEndInternal = new Subject(); this.transitionEnd = this.transitionEndInternal.asObservable().pipe(debounceTime(20)); this.defaultAreaSizeCalculation = new AreaSizeCalculation(); this.isViewInitialized = false; this.isDragging = false; this.draggingWithoutMove = false; this.currentGutterNum = 0; this.displayedAreas = []; this.hidedAreas = []; this.dragListeners = []; this.dragStartValues = { sizePixelContainer: 0, sizePixelA: 0, sizePixelB: 0, sizePercentA: 0, sizePercentB: 0, }; this.setMinSizeOnContainer = false; } set direction(v) { v = (v === 'vertical') ? 'vertical' : 'horizontal'; this._direction = v; [...this.displayedAreas, ...this.hidedAreas].forEach(area => { area.comp.setStyleVisibleAndDir(area.comp.visible, this.isDragging, this.direction); }); this.build(false, false); } get direction() { return this._direction; } set useTransition(v) { v = (typeof (v) === 'boolean') ? v : (v === 'false' ? false : true); this._useTransition = v; } get useTransition() { return this._useTransition; } set disabled(v) { v = (typeof (v) === 'boolean') ? v : (v === 'false' ? false : true); this._disabled = v; // Force repaint if modified from TS class (instead of the template) this.cdRef.markForCheck(); } get disabled() { return this._disabled; } set width(v) { v = Number(v); this._width = (!isNaN(v) && v > 0) ? v : null; this.build(false, false); } get width() { return this._width; } set height(v) { v = Number(v); this._height = (!isNaN(v) && v > 0) ? v : null; this.build(false, false); } get height() { return this._height; } set gutterSize(v) { v = Number(v); this._gutterSize = (!isNaN(v) && v > 0) ? v : 11; this.build(false, false); } get gutterSize() { return this._gutterSize; } set gutterColor(v) { this._gutterColor = (typeof v === 'string' && v !== '') ? v : ''; // Force repaint if modified from TS class (instead of the template) this.cdRef.markForCheck(); } get gutterColor() { return this._gutterColor; } set gutterImageH(v) { this._gutterImageH = (typeof v === 'string' && v !== '') ? v : ''; // Force repaint if modified from TS class (instead of the template) this.cdRef.markForCheck(); } get gutterImageH() { return this._gutterImageH; } set gutterImageV(v) { this._gutterImageV = (typeof v === 'string' && v !== '') ? v : ''; // Force repaint if modified from TS class (instead of the template) this.cdRef.markForCheck(); } get gutterImageV() { return this._gutterImageV; } set dir(v) { v = (v === 'rtl') ? 'rtl' : 'ltr'; this._dir = v; } get dir() { return this._dir; } get cssFlexdirection() { return (this.direction === 'horizontal') ? 'row' : 'column'; } get cssWidth() { return this.width ? `${this.width}px` : '100%'; } get cssHeight() { return this.height ? `${this.height}px` : '100%'; } get cssMinwidth() { return (this.direction === 'horizontal') ? `${this.getNbGutters() * this.gutterSize}px` : null; } get cssMinheight() { return (this.direction === 'vertical') ? `${this.getNbGutters() * this.gutterSize}px` : null; } set areaSizeCalculation(v) { this._areaSizeCalculation = v; } get areaSizeCalculation() { return this._areaSizeCalculation; } get areaSizeCalculationToBeUsed() { return this.areaSizeCalculation ? this.areaSizeCalculation : this.defaultAreaSizeCalculation; } addArea(comp) { const newArea = new Area({ comp, minSizePx: comp.minSizePx, maxSizePx: comp.maxSizePx, order: 0, size: 0 }); newArea.sizeChanged.subscribe((newSize) => this.fireSizeChanged(newArea, newSize * 100)); if (comp.visible === true) { this.displayedAreas.push(newArea); } else { this.hidedAreas.push(newArea); } comp.setStyleVisibleAndDir(comp.visible, this.isDragging, this.direction); this.build(true, true); } removeArea(comp) { if (this.displayedAreas.some((a) => a.comp === comp)) { const area = this.displayedAreas.find((a) => a.comp === comp); this.displayedAreas.splice(this.displayedAreas.indexOf(area), 1); this.build(true, true); } else if (this.hidedAreas.some((a) => a.comp === comp)) { const area = this.hidedAreas.find((a) => a.comp === comp); this.hidedAreas.splice(this.hidedAreas.indexOf(area), 1); } } updateArea(comp, resetOrders, resetSizes) { // Only refresh if area is displayed (No need to check inside 'hidedAreas') const item = this.displayedAreas.find((a) => a.comp === comp); if (item) { // use minSizePx of the component item.minSizePx = comp.minSizePx; // use maxSizePx of the component item.maxSizePx = comp.maxSizePx; this.build(resetOrders, resetSizes); } } showArea(comp) { const area = this.hidedAreas.find(a => a.comp === comp); if (area) { comp.setStyleVisibleAndDir(comp.visible, this.isDragging, this.direction); const areas = this.hidedAreas.splice(this.hidedAreas.indexOf(area), 1); this.displayedAreas.push(...areas); this.build(true, true); } } hideArea(comp) { const area = this.displayedAreas.find(a => a.comp === comp); if (area) { comp.setStyleVisibleAndDir(comp.visible, this.isDragging, this.direction); const areas = this.displayedAreas.splice(this.displayedAreas.indexOf(area), 1); areas.forEach((currentArea) => { currentArea.order = 0; currentArea.size = 0; }); this.hidedAreas.push(...areas); this.build(true, true); } } /** * Min-Width of the container */ get containerMinWidth() { if (!this.setMinSizeOnContainer) return undefined; // if split direction is NOT vertical if (this.direction !== "horizontal") { // min-width is 0 (unset) return 0; } // return the sum of all area minSizes + gutter sizes return this.getMinSizeOfAllDisplayedComponentsPlusGutterSize(); } /** * Min-Height of the container */ get containerMinHeight() { if (!this.setMinSizeOnContainer) return undefined; // if split direction is NOT vertical if (this.direction !== "vertical") { // min-height is 0 (unset) return 0; } // return the sum of all area minSizes + gutter sizes return this.getMinSizeOfAllDisplayedComponentsPlusGutterSize(); } ngAfterViewInit() { this.isViewInitialized = true; } /** * Sum of all area minSizes + gutter sizes */ getMinSizeOfAllDisplayedComponentsPlusGutterSize() { if (!this.displayedAreas || this.displayedAreas.length === 0) { return 0; } return this.getMinSizeOfAllDisplayedComponents() + (this.gutterSize * (this.displayedAreas.length - 1)); } /** * Sum of all area minSizes */ getMinSizeOfAllDisplayedComponents() { if (!this.displayedAreas || this.displayedAreas.length === 0) { return 0; } let result = 0; this.displayedAreas.forEach((area) => { if (area.minSizePx) { result += area.minSizePx; } }); return result; } getNbGutters() { return this.displayedAreas.length - 1; } build(resetOrders, resetSizes) { this.stopDragging(); // ¤ AREAS ORDER if (resetOrders === true) { // If user provided 'order' for each area, use it to sort them. if (this.displayedAreas.every((a) => a.comp.order !== null)) { this.displayedAreas.sort((a, b) => a.comp.order - b.comp.order); } // Then set real order with multiples of 2, numbers between will be used by gutters. this.displayedAreas.forEach((area, i) => { area.order = i * 2; area.comp.setStyleOrder(area.order); }); } // ¤ AREAS SIZE PERCENT if (resetSizes === true) { const totalUserSize = this.displayedAreas.reduce((total, s) => s.comp.size ? total + s.comp.size : total, 0); // If user provided 'size' for each area and total == 1, use it. if (this.displayedAreas.every((a) => a.comp.size !== null) && totalUserSize > .999 && totalUserSize < 1.001) { this.displayedAreas.forEach((area) => { area.size = area.comp.size; }); } // Else set equal sizes for all areas. else { const size = 1 / this.displayedAreas.length; this.displayedAreas.forEach(area => { area.size = size; }); } } // ¤ // If some real area sizes are less than gutterSize, // set them to zero and dispatch size to others. let percentToDispatch = 0; // Get container pixel size const containerSizePixel = this.containerSizePx; this.displayedAreas.forEach(area => { if (area.size * containerSizePixel < this.gutterSize) { percentToDispatch += area.size; area.size = 0; } }); if (percentToDispatch > 0 && this.displayedAreas.length > 0) { const nbAreasNotZero = this.displayedAreas.filter(a => a.size !== 0).length; if (nbAreasNotZero > 0) { const percentToAdd = percentToDispatch / nbAreasNotZero; this.displayedAreas.filter(a => a.size !== 0).forEach(area => { area.size += percentToAdd; }); } // All area sizes (container percentage) are less than guterSize, // It means containerSize < ngGutters * gutterSize else { this.displayedAreas[this.displayedAreas.length - 1].size = 1; } } // do the extra calculation this.areaSizeCalculationToBeUsed.calculate(this.createAreaSizeCalculationOptions()); this.refreshStyleSizes(); this.cdRef.markForCheck(); } /** * Creates an AreaSizeCalculation-Object of currently set instance parameters */ createAreaSizeCalculationOptions(calculationSource) { return { calculationSource, containerSizePx: this.containerSizePx, displayedAreas: this.displayedAreas, gutterSizePx: this.gutterSize }; } /** * Size of the container in pixels (corresponding to direction: height or width) */ get containerSizePx() { // Get container pixel size let result = this.getNbGutters() * this.gutterSize; if (this.direction === 'horizontal') { result = this.width ? this.width : this.elRef.nativeElement.offsetWidth; } else { result = this.height ? this.height : this.elRef.nativeElement.offsetHeight; } return result; } refreshStyleSizes() { const sumGutterSize = this.getNbGutters() * this.gutterSize; this.displayedAreas.forEach((area) => { area.comp.setStyleFlexbasis(`calc( ${area.size * 100}% - ${area.size * sumGutterSize}px )`, this.isDragging); }); } startDragging(startEvent, gutterOrder, gutterNum) { startEvent.preventDefault(); // Place code here to allow '(gutterClick)' event even if '[disabled]="true"'. this.currentGutterNum = gutterNum; this.draggingWithoutMove = true; this.ngZone.runOutsideAngular(() => { this.dragListeners.push(this.renderer.listen('document', 'mouseup', (e) => this.stopDragging())); this.dragListeners.push(this.renderer.listen('document', 'touchend', (e) => this.stopDragging())); this.dragListeners.push(this.renderer.listen('document', 'touchcancel', (e) => this.stopDragging())); }); if (this.disabled) { return; } const areaA = this.displayedAreas.find(a => a.order === gutterOrder - 1); const areaB = this.displayedAreas.find(a => a.order === gutterOrder + 1); if (!areaA || !areaB) { return; } const prop = (this.direction === 'horizontal') ? 'offsetWidth' : 'offsetHeight'; this.dragStartValues.sizePixelContainer = this.elRef.nativeElement[prop]; this.dragStartValues.sizePixelA = areaA.comp.getSizePixel(prop); this.dragStartValues.sizePixelB = areaB.comp.getSizePixel(prop); this.dragStartValues.sizePercentA = areaA.size; this.dragStartValues.sizePercentB = areaB.size; let start; if (startEvent instanceof MouseEvent) { start = { x: startEvent.screenX, y: startEvent.screenY, }; } else if (startEvent instanceof TouchEvent) { start = { x: startEvent.touches[0].screenX, y: startEvent.touches[0].screenY, }; } else { return; } this.ngZone.runOutsideAngular(() => { this.dragListeners.push(this.renderer.listen('document', 'mousemove', (e) => this.dragEvent(e, start, areaA, areaB))); this.dragListeners.push(this.renderer.listen('document', 'touchmove', (e) => this.dragEvent(e, start, areaA, areaB))); }); areaA.comp.lockEvents(); areaB.comp.lockEvents(); this.isDragging = true; this.notify('start'); } dragEvent(event, start, areaA, areaB) { if (!this.isDragging) { return; } let end; if (event instanceof MouseEvent) { end = { x: event.screenX, y: event.screenY, }; } else if (event instanceof TouchEvent) { end = { x: event.touches[0].screenX, y: event.touches[0].screenY, }; } else { return; } this.draggingWithoutMove = false; this.drag(start, end, areaA, areaB); } drag(start, end, areaA, areaB) { // ¤ AREAS SIZE PIXEL let offsetPixel = (this.direction === 'horizontal') ? (start.x - end.x) : (start.y - end.y); /* const devicePixelRatio = window.devicePixelRatio || 1; //doesn't work offsetPixel = offsetPixel / devicePixelRatio; //doesn't work */ if (this.dir === 'rtl') { offsetPixel = -offsetPixel; } let newSizePixelA = this.dragStartValues.sizePixelA - offsetPixel; let newSizePixelB = this.dragStartValues.sizePixelB + offsetPixel; if (newSizePixelA < this.gutterSize && newSizePixelB < this.gutterSize) { // WTF.. get out of here! return; } else if (newSizePixelA < this.gutterSize) { newSizePixelB += newSizePixelA; newSizePixelA = 0; } else if (newSizePixelB < this.gutterSize) { newSizePixelA += newSizePixelB; newSizePixelB = 0; } // ¤ AREAS SIZE PERCENT if (newSizePixelA === 0) { areaB.size += areaA.size; areaA.size = 0; } else if (newSizePixelB === 0) { areaA.size += areaB.size; areaB.size = 0; } else { // NEW_PERCENT = START_PERCENT / START_PIXEL * NEW_PIXEL; if (this.dragStartValues.sizePercentA === 0) { areaB.size = this.dragStartValues.sizePercentB / this.dragStartValues.sizePixelB * newSizePixelB; areaA.size = this.dragStartValues.sizePercentB - areaB.size; } else if (this.dragStartValues.sizePercentB === 0) { areaA.size = this.dragStartValues.sizePercentA / this.dragStartValues.sizePixelA * newSizePixelA; areaB.size = this.dragStartValues.sizePercentA - areaA.size; } else { areaA.size = this.dragStartValues.sizePercentA / this.dragStartValues.sizePixelA * newSizePixelA; areaB.size = (this.dragStartValues.sizePercentA + this.dragStartValues.sizePercentB) - areaA.size; } } this.areaSizeCalculationToBeUsed.calculate(this.createAreaSizeCalculationOptions({ areaA, areaB, isDragAndDrop: true, offsetPixel })); this.refreshStyleSizes(); this.notify('progress'); } stopDragging() { if (this.isDragging === false && this.draggingWithoutMove === false) { return; } this.displayedAreas.forEach(area => { area.comp.unlockEvents(); }); while (this.dragListeners.length > 0) { const fct = this.dragListeners.pop(); if (fct) { fct(); } } if (this.draggingWithoutMove === true) { this.notify('click'); } else { this.notify('end'); } this.isDragging = false; this.draggingWithoutMove = false; //trigger resize-event - components can handle it and react to new sizes accordingly this.triggerWindowResize(); } notify(type) { const areasSize = this.displayedAreas.map(a => a.size * 100); switch (type) { case 'start': return this.dragStart.emit({ gutterNum: this.currentGutterNum, sizes: areasSize }); case 'progress': return this.dragProgress.emit({ gutterNum: this.currentGutterNum, sizes: areasSize }); case 'end': return this.dragEnd.emit({ gutterNum: this.currentGutterNum, sizes: areasSize }); case 'click': return this.gutterClick.emit({ gutterNum: this.currentGutterNum, sizes: areasSize }); case 'transitionEnd': return this.transitionEndInternal.next(areasSize); } } /** * Moves the gutter to it's max position between two areas * * @param gutterOrderIndex Index of the gutter to be moved */ moveGutterMax(gutterOrderIndex) { this.moveGutterToPosition(gutterOrderIndex, Number.MAX_VALUE); } /** * Moves the gutter to it's min position between two areas * * @param gutterOrderIndex Index of the gutter to be moved */ moveGutterMin(gutterOrderIndex) { this.moveGutterToPosition(gutterOrderIndex, 0); } /** * Moves the gutter to given position in Pixels * * @param gutterOrderIndex Index of the gutter to be moved * @param positionInPx new gutter position in px * * @version 0.2.8 triggers Window-Resize */ moveGutterToPosition(gutterOrderIndex, positionInPx) { // area on left side of the gutter const areaA = this.displayedAreas.find(a => a.order === gutterOrderIndex - 1); // area on right side of the gutter const areaB = this.displayedAreas.find(a => a.order === gutterOrderIndex + 1); // if one of both areas couldn't be found if (!areaA || !areaB) { // the gutter in between can't be moved return; } const sizePixelA = areaA.comp.getSizePixel((this.direction === 'horizontal') ? 'offsetWidth' : 'offsetHeight'); const sizePixelB = areaB.comp.getSizePixel((this.direction === 'horizontal') ? 'offsetWidth' : 'offsetHeight'); const totalSizePx = sizePixelA + sizePixelB; const totalSizePcnt = areaA.size + areaB.size; let newSizePixelA = sizePixelA; let newSizePixelB = sizePixelB; // sizing left side component and right side component takes available space (=== true) const isLeftSideSizing = positionInPx >= 0; //sizing left side component, if positionInPx is > 0 if (isLeftSideSizing) { // left side componen will be positionInPx or totalSizePx (if it is smaller than positionInPx) newSizePixelA = positionInPx < totalSizePx ? positionInPx : totalSizePx; // right side component will take space still available newSizePixelB = totalSizePx - newSizePixelA; } //sizing right side component, if positionInPx is < 0 else { // absolute size of required right side component width const absolutePositionInPx = Math.abs(positionInPx); // right side componen will be absolutePositionInPx or totalSizePx (if it is smaller than absolutePositionInPx) newSizePixelB = absolutePositionInPx < totalSizePx ? absolutePositionInPx : totalSizePx; // left side component will take space still available newSizePixelA = totalSizePx - newSizePixelB; } if (newSizePixelA < this.gutterSize && newSizePixelB < this.gutterSize) { // WTF.. get out of here! return; } else if (newSizePixelA < this.gutterSize) { newSizePixelB += newSizePixelA; newSizePixelA = 0; } else if (newSizePixelB < this.gutterSize) { newSizePixelA += newSizePixelB; newSizePixelB = 0; } areaA.size = newSizePixelA / totalSizePx * totalSizePcnt; areaB.size = newSizePixelB / totalSizePx * totalSizePcnt; let draggedAreas = []; // component sizes will be checked / corrected in given order // if gutter movement is negative (= sizing area B) if (!isLeftSideSizing) { // areaB size should be checked / fixed first and diff space will be taken from areaA draggedAreas = [areaB, areaA]; } else { // areaA size should be checked / fixed first and diff space will be taken from areaB draggedAreas = [areaA, areaB]; } //drag-offset in pixel = size of areaA in px before resizing - size of areaA in px after resizing const pseudoDragOffset = sizePixelA - newSizePixelA; // reuse drag-and-drop calculation (because it is like a "fast drag-and-drop") this.areaSizeCalculationToBeUsed.calculate(this.createAreaSizeCalculationOptions({ areaA: draggedAreas[0], areaB: draggedAreas[1], isDragAndDrop: true, offsetPixel: pseudoDragOffset, })); this.refreshStyleSizes(); this.triggerWindowResize(); } ngOnDestroy() { this.stopDragging(); } handleWindowResize() { // if container size remains the same as the size we have checked if (this.lastCheckedContainerSizePx === this.containerSizePx) { //there is nothing to do return; } this.lastCheckedContainerSizePx = this.containerSizePx; this.areaSizeCalculationToBeUsed.calculate(this.createAreaSizeCalculationOptions({ isWindowResize: true })); this.refreshStyleSizes(); this.cdRef.markForCheck(); //trigger resize-event - components can handle it and react to new sizes accordingly this.triggerWindowResize(); } fireSizeChanged(area, newSize) { this.currentSizeChange.next({ size: newSize, areaOrder: area.order }); } /** * Triggers the window-resize event, so component inside the (resized) areas can re-layout with given widths / heights */ triggerWindowResize() { // save current sizePx, so we don't check the sizes within handleWindowResize again this.lastCheckedContainerSizePx = this.containerSizePx; try { // For a full list of event types: https://developer.mozilla.org/en-US/docs/Web/API/document.createEvent var event = document.createEvent('HTMLEvents'); event.initEvent('resize', true, false); window.dispatchEvent(event); } catch (error) { console.warn("triggerWindowResize", error); } } }; __decorate$1([ Input(), __metadata("design:type", String), __metadata("design:paramtypes", [String]) ], SplitComponent.prototype, "direction", null); __decorate$1([ Input(), __metadata("design:type", Boolean), __metadata("design:paramtypes", [Boolean]) ], SplitComponent.prototype, "useTransition", null); __decorate$1([ Input(), __metadata("design:type", Boolean), __metadata("design:paramtypes", [Boolean]) ], SplitComponent.prototype, "disabled", null); __decorate$1([ Input(), __metadata("design:type", Number), __metadata("design:paramtypes", [Number]) ], SplitComponent.prototype, "width", null); __decorate$1([ Input(), __metadata("design:type", Number), __metadata("design:paramtypes", [Number]) ], SplitComponent.prototype, "height", null); __decorate$1([ Input(), __metadata("design:type", Number), __metadata("design:paramtypes", [Number]) ], SplitComponent.prototype, "gutterSize", null); __decorate$1([ Input(), __metadata("design:type", String), __metadata("design:paramtypes", [String]) ], SplitComponent.prototype, "gutterColor", null); __decorate$1([ Input(), __metadata("design:type", String), __metadata("design:paramtypes", [String]) ], SplitComponent.prototype, "gutterImageH", null); __decorate$1([ Input(), __metadata("design:type", String), __metadata("design:paramtypes", [String]) ], SplitComponent.prototype, "gutterImageV", null); __decorate$1([ Input(), __metadata("design:type", String), __metadata("design:paramtypes", [String]) ], SplitComponent.prototype, "dir", null); __decorate$1([ Output(), __metadata("design:type", Object) ], SplitComponent.prototype, "dragStart", void 0); __decorate$1([ Output(), __metadata("design:type", Object) ], SplitComponent.prototype, "dragProgress", void 0); __decorate$1([ Output(), __metadata("design:type", Object) ], SplitComponent.prototype, "dragEnd", void 0); __decorate$1([ Output(), __metadata("design:type", Object) ], SplitComponent.prototype, "gutterClick", void 0); __decorate$1([ Output(), __metadata("design:type", Object) ], SplitComponent.prototype, "currentSizeChange", void 0); __decorate$1([ Output(), __metadata("design:type", Object) ], SplitComponent.prototype, "transitionEnd", void 0); __decorate$1([ HostBinding('style.flex-direction'), __metadata("design:type", Object), __metadata("design:paramtypes", []) ], SplitComponent.prototype, "cssFlexdirection", null); __decorate$1([ HostBinding('style.width'), __metadata("design:type", Object), __metadata("design:paramtypes", []) ], SplitComponent.prototype,