@memberjunction/ng-container-directives
Version:
MemberJunction: Angular Container Directives - Fill Container for Auto-Resizing, and plain container just for element identification/binding
253 lines • 11.1 kB
JavaScript
import { Directive, Input } from '@angular/core';
import { LogError, LogStatus } from '@memberjunction/core';
import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { MJEventType, MJGlobal } from '@memberjunction/global';
import * as i0 from "@angular/core";
/**
* Directive that automatically resizes an element to fill its parent container.
* This directive calculates and sets dimensions based on the parent container's size,
* accounting for the element's position within the parent and any specified margins.
*
* It listens for window resize events and custom MJ application events to update dimensions.
* The directive is context-aware and will automatically skip resizing in certain conditions.
*
* @example
* <!-- Basic usage (fills both width and height) -->
* <div >Content</div>
*
* <!-- With custom settings -->
* <div [fillWidth]="true" [fillHeight]="true" [rightMargin]="10" [bottomMargin]="20">
* Content with margins
* </div>
*/
export class FillContainer {
elementRef;
/** Whether to fill the parent's width. Default is true. */
fillWidth = true;
/** Whether to fill the parent's height. Default is true. */
fillHeight = true;
/** Right margin in pixels. Default is 0. */
rightMargin = 0;
/** Bottom margin in pixels. Default is 0. */
bottomMargin = 0;
/** Flag to globally disable resize functionality for all instances */
static DisableResize = false;
/** Flag to enable debug logging for resize operations */
static OutputDebugInfo = false;
/**
* Outputs a debug message if OutputDebugInfo is enabled
* @param message The message to output
*/
static OutputDebugMessage(message) {
if (FillContainer.OutputDebugInfo) {
console.log(message);
}
}
constructor(elementRef) {
this.elementRef = elementRef;
}
/** Debounce time for resize events during active resizing (milliseconds) */
_resizeDebounceTime = 100;
/** Debounce time for resize end events (milliseconds) */
_resizeEndDebounceTime = 500;
/** Subscription for resize end events */
_resizeSubscription = null;
/** Subscription for immediate resize events */
_resizeImmediateSubscription = null;
/**
* Initializes the directive, sets up resize event listeners and performs initial resize
*/
ngOnInit() {
const el = this.elementRef.nativeElement;
if (el && el.style) {
// initial resize
FillContainer.OutputDebugMessage('');
FillContainer.OutputDebugMessage('Initial resize event');
this.resizeElement();
// This will fire more frequently while the user is resizing so use a shorter debounce time
this._resizeImmediateSubscription = fromEvent(window, 'resize')
.pipe(debounceTime(this._resizeDebounceTime))
.subscribe(() => {
FillContainer.OutputDebugMessage('');
FillContainer.OutputDebugMessage('RECEIVED resize event');
this.resizeElement();
});
// This will fire once the user has stopped resizing for _resizeEndDebounceTime milliseconds
this._resizeSubscription = fromEvent(window, 'resize')
.pipe(debounceTime(this._resizeEndDebounceTime))
.subscribe(() => {
FillContainer.OutputDebugMessage('');
FillContainer.OutputDebugMessage('RECEIVED resize end event');
this.resizeElement();
});
// // Subscribe once but handle both scenarios
// this._resizeSubscription = fromEvent(window, 'resize').pipe(
// throttleTime(this._resizeDebounceTime), // handles frequent resizes
// tap(() => {
// this.resizeElement();
// FillContainer.OutputDebugMessage('RECEIVED resize event');
// }),
// debounceTime(this._resizeEndDebounceTime), // handles end of resizing
// finalize(() => {
// this.resizeElement();
// FillContainer.OutputDebugMessage('RECEIVED resize end event');
// })
// ).subscribe();
// also subscribe to MJGlobal events so we can monitor for a manually invoked resize event request
// from another component
MJGlobal.Instance.GetEventListener(true)
//.pipe(debounceTime(this._resizeDebounceTime))
.subscribe((event) => {
if (event.event === MJEventType.ManualResizeRequest) {
FillContainer.OutputDebugMessage('');
FillContainer.OutputDebugMessage('RECEIVED manual resize request');
this.resizeElement();
}
});
}
}
/**
* Cleans up subscriptions when the directive is destroyed
*/
ngOnDestroy() {
this._resizeImmediateSubscription?.unsubscribe();
this._resizeSubscription?.unsubscribe();
}
/**
* Finds the nearest block-level parent element
* @param element The element to find the parent for
* @returns The parent element or null if none found
*/
getParent(element) {
let curElement = element;
while (curElement && curElement.nodeName !== 'HTML') {
if (curElement.parentElement && window.getComputedStyle(curElement.parentElement).display === 'block') {
return curElement.parentElement;
}
curElement = curElement.parentElement;
}
return curElement;
}
/**
* Performs the actual resize calculation and applies dimensions to the element
*/
resizeElement() {
if (FillContainer.DisableResize) {
// global disable flag
return;
}
const element = this.elementRef.nativeElement;
try {
if (element && element.style && !this.shouldSkipResize(element)) {
const parent = this.getParent(element);
if (parent && !this.elementBelowHiddenTab(element)) {
let parentStyle = window.getComputedStyle(parent);
if (parentStyle.visibility === 'hidden' || parentStyle.display === 'none') {
LogStatus('skipping hidden element: ' + parent.nodeName);
}
else {
FillContainer.OutputDebugMessage('Resizing element: ' + element.nodeName + ' parent: ' + parent.nodeName);
const parentRect = parent.getBoundingClientRect();
if (parent.nodeName === 'HTML') {
parentRect.height = window.innerHeight;
}
const elementRect = element.getBoundingClientRect();
let paddingTop = parseInt(parentStyle.getPropertyValue('padding-top'));
let paddingLeft = parseInt(parentStyle.getPropertyValue('padding-left'));
if (this.fillWidth) {
const widthVariance = (elementRect.left - parentRect.left) + paddingLeft + (paddingLeft > 0 ? 1 : 0); // add 1 to account for rounding errors
const newWidth = Math.floor(parentRect.width - this.rightMargin - widthVariance);
if (Math.floor(elementRect.width) !== newWidth) {
element.style.width = newWidth + 'px';
}
}
if (this.fillHeight) {
const heightVariance = (elementRect.top - parentRect.top) + paddingTop + (paddingTop > 0 ? 1 : 0); // add 1 to account for rounding errors
const newHeight = Math.floor(parentRect.height - this.bottomMargin - heightVariance);
if (Math.floor(elementRect.height) !== newHeight) {
element.style.height = newHeight + 'px';
}
}
}
}
}
}
catch (err) {
LogError(err);
}
}
/**
* Determines if resizing should be skipped for this element
* @param el The element to check
* @returns True if resizing should be skipped, false otherwise
*/
shouldSkipResize(el) {
let cur = el;
while (cur) {
if (cur.hasAttribute('mjSkipResize') || cur.role === 'grid') {
return true;
}
cur = cur.parentElement;
}
return false;
}
;
/**
* Checks if element is below a hidden tab
* @param element The element to check
* @returns True if element is below a hidden tab, false otherwise
*/
elementBelowHiddenTab(element) {
// check if the element is below a hidden tab, a hidden tab will have a class of .k-tabstrip-content and also have .k-active applied
// we can go all the way up the tree to look for this
let parent = element.parentElement;
while (parent) {
if (parent.role === 'tabpanel') {
// element is below a tab
if (!parent.classList.contains('k-active'))
return true; // tab is NOT active
else
return false; // tab IS active
}
parent = parent.parentElement;
}
// not below a tab at all
return false;
}
/**
* Checks if element is within a grid
* @param element The element to check
* @returns True if element is within a grid, false otherwise
*/
elementWithinGrid(element) {
// check if the element is within a kendo grid
let parent = element.parentElement;
while (parent) {
if (parent.role === 'grid') {
// element is below a grid
return true;
}
parent = parent.parentElement;
}
// not below a grid
return false;
}
static ɵfac = function FillContainer_Factory(t) { return new (t || FillContainer)(i0.ɵɵdirectiveInject(i0.ElementRef)); };
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: FillContainer, selectors: [["", "mjFillContainer", ""]], inputs: { fillWidth: "fillWidth", fillHeight: "fillHeight", rightMargin: "rightMargin", bottomMargin: "bottomMargin" } });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FillContainer, [{
type: Directive,
args: [{
selector: '[mjFillContainer]'
}]
}], () => [{ type: i0.ElementRef }], { fillWidth: [{
type: Input
}], fillHeight: [{
type: Input
}], rightMargin: [{
type: Input
}], bottomMargin: [{
type: Input
}] }); })();
//# sourceMappingURL=ng-fill-container-directive.js.map