UNPKG

ngx-foldable

Version:

Angular library to help you build dual-screen experiences for foldable or dual-screen devices

667 lines (656 loc) 25.2 kB
import * as i0 from '@angular/core'; import { Injectable, Directive, Input, HostBinding, Host, NgModule } from '@angular/core'; import { ReplaySubject, merge, fromEvent } from 'rxjs'; import { filter, startWith, map, shareReplay, takeUntil } from 'rxjs/operators'; /** Media query used to detect dual vertical viewports screen mode. */ const dualVerticalViewport = '(vertical-viewport-segments: 2)'; /** Media query used to detect dual horizontal viewports screen mode. */ const dualHorizontalViewport = '(horizontal-viewport-segments: 2)'; /** Enumeration of screen spanning mode values. */ const ScreenSpanning = { /** Screen spanning mode is dual horizontal viewports. */ DualHorizontal: 'dual-horizontal', /** Screen spanning mode is dual vertical viewports. */ DualVertical: 'dual-vertical', /** No screen spanning (single screen mode). */ None: 'none', }; /** * This service allows to query and receive updates about current device's * screen context. * * See {@link ScreenContextData} */ class ScreenContext { constructor() { this.destroyed$ = new ReplaySubject(1); this.currentContext = this.getScreenContext(); this.screenContext$ = merge(fromEvent(matchMedia(dualVerticalViewport), 'change'), fromEvent(matchMedia(dualHorizontalViewport), 'change')).pipe(filter(() => this.getScreenSpanning() !== this.currentContext.screenSpanning), startWith(1), map(() => { this.currentContext = this.getScreenContext(); return this.currentContext; }), shareReplay(1), takeUntil(this.destroyed$)); this.screenContext$.subscribe(); } /** @ignored */ ngOnDestroy() { this.destroyed$.next(); this.destroyed$.complete(); } /** * The list of available window segments. */ get windowSegments() { return this.currentContext.windowSegments; } /** * The current screen spanning mode. */ get screenSpanning() { return this.currentContext.screenSpanning; } /** * True is current device have multiple screens available. */ get isMultiScreen() { return this.currentContext.isMultiScreen; } /** * Gets an observable emitting when the screen context changes. */ asObservable() { return this.screenContext$; } /** * Gets the current screen context. */ asObject() { return this.currentContext; } getScreenContext() { const windowSegments = this.getWindowSegments(); const screenSpanning = this.getScreenSpanning(); return { windowSegments, screenSpanning, isMultiScreen: screenSpanning !== ScreenSpanning.None, }; } getScreenSpanning() { if (matchMedia(dualVerticalViewport).matches) { return ScreenSpanning.DualVertical; } else if (matchMedia(dualHorizontalViewport).matches) { return ScreenSpanning.DualHorizontal; } return ScreenSpanning.None; } getWindowSegments() { if ('getWindowSegments' in window) { console.warn('getWindowSegments() is not supported anymore, please update your browser to use the new visualViewport API'); } if ('visualViewport' in window) { return window.visualViewport.segments; } return [ new DOMRect(window.pageXOffset, window.pageYOffset, window.innerWidth, window.innerHeight), ]; } } ScreenContext.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: ScreenContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); ScreenContext.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: ScreenContext, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: ScreenContext, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return []; } }); /** * Enumeration of split layout modes values for use with * {@link SplitLayoutDirective}. */ const SplitLayoutMode = { Flex: 'flex', Grid: 'grid', Absolute: 'absolute', /** * Checks if the given string value is a valid {@link SplitLayoutMode}. * * @param value The value to check. * @return true if the value is a valid {@link SplitLayoutMode}. */ isValid: (value) => { switch (value) { case SplitLayoutMode.Flex: case SplitLayoutMode.Grid: case SplitLayoutMode.Absolute: return true; } return false; }, }; /** * Enumeration of window order values for use with * {@link SplitLayoutDirective}. */ const WindowOrder = { Normal: 'normal', Reverse: 'reverse', /** * Checks if the given string value is a valid {@link WindowOrder}. * * @param value The value to check. * @return true if the value is a valid {@link WindowOrder}. */ isValid: (value) => { switch (value) { case WindowOrder.Normal: case WindowOrder.Reverse: return true; } return false; }, }; /** * Enumeration of the text reading direction values. */ const ReadingDirection = { LeftToRight: 'ltr', RightToLeft: 'rtl', }; /** * Look 'ma, CSS-in-JS with Angular! ಠ_ಠ * * @ignore */ const layoutStyles$1 = { [SplitLayoutMode.Flex]: { common: { display: 'flex', justifyContent: 'space-between', height: 'env(viewport-segment-bottom 0 1)', }, [ScreenSpanning.DualHorizontal]: { flexDirection: 'row', }, [ScreenSpanning.DualVertical]: { flexDirection: 'column', }, [WindowOrder.Reverse]: { flexDirection: 'column-reverse', }, }, [SplitLayoutMode.Grid]: { common: { display: 'grid', height: 'env(viewport-segment-bottom 0 1)', }, [ScreenSpanning.DualHorizontal]: { gridTemplateColumns: '1fr 1fr', gridTemplateAreas: '"segment0 segment1"', gridAutoFlow: 'row', columnGap: 'calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0))', }, [ScreenSpanning.DualVertical]: { gridTemplateRows: '1fr 1fr', gridTemplateAreas: '"segment0" "segment1"', gridAutoFlow: 'row', rowGap: 'calc(env(viewport-segment-top 0 1) - env(viewport-segment-bottom 0 0))', }, [WindowOrder.Reverse]: { gridTemplateRows: '1fr 1fr', gridTemplateAreas: '"segment0" "segment1"', gridAutoFlow: 'row', rowGap: 'calc(env(viewport-segment-top 0 1) - env(viewport-segment-bottom 0 0))', }, }, [SplitLayoutMode.Absolute]: { common: { position: 'relative', height: 'env(viewport-segment-bottom 0 1)', }, [ScreenSpanning.DualHorizontal]: {}, [ScreenSpanning.DualVertical]: {}, [WindowOrder.Reverse]: {}, }, }; /** * Defines a parent layout container for creating a split layout on multi * screen devices. * * When used on a single screen device, no layout change (CSS) is added. * You can choose between different {@link SplitLayoutMode} to suit your * design. * * This directive should be used along with {@link WindowDirective}. * * @example * <div fdSplitLayout="grid"> * <section fdWindow="0">Will be displayed on first screen</section> * <section fdWindow="1">Will be displayed on second screen (if available)</section> * </div> * * In addition, you can also choose keep the same window segments order or * reverse it when the spanning mode change from vertical to horizontal using * a second optional parameter on the directive: * * @example * <div fdSplitLayout="flex reverse"> * <section fdWindow="0"> * Will be displayed on first screen in vertical spanning mode * and on the second screen in horizontal spanning mode. * </section> * <section fdWindow="1"> * Will be displayed on second screen in vertical spanning mode * and on the first screen in horizontal spanning mode. * </section> * </div> */ class SplitLayoutDirective { constructor(element, screenContext) { this.element = element; this.screenContext = screenContext; this.mode = SplitLayoutMode.Flex; this.order = WindowOrder.Normal; this.layoutStyle = {}; this.screenContextSubscription = null; this.direction = 'ltr'; this.updateStyle(); this.screenContextSubscription = this.screenContext .asObservable() .subscribe(() => this.updateStyle()); } /** * Sets the current split layout options to use when multi screen is * detected. * * @param options The split layout options to use. * Format: `[mode] [order]` * - The {@link SplitLayoutMode} to use (default is {@link SplitLayoutMode.Flex}). * - The {@link WindowOrder} to use (default is {@link WindowOrder.Normal}). */ set fdSplitLayout(options) { this.parseOptions(options || ''); this.updateStyle(); } /** @ignore */ get style() { return this.layoutStyle; } /** * The current split layout mode to use when multi screen is detected. * * @return The current split layout mode. */ get layoutMode() { return this.mode; } /** * The window segments order to use when in horizontal spanning mode. * * @return The current window order. */ get windowOrder() { return this.order; } /** * The text reading direction for the host element. * * @return The text reading direction. */ get readingDirection() { return this.direction; } /** @ignore */ ngOnDestroy() { if (this.screenContextSubscription !== null) { this.screenContextSubscription.unsubscribe(); } } parseOptions(options) { let [mode, order] = options.trim().split(' '); mode = SplitLayoutMode.isValid(mode) ? mode : SplitLayoutMode.Flex; order = WindowOrder.isValid(order) ? order : WindowOrder.Normal; this.mode = mode; this.order = order; } updateStyle() { var _a; const isMultiScreen = this.screenContext.isMultiScreen; const spanning = this.screenContext.screenSpanning; const reverse = spanning === ScreenSpanning.DualVertical && this.order === WindowOrder.Reverse; this.direction = ((_a = getComputedStyle(this.element.nativeElement)) === null || _a === void 0 ? void 0 : _a.direction) || ReadingDirection.LeftToRight; if (isMultiScreen && spanning !== ScreenSpanning.None) { this.layoutStyle = Object.assign(Object.assign({}, layoutStyles$1[this.mode].common), layoutStyles$1[this.mode][reverse ? WindowOrder.Reverse : spanning]); } else { this.layoutStyle = {}; } } } SplitLayoutDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: SplitLayoutDirective, deps: [{ token: i0.ElementRef }, { token: ScreenContext }], target: i0.ɵɵFactoryTarget.Directive }); SplitLayoutDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.1", type: SplitLayoutDirective, selector: "[fdSplitLayout]", inputs: { fdSplitLayout: "fdSplitLayout" }, host: { properties: { "style": "this.style" } }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: SplitLayoutDirective, decorators: [{ type: Directive, args: [{ selector: '[fdSplitLayout]', }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: ScreenContext }]; }, propDecorators: { fdSplitLayout: [{ type: Input }], style: [{ type: HostBinding, args: ['style'] }] } }); /** * Look 'ma, CSS-in-JS with Angular! ಠ_ಠ * * @ignore */ const layoutStyles = { [SplitLayoutMode.Flex]: { [ScreenSpanning.DualHorizontal]: [ { flex: '0 0 env(viewport-segment-width 0 0)' }, { flex: '0 0 env(viewport-segment-width 1 0)' }, ], [ScreenSpanning.DualVertical]: [ { flex: '0 0 env(viewport-segment-height 0 0)' }, { flex: '0 0 env(viewport-segment-height 0 1)' }, ], }, [SplitLayoutMode.Grid]: { [ScreenSpanning.DualHorizontal]: [ { gridArea: 'segment0' }, { gridArea: 'segment1' }, ], [ScreenSpanning.DualVertical]: [ { gridArea: 'segment0', height: 'env(viewport-segment-height 0 0)' }, { gridArea: 'segment1', height: 'env(viewport-segment-height 0 1)' }, ], }, [SplitLayoutMode.Absolute]: { [ScreenSpanning.DualHorizontal]: [ { position: 'absolute', left: 0, width: 'env(viewport-segment-right 0 0)', }, { position: 'absolute', left: 'env(viewport-segment-left 1 0)', right: 0, }, ], [ScreenSpanning.DualVertical]: [ { position: 'absolute', top: 0, width: '100%', maxHeight: 'env(viewport-segment-height 0 0)', }, { position: 'absolute', top: 'env(viewport-segment-top 0 1)', width: '100%', maxHeight: 'env(viewport-segment-height 0 1)', }, ], }, }; /** * This directive is used to set specify on which window segment the container * should be placed on multi screen devices. * * When used on a single screen device, no layout change (CSS) is added. * Only devices with up to two screen are currently supported, meaning that the * window segment value must be either 0 or 1. * * This directive can only be used within a {@link SplitLayoutDirective}. * If {@link SplitLayoutMode} is set to `absolute`, you can assign multiple * container element to the same window segment. * * Note that if you have set the read direction to Right-To-Left mode (`rtl`) * in CSS, the first segment will be the rightmost one. * * If the {@link WindowOrder} option is set to {@link WindowOrder.Reverse}, * the window segments order will be reversed in horizontal spanning mode. * * @example * <div fdSplitLayout="grid"> * <section fdWindow="0">Will be displayed on first screen</section> * <section fdWindow="1">Will be displayed on second screen (if available)</section> * </div> */ class WindowDirective { constructor(screenContext, splitLayout) { this.screenContext = screenContext; this.splitLayout = splitLayout; this.segment = -1; this.layoutStyle = {}; this.screenContextSubscription = null; this.screenContextSubscription = this.screenContext .asObservable() .subscribe(() => this.updateStyle()); } /** @ignore */ get style() { return this.layoutStyle; } /** * Sets the target window segment to display this container on when multi * screen is detected. * * @param segment The target window segment, must be 0 or 1. */ set fdWindow(segment) { segment = typeof segment === 'string' ? parseInt(segment, 10) : segment; if (segment !== this.segment) { this.segment = segment; this.updateStyle(); } } /** @ignore */ ngOnDestroy() { if (this.screenContextSubscription !== null) { this.screenContextSubscription.unsubscribe(); } } updateStyle() { if (this.segment === -1) { return; } const isMultiScreen = this.screenContext.isMultiScreen; const spanning = this.screenContext.screenSpanning; if (isMultiScreen && spanning !== ScreenSpanning.None) { if (this.segment < 0 || this.segment > 1) { throw new Error('Segment index must be 0 or 1'); } const mode = this.splitLayout.layoutMode; const order = this.splitLayout.windowOrder; const direction = this.splitLayout.readingDirection; // Swap segments for vertical span and RTL mode or // horizontal span and reverse window order const swap = (spanning === ScreenSpanning.DualHorizontal && mode !== SplitLayoutMode.Grid && direction === ReadingDirection.RightToLeft) || (spanning === ScreenSpanning.DualVertical && order === WindowOrder.Reverse); const segment = swap ? 1 - this.segment : this.segment; this.layoutStyle = layoutStyles[mode][spanning][segment]; } else { this.layoutStyle = {}; } } } WindowDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: WindowDirective, deps: [{ token: ScreenContext }, { token: SplitLayoutDirective, host: true }], target: i0.ɵɵFactoryTarget.Directive }); WindowDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.1", type: WindowDirective, selector: "[fdWindow]", inputs: { fdWindow: "fdWindow" }, host: { properties: { "style": "this.style" } }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: WindowDirective, decorators: [{ type: Directive, args: [{ selector: '[fdWindow]', }] }], ctorParameters: function () { return [{ type: ScreenContext }, { type: SplitLayoutDirective, decorators: [{ type: Host }] }]; }, propDecorators: { style: [{ type: HostBinding, args: ['style'] }], fdWindow: [{ type: Input }] } }); /** * Enumeration of spanning mode conditions values for use with * {@link IfSpanDirective}. */ const SpanCondition = { /** Screen spanning mode is dual horizontal viewports. */ Vertical: 'dual-horizontal', /** Screen spanning mode is dual vertical viewports. */ Horizontal: 'dual-vertical', /** No screen spanning (single screen mode). */ None: 'none', /** Any screen spanning mode is active (multi screen mode). */ Multi: 'multi', }; /** * Shows template only if the current screen spanning mode matches * specified {@link SpanCondition}. * * Behaves like `ngIf`, except that it accepts a {@link SpanCondition} instead of * a boolean expression. * * @example * <p *fdIdSpan="'multi'">This text will only be visible on multi screen devices.</p> * @example * <p *fdIdSpan="'none'; else alt">This text will only be visible on single screen devices.</p> * <ng-template #alt">This text will only be visible on multi screen devices.</ng-template> */ class IfSpanDirective { constructor(screenContext, viewContainer, templateRef) { this.screenContext = screenContext; this.viewContainer = viewContainer; this.screenContextSubscription = null; this.condition = null; this.thenTemplateRef = null; this.elseTemplateRef = null; this.thenViewRef = null; this.elseViewRef = null; this.thenTemplateRef = templateRef; this.screenContextSubscription = this.screenContext .asObservable() .subscribe(() => this.updateView()); } /** * The spanning mode condition that defines if the template should be shown. * * @param condition The spanning mode condition for showing the template. */ set fdIfSpan(condition) { if (condition !== this.condition) { this.condition = condition; this.updateView(); } } /** A template to show if the span condition evaluates to true. */ set fdIfSpanThen(templateRef) { this.thenTemplateRef = templateRef; this.thenViewRef = null; this.updateView(); } /** A template to show if the span condition evaluates to false. */ set fdIfSpanElse(templateRef) { this.elseTemplateRef = templateRef; this.thenViewRef = null; this.updateView(); } /** ignore */ ngOnDestroy() { if (this.screenContextSubscription !== null) { this.screenContextSubscription.unsubscribe(); } } matchCondition() { switch (this.condition) { case SpanCondition.Multi: return this.screenContext.isMultiScreen; case SpanCondition.Horizontal: return this.screenContext.screenSpanning === ScreenSpanning.DualVertical; case SpanCondition.Vertical: return this.screenContext.screenSpanning === ScreenSpanning.DualHorizontal; default: return this.screenContext.screenSpanning === ScreenSpanning.None; } } updateView() { const match = this.matchCondition(); if (match) { if (!this.thenViewRef) { this.viewContainer.clear(); this.elseViewRef = null; if (this.thenTemplateRef) { this.thenViewRef = this.viewContainer.createEmbeddedView(this.thenTemplateRef); } } } else { if (!this.elseViewRef) { this.viewContainer.clear(); this.thenViewRef = null; if (this.elseTemplateRef) { this.elseViewRef = this.viewContainer.createEmbeddedView(this.elseTemplateRef); } } } } } IfSpanDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: IfSpanDirective, deps: [{ token: ScreenContext }, { token: i0.ViewContainerRef }, { token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); IfSpanDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.1", type: IfSpanDirective, selector: "[fdIfSpan]", inputs: { fdIfSpan: "fdIfSpan", fdIfSpanThen: "fdIfSpanThen", fdIfSpanElse: "fdIfSpanElse" }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: IfSpanDirective, decorators: [{ type: Directive, args: [{ selector: '[fdIfSpan]', }] }], ctorParameters: function () { return [{ type: ScreenContext }, { type: i0.ViewContainerRef }, { type: i0.TemplateRef }]; }, propDecorators: { fdIfSpan: [{ type: Input }], fdIfSpanThen: [{ type: Input }], fdIfSpanElse: [{ type: Input }] } }); /** * This module contains utilities to help you build your app layout for multi * screen devices. * * See {@link SplitLayoutDirective}, * {@link WindowDirective}, * {@link IfSpanDirective}, * {@link ScreenContext} */ class FoldableModule { } FoldableModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: FoldableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); FoldableModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.0.1", ngImport: i0, type: FoldableModule, declarations: [IfSpanDirective, WindowDirective, SplitLayoutDirective], exports: [IfSpanDirective, WindowDirective, SplitLayoutDirective] }); FoldableModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: FoldableModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.1", ngImport: i0, type: FoldableModule, decorators: [{ type: NgModule, args: [{ declarations: [IfSpanDirective, WindowDirective, SplitLayoutDirective], exports: [IfSpanDirective, WindowDirective, SplitLayoutDirective], }] }] }); /* * Public API Surface of ngx-foldable */ /** * Generated bundle index. Do not edit. */ export { FoldableModule, IfSpanDirective, ReadingDirection, ScreenContext, ScreenSpanning, SpanCondition, SplitLayoutDirective, SplitLayoutMode, WindowDirective, WindowOrder, dualHorizontalViewport, dualVerticalViewport }; //# sourceMappingURL=ngx-foldable.mjs.map