@ng-matero/extensions
Version:
Angular Material Extensions
1,020 lines (1,015 loc) • 50.5 kB
JavaScript
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],