UNPKG

@ng-matero/extensions

Version:
1,020 lines (1,015 loc) 50.5 kB
import * as i0 from '@angular/core'; import { InjectionToken, inject, NgZone, ElementRef, ChangeDetectorRef, Renderer2, EventEmitter, booleanAttribute, ViewChildren, Output, Input, ChangeDetectionStrategy, ViewEncapsulation, Component, Directive, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Observable, Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; function getPointFromEvent(event) { // TouchEvent if (event.changedTouches !== undefined && event.changedTouches.length > 0) { return { x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY, }; } // MouseEvent else if (event.clientX !== undefined && event.clientY !== undefined) { return { x: event.clientX, y: event.clientY, }; } return null; } function getElementPixelSize(elRef, direction) { const rect = elRef.nativeElement.getBoundingClientRect(); return direction === 'horizontal' ? rect.width : rect.height; } function getInputPositiveNumber(v, defaultValue) { if (v === null || v === undefined) { return defaultValue; } v = Number(v); return !isNaN(v) && v >= 0 ? v : defaultValue; } function isUserSizesValid(unit, sizes) { // All sizes have to be not null and total should be 100 if (unit === 'percent') { const total = sizes.reduce((_total, s) => (s !== null ? _total + s : _total), 0); return sizes.every(s => s !== null) && total && total > 99.9 && total < 100.1; } // A size at null is mandatory but only one. if (unit === 'pixel') { return sizes.filter(s => s === null).length === 1; } } function getAreaMinSize(a) { if (a.size === null) { return null; } if (a.component.lockSize === true) { return a.size; } if (a.component.minSize === null) { return null; } if (a.component.minSize > a.size) { return a.size; } return a.component.minSize; } function getAreaMaxSize(a) { if (a.size === null) { return null; } if (a.component.lockSize === true) { return a.size; } if (a.component.maxSize === null) { return null; } if (a.component.maxSize < a.size) { return a.size; } return a.component.maxSize; } function getGutterSideAbsorptionCapacity(unit, sideAreas, pixels, allAreasSizePixel) { return sideAreas.reduce((acc, area) => { const res = getAreaAbsorptionCapacity(unit, area, acc.remain, allAreasSizePixel); acc.list.push(res); acc.remain = res && res.pixelRemain; return acc; }, { remain: pixels, list: [] }); } function getAreaAbsorptionCapacity(unit, areaSnapshot, pixels, allAreasSizePixel) { // No pain no gain if (pixels === 0) { return { areaSnapshot, pixelAbsorb: 0, percentAfterAbsorption: areaSnapshot.sizePercentAtStart, pixelRemain: 0, }; } // Area start at zero and need to be reduced, not possible if (areaSnapshot.sizePixelAtStart === 0 && pixels < 0) { return { areaSnapshot, pixelAbsorb: 0, percentAfterAbsorption: 0, pixelRemain: pixels, }; } if (unit === 'percent') { return getAreaAbsorptionCapacityPercent(areaSnapshot, pixels, allAreasSizePixel); } if (unit === 'pixel') { return getAreaAbsorptionCapacityPixel(areaSnapshot, pixels, allAreasSizePixel); } } function getAreaAbsorptionCapacityPercent(areaSnapshot, pixels, allAreasSizePixel) { const tempPixelSize = areaSnapshot.sizePixelAtStart + pixels; const tempPercentSize = (tempPixelSize / allAreasSizePixel) * 100; // ENLARGE AREA if (pixels > 0) { // If maxSize & newSize bigger than it > absorb to max and return remaining pixels if (areaSnapshot.area.maxSize !== null && tempPercentSize > areaSnapshot.area.maxSize) { // Use area.area.maxSize as newPercentSize and return calculate pixels remaining const maxSizePixel = (areaSnapshot.area.maxSize / 100) * allAreasSizePixel; return { areaSnapshot, pixelAbsorb: maxSizePixel, percentAfterAbsorption: areaSnapshot.area.maxSize, pixelRemain: areaSnapshot.sizePixelAtStart + pixels - maxSizePixel, }; } return { areaSnapshot, pixelAbsorb: pixels, percentAfterAbsorption: tempPercentSize > 100 ? 100 : tempPercentSize, pixelRemain: 0, }; } // REDUCE AREA else if (pixels < 0) { // If minSize & newSize smaller than it > absorb to min and return remaining pixels if (areaSnapshot.area.minSize !== null && tempPercentSize < areaSnapshot.area.minSize) { // Use area.area.minSize as newPercentSize and return calculate pixels remaining const minSizePixel = (areaSnapshot.area.minSize / 100) * allAreasSizePixel; return { areaSnapshot, pixelAbsorb: minSizePixel, percentAfterAbsorption: areaSnapshot.area.minSize, pixelRemain: areaSnapshot.sizePixelAtStart + pixels - minSizePixel, }; } // If reduced under zero > return remaining pixels else if (tempPercentSize < 0) { // Use 0 as newPercentSize and return calculate pixels remaining return { areaSnapshot, pixelAbsorb: -areaSnapshot.sizePixelAtStart, percentAfterAbsorption: 0, pixelRemain: pixels + areaSnapshot.sizePixelAtStart, }; } return { areaSnapshot, pixelAbsorb: pixels, percentAfterAbsorption: tempPercentSize, pixelRemain: 0, }; } } function getAreaAbsorptionCapacityPixel(areaSnapshot, pixels, containerSizePixel) { const tempPixelSize = areaSnapshot.sizePixelAtStart + pixels; // ENLARGE AREA if (pixels > 0) { // If maxSize & newSize bigger than it > absorb to max and return remaining pixels if (areaSnapshot.area.maxSize !== null && tempPixelSize > areaSnapshot.area.maxSize) { return { areaSnapshot, pixelAbsorb: areaSnapshot.area.maxSize - areaSnapshot.sizePixelAtStart, percentAfterAbsorption: -1, pixelRemain: tempPixelSize - areaSnapshot.area.maxSize, }; } return { areaSnapshot, pixelAbsorb: pixels, percentAfterAbsorption: -1, pixelRemain: 0, }; } // REDUCE AREA else if (pixels < 0) { // If minSize & newSize smaller than it > absorb to min and return remaining pixels if (areaSnapshot.area.minSize !== null && tempPixelSize < areaSnapshot.area.minSize) { return { areaSnapshot, pixelAbsorb: areaSnapshot.area.minSize + pixels - tempPixelSize, percentAfterAbsorption: -1, pixelRemain: tempPixelSize - areaSnapshot.area.minSize, }; } // If reduced under zero > return remaining pixels else if (tempPixelSize < 0) { return { areaSnapshot, pixelAbsorb: -areaSnapshot.sizePixelAtStart, percentAfterAbsorption: -1, pixelRemain: pixels + areaSnapshot.sizePixelAtStart, }; } return { areaSnapshot, pixelAbsorb: pixels, percentAfterAbsorption: -1, pixelRemain: 0, }; } } function updateAreaSize(unit, item) { if (unit === 'percent') { item.areaSnapshot.area.size = item.percentAfterAbsorption; } else if (unit === 'pixel') { // Update size except for the wildcard size area if (item.areaSnapshot.area.size !== null) { item.areaSnapshot.area.size = item.areaSnapshot.sizePixelAtStart + item.pixelAbsorb; } } } /** Injection token that can be used to specify default split options. */ const MTX_SPLIT_DEFAULT_OPTIONS = new InjectionToken('mtx-split-default-options'); /** * mtx-split * * * PERCENT MODE ([unit]="'percent'") * ___________________________________________________________________________________________ * | A [g1] B [g2] C [g3] D [g4] E | * |-------------------------------------------------------------------------------------------| * | 20 30 20 15 15 | <-- [size]="x" * | 10px 10px 10px 10px | <-- [gutterSize]="10" * |calc(20% - 8px) calc(30% - 12px) calc(20% - 8px) calc(15% - 6px) calc(15% - 6px)| <-- CSS flex-basis property (with flex-grow&shrink at 0) * | 152px 228px 152px 114px 114px | <-- el.getBoundingClientRect().width * |___________________________________________________________________________________________| * 800px <-- el.getBoundingClientRect().width * flex-basis = calc( { area.size }% - { area.size/100 * nbGutter*gutterSize }px ); * * * PIXEL MODE ([unit]="'pixel'") * ___________________________________________________________________________________________ * | A [g1] B [g2] C [g3] D [g4] E | * |-------------------------------------------------------------------------------------------| * | 100 250 * 150 100 | <-- [size]="y" * | 10px 10px 10px 10px | <-- [gutterSize]="10" * | 0 0 100px 0 0 250px 1 1 auto 0 0 150px 0 0 100px | <-- CSS flex property (flex-grow/flex-shrink/flex-basis) * | 100px 250px 200px 150px 100px | <-- el.getBoundingClientRect().width * |___________________________________________________________________________________________| * 800px <-- el.getBoundingClientRect().width * */ class MtxSplit { /** The split direction. */ get direction() { return this._direction; } set direction(v) { this._direction = v === 'vertical' ? 'vertical' : 'horizontal'; this.renderer.addClass(this.elRef.nativeElement, `mtx-split-${this._direction}`); this.renderer.removeClass(this.elRef.nativeElement, `mtx-split-${this._direction === 'vertical' ? 'horizontal' : 'vertical'}`); this.build(false, false); } /** The unit you want to specify area sizes. */ get unit() { return this._unit; } set unit(v) { this._unit = v === 'pixel' ? 'pixel' : 'percent'; this.renderer.addClass(this.elRef.nativeElement, `mtx-split-${this._unit}`); this.renderer.removeClass(this.elRef.nativeElement, `mtx-split-${this._unit === 'pixel' ? 'percent' : 'pixel'}`); this.build(false, true); } /** Gutters's size (dragging elements) in pixels. */ get gutterSize() { return this._gutterSize; } set gutterSize(v) { this._gutterSize = getInputPositiveNumber(v, 4); this.build(false, false); } /** Gutter step while moving in pixels. */ get gutterStep() { return this._gutterStep; } set gutterStep(v) { this._gutterStep = getInputPositiveNumber(v, 1); } /** Add transition when toggling visibility using `visible` or `size` changes. */ get useTransition() { return this._useTransition; } set useTransition(v) { this._useTransition = v; if (this._useTransition) { this.renderer.addClass(this.elRef.nativeElement, 'mtx-split-transition'); } else { this.renderer.removeClass(this.elRef.nativeElement, 'mtx-split-transition'); } } /** * Disable the dragging feature (remove cursor/image on gutters). * `gutterClick`/`gutterDblClick` still emits. */ get disabled() { return this._disabled; } set disabled(v) { this._disabled = v; if (this._disabled) { this.renderer.addClass(this.elRef.nativeElement, 'mtx-split-disabled'); } else { this.renderer.removeClass(this.elRef.nativeElement, 'mtx-split-disabled'); } } /** Indicates the directionality of the areas. */ get dir() { return this._dir; } set dir(v) { this._dir = v === 'rtl' ? 'rtl' : 'ltr'; this.renderer.setAttribute(this.elRef.nativeElement, 'dir', this._dir); } /** * Milliseconds to detect a double click on a gutter. Set it around 300-500ms if * you want to use `gutterDblClick` event. */ get gutterDblClickDuration() { return this._gutterDblClickDuration; } set gutterDblClickDuration(v) { this._gutterDblClickDuration = getInputPositiveNumber(v, 0); } /** * Event emitted when transition ends (could be triggered from `visible` or `size` changes). * Only if `useTransition` equals true. */ get transitionEnd() { return new Observable(subscriber => (this.transitionEndSubscriber = subscriber)).pipe(debounceTime(20)); } constructor() { this.ngZone = inject(NgZone); this.elRef = inject(ElementRef); this.cdRef = inject(ChangeDetectorRef); this.renderer = inject(Renderer2); this._defaultOptions = inject(MTX_SPLIT_DEFAULT_OPTIONS, { optional: true, }); this._direction = 'horizontal'; this._unit = 'percent'; this._gutterSize = 4; this._gutterStep = 1; /** Set to true if you want to limit gutter move to adjacent areas only. */ this.restrictMove = false; this._useTransition = false; this._disabled = false; this._dir = 'ltr'; this._gutterDblClickDuration = 0; /** Event emitted when drag starts. */ this.dragStart = new EventEmitter(false); /** Event emitted when drag ends. */ this.dragEnd = new EventEmitter(false); /** Event emitted when user clicks on a gutter. */ this.gutterClick = new EventEmitter(false); /** Event emitted when user double clicks on a gutter. */ this.gutterDblClick = new EventEmitter(false); this.dragProgressSubject = new Subject(); this.dragProgress$ = this.dragProgressSubject.asObservable(); this.isDragging = false; this.dragListeners = []; this.snapshot = null; this.startPoint = null; this.endPoint = null; this.displayedAreas = []; this.hidedAreas = []; this._clickTimeout = null; const _defaultOptions = this._defaultOptions; if (_defaultOptions) { this.color = _defaultOptions.color ?? 'primary'; this.direction = _defaultOptions.direction ?? 'horizontal'; this.dir = _defaultOptions.dir ?? 'ltr'; this.unit = _defaultOptions.unit ?? 'percent'; this.gutterDblClickDuration = _defaultOptions.gutterDblClickDuration ?? 0; this.gutterSize = _defaultOptions.gutterSize ?? 4; this.gutterStep = _defaultOptions.gutterStep ?? 1; this.restrictMove = _defaultOptions.restrictMove ?? false; this.useTransition = _defaultOptions.useTransition ?? false; } } ngAfterViewInit() { this.ngZone.runOutsideAngular(() => { // To avoid transition at first rendering setTimeout(() => this.renderer.addClass(this.elRef.nativeElement, 'mtx-split-init')); }); } getNbGutters() { return this.displayedAreas.length === 0 ? 0 : this.displayedAreas.length - 1; } addArea(component) { const newArea = { component, order: 0, size: 0, minSize: null, maxSize: null, }; if (component.visible === true) { this.displayedAreas.push(newArea); this.build(true, true); } else { this.hidedAreas.push(newArea); } } removeArea(component) { if (this.displayedAreas.some(a => a.component === component)) { const area = this.displayedAreas.find(a => a.component === component); this.displayedAreas.splice(this.displayedAreas.indexOf(area), 1); this.build(true, true); } else if (this.hidedAreas.some(a => a.component === component)) { const area = this.hidedAreas.find(a => a.component === component); this.hidedAreas.splice(this.hidedAreas.indexOf(area), 1); } } updateArea(component, resetOrders, resetSizes) { if (component.visible === true) { this.build(resetOrders, resetSizes); } } showArea(component) { const area = this.hidedAreas.find(a => a.component === component); if (area === undefined) { return; } 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.component === comp); if (area === undefined) { return; } const areas = this.displayedAreas.splice(this.displayedAreas.indexOf(area), 1); areas.forEach(_area => { _area.order = 0; _area.size = 0; }); this.hidedAreas.push(...areas); this.build(true, true); } getVisibleAreaSizes() { return this.displayedAreas.map(a => (a.size === null ? '*' : a.size)); } setVisibleAreaSizes(sizes) { if (sizes.length !== this.displayedAreas.length) { return false; } const formatedSizes = sizes.map(s => getInputPositiveNumber(s, null)); const isValid = isUserSizesValid(this.unit, formatedSizes); if (isValid === false) { return false; } this.displayedAreas.forEach((area, i) => (area.component.size = formatedSizes[i])); this.build(false, true); return true; } 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.component.order !== null)) { this.displayedAreas.sort((a, b) => a.component.order - b.component.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.component.setStyleOrder(area.order); }); } // ¤ AREAS SIZE if (resetSizes === true) { const useUserSizes = isUserSizesValid(this.unit, this.displayedAreas.map(a => a.component.size)); switch (this.unit) { case 'percent': { const defaultSize = 100 / this.displayedAreas.length; this.displayedAreas.forEach(area => { area.size = useUserSizes ? area.component.size : defaultSize; area.minSize = getAreaMinSize(area); area.maxSize = getAreaMaxSize(area); }); break; } case 'pixel': { if (useUserSizes) { this.displayedAreas.forEach(area => { area.size = area.component.size; area.minSize = getAreaMinSize(area); area.maxSize = getAreaMaxSize(area); }); } else { const wildcardSizeAreas = this.displayedAreas.filter(a => a.component.size === null); // No wildcard area > Need to select one arbitrarily > first if (wildcardSizeAreas.length === 0 && this.displayedAreas.length > 0) { this.displayedAreas.forEach((area, i) => { area.size = i === 0 ? null : area.component.size; area.minSize = i === 0 ? null : getAreaMinSize(area); area.maxSize = i === 0 ? null : getAreaMaxSize(area); }); } // More than one wildcard area > Need to keep only one arbitrarly > first else if (wildcardSizeAreas.length > 1) { let alreadyGotOne = false; this.displayedAreas.forEach(area => { if (area.component.size === null) { if (alreadyGotOne === false) { area.size = null; area.minSize = null; area.maxSize = null; alreadyGotOne = true; } else { area.size = 100; area.minSize = null; area.maxSize = null; } } else { area.size = area.component.size; area.minSize = getAreaMinSize(area); area.maxSize = getAreaMaxSize(area); } }); } } break; } } } this.refreshStyleSizes(); this.cdRef.markForCheck(); } refreshStyleSizes() { /////////////////////////////////////////// // PERCENT MODE if (this.unit === 'percent') { // Only one area > flex-basis 100% if (this.displayedAreas.length === 1) { this.displayedAreas[0].component.setStyleFlex(0, 0, `100%`, false, false); } // Multiple areas > use each percent basis else { const sumGutterSize = this.getNbGutters() * this.gutterSize; this.displayedAreas.forEach(area => { area.component.setStyleFlex(0, 0, `calc( ${area.size}% - ${(area.size / 100) * sumGutterSize}px )`, area.minSize !== null && area.minSize === area.size ? true : false, area.maxSize !== null && area.maxSize === area.size ? true : false); }); } } /////////////////////////////////////////// // PIXEL MODE else if (this.unit === 'pixel') { this.displayedAreas.forEach(area => { // Area with wildcard size if (area.size === null) { if (this.displayedAreas.length === 1) { area.component.setStyleFlex(1, 1, `100%`, false, false); } else { area.component.setStyleFlex(1, 1, `auto`, false, false); } } // Area with pixel size else { // Only one area > flex-basis 100% if (this.displayedAreas.length === 1) { area.component.setStyleFlex(0, 0, `100%`, false, false); } // Multiple areas > use each pixel basis else { area.component.setStyleFlex(0, 0, `${area.size}px`, area.minSize !== null && area.minSize === area.size ? true : false, area.maxSize !== null && area.maxSize === area.size ? true : false); } } }); } } clickGutter(event, gutterNum) { const tempPoint = getPointFromEvent(event); // Be sure mouseup/touchend happened at same point as mousedown/touchstart to trigger click/dblclick if (this.startPoint && this.startPoint.x === tempPoint.x && this.startPoint.y === tempPoint.y) { // If timeout in progress and new click > clearTimeout & dblClickEvent if (this._clickTimeout !== null) { window.clearTimeout(this._clickTimeout); this._clickTimeout = null; this.notify('dblclick', gutterNum); this.stopDragging(); } // Else start timeout to call clickEvent at end else { this._clickTimeout = window.setTimeout(() => { this._clickTimeout = null; this.notify('click', gutterNum); this.stopDragging(); }, this.gutterDblClickDuration); } } } startDragging(event, gutterOrder, gutterNum) { event.preventDefault(); event.stopPropagation(); this.startPoint = getPointFromEvent(event); if (this.startPoint === null || this.disabled === true) { return; } this.snapshot = { gutterNum, lastSteppedOffset: 0, allAreasSizePixel: getElementPixelSize(this.elRef, this.direction) - this.getNbGutters() * this.gutterSize, allInvolvedAreasSizePercent: 100, areasBeforeGutter: [], areasAfterGutter: [], }; this.displayedAreas.forEach(area => { const areaSnapshot = { area, sizePixelAtStart: getElementPixelSize(area.component.elRef, this.direction), sizePercentAtStart: (this.unit === 'percent' ? area.size : -1), // If pixel mode, anyway, will not be used. }; if (area.order < gutterOrder) { if (this.restrictMove === true) { this.snapshot.areasBeforeGutter = [areaSnapshot]; } else { this.snapshot.areasBeforeGutter.unshift(areaSnapshot); } } else if (area.order > gutterOrder) { if (this.restrictMove === true) { if (this.snapshot.areasAfterGutter.length === 0) { this.snapshot.areasAfterGutter = [areaSnapshot]; } } else { this.snapshot.areasAfterGutter.push(areaSnapshot); } } }); this.snapshot.allInvolvedAreasSizePercent = [ ...this.snapshot.areasBeforeGutter, ...this.snapshot.areasAfterGutter, ].reduce((t, a) => t + a.sizePercentAtStart, 0); if (this.snapshot.areasBeforeGutter.length === 0 || this.snapshot.areasAfterGutter.length === 0) { return; } this.dragListeners.push(this.renderer.listen('document', 'mouseup', this.stopDragging.bind(this))); this.dragListeners.push(this.renderer.listen('document', 'touchend', this.stopDragging.bind(this))); this.dragListeners.push(this.renderer.listen('document', 'touchcancel', this.stopDragging.bind(this))); this.ngZone.runOutsideAngular(() => { this.dragListeners.push(this.renderer.listen('document', 'mousemove', this.dragEvent.bind(this))); this.dragListeners.push(this.renderer.listen('document', 'touchmove', this.dragEvent.bind(this))); }); this.displayedAreas.forEach(area => area.component.lockEvents()); this.isDragging = true; this.renderer.addClass(this.elRef.nativeElement, 'mtx-dragging'); this.renderer.addClass(this.gutterEls.toArray()[this.snapshot.gutterNum - 1].nativeElement, 'mtx-dragged'); this.notify('start', this.snapshot.gutterNum); } dragEvent(event) { event.preventDefault(); event.stopPropagation(); if (this._clickTimeout !== null) { window.clearTimeout(this._clickTimeout); this._clickTimeout = null; } if (this.isDragging === false) { return; } this.endPoint = getPointFromEvent(event); if (this.endPoint === null) { return; } // Calculate steppedOffset let offset = this.direction === 'horizontal' ? this.startPoint.x - this.endPoint.x : this.startPoint.y - this.endPoint.y; if (this.dir === 'rtl' && this.direction === 'horizontal') { offset = -offset; } const steppedOffset = Math.round(offset / this.gutterStep) * this.gutterStep; if (steppedOffset === this.snapshot.lastSteppedOffset) { return; } this.snapshot.lastSteppedOffset = steppedOffset; // Need to know if each gutter side areas could reacts to steppedOffset let areasBefore = getGutterSideAbsorptionCapacity(this.unit, this.snapshot.areasBeforeGutter, -steppedOffset, this.snapshot.allAreasSizePixel); let areasAfter = getGutterSideAbsorptionCapacity(this.unit, this.snapshot.areasAfterGutter, steppedOffset, this.snapshot.allAreasSizePixel); // Each gutter side areas can't absorb all offset if (areasBefore.remain !== 0 && areasAfter.remain !== 0) { if (Math.abs(areasBefore.remain) === Math.abs(areasAfter.remain)) { /** */ } else if (Math.abs(areasBefore.remain) > Math.abs(areasAfter.remain)) { areasAfter = getGutterSideAbsorptionCapacity(this.unit, this.snapshot.areasAfterGutter, steppedOffset + areasBefore.remain, this.snapshot.allAreasSizePixel); } else { areasBefore = getGutterSideAbsorptionCapacity(this.unit, this.snapshot.areasBeforeGutter, -(steppedOffset - areasAfter.remain), this.snapshot.allAreasSizePixel); } } // Areas before gutter can't absorbs all offset > need to recalculate sizes for areas after gutter. else if (areasBefore.remain !== 0) { areasAfter = getGutterSideAbsorptionCapacity(this.unit, this.snapshot.areasAfterGutter, steppedOffset + areasBefore.remain, this.snapshot.allAreasSizePixel); } // Areas after gutter can't absorbs all offset > need to recalculate sizes for areas before gutter. else if (areasAfter.remain !== 0) { areasBefore = getGutterSideAbsorptionCapacity(this.unit, this.snapshot.areasBeforeGutter, -(steppedOffset - areasAfter.remain), this.snapshot.allAreasSizePixel); } if (this.unit === 'percent') { // Hack because of browser messing up with sizes using calc(X% - Ypx) -> el.getBoundingClientRect() // If not there, playing with gutters makes total going down to 99.99875% then 99.99286%, 99.98986%,.. const all = [...areasBefore.list, ...areasAfter.list]; const areaToReset = all.find(a => a.percentAfterAbsorption !== 0 && a.percentAfterAbsorption !== a.areaSnapshot.area.minSize && a.percentAfterAbsorption !== a.areaSnapshot.area.maxSize); if (areaToReset) { areaToReset.percentAfterAbsorption = this.snapshot.allInvolvedAreasSizePercent - all .filter(a => a !== areaToReset) .reduce((total, a) => total + a.percentAfterAbsorption, 0); } } // Now we know areas could absorb steppedOffset, time to really update sizes areasBefore.list.forEach(item => updateAreaSize(this.unit, item)); areasAfter.list.forEach(item => updateAreaSize(this.unit, item)); this.refreshStyleSizes(); this.notify('progress', this.snapshot.gutterNum); } stopDragging(event) { if (event) { event.preventDefault(); event.stopPropagation(); } if (this.isDragging === false) { return; } this.displayedAreas.forEach(area => area.component.unlockEvents()); while (this.dragListeners.length > 0) { const fct = this.dragListeners.pop(); if (fct) { fct(); } } // Warning: Have to be before "notify('end')" // because "notify('end')"" can be linked to "[size]='x'" > "build()" > "stopDragging()" this.isDragging = false; // If moved from starting point, notify end if (this.endPoint && (this.startPoint.x !== this.endPoint.x || this.startPoint.y !== this.endPoint.y)) { this.notify('end', this.snapshot.gutterNum); } this.renderer.removeClass(this.elRef.nativeElement, 'mtx-dragging'); this.renderer.removeClass(this.gutterEls.toArray()[this.snapshot.gutterNum - 1].nativeElement, 'mtx-dragged'); this.snapshot = null; // Needed to let (click)="clickGutter(...)" event run and verify if mouse moved or not this.ngZone.runOutsideAngular(() => { setTimeout(() => { this.startPoint = null; this.endPoint = null; }); }); } notify(type, gutterNum) { const sizes = this.getVisibleAreaSizes(); if (type === 'start') { this.dragStart.emit({ gutterNum, sizes }); } else if (type === 'end') { this.dragEnd.emit({ gutterNum, sizes }); } else if (type === 'click') { this.gutterClick.emit({ gutterNum, sizes }); } else if (type === 'dblclick') { this.gutterDblClick.emit({ gutterNum, sizes }); } else if (type === 'transitionEnd') { if (this.transitionEndSubscriber) { this.ngZone.run(() => this.transitionEndSubscriber.next(sizes)); } } else if (type === 'progress') { // Stay outside zone to allow users do what they want about change detection mechanism. this.dragProgressSubject.next({ gutterNum, sizes }); } } ngOnDestroy() { this.stopDragging(); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxSplit, deps: [], target: i0.ɵɵFactoryTarget.Component }); } /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MtxSplit, isStandalone: true, selector: "mtx-split", inputs: { color: "color", direction: "direction", unit: "unit", gutterSize: "gutterSize", gutterStep: "gutterStep", restrictMove: ["restrictMove", "restrictMove", booleanAttribute], useTransition: ["useTransition", "useTransition", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], dir: "dir", gutterDblClickDuration: "gutterDblClickDuration" }, outputs: { dragStart: "dragStart", dragEnd: "dragEnd", gutterClick: "gutterClick", gutterDblClick: "gutterDblClick", transitionEnd: "transitionEnd" }, host: { classAttribute: "mtx-split" }, viewQueries: [{ propertyName: "gutterEls", predicate: ["gutterEls"], descendants: true }], exportAs: ["mtxSplit"], ngImport: i0, template: "<ng-content></ng-content>\r\n@for (area of displayedAreas; track area; let index = $index; let last = $last) {\r\n @if (!last) {\r\n <div\r\n #gutterEls\r\n class=\"mtx-split-gutter\"\r\n [class]=\"color ? 'mat-' + color : ''\"\r\n [style.flex-basis.px]=\"gutterSize\"\r\n [style.order]=\"index * 2 + 1\"\r\n (mousedown)=\"startDragging($event, index * 2 + 1, index + 1)\"\r\n (touchstart)=\"startDragging($event, index * 2 + 1, index + 1)\"\r\n (mouseup)=\"clickGutter($event, index + 1)\"\r\n (touchend)=\"clickGutter($event, index + 1)\"\r\n >\r\n <div class=\"mtx-split-gutter-handle\"></div>\r\n </div>\r\n }\r\n}\r\n", styles: [".mtx-split{display:flex;flex-wrap:nowrap;justify-content:flex-start;align-items:stretch;overflow:hidden;width:100%;height:100%}.mtx-split>.mtx-split-gutter{position:relative;display:flex;flex-grow:0;flex-shrink:0;align-items:center;justify-content:center;background-color:var(--mtx-split-gutter-background-color, var(--mat-sys-outline-variant))}.mtx-split>.mtx-split-gutter:hover{background-color:var(--mtx-split-gutter-hover-state-background-color, var(--mat-sys-primary))}.mtx-split>.mtx-split-gutter>.mtx-split-gutter-handle{position:absolute;opacity:0}.mtx-split>.mtx-split-pane{flex-grow:0;flex-shrink:0;overflow:hidden auto}.mtx-split>.mtx-split-pane.mtx-split-pane-hidden{flex:0 1 0!important;overflow:hidden}.mtx-split.mtx-split-horizontal{flex-direction:row}.mtx-split.mtx-split-horizontal>.mtx-split-gutter{flex-direction:row;height:100%;cursor:col-resize}.mtx-split.mtx-split-horizontal>.mtx-split-gutter>.mtx-split-gutter-handle{width:8px;height:100%;left:-2px;right:2px}.mtx-split.mtx-split-horizontal>.mtx-split-pane{height:100%}.mtx-split.mtx-split-vertical{flex-direction:column}.mtx-split.mtx-split-vertical>.mtx-split-gutter{flex-direction:column;width:100%;cursor:row-resize}.mtx-split.mtx-split-vertical>.mtx-split-gutter>.mtx-split-gutter-handle{width:100%;height:8px;top:-2px;bottom:2px}.mtx-split.mtx-split-vertical>.mtx-split-pane{width:100%}.mtx-split.mtx-split-vertical>.mtx-split-pane.mtx-split-pane-hidden{max-width:0}.mtx-split.mtx-split-disabled>.mtx-split-gutter{cursor:default}.mtx-split.mtx-split-disabled>.mtx-split-gutter .mtx-split-gutter-handle{background-image:none}.mtx-split.mtx-split-transition.mtx-split-init:not(.mtx-dragging)>.mtx-split-gutter,.mtx-split.mtx-split-transition.mtx-split-init:not(.mtx-dragging)>.mtx-split-pane{transition:flex-basis .3s}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxSplit, decorators: [{ type: Component, args: [{ selector: 'mtx-split', exportAs: 'mtxSplit', host: { class: 'mtx-split', }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>\r\n@for (area of displayedAreas; track area; let index = $index; let last = $last) {\r\n @if (!last) {\r\n <div\r\n #gutterEls\r\n class=\"mtx-split-gutter\"\r\n [class]=\"color ? 'mat-' + color : ''\"\r\n [style.flex-basis.px]=\"gutterSize\"\r\n [style.order]=\"index * 2 + 1\"\r\n (mousedown)=\"startDragging($event, index * 2 + 1, index + 1)\"\r\n (touchstart)=\"startDragging($event, index * 2 + 1, index + 1)\"\r\n (mouseup)=\"clickGutter($event, index + 1)\"\r\n (touchend)=\"clickGutter($event, index + 1)\"\r\n >\r\n <div class=\"mtx-split-gutter-handle\"></div>\r\n </div>\r\n }\r\n}\r\n", styles: [".mtx-split{display:flex;flex-wrap:nowrap;justify-content:flex-start;align-items:stretch;overflow:hidden;width:100%;height:100%}.mtx-split>.mtx-split-gutter{position:relative;display:flex;flex-grow:0;flex-shrink:0;align-items:center;justify-content:center;background-color:var(--mtx-split-gutter-background-color, var(--mat-sys-outline-variant))}.mtx-split>.mtx-split-gutter:hover{background-color:var(--mtx-split-gutter-hover-state-background-color, var(--mat-sys-primary))}.mtx-split>.mtx-split-gutter>.mtx-split-gutter-handle{position:absolute;opacity:0}.mtx-split>.mtx-split-pane{flex-grow:0;flex-shrink:0;overflow:hidden auto}.mtx-split>.mtx-split-pane.mtx-split-pane-hidden{flex:0 1 0!important;overflow:hidden}.mtx-split.mtx-split-horizontal{flex-direction:row}.mtx-split.mtx-split-horizontal>.mtx-split-gutter{flex-direction:row;height:100%;cursor:col-resize}.mtx-split.mtx-split-horizontal>.mtx-split-gutter>.mtx-split-gutter-handle{width:8px;height:100%;left:-2px;right:2px}.mtx-split.mtx-split-horizontal>.mtx-split-pane{height:100%}.mtx-split.mtx-split-vertical{flex-direction:column}.mtx-split.mtx-split-vertical>.mtx-split-gutter{flex-direction:column;width:100%;cursor:row-resize}.mtx-split.mtx-split-vertical>.mtx-split-gutter>.mtx-split-gutter-handle{width:100%;height:8px;top:-2px;bottom:2px}.mtx-split.mtx-split-vertical>.mtx-split-pane{width:100%}.mtx-split.mtx-split-vertical>.mtx-split-pane.mtx-split-pane-hidden{max-width:0}.mtx-split.mtx-split-disabled>.mtx-split-gutter{cursor:default}.mtx-split.mtx-split-disabled>.mtx-split-gutter .mtx-split-gutter-handle{background-image:none}.mtx-split.mtx-split-transition.mtx-split-init:not(.mtx-dragging)>.mtx-split-gutter,.mtx-split.mtx-split-transition.mtx-split-init:not(.mtx-dragging)>.mtx-split-pane{transition:flex-basis .3s}\n"] }] }], ctorParameters: () => [], propDecorators: { color: [{ type: Input }], direction: [{ type: Input }], unit: [{ type: Input }], gutterSize: [{ type: Input }], gutterStep: [{ type: Input }], restrictMove: [{ type: Input, args: [{ transform: booleanAttribute }] }], useTransition: [{ type: Input, args: [{ transform: booleanAttribute }] }], disabled: [{ type: Input, args: [{ transform: booleanAttribute }] }], dir: [{ type: Input }], gutterDblClickDuration: [{ type: Input }], dragStart: [{ type: Output }], dragEnd: [{ type: Output }], gutterClick: [{ type: Output }], gutterDblClick: [{ type: Output }], transitionEnd: [{ type: Output }], gutterEls: [{ type: ViewChildren, args: ['gutterEls'] }] } }); class MtxSplitPane { /** * Order of the area. Used to maintain the order of areas when toggling their visibility. * Toggling area visibility without specifying an `order` leads to weird behavior. */ get order() { return this._order; } set order(v) { this._order = getInputPositiveNumber(v, null); this.split.updateArea(this, true, false); } /** * Size of the area in selected unit (percent/pixel). * - Percent: All areas sizes should equal to `100`, If not, all areas will have the same size. * - Pixel: An area with wildcard size (`size="*"`) is mandatory (only one) and * can't have `visible="false"` or `minSize`/`maxSize`/`lockSize` properties. */ get size() { return this._size; } set size(v) { this._size = getInputPositiveNumber(v, null); this.split.updateArea(this, false, true); } /** Minimum pixel or percent size, should be equal to or smaller than provided `size`. */ get minSize() { return this._minSize; } set minSize(v) { this._minSize = getInputPositiveNumber(v, null); this.split.updateArea(this, false, true); } /** Maximum pixel or percent size, should be equal to or larger than provided `size`. */ get maxSize() { return this._maxSize; } set maxSize(v) { this._maxSize = getInputPositiveNumber(v, null); this.split.updateArea(this, false, true); } /** Lock area size, same as `minSize`=`maxSize`=`size`. */ get lockSize() { return this._lockSize; } set lockSize(v) { this._lockSize = v; this.split.updateArea(this, false, true); } /** Hide area visually but still present in the DOM, use `ngIf` to completely remove it. */ get visible() { return this._visible; } set visible(v) { this._visible = v; if (this._visible) { this.split.showArea(this); this.renderer.removeClass(this.elRef.nativeElement, 'mtx-split-pane-hidden'); } else { this.split.hideArea(this); this.renderer.addClass(this.elRef.nativeElement, 'mtx-split-pane-hidden'); } } constructor() { this.ngZone = inject(NgZone); this.renderer = inject(Renderer2); this.split = inject(MtxSplit); this.elRef = inject(ElementRef); this._order = null; this._size = null; this._minSize = null; this._maxSize = null; this._lockSize = false; this._visible = true; this.lockListeners = []; this.renderer.addClass(this.elRef.nativeElement, 'mtx-split-pane'); } ngOnInit() { this.split.addArea(this); this.ngZone.runOutsideAngular(() => { this.transitionListener = this.renderer.listen(this.elRef.nativeElement, 'transitionend', (event) => { // Limit only flex-basis transition to trigger the event if (event.propertyName === 'flex-basis') { this.split.notify('transitionEnd', -1); } }); }); } setStyleOrder(value) { this.renderer.setStyle(this.elRef.nativeElement, 'order', value); } setStyleFlex(grow, shrink, basis, isMin, isMax) { // Need 3 separated properties to work on IE11 (https://github.com/angular/flex-layout/issues/323) this.renderer.setStyle(this.elRef.nativeElement, 'flex-grow', grow); this.renderer.setStyle(this.elRef.nativeElement, 'flex-shrink', shrink); this.renderer.setStyle(this.elRef.nativeElement, 'flex-basis', basis); if (isMin === true) { this.renderer.addClass(this.elRef.nativeElement, 'mtx-min'); } else { this.renderer.removeClass(this.elRef.nativeElement, 'mtx-min'); } if (isMax === true) { this.renderer.addClass(this.elRef.nativeElement, 'mtx-max'); } else { this.renderer.removeClass(this.elRef.nativeElement, 'mtx-max'); } } lockEvents() { this.ngZone.runOutsideAngular(() => { this.lockListeners.push(this.renderer.listen(this.elRef.nativeElement, 'selectstart', (e) => false)); this.lockListeners.push(this.renderer.listen(this.elRef.nativeElement, 'dragstart', (e) => false)); }); } unlockEvents() { while (this.lockListeners.length > 0) { const fct = this.lockListeners.pop(); if (fct) { fct(); } } } ngOnDestroy() { this.unlockEvents(); if (this.transitionListener) { this.transitionListener(); } this.split.removeArea(this); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxSplitPane, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "21.2.5", type: MtxSplitPane, isStandalone: true, selector: "mtx-split-pane, [mtx-split-pane]", inputs: { order: "order", size: "size", minSize: "minSize", maxSize: "maxSize", lockSize: ["lockSize", "lockSize", booleanAttribute], visible: ["visible", "visible", booleanAttribute] }, exportAs: ["mtxSplitPane"], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxSplitPane, decorators: [{ type: Directive, args: [{ selector: 'mtx-split-pane, [mtx-split-pane]', exportAs: 'mtxSplitPane', }] }], ctorParameters: () => [], propDecorators: { order: [{ type: Input }], size: [{ type: Input }], minSize: [{ type: Input }], maxSize: [{ type: Input }], lockSize: [{ type: Input, args: [{ transform: booleanAttribute }] }], visible: [{ type: Input, args: [{ transform: booleanAttribute }] }] } }); class MtxSplitModule { /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxSplitModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } /** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.5", ngImport: i0, type: MtxSplitModule, imports: [CommonModule, MtxSplit, MtxSplitPane], exports: [MtxSplit, MtxSplitPane] }); } /** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxSplitModule, imports: [CommonModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxSplitModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule, MtxSplit, MtxSplitPane],