This repository was archived by the owner on Jan 6, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 768
/
Copy pathresponsive-activation.ts
218 lines (192 loc) · 7.13 KB
/
responsive-activation.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Subscription} from 'rxjs/Subscription';
import 'rxjs/add/operator/map';
import {extendObject} from '../../utils/object-extend';
import {MediaChange, MediaQuerySubscriber} from '../../media-query/media-change';
import {BreakPoint} from '../../media-query/breakpoints/break-point';
import {MediaMonitor} from '../../media-query/media-monitor';
export declare type SubscriptionList = Subscription[ ];
export interface BreakPointX extends BreakPoint {
key: string;
baseKey: string;
}
export class KeyOptions {
constructor(public baseKey: string,
public defaultValue: string|number|boolean,
public inputKeys: {[key: string]: any}) {
}
}
/**
* ResponsiveActivation acts as a proxy between the MonitorMedia service (which emits mediaQuery
* changes) and the fx API directives. The MQA proxies mediaQuery change events and notifies the
* directive via the specified callback.
*
* - The MQA also determines which directive property should be used to determine the
* current change 'value'... BEFORE the original `onMediaQueryChanges()` method is called.
* - The `ngOnDestroy()` method is also head-hooked to enable auto-unsubscribe from the
* MediaQueryServices.
*
* NOTE: these interceptions enables the logic in the fx API directives to remain terse and clean.
*/
export class ResponsiveActivation {
private _subscribers: SubscriptionList = [];
private _activatedInputKey: string;
/**
* Constructor
*/
constructor(private _options: KeyOptions,
private _mediaMonitor: MediaMonitor,
private _onMediaChanges: MediaQuerySubscriber) {
this._subscribers = this._configureChangeObservers();
}
/**
* Accessor to the DI'ed directive property
* Each directive instance has a reference to the MediaMonitor which is
* used HERE to subscribe to mediaQuery change notifications.
*/
get mediaMonitor(): MediaMonitor {
return this._mediaMonitor;
}
/**
* Determine which directive @Input() property is currently active (for the viewport size):
* The key must be defined (in use) or fallback to the 'closest' overlapping property key
* that is defined; otherwise the default property key will be used.
* e.g.
* if `<div fxHide fxHide.gt-sm="false">` is used but the current activated mediaQuery alias
* key is `.md` then `.gt-sm` should be used instead
*/
get activatedInputKey(): string {
return this._activatedInputKey || this._options.baseKey;
}
/**
* Get the currently activated @Input value or the fallback default @Input value
*/
get activatedInput(): any {
let key = this.activatedInputKey;
return this._hasKeyValue(key) ? this._lookupKeyValue(key) : this._options.defaultValue;
}
/**
* Fast validator for presence of attribute on the host element
*/
public hasKeyValue(key) {
let value = this._options.inputKeys[key];
return typeof value !== 'undefined';
}
/**
* Remove interceptors, restore original functions, and forward the onDestroy() call
*/
destroy() {
this._subscribers.forEach((link: Subscription) => {
link.unsubscribe();
});
this._subscribers = [];
}
/**
* For each *defined* API property, register a callback to `_onMonitorEvents( )`
* Cache 1..n subscriptions for internal auto-unsubscribes when the the directive destructs
*/
private _configureChangeObservers(): SubscriptionList {
let subscriptions = [];
this._buildRegistryMap().forEach((bp: BreakPointX) => {
if (this._keyInUse(bp.key)) {
// Inject directive default property key name: to let onMediaChange() calls
// know which property is being triggered...
let buildChanges = (change: MediaChange) => {
change.property = this._options.baseKey;
return change;
};
subscriptions.push(
this.mediaMonitor.observe(bp.alias)
.map(buildChanges)
.subscribe(change => {
this._onMonitorEvents(change);
})
);
}
});
return subscriptions;
}
/**
* Build mediaQuery key-hashmap; only for the directive properties that are actually defined/used
* in the HTML markup
*/
private _buildRegistryMap() {
return this.mediaMonitor.breakpoints
.map(bp => {
return <BreakPointX> extendObject({}, bp, {
baseKey: this._options.baseKey, // e.g. layout, hide, self-align, flex-wrap
key: this._options.baseKey + bp.suffix // e.g. layoutGtSm, layoutMd, layoutGtLg
});
})
.filter(bp => this._keyInUse(bp.key));
}
/**
* Synchronizes change notifications with the current mq-activated @Input and calculates the
* mq-activated input value or the default value
*/
_onMonitorEvents(change: MediaChange) {
if (change.property == this._options.baseKey) {
change.value = this._calculateActivatedValue(change);
this._onMediaChanges(change);
}
}
/**
* Has the key been specified in the HTML markup and thus is intended
* to participate in activation processes.
*/
private _keyInUse(key): boolean {
return this._lookupKeyValue(key) !== undefined;
}
/**
* Map input key associated with mediaQuery activation to closest defined input key
* then return the values associated with the targeted input property
*
* !! change events may arrive out-of-order (activate before deactivate)
* so make sure the deactivate is used ONLY when the keys match
* (since a different activate may be in use)
*/
private _calculateActivatedValue(current: MediaChange): any {
const currentKey = this._options.baseKey + current.suffix; // e.g. suffix == 'GtSm',
let newKey = this._activatedInputKey; // e.g. newKey == hideGtSm
newKey = current.matches ? currentKey : ((newKey == currentKey) ? null : newKey);
this._activatedInputKey = this._validateInputKey(newKey);
return this.activatedInput;
}
/**
* For the specified input property key, validate it is defined (used in the markup)
* If not see if a overlapping mediaQuery-related input key fallback has been defined
*
* NOTE: scans in the order defined by activeOverLaps (largest viewport ranges -> smallest ranges)
*/
private _validateInputKey(inputKey) {
let items: BreakPoint[] = this.mediaMonitor.activeOverlaps;
let isMissingKey = (key) => !this._keyInUse(key);
if (isMissingKey(inputKey)) {
items.some(bp => {
let key = this._options.baseKey + bp.suffix;
if (!isMissingKey(key)) {
inputKey = key;
return true; // exit .some()
}
return false;
});
}
return inputKey;
}
/**
* Get the value (if any) for the directive instances @Input property (aka key)
*/
private _lookupKeyValue(key) {
return this._options.inputKeys[key];
}
private _hasKeyValue(key) {
let value = this._options.inputKeys[key];
return typeof value !== 'undefined';
}
}