Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
feat(core): add print support with mediaQuery override (#954)
Browse files Browse the repository at this point in the history
When printing developers can now configure how layouts should render. Default print will use the current layout and current elements shown/visible. By specifying 1..n mediaQuery aliases, developers can specify alternate layouts with alternate breakpoints to be used for printing. Elements can also be shown and hidden for printing-only. 

>  This feature supports totally different print outputs without modifying the current browser layout.

Implement PrintHook service to intercept print mediaQuery activation events.
  * add fxShow.print and fxHide.print support to show/hide elements during printing
  * suspend activation changes in MediaMarshaller while print-mode is active
  * trigger MediaObserver to notify subscribers with print mqAlias(es)
  * use PrintHook to intercept activation changes in MediaMarshaller while print-mode is active
  * trigger MediaObserver to notify subscribers with print mqAlias(es)
  * add watermark component to Demo app that is shown only during printing

Using the new `printWithBreakpoint` allows developers to specify a breakpoint that should be used to render layouts only during printing. With the configuration below, the breakpoint associated with the **`md`** alias will be used.

```ts
    FlexLayoutModule.withConfig({
      useColumnBasisZero: false,
      printWithBreakpoints: ['md', 'lt-lg', 'lt-xl', 'gt-sm', 'gt-xs']
    })
```
Shown below is the print layout rendered in floating dialog over the normal layout that is currently using 'lg' breakpoints.

![angular-layout-printing](https://user-images.githubusercontent.com/210413/50407211-2e04ca00-0798-11e9-8f35-b4e9e2fca864.jpg)

Fixes #603.
  • Loading branch information
ThomasBurleson authored Jan 2, 2019
1 parent d57b293 commit 0c9e9cb
Show file tree
Hide file tree
Showing 32 changed files with 1,058 additions and 374 deletions.
4 changes: 2 additions & 2 deletions docs/documentation/BreakPoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {BREAKPOINT} from '@angular/flex-layout';
const PRINT_BREAKPOINTS = [{
alias: 'xs.print',
suffix: 'XsPrint',
mediaQuery: 'print and (max-width: 297px)',
mediaQuery: 'screen and (max-width: 297px)',
overlapping: false
}];

Expand Down Expand Up @@ -157,4 +157,4 @@ export class CustomShowHideDirective extends ShowHideDirective {
this._cacheInput("showXsPrint", negativeOf(val));
}
}
```
```
2 changes: 1 addition & 1 deletion docs/documentation/fxHide-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ e.g.
### Using Responsive API

When a mediaQuery range activates, the directive instances will be notified. If the current activate mediaQuery range
When a mediaQuery range activates, the directive instances will be notified. If the current activated mediaQuery range
(and its associated alias) are not used, then the static API value is restored as the fallback value.

The *fallback* solution uses a **`largest_range-to-smallest_range`** search algorithm. Consider the following:
Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/fxShow-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ e.g.
### Using Responsive API

When a mediaQuery range activates, the directive instances will be notified. If the current activate mediaQuery range
When a mediaQuery range activates, the directive instances will be notified. If the current activated mediaQuery range
(and its associated alias) are not used, then the static API value is restored as the fallback value.

The *fallback* solution uses a **`largest_range-to-smallest_range`** search algorithm. Consider the following:
Expand Down
6 changes: 5 additions & 1 deletion src/apps/demo-app/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ <h2>Layout Demos: </h2>
</span>
</div>
<div fxLayout="row"
fxLayoutAlign="start center"
fxLayoutGap="20px"
fxHide.print
style="height:40px; min-height:40px;">
<button mat-raised-button color="primary" [routerLink]="['']">
Static
Expand All @@ -30,4 +30,8 @@ <h2>Layout Demos: </h2>

<div class="demo-content">
<router-outlet></router-outlet>
<watermark
title="@angular/layout"
message="HTML Layouts w/ Flex and Grid CSS"
fxHide fxShow.print></watermark>
</div>
19 changes: 12 additions & 7 deletions src/apps/demo-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,31 @@ import {FlexLayoutModule, BREAKPOINT} from '@angular/flex-layout';
import {RoutingModule} from './routing.module';
import {AppComponent} from './app.component';
import {DemoMaterialModule} from './material.module';
import {WatermarkComponent} from './watermark.component';

const PRINT_BREAKPOINTS = [{
alias: 'xs.print',
suffix: 'XsPrint',
mediaQuery: 'print and (max-width: 297px)',
const EXTRA_BREAKPOINT = [{
alias: 'xs.landscape',
suffix: 'XsLandscape',
mediaQuery: 'screen and (orientation: landscape) and (max-width: 559px)',
priority: 1000,
overlapping: false
}];

@NgModule({
declarations: [
AppComponent,
AppComponent, WatermarkComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
BrowserAnimationsModule,
RoutingModule,
DemoMaterialModule,
FlexLayoutModule.withConfig({useColumnBasisZero: false}),
FlexLayoutModule.withConfig({
useColumnBasisZero: false,
printWithBreakpoints: ['md', 'lt-lg', 'lt-xl', 'gt-sm', 'gt-xs']
}),
],
providers: [{provide: BREAKPOINT, useValue: PRINT_BREAKPOINTS, multi: true}],
providers: [{provide: BREAKPOINT, useValue: EXTRA_BREAKPOINT, multi: true}],
bootstrap: [AppComponent]
})
export class AppModule { }
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {Component} from '@angular/core';
@Component({
selector: 'demo-docs-responsive',
template: `
<demo-responsive-layout-direction class='small-demo'> </demo-responsive-layout-direction>
<demo-responsive-layout-direction class='small-demo' fxHide.print>
</demo-responsive-layout-direction>
<demo-responsive-row-column class='small-demo'> </demo-responsive-row-column>
<demo-responsive-flex-directive class='small-demo'> </demo-responsive-flex-directive>
<demo-responsive-flex-order class='small-demo'> </demo-responsive-flex-order>
Expand Down
16 changes: 16 additions & 0 deletions src/apps/demo-app/src/app/watermark.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
:host {
display: block;
position: absolute;

width: 100vw;
min-height: 100vh;
top: 0;
left: 0;
right: 0;
bottom: 0;

div {
width: 100vw;
min-height: 100vh;
}
}
56 changes: 56 additions & 0 deletions src/apps/demo-app/src/app/watermark.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {Component, Input} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';

@Component({
selector: 'watermark',
styleUrls: ['watermark.component.scss'],
template: `
<div [style.background]="backgroundImage">
</div>
`,
})
export class WatermarkComponent {
@Input() title = '@angular/layout';
@Input() message = 'Layout with FlexBox + CSS Grid';

constructor(private _sanitizer: DomSanitizer) {
}

/* tslint:disable:max-line-length */
get backgroundImage() {
const rawSVG = `
<svg id="diagonalWatermark"
width="100%" height="100%"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" >
<style type="text/css">
text {
fill: currentColor;
font-family: Avenir, Arial, Helvetica, sans-serif;
opacity: 0.25;
}
</style>
<defs>
<pattern id="titlePattern" patternUnits="userSpaceOnUse" width="350" height="150">
<text y="30" font-size="30" id="title">
${this.title}
</text>
</pattern>
<pattern xlink:href="#titlePattern">
<text y="60" x="0" font-size="16" id="message" width="350" height="150">
${this.message}
</text>
</pattern>
<pattern id="combo" xlink:href="#titlePattern" patternTransform="rotate(-30)">
<use xlink:href="#title"/>
<use xlink:href="#message"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#combo)"/>
</svg>
`;
const bkgrndImageUrl = `data:image/svg+xml;base64,${window.btoa(rawSVG)}`;

return this._sanitizer.bypassSecurityTrustStyle(`url('${bkgrndImageUrl}') repeat-y`);
}
}
12 changes: 12 additions & 0 deletions src/apps/demo-app/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ body {
font-size: 16px;
line-height: 1.4;
-webkit-font-smoothing: antialiased;
-webkit-print-color-adjust: exact !important;
}

@page {
size: auto; /* auto is the initial value */
margin: 2cm;
}

@media print {
body {
background: white;
}
}

h3 {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/add-alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {extendObject} from '../utils/object-extend';
* and suffix (if available).
*/
export function mergeAlias(dest: MediaChange, source: BreakPoint | null): MediaChange {
return extendObject(dest, source ? {
return extendObject(dest || {}, source ? {
mqAlias: source.alias,
suffix: source.suffix
} : {});
Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/base/base2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export abstract class BaseDirective2 implements OnChanges, OnDestroy {
}
set activatedValue(value: string) {
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, value,
this.marshal.activatedBreakpoint);
this.marshal.activatedAlias);
}

/** Cache map for style computation */
Expand Down
4 changes: 2 additions & 2 deletions src/lib/core/breakpoints/break-point-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {BreakPoint} from './break-point';
import {BREAKPOINTS} from './break-points-token';
import {sortAscendingPriority} from './breakpoint-tools';

type OptionalBreakPoint = BreakPoint | null;
export type OptionalBreakPoint = BreakPoint | null;

/**
* Registry of 1..n MediaQuery breakpoint ranges
Expand All @@ -30,7 +30,7 @@ export class BreakPointRegistry {
* Search breakpoints by alias (e.g. gt-xs)
*/
findByAlias(alias: string): OptionalBreakPoint {
return this.findWithPredicate(alias, (bp) => bp.alias == alias);
return !alias ? null : this.findWithPredicate(alias, (bp) => bp.alias == alias);
}

findByQuery(query: string): OptionalBreakPoint {
Expand Down
7 changes: 4 additions & 3 deletions src/lib/core/breakpoints/breakpoint-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {OptionalBreakPoint} from './break-point-registry';
import {BreakPoint} from './break-point';
import {extendObject} from '../../utils/object-extend';

Expand Down Expand Up @@ -65,9 +66,9 @@ export function mergeByAlias(defaults: BreakPoint[], custom: BreakPoint[] = []):
}

/** HOF to sort the breakpoints by priority */
export function sortDescendingPriority(a: BreakPoint, b: BreakPoint): number {
const priorityA = a.priority || 0;
const priorityB = b.priority || 0;
export function sortDescendingPriority(a: OptionalBreakPoint, b: OptionalBreakPoint): number {
const priorityA = a ? a.priority || 0 : 0;
const priorityB = b ? b.priority || 0 : 0;
return priorityB - priorityA;
}

Expand Down
29 changes: 14 additions & 15 deletions src/lib/core/breakpoints/data/orientation-break-points.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {TestBed, inject} from '@angular/core/testing';
import {TestBed, inject, async} from '@angular/core/testing';

import {BreakPoint} from '../break-point';
import {DEFAULT_BREAKPOINTS} from './break-points';
Expand Down Expand Up @@ -70,45 +70,44 @@ describe('break-point-provider', () => {

describe('with custom breakpoint overrides', () => {
const gtXsMediaQuery = 'screen and (max-width:20px) and (orientations: landscape)';
const mdMediaQuery = 'print and (min-width:10000px)';
const xxxlQuery = 'screen and (min-width:10000px)';
const EXTRAS: BreakPoint[] = [
{alias: 'md', mediaQuery: mdMediaQuery},
{alias: 'gt-xs', mediaQuery: gtXsMediaQuery},
{alias: 'lt-ab', mediaQuery: '(max-width: 297px)'},
{alias: 'cd', mediaQuery: '(min-width: 298px) and (max-width:414px)'}
{alias: 'xxl', priority: 2000, mediaQuery: xxxlQuery},
{alias: 'gt-xsl', priority: 2000, mediaQuery: gtXsMediaQuery},
{alias: 'lt-ab', priority: 2000, mediaQuery: '(max-width: 297px)'},
{alias: 'cd', priority: 2000, mediaQuery: '(min-width: 298px) and (max-width:414px)'}
];
const NUM_EXTRAS = 2; // since md and gt-xs will not be added but merged
let bpList: BreakPoint[];
let accumulator: BreakPoint | null = null;
let byAlias = (alias: string): BreakPoint | null => bpList.reduce((pos, it) => {
return pos || ((it.alias === alias) ? it : null);
}, accumulator);

beforeEach(() => {
beforeEach(async (() => {
// Configure testbed to prepare services
TestBed.configureTestingModule({
imports: [FlexLayoutModule.withConfig({addOrientationBps: true}, EXTRAS)]
});
});
}));
// tslint:disable-next-line:no-shadowed-variable
beforeEach(inject([BREAKPOINTS], (breakPoints: BreakPoint[]) => {
bpList = breakPoints;
}));

it('has merged the custom breakpoints as overrides to existing defaults', () => {
const total = ORIENTATION_BREAKPOINTS.length + DEFAULT_BREAKPOINTS.length + NUM_EXTRAS;
const total = ORIENTATION_BREAKPOINTS.length + DEFAULT_BREAKPOINTS.length + EXTRAS.length;

expect(bpList.length).toEqual(total);

expect(byAlias('gt-xs')).toBeDefined();
expect(byAlias('gt-xs')!.mediaQuery).toEqual(gtXsMediaQuery);
expect(byAlias('gt-xsl')).toBeDefined();
expect(byAlias('gt-xsl')!.mediaQuery).toEqual(gtXsMediaQuery);

expect(byAlias('md')).toBeDefined();
expect(byAlias('md')!.mediaQuery).toEqual(mdMediaQuery);
expect(byAlias('xxl')).toBeDefined();
expect(byAlias('xxl')!.mediaQuery).toEqual(xxxlQuery);
});

it('can extend existing default breakpoints with custom settings', () => {
const total = ORIENTATION_BREAKPOINTS.length + DEFAULT_BREAKPOINTS.length + NUM_EXTRAS;
const total = ORIENTATION_BREAKPOINTS.length + DEFAULT_BREAKPOINTS.length + EXTRAS.length;

expect(bpList.length).toEqual(total);
expect(bpList[bpList.length - 2].alias).toEqual('lt-ab');
Expand Down
18 changes: 9 additions & 9 deletions src/lib/core/breakpoints/data/orientation-break-points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ export const ScreenTypes = {
* Extended Breakpoints for handset/tablets with landscape or portrait orientations
*/
export const ORIENTATION_BREAKPOINTS : BreakPoint[] = [
{'alias': 'handset', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET},
{'alias': 'handset.landscape', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET_LANDSCAPE},
{'alias': 'handset.portrait', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET_PORTRAIT},
{'alias': 'handset', priority: 2000, 'mediaQuery': ScreenTypes.HANDSET},
{'alias': 'handset.landscape', priority: 2000, 'mediaQuery': ScreenTypes.HANDSET_LANDSCAPE},
{'alias': 'handset.portrait', priority: 2000, 'mediaQuery': ScreenTypes.HANDSET_PORTRAIT},

{'alias': 'tablet', priority: 8000, 'mediaQuery': ScreenTypes.TABLET},
{'alias': 'tablet.landscape', priority: 8000, 'mediaQuery': ScreenTypes.TABLET},
{'alias': 'tablet.portrait', priority: 8000, 'mediaQuery': ScreenTypes.TABLET_PORTRAIT},
{'alias': 'tablet', priority: 2100, 'mediaQuery': ScreenTypes.TABLET},
{'alias': 'tablet.landscape', priority: 2100, 'mediaQuery': ScreenTypes.TABLET},
{'alias': 'tablet.portrait', priority: 2100, 'mediaQuery': ScreenTypes.TABLET_PORTRAIT},

{'alias': 'web', priority: 9000, 'mediaQuery': ScreenTypes.WEB, overlapping : true },
{'alias': 'web.landscape', priority: 9000, 'mediaQuery': ScreenTypes.WEB_LANDSCAPE, overlapping : true },
{'alias': 'web.portrait', priority: 9000, 'mediaQuery': ScreenTypes.WEB_PORTRAIT, overlapping : true }
{'alias': 'web', priority: 2200, 'mediaQuery': ScreenTypes.WEB, overlapping : true },
{'alias': 'web.landscape', priority: 2200, 'mediaQuery': ScreenTypes.WEB_LANDSCAPE, overlapping : true },
{'alias': 'web.portrait', priority: 2200, 'mediaQuery': ScreenTypes.WEB_PORTRAIT, overlapping : true }
];
5 changes: 3 additions & 2 deletions src/lib/core/match-media/match-media.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe('match-media-observable', () => {
});

/**
* Only the ObservableMedia ignores de-activations;
* Only the MediaObserver ignores de-activations;
* MediaMonitor and MatchMedia report both activations and de-activations!
*/
it('ignores mediaQuery de-activations', () => {
Expand All @@ -189,9 +189,10 @@ describe('match-media-observable', () => {

matchMedia.activate(breakPoints.findByAlias('md')!.mediaQuery);
matchMedia.activate(breakPoints.findByAlias('gt-md')!.mediaQuery);
matchMedia.activate(breakPoints.findByAlias('lg')!.mediaQuery);

// 'all' mediaQuery is already active; total count should be (3)
expect(activationCount).toEqual(3);
expect(activationCount).toEqual(4);
expect(deactivationCount).toEqual(0);

subscription.unsubscribe();
Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/match-media/mock/mock-match-media.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ describe('mock-match-media', () => {
subscription.unsubscribe();
});

it('can activate with either a mediaQuery or an alias', () => {
it('can onMediaChange with either a mediaQuery or an alias', () => {
let activates = 0;
let bpGtSM = breakPoints.findByAlias('gt-sm'),
bpLg = breakPoints.findByAlias('lg');
Expand Down
Loading

0 comments on commit 0c9e9cb

Please sign in to comment.