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
JavaScript
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