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

Commit 0c9e9cb

Browse files
feat(core): add print support with mediaQuery override (#954)
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.
1 parent d57b293 commit 0c9e9cb

32 files changed

+1058
-374
lines changed

docs/documentation/BreakPoints.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {BREAKPOINT} from '@angular/flex-layout';
4242
const PRINT_BREAKPOINTS = [{
4343
alias: 'xs.print',
4444
suffix: 'XsPrint',
45-
mediaQuery: 'print and (max-width: 297px)',
45+
mediaQuery: 'screen and (max-width: 297px)',
4646
overlapping: false
4747
}];
4848

@@ -157,4 +157,4 @@ export class CustomShowHideDirective extends ShowHideDirective {
157157
this._cacheInput("showXsPrint", negativeOf(val));
158158
}
159159
}
160-
```
160+
```

docs/documentation/fxHide-API.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ e.g.
2424
2525
### Using Responsive API
2626

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

3030
The *fallback* solution uses a **`largest_range-to-smallest_range`** search algorithm. Consider the following:

docs/documentation/fxShow-API.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ e.g.
2424
2525
### Using Responsive API
2626

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

3030
The *fallback* solution uses a **`largest_range-to-smallest_range`** search algorithm. Consider the following:

src/apps/demo-app/src/app/app.component.html

+5-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ <h2>Layout Demos: </h2>
1313
</span>
1414
</div>
1515
<div fxLayout="row"
16-
fxLayoutAlign="start center"
1716
fxLayoutGap="20px"
17+
fxHide.print
1818
style="height:40px; min-height:40px;">
1919
<button mat-raised-button color="primary" [routerLink]="['']">
2020
Static
@@ -30,4 +30,8 @@ <h2>Layout Demos: </h2>
3030

3131
<div class="demo-content">
3232
<router-outlet></router-outlet>
33+
<watermark
34+
title="@angular/layout"
35+
message="HTML Layouts w/ Flex and Grid CSS"
36+
fxHide fxShow.print></watermark>
3337
</div>

src/apps/demo-app/src/app/app.module.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,31 @@ import {FlexLayoutModule, BREAKPOINT} from '@angular/flex-layout';
66
import {RoutingModule} from './routing.module';
77
import {AppComponent} from './app.component';
88
import {DemoMaterialModule} from './material.module';
9+
import {WatermarkComponent} from './watermark.component';
910

10-
const PRINT_BREAKPOINTS = [{
11-
alias: 'xs.print',
12-
suffix: 'XsPrint',
13-
mediaQuery: 'print and (max-width: 297px)',
11+
const EXTRA_BREAKPOINT = [{
12+
alias: 'xs.landscape',
13+
suffix: 'XsLandscape',
14+
mediaQuery: 'screen and (orientation: landscape) and (max-width: 559px)',
15+
priority: 1000,
1416
overlapping: false
1517
}];
1618

1719
@NgModule({
1820
declarations: [
19-
AppComponent,
21+
AppComponent, WatermarkComponent
2022
],
2123
imports: [
2224
BrowserModule.withServerTransition({ appId: 'serverApp' }),
2325
BrowserAnimationsModule,
2426
RoutingModule,
2527
DemoMaterialModule,
26-
FlexLayoutModule.withConfig({useColumnBasisZero: false}),
28+
FlexLayoutModule.withConfig({
29+
useColumnBasisZero: false,
30+
printWithBreakpoints: ['md', 'lt-lg', 'lt-xl', 'gt-sm', 'gt-xs']
31+
}),
2732
],
28-
providers: [{provide: BREAKPOINT, useValue: PRINT_BREAKPOINTS, multi: true}],
33+
providers: [{provide: BREAKPOINT, useValue: EXTRA_BREAKPOINT, multi: true}],
2934
bootstrap: [AppComponent]
3035
})
3136
export class AppModule { }

src/apps/demo-app/src/app/responsive/docs-responsive/docs-responsive.component.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {Component} from '@angular/core';
33
@Component({
44
selector: 'demo-docs-responsive',
55
template: `
6-
<demo-responsive-layout-direction class='small-demo'> </demo-responsive-layout-direction>
6+
<demo-responsive-layout-direction class='small-demo' fxHide.print>
7+
</demo-responsive-layout-direction>
78
<demo-responsive-row-column class='small-demo'> </demo-responsive-row-column>
89
<demo-responsive-flex-directive class='small-demo'> </demo-responsive-flex-directive>
910
<demo-responsive-flex-order class='small-demo'> </demo-responsive-flex-order>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
:host {
2+
display: block;
3+
position: absolute;
4+
5+
width: 100vw;
6+
min-height: 100vh;
7+
top: 0;
8+
left: 0;
9+
right: 0;
10+
bottom: 0;
11+
12+
div {
13+
width: 100vw;
14+
min-height: 100vh;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {Component, Input} from '@angular/core';
2+
import {DomSanitizer} from '@angular/platform-browser';
3+
4+
@Component({
5+
selector: 'watermark',
6+
styleUrls: ['watermark.component.scss'],
7+
template: `
8+
<div [style.background]="backgroundImage">
9+
</div>
10+
`,
11+
})
12+
export class WatermarkComponent {
13+
@Input() title = '@angular/layout';
14+
@Input() message = 'Layout with FlexBox + CSS Grid';
15+
16+
constructor(private _sanitizer: DomSanitizer) {
17+
}
18+
19+
/* tslint:disable:max-line-length */
20+
get backgroundImage() {
21+
const rawSVG = `
22+
<svg id="diagonalWatermark"
23+
width="100%" height="100%"
24+
xmlns="http://www.w3.org/2000/svg"
25+
xmlns:xlink="http://www.w3.org/1999/xlink" >
26+
<style type="text/css">
27+
text {
28+
fill: currentColor;
29+
font-family: Avenir, Arial, Helvetica, sans-serif;
30+
opacity: 0.25;
31+
}
32+
</style>
33+
<defs>
34+
<pattern id="titlePattern" patternUnits="userSpaceOnUse" width="350" height="150">
35+
<text y="30" font-size="30" id="title">
36+
${this.title}
37+
</text>
38+
</pattern>
39+
<pattern xlink:href="#titlePattern">
40+
<text y="60" x="0" font-size="16" id="message" width="350" height="150">
41+
${this.message}
42+
</text>
43+
</pattern>
44+
<pattern id="combo" xlink:href="#titlePattern" patternTransform="rotate(-30)">
45+
<use xlink:href="#title"/>
46+
<use xlink:href="#message"/>
47+
</pattern>
48+
</defs>
49+
<rect width="100%" height="100%" fill="url(#combo)"/>
50+
</svg>
51+
`;
52+
const bkgrndImageUrl = `data:image/svg+xml;base64,${window.btoa(rawSVG)}`;
53+
54+
return this._sanitizer.bypassSecurityTrustStyle(`url('${bkgrndImageUrl}') repeat-y`);
55+
}
56+
}

src/apps/demo-app/src/styles.scss

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ body {
88
font-size: 16px;
99
line-height: 1.4;
1010
-webkit-font-smoothing: antialiased;
11+
-webkit-print-color-adjust: exact !important;
12+
}
13+
14+
@page {
15+
size: auto; /* auto is the initial value */
16+
margin: 2cm;
17+
}
18+
19+
@media print {
20+
body {
21+
background: white;
22+
}
1123
}
1224

1325
h3 {

src/lib/core/add-alias.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {extendObject} from '../utils/object-extend';
1414
* and suffix (if available).
1515
*/
1616
export function mergeAlias(dest: MediaChange, source: BreakPoint | null): MediaChange {
17-
return extendObject(dest, source ? {
17+
return extendObject(dest || {}, source ? {
1818
mqAlias: source.alias,
1919
suffix: source.suffix
2020
} : {});

src/lib/core/base/base2.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export abstract class BaseDirective2 implements OnChanges, OnDestroy {
3737
}
3838
set activatedValue(value: string) {
3939
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, value,
40-
this.marshal.activatedBreakpoint);
40+
this.marshal.activatedAlias);
4141
}
4242

4343
/** Cache map for style computation */

src/lib/core/breakpoints/break-point-registry.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {BreakPoint} from './break-point';
1111
import {BREAKPOINTS} from './break-points-token';
1212
import {sortAscendingPriority} from './breakpoint-tools';
1313

14-
type OptionalBreakPoint = BreakPoint | null;
14+
export type OptionalBreakPoint = BreakPoint | null;
1515

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

3636
findByQuery(query: string): OptionalBreakPoint {

src/lib/core/breakpoints/breakpoint-tools.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {OptionalBreakPoint} from './break-point-registry';
910
import {BreakPoint} from './break-point';
1011
import {extendObject} from '../../utils/object-extend';
1112

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

6768
/** HOF to sort the breakpoints by priority */
68-
export function sortDescendingPriority(a: BreakPoint, b: BreakPoint): number {
69-
const priorityA = a.priority || 0;
70-
const priorityB = b.priority || 0;
69+
export function sortDescendingPriority(a: OptionalBreakPoint, b: OptionalBreakPoint): number {
70+
const priorityA = a ? a.priority || 0 : 0;
71+
const priorityB = b ? b.priority || 0 : 0;
7172
return priorityB - priorityA;
7273
}
7374

src/lib/core/breakpoints/data/orientation-break-points.spec.ts

+14-15
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {TestBed, inject} from '@angular/core/testing';
9+
import {TestBed, inject, async} from '@angular/core/testing';
1010

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

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

87-
beforeEach(() => {
86+
beforeEach(async (() => {
8887
// Configure testbed to prepare services
8988
TestBed.configureTestingModule({
9089
imports: [FlexLayoutModule.withConfig({addOrientationBps: true}, EXTRAS)]
9190
});
92-
});
91+
}));
9392
// tslint:disable-next-line:no-shadowed-variable
9493
beforeEach(inject([BREAKPOINTS], (breakPoints: BreakPoint[]) => {
9594
bpList = breakPoints;
9695
}));
9796

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

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

103-
expect(byAlias('gt-xs')).toBeDefined();
104-
expect(byAlias('gt-xs')!.mediaQuery).toEqual(gtXsMediaQuery);
102+
expect(byAlias('gt-xsl')).toBeDefined();
103+
expect(byAlias('gt-xsl')!.mediaQuery).toEqual(gtXsMediaQuery);
105104

106-
expect(byAlias('md')).toBeDefined();
107-
expect(byAlias('md')!.mediaQuery).toEqual(mdMediaQuery);
105+
expect(byAlias('xxl')).toBeDefined();
106+
expect(byAlias('xxl')!.mediaQuery).toEqual(xxxlQuery);
108107
});
109108

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

113112
expect(bpList.length).toEqual(total);
114113
expect(bpList[bpList.length - 2].alias).toEqual('lt-ab');

src/lib/core/breakpoints/data/orientation-break-points.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ export const ScreenTypes = {
3636
* Extended Breakpoints for handset/tablets with landscape or portrait orientations
3737
*/
3838
export const ORIENTATION_BREAKPOINTS : BreakPoint[] = [
39-
{'alias': 'handset', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET},
40-
{'alias': 'handset.landscape', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET_LANDSCAPE},
41-
{'alias': 'handset.portrait', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET_PORTRAIT},
39+
{'alias': 'handset', priority: 2000, 'mediaQuery': ScreenTypes.HANDSET},
40+
{'alias': 'handset.landscape', priority: 2000, 'mediaQuery': ScreenTypes.HANDSET_LANDSCAPE},
41+
{'alias': 'handset.portrait', priority: 2000, 'mediaQuery': ScreenTypes.HANDSET_PORTRAIT},
4242

43-
{'alias': 'tablet', priority: 8000, 'mediaQuery': ScreenTypes.TABLET},
44-
{'alias': 'tablet.landscape', priority: 8000, 'mediaQuery': ScreenTypes.TABLET},
45-
{'alias': 'tablet.portrait', priority: 8000, 'mediaQuery': ScreenTypes.TABLET_PORTRAIT},
43+
{'alias': 'tablet', priority: 2100, 'mediaQuery': ScreenTypes.TABLET},
44+
{'alias': 'tablet.landscape', priority: 2100, 'mediaQuery': ScreenTypes.TABLET},
45+
{'alias': 'tablet.portrait', priority: 2100, 'mediaQuery': ScreenTypes.TABLET_PORTRAIT},
4646

47-
{'alias': 'web', priority: 9000, 'mediaQuery': ScreenTypes.WEB, overlapping : true },
48-
{'alias': 'web.landscape', priority: 9000, 'mediaQuery': ScreenTypes.WEB_LANDSCAPE, overlapping : true },
49-
{'alias': 'web.portrait', priority: 9000, 'mediaQuery': ScreenTypes.WEB_PORTRAIT, overlapping : true }
47+
{'alias': 'web', priority: 2200, 'mediaQuery': ScreenTypes.WEB, overlapping : true },
48+
{'alias': 'web.landscape', priority: 2200, 'mediaQuery': ScreenTypes.WEB_LANDSCAPE, overlapping : true },
49+
{'alias': 'web.portrait', priority: 2200, 'mediaQuery': ScreenTypes.WEB_PORTRAIT, overlapping : true }
5050
];

src/lib/core/match-media/match-media.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ describe('match-media-observable', () => {
171171
});
172172

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

190190
matchMedia.activate(breakPoints.findByAlias('md')!.mediaQuery);
191191
matchMedia.activate(breakPoints.findByAlias('gt-md')!.mediaQuery);
192+
matchMedia.activate(breakPoints.findByAlias('lg')!.mediaQuery);
192193

193194
// 'all' mediaQuery is already active; total count should be (3)
194-
expect(activationCount).toEqual(3);
195+
expect(activationCount).toEqual(4);
195196
expect(deactivationCount).toEqual(0);
196197

197198
subscription.unsubscribe();

src/lib/core/match-media/mock/mock-match-media.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ describe('mock-match-media', () => {
181181
subscription.unsubscribe();
182182
});
183183

184-
it('can activate with either a mediaQuery or an alias', () => {
184+
it('can onMediaChange with either a mediaQuery or an alias', () => {
185185
let activates = 0;
186186
let bpGtSM = breakPoints.findByAlias('gt-sm'),
187187
bpLg = breakPoints.findByAlias('lg');

0 commit comments

Comments
 (0)