@ssv/ngx.ux 2.1.2-dev.37 → 3.0.0-dev.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -114
- package/eslint.config.js +43 -0
- package/index.ts +1 -0
- package/jest.config.ts +21 -0
- package/ng-package.json +7 -0
- package/package.json +4 -30
- package/project.json +36 -0
- package/src/index.ts +4 -0
- package/src/internal/internal.model.ts +3 -0
- package/src/platform/window.ts +31 -0
- package/src/test-setup.ts +8 -0
- package/src/ux.module.ts +15 -0
- package/src/version.ts +1 -0
- package/src/viewport/index.ts +20 -0
- package/src/viewport/viewport-data/README.md +47 -0
- package/src/viewport/viewport-data/index.ts +3 -0
- package/src/viewport/viewport-data/viewport-data-matcher.spec.ts +227 -0
- package/src/viewport/viewport-data/viewport-data-matcher.ts +175 -0
- package/src/viewport/viewport-data/viewport-data.pipe.ts +51 -0
- package/src/viewport/viewport-data/viewport-data.service.ts +48 -0
- package/src/viewport/viewport-data/viewport-data.utils.spec.ts +228 -0
- package/src/viewport/viewport-data/viewport-data.utils.ts +137 -0
- package/src/viewport/viewport-matcher-var.directive.ts +85 -0
- package/src/viewport/viewport-matcher.directive.ts +170 -0
- package/src/viewport/viewport-server-size.service.ts +37 -0
- package/src/viewport/viewport.model.ts +54 -0
- package/src/viewport/viewport.module.ts +19 -0
- package/src/viewport/viewport.options.ts +74 -0
- package/src/viewport/viewport.service.ts +123 -0
- package/src/viewport/viewport.util.spec.ts +254 -0
- package/src/viewport/viewport.util.ts +152 -0
- package/tsconfig.json +28 -0
- package/tsconfig.lib.json +12 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +11 -0
- package/LICENSE +0 -21
- package/config.d.ts +0 -7
- package/esm2020/config.mjs +0 -7
- package/esm2020/index.mjs +0 -5
- package/esm2020/internal/internal.model.mjs +0 -2
- package/esm2020/module.mjs +0 -65
- package/esm2020/platform/window.mjs +0 -30
- package/esm2020/ssv-ngx.ux.mjs +0 -5
- package/esm2020/version.mjs +0 -2
- package/esm2020/viewport/index.mjs +0 -9
- package/esm2020/viewport/viewport-data/index.mjs +0 -4
- package/esm2020/viewport/viewport-data/viewport-data-matcher.mjs +0 -108
- package/esm2020/viewport/viewport-data/viewport-data.pipe.mjs +0 -43
- package/esm2020/viewport/viewport-data/viewport-data.service.mjs +0 -37
- package/esm2020/viewport/viewport-data/viewport-data.utils.mjs +0 -100
- package/esm2020/viewport/viewport-matcher-var.directive.mjs +0 -63
- package/esm2020/viewport/viewport-matcher.directive.mjs +0 -131
- package/esm2020/viewport/viewport-server-size.service.mjs +0 -43
- package/esm2020/viewport/viewport.const.mjs +0 -17
- package/esm2020/viewport/viewport.model.mjs +0 -30
- package/esm2020/viewport/viewport.service.mjs +0 -66
- package/esm2020/viewport/viewport.util.mjs +0 -117
- package/fesm2015/ssv-ngx.ux.mjs +0 -826
- package/fesm2015/ssv-ngx.ux.mjs.map +0 -1
- package/fesm2020/ssv-ngx.ux.mjs +0 -820
- package/fesm2020/ssv-ngx.ux.mjs.map +0 -1
- package/index.d.ts +0 -4
- package/internal/internal.model.d.ts +0 -9
- package/module.d.ts +0 -19
- package/platform/window.d.ts +0 -13
- package/version.d.ts +0 -1
- package/viewport/index.d.ts +0 -8
- package/viewport/viewport-data/index.d.ts +0 -3
- package/viewport/viewport-data/viewport-data-matcher.d.ts +0 -32
- package/viewport/viewport-data/viewport-data.pipe.d.ts +0 -18
- package/viewport/viewport-data/viewport-data.service.d.ts +0 -20
- package/viewport/viewport-data/viewport-data.utils.d.ts +0 -21
- package/viewport/viewport-matcher-var.directive.d.ts +0 -25
- package/viewport/viewport-matcher.directive.d.ts +0 -33
- package/viewport/viewport-server-size.service.d.ts +0 -12
- package/viewport/viewport.const.d.ts +0 -5
- package/viewport/viewport.model.d.ts +0 -59
- package/viewport/viewport.service.d.ts +0 -37
- package/viewport/viewport.util.d.ts +0 -25
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { generateViewportSizeTypeInfoList, generateViewportSizeTypeInfoRefs, } from "../viewport.util";
|
|
2
|
+
import { ViewportSizeTypeInfo } from "../viewport.model";
|
|
3
|
+
import {
|
|
4
|
+
generateViewportRulesRangeFromDataMatcher as generateViewportRulesRangeFromDataMatcher_,
|
|
5
|
+
} from "./viewport-data.utils";
|
|
6
|
+
import { TestViewportSizeType } from "../viewport.util.spec";
|
|
7
|
+
import { ViewportDataConfig, ViewportDataMatchStrategy } from "./viewport-data-matcher";
|
|
8
|
+
|
|
9
|
+
type TestViewportDataConfig<T> = ViewportDataConfig<T, Partial<Record<keyof typeof TestViewportSizeType, T>>>;
|
|
10
|
+
|
|
11
|
+
const breakpoints: Record<keyof typeof TestViewportSizeType, number> = {
|
|
12
|
+
xsmall: 450,
|
|
13
|
+
small: 767,
|
|
14
|
+
medium: 992,
|
|
15
|
+
large: 1200,
|
|
16
|
+
hd: 1280,
|
|
17
|
+
fullHd: 1920,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const sizeTypes = generateViewportSizeTypeInfoList(breakpoints);
|
|
21
|
+
const sizeRefs = generateViewportSizeTypeInfoRefs(sizeTypes) as Record<keyof typeof TestViewportSizeType, ViewportSizeTypeInfo>;
|
|
22
|
+
|
|
23
|
+
const generateViewportRulesRangeFromDataMatcher = <T>(
|
|
24
|
+
dataConfig: ViewportDataConfig<T>,
|
|
25
|
+
strategy: ViewportDataMatchStrategy,
|
|
26
|
+
) => generateViewportRulesRangeFromDataMatcher_<T>(dataConfig, strategy, sizeTypes, sizeRefs);
|
|
27
|
+
|
|
28
|
+
describe("viewportDataUtils", () => {
|
|
29
|
+
|
|
30
|
+
describe("generateViewportRulesRangeFromDataMatcher", () => {
|
|
31
|
+
|
|
32
|
+
describe("given strategy is exact", () => {
|
|
33
|
+
const strategy = ViewportDataMatchStrategy.exact;
|
|
34
|
+
const dataConfig: TestViewportDataConfig<number> = {
|
|
35
|
+
default: 12,
|
|
36
|
+
xsmall: undefined,
|
|
37
|
+
small: 10,
|
|
38
|
+
medium: undefined,
|
|
39
|
+
large: 20,
|
|
40
|
+
hd: 25,
|
|
41
|
+
fullHd: undefined,
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* default: 12
|
|
45
|
+
* [451-767]=10
|
|
46
|
+
* [993-1200]=20
|
|
47
|
+
* [1201-1280]=25
|
|
48
|
+
* */
|
|
49
|
+
|
|
50
|
+
it("should return matching rules", () => {
|
|
51
|
+
const result = generateViewportRulesRangeFromDataMatcher(dataConfig, strategy);
|
|
52
|
+
expect(result).toEqual([
|
|
53
|
+
{ min: undefined, max: undefined, value: dataConfig.default },
|
|
54
|
+
{ min: breakpoints.xsmall + 1, max: breakpoints.small, value: dataConfig.small },
|
|
55
|
+
{ min: breakpoints.medium + 1, max: breakpoints.large, value: dataConfig.large },
|
|
56
|
+
{ min: breakpoints.large + 1, max: breakpoints.hd, value: dataConfig.hd },
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("given strategy is smaller", () => {
|
|
63
|
+
const strategy = ViewportDataMatchStrategy.smaller;
|
|
64
|
+
const dataConfig: TestViewportDataConfig<number> = {
|
|
65
|
+
default: 12,
|
|
66
|
+
medium: 15,
|
|
67
|
+
large: 20,
|
|
68
|
+
hd: 30,
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* default: 12
|
|
72
|
+
* [768-992]=15
|
|
73
|
+
* [993-1200]=20
|
|
74
|
+
* [1201-*]=30
|
|
75
|
+
* */
|
|
76
|
+
|
|
77
|
+
it("should return matching rules", () => {
|
|
78
|
+
const result = generateViewportRulesRangeFromDataMatcher(dataConfig, strategy);
|
|
79
|
+
expect(result).toEqual([
|
|
80
|
+
{ min: undefined, max: undefined, value: dataConfig.default }, // default
|
|
81
|
+
{ min: breakpoints.small + 1, max: breakpoints.medium, value: dataConfig.medium }, // medium
|
|
82
|
+
{ min: breakpoints.medium + 1, max: breakpoints.large, value: dataConfig.large }, // large
|
|
83
|
+
{ min: breakpoints.large + 1, max: undefined, value: dataConfig.hd }, // hd
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("given strategy is larger", () => {
|
|
89
|
+
const strategy = ViewportDataMatchStrategy.larger;
|
|
90
|
+
const dataConfig: TestViewportDataConfig<number> = {
|
|
91
|
+
default: 12,
|
|
92
|
+
medium: 15,
|
|
93
|
+
large: 20,
|
|
94
|
+
hd: 30,
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* default: 12
|
|
98
|
+
* [*-992]=15
|
|
99
|
+
* [993-1200]=20
|
|
100
|
+
* [1201-1280]=30
|
|
101
|
+
* */
|
|
102
|
+
it("should return matching rules", () => {
|
|
103
|
+
const result = generateViewportRulesRangeFromDataMatcher(dataConfig, strategy);
|
|
104
|
+
expect(result).toEqual([
|
|
105
|
+
{ min: undefined, max: undefined, value: dataConfig.default }, // default
|
|
106
|
+
{ min: undefined, max: breakpoints.medium, value: dataConfig.medium }, // medium
|
|
107
|
+
{ min: breakpoints.medium + 1, max: breakpoints.large, value: dataConfig.large }, // large
|
|
108
|
+
{ min: breakpoints.large + 1, max: breakpoints.hd, value: dataConfig.hd }, // hd
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("given strategy is closestSmallFirst", () => {
|
|
114
|
+
const strategy = ViewportDataMatchStrategy.closestSmallerFirst;
|
|
115
|
+
|
|
116
|
+
// todo: name
|
|
117
|
+
describe("when dataset A", () => {
|
|
118
|
+
const dataConfig: TestViewportDataConfig<number> = {
|
|
119
|
+
default: 12,
|
|
120
|
+
xsmall: 5,
|
|
121
|
+
small: undefined,
|
|
122
|
+
medium: 15,
|
|
123
|
+
large: undefined,
|
|
124
|
+
hd: undefined,
|
|
125
|
+
fullHd: 50,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
it("should return matching rules", () => {
|
|
129
|
+
const result = generateViewportRulesRangeFromDataMatcher(dataConfig, strategy);
|
|
130
|
+
expect(result).toEqual([
|
|
131
|
+
{ min: undefined, max: undefined, value: dataConfig.default },
|
|
132
|
+
{ min: undefined, max: breakpoints.small, value: dataConfig.xsmall },
|
|
133
|
+
{ min: breakpoints.small + 1, max: breakpoints.large, value: dataConfig.medium },
|
|
134
|
+
{ min: breakpoints.large + 1, max: undefined, value: dataConfig.fullHd },
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// todo: name
|
|
140
|
+
describe("when dataset B", () => {
|
|
141
|
+
const dataConfig: TestViewportDataConfig<number> = {
|
|
142
|
+
default: 12,
|
|
143
|
+
xsmall: undefined,
|
|
144
|
+
small: 10,
|
|
145
|
+
medium: undefined,
|
|
146
|
+
large: 20,
|
|
147
|
+
hd: 25,
|
|
148
|
+
fullHd: undefined,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
it("should return matching rules", () => {
|
|
152
|
+
const result = generateViewportRulesRangeFromDataMatcher(dataConfig, strategy);
|
|
153
|
+
expect(result).toEqual([
|
|
154
|
+
{ min: undefined, max: undefined, value: dataConfig.default },
|
|
155
|
+
{ min: undefined, max: breakpoints.medium, value: dataConfig.small },
|
|
156
|
+
{ min: breakpoints.medium + 1, max: breakpoints.large, value: dataConfig.large },
|
|
157
|
+
{ min: breakpoints.large + 1, max: undefined, value: dataConfig.hd },
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("given strategy is closestLargerFirst", () => {
|
|
165
|
+
const strategy = ViewportDataMatchStrategy.closestLargerFirst;
|
|
166
|
+
|
|
167
|
+
// todo: name
|
|
168
|
+
describe("when dataset A", () => {
|
|
169
|
+
const dataConfig: TestViewportDataConfig<number> = {
|
|
170
|
+
default: 12,
|
|
171
|
+
xsmall: 5,
|
|
172
|
+
small: undefined,
|
|
173
|
+
medium: 15,
|
|
174
|
+
large: undefined,
|
|
175
|
+
hd: undefined,
|
|
176
|
+
fullHd: 50,
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* [*-450]=5
|
|
180
|
+
* [451-1200]=10
|
|
181
|
+
* [1201-*]=50
|
|
182
|
+
* */
|
|
183
|
+
|
|
184
|
+
it("should return matching rules", () => {
|
|
185
|
+
const result = generateViewportRulesRangeFromDataMatcher(dataConfig, strategy);
|
|
186
|
+
expect(result).toEqual([
|
|
187
|
+
{ min: undefined, max: undefined, value: dataConfig.default },
|
|
188
|
+
{ min: undefined, max: breakpoints.xsmall, value: dataConfig.xsmall },
|
|
189
|
+
{ min: breakpoints.xsmall + 1, max: breakpoints.large, value: dataConfig.medium },
|
|
190
|
+
{ min: breakpoints.large + 1, max: undefined, value: dataConfig.fullHd },
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// todo: name
|
|
196
|
+
describe("when dataset B", () => {
|
|
197
|
+
const dataConfig: TestViewportDataConfig<number> = {
|
|
198
|
+
default: 12,
|
|
199
|
+
xsmall: undefined,
|
|
200
|
+
small: 10,
|
|
201
|
+
medium: undefined,
|
|
202
|
+
large: 20,
|
|
203
|
+
hd: 25,
|
|
204
|
+
fullHd: undefined,
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* [*-767]=10
|
|
208
|
+
* [768-1200]=20
|
|
209
|
+
* [1201-*]=25
|
|
210
|
+
* */
|
|
211
|
+
it("should return matching rules", () => {
|
|
212
|
+
const result = generateViewportRulesRangeFromDataMatcher(dataConfig, strategy);
|
|
213
|
+
expect(result).toEqual([
|
|
214
|
+
{ min: undefined, max: undefined, value: dataConfig.default },
|
|
215
|
+
{ min: undefined, max: breakpoints.small, value: dataConfig.small },
|
|
216
|
+
{ min: breakpoints.small + 1, max: breakpoints.large, value: dataConfig.large },
|
|
217
|
+
{ min: breakpoints.large + 1, max: undefined, value: dataConfig.hd },
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Dictionary } from "../../internal/internal.model";
|
|
2
|
+
import { ViewportSizeTypeInfo } from "../viewport.model";
|
|
3
|
+
import { ViewportDataConfig, ViewportDataMatchStrategy } from "./viewport-data-matcher";
|
|
4
|
+
|
|
5
|
+
export interface ViewportDataRule<T> {
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
value: T;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Utility function to generate rules based on strategies.
|
|
13
|
+
*
|
|
14
|
+
* @param dataConfig Data config to generate rules based on.
|
|
15
|
+
* @param strategy Strategy to use when building rules.
|
|
16
|
+
* @param sizeTypes Available size types ordered by index type. (Can be obtained from `ViewportService`)
|
|
17
|
+
* @param sizeTypeMap Available size type map. (Can be obtained from `ViewportService`)
|
|
18
|
+
* @returns Returns a collection of rules (ordered).
|
|
19
|
+
*/
|
|
20
|
+
export function generateViewportRulesRangeFromDataMatcher<T>(
|
|
21
|
+
dataConfig: ViewportDataConfig<T>,
|
|
22
|
+
strategy: ViewportDataMatchStrategy,
|
|
23
|
+
sizeTypes: ViewportSizeTypeInfo[],
|
|
24
|
+
sizeTypeMap: Dictionary<ViewportSizeTypeInfo>,
|
|
25
|
+
): ViewportDataRule<T>[] {
|
|
26
|
+
const ruleBuilderFn = matchStrategyHandlerMap[strategy];
|
|
27
|
+
if (!ruleBuilderFn) {
|
|
28
|
+
throw Error(`generateViewportRulesRangeFromDataMatcher: Viewport Data strategy not implemented. Strategy: '${strategy}'`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let dataSizes: ViewportSizeTypeInfo[] = [];
|
|
32
|
+
for (const key in dataConfig) {
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(dataConfig, key)) {
|
|
34
|
+
const data = dataConfig[key];
|
|
35
|
+
if (data === undefined) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const size = sizeTypeMap[key];
|
|
39
|
+
if (size) {
|
|
40
|
+
dataSizes.push(size);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
dataSizes = dataSizes.sort(({ type: typeA }, { type: typeB }) => typeA - typeB);
|
|
45
|
+
|
|
46
|
+
const rules: ViewportDataRule<T>[] = [];
|
|
47
|
+
if (dataConfig.default) {
|
|
48
|
+
rules.push({ value: dataConfig.default, min: undefined, max: undefined });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let prevRule: ViewportDataRule<T> | undefined;
|
|
52
|
+
for (let index = 0; index < dataSizes.length; index++) {
|
|
53
|
+
const prevDataSize = dataSizes[index - 1];
|
|
54
|
+
const nextDataSize = dataSizes[index + 1];
|
|
55
|
+
const dataSize = dataSizes[index];
|
|
56
|
+
const prevSize = sizeTypes[dataSize.type - 1];
|
|
57
|
+
// const nextSize = sizeTypes[dataSize.type + 1];
|
|
58
|
+
const data = dataConfig[dataSize.name];
|
|
59
|
+
const rule: ViewportDataRule<T> = {
|
|
60
|
+
value: data,
|
|
61
|
+
min: undefined,
|
|
62
|
+
max: undefined,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
ruleBuilderFn(rule, dataSize, nextDataSize, prevDataSize, prevSize, prevRule, sizeTypes);
|
|
66
|
+
|
|
67
|
+
prevRule = rule;
|
|
68
|
+
rules.push(rule);
|
|
69
|
+
}
|
|
70
|
+
return rules;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ViewportRuleRangeBuilder<T = unknown> {
|
|
74
|
+
(
|
|
75
|
+
rule: ViewportDataRule<T>,
|
|
76
|
+
dataSize: ViewportSizeTypeInfo,
|
|
77
|
+
nextDataSize: ViewportSizeTypeInfo | undefined,
|
|
78
|
+
prevDataSize: ViewportSizeTypeInfo | undefined,
|
|
79
|
+
prevSize: ViewportSizeTypeInfo | undefined,
|
|
80
|
+
prevRule: ViewportDataRule<T> | undefined,
|
|
81
|
+
sizeTypes: ViewportSizeTypeInfo[],
|
|
82
|
+
): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const matchStrategyHandlerMap: Dictionary<ViewportRuleRangeBuilder> = {
|
|
86
|
+
[ViewportDataMatchStrategy.exact]: (rule, dataSize, _nextDataSize, _prevDataSize, prevSize) => {
|
|
87
|
+
rule.max = dataSize.widthThreshold;
|
|
88
|
+
if (prevSize) {
|
|
89
|
+
rule.min = prevSize.widthThreshold + 1;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
[ViewportDataMatchStrategy.smaller]: (rule, dataSize, nextDataSize, _prevDataSize, prevSize) => {
|
|
93
|
+
if (nextDataSize) {
|
|
94
|
+
rule.max = dataSize.widthThreshold;
|
|
95
|
+
}
|
|
96
|
+
if (prevSize) {
|
|
97
|
+
rule.min = prevSize.widthThreshold + 1;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[ViewportDataMatchStrategy.larger]: (rule, dataSize, _nextDataSize, prevDataSize) => {
|
|
101
|
+
if (dataSize) {
|
|
102
|
+
rule.max = dataSize.widthThreshold;
|
|
103
|
+
}
|
|
104
|
+
if (prevDataSize) {
|
|
105
|
+
rule.min = prevDataSize.widthThreshold + 1;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[ViewportDataMatchStrategy.closestSmallerFirst]: (rule, dataSize, nextDataSize, _prevDataSize, _prevSize, prevRule, sizeTypes) => {
|
|
109
|
+
if (nextDataSize) {
|
|
110
|
+
rule.max = calculateClosestWidthThreshold(nextDataSize, dataSize, sizeTypes, true);
|
|
111
|
+
}
|
|
112
|
+
if (prevRule?.max) {
|
|
113
|
+
rule.min = prevRule.max + 1;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
[ViewportDataMatchStrategy.closestLargerFirst]: (rule, dataSize, nextDataSize, _prevDataSize, _prevSize, prevRule, sizeTypes) => {
|
|
117
|
+
if (nextDataSize) {
|
|
118
|
+
rule.max = calculateClosestWidthThreshold(nextDataSize, dataSize, sizeTypes, false);
|
|
119
|
+
}
|
|
120
|
+
if (prevRule?.max) {
|
|
121
|
+
rule.min = prevRule.max + 1;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function calculateClosestWidthThreshold(
|
|
127
|
+
nextDataSize: ViewportSizeTypeInfo,
|
|
128
|
+
dataSize: ViewportSizeTypeInfo,
|
|
129
|
+
sizeTypes: ViewportSizeTypeInfo[],
|
|
130
|
+
isSmallerPreferred: boolean,
|
|
131
|
+
) {
|
|
132
|
+
const fn = isSmallerPreferred ? Math.ceil : Math.floor;
|
|
133
|
+
// get closest between curr and next
|
|
134
|
+
const diffIndex = fn((nextDataSize.type - dataSize.type - 1) / 2);
|
|
135
|
+
const diffNextSize = sizeTypes[dataSize.type + diffIndex];
|
|
136
|
+
return (diffNextSize || dataSize).widthThreshold;
|
|
137
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OnInit,
|
|
3
|
+
OnDestroy,
|
|
4
|
+
Directive,
|
|
5
|
+
Input,
|
|
6
|
+
TemplateRef,
|
|
7
|
+
ViewContainerRef,
|
|
8
|
+
EmbeddedViewRef,
|
|
9
|
+
} from "@angular/core";
|
|
10
|
+
import { combineLatest, ReplaySubject, Subject, tap, map, takeUntil } from "rxjs";
|
|
11
|
+
|
|
12
|
+
import { ViewportService } from "./viewport.service";
|
|
13
|
+
import {
|
|
14
|
+
isViewportSizeMatcherExpression,
|
|
15
|
+
isViewportSizeMatcherTupleExpression,
|
|
16
|
+
isViewportConditionMatch
|
|
17
|
+
} from "./viewport.util";
|
|
18
|
+
import { ViewportMatchConditions, ViewportSizeMatcherExpression } from "./viewport.model";
|
|
19
|
+
|
|
20
|
+
const NAME_CAMEL = "ssvViewportMatcherVar";
|
|
21
|
+
|
|
22
|
+
export class SsvViewportMatcherVarContext {
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
public $implicit = false,
|
|
26
|
+
) { }
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Directive({
|
|
31
|
+
selector: `[${NAME_CAMEL}]`,
|
|
32
|
+
standalone: true,
|
|
33
|
+
})
|
|
34
|
+
export class SsvViewportMatcherVarDirective implements OnInit, OnDestroy {
|
|
35
|
+
|
|
36
|
+
private _matchConditions: ViewportMatchConditions = {};
|
|
37
|
+
private _context = new SsvViewportMatcherVarContext();
|
|
38
|
+
private readonly _destroy$ = new Subject<void>();
|
|
39
|
+
private readonly _update$ = new ReplaySubject<void>(1);
|
|
40
|
+
private _viewRef!: EmbeddedViewRef<SsvViewportMatcherVarContext>;
|
|
41
|
+
|
|
42
|
+
@Input(`${NAME_CAMEL}When`) set condition(value: string | string[] | ViewportSizeMatcherExpression) {
|
|
43
|
+
if (isViewportSizeMatcherExpression(value)) {
|
|
44
|
+
this._matchConditions.expression = value;
|
|
45
|
+
} else if (isViewportSizeMatcherTupleExpression(value)) {
|
|
46
|
+
const [op, size] = value;
|
|
47
|
+
this._matchConditions.expression = {
|
|
48
|
+
operation: op,
|
|
49
|
+
size
|
|
50
|
+
};
|
|
51
|
+
} else {
|
|
52
|
+
this._matchConditions.sizeType = value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this._update$.next();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
private viewport: ViewportService,
|
|
60
|
+
private viewContainer: ViewContainerRef,
|
|
61
|
+
private templateRef: TemplateRef<SsvViewportMatcherVarContext>,
|
|
62
|
+
) {
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ngOnInit(): void {
|
|
66
|
+
this.updateView();
|
|
67
|
+
combineLatest([this.viewport.sizeType$, this._update$]).pipe(
|
|
68
|
+
map(([sizeType]) => isViewportConditionMatch(sizeType, this._matchConditions, this.viewport.sizeTypeMap)),
|
|
69
|
+
tap(x => this._context.$implicit = x),
|
|
70
|
+
tap(() => this._viewRef.markForCheck()),
|
|
71
|
+
takeUntil(this._destroy$),
|
|
72
|
+
).subscribe();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ngOnDestroy(): void {
|
|
76
|
+
this._destroy$.next();
|
|
77
|
+
this._destroy$.complete();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private updateView(): void {
|
|
81
|
+
this.viewContainer.clear();
|
|
82
|
+
this._viewRef = this.viewContainer.createEmbeddedView(this.templateRef, this._context);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OnInit,
|
|
3
|
+
OnDestroy,
|
|
4
|
+
Directive,
|
|
5
|
+
Renderer2,
|
|
6
|
+
ViewContainerRef,
|
|
7
|
+
Input,
|
|
8
|
+
EmbeddedViewRef,
|
|
9
|
+
TemplateRef,
|
|
10
|
+
ChangeDetectorRef,
|
|
11
|
+
} from "@angular/core";
|
|
12
|
+
import { Subscription, Subject, tap, filter, pairwise, startWith } from "rxjs";
|
|
13
|
+
|
|
14
|
+
import { ViewportService } from "./viewport.service";
|
|
15
|
+
import {
|
|
16
|
+
isViewportSizeMatcherExpression,
|
|
17
|
+
isViewportSizeMatcherTupleExpression,
|
|
18
|
+
isViewportConditionMatch
|
|
19
|
+
} from "./viewport.util";
|
|
20
|
+
import { ViewportSizeTypeInfo, ViewportMatchConditions, ViewportSizeMatcherExpression } from "./viewport.model";
|
|
21
|
+
|
|
22
|
+
export class SsvViewportMatcherContext implements ViewportMatchConditions {
|
|
23
|
+
|
|
24
|
+
sizeType: string | string[] | null = null;
|
|
25
|
+
sizeTypeExclude: string | string[] | null = null;
|
|
26
|
+
expression?: ViewportSizeMatcherExpression;
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Directive({
|
|
31
|
+
selector: "[ssvViewportMatcher]",
|
|
32
|
+
exportAs: "ssvViewportMatcher",
|
|
33
|
+
standalone: true,
|
|
34
|
+
})
|
|
35
|
+
export class SsvViewportMatcherDirective implements OnInit, OnDestroy {
|
|
36
|
+
|
|
37
|
+
sizeInfo: ViewportSizeTypeInfo | undefined;
|
|
38
|
+
|
|
39
|
+
private _context: SsvViewportMatcherContext = new SsvViewportMatcherContext();
|
|
40
|
+
private _thenTemplateRef: TemplateRef<SsvViewportMatcherContext> | null = null;
|
|
41
|
+
private _elseTemplateRef: TemplateRef<SsvViewportMatcherContext> | null = null;
|
|
42
|
+
private _thenViewRef: EmbeddedViewRef<SsvViewportMatcherContext> | null = null;
|
|
43
|
+
private _elseViewRef: EmbeddedViewRef<SsvViewportMatcherContext> | null = null;
|
|
44
|
+
private sizeType$$ = Subscription.EMPTY;
|
|
45
|
+
private cssClass$$ = Subscription.EMPTY;
|
|
46
|
+
private readonly _update$ = new Subject<SsvViewportMatcherContext>();
|
|
47
|
+
|
|
48
|
+
@Input() set ssvViewportMatcher(value: string | string[] | ViewportSizeMatcherExpression) {
|
|
49
|
+
if (isViewportSizeMatcherExpression(value)) {
|
|
50
|
+
this._context.expression = value;
|
|
51
|
+
} else if (isViewportSizeMatcherTupleExpression(value)) {
|
|
52
|
+
const [op, size] = value;
|
|
53
|
+
this._context.expression = {
|
|
54
|
+
operation: op,
|
|
55
|
+
size
|
|
56
|
+
};
|
|
57
|
+
} else {
|
|
58
|
+
this._context.sizeType = value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.sizeInfo) {
|
|
62
|
+
this._update$.next(this._context);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Input() set ssvViewportMatcherExclude(value: string | string[]) {
|
|
67
|
+
this._context.sizeTypeExclude = value;
|
|
68
|
+
|
|
69
|
+
if (this.sizeInfo) {
|
|
70
|
+
this._update$.next(this._context);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Input() set ssvViewportMatcherElse(templateRef: TemplateRef<SsvViewportMatcherContext> | null) {
|
|
75
|
+
this._elseTemplateRef = templateRef;
|
|
76
|
+
this._elseViewRef = null; // clear previous view if any.
|
|
77
|
+
if (this.sizeInfo) {
|
|
78
|
+
this._update$.next(this._context);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private viewport: ViewportService,
|
|
84
|
+
private renderer: Renderer2,
|
|
85
|
+
private viewContainer: ViewContainerRef,
|
|
86
|
+
private cdr: ChangeDetectorRef,
|
|
87
|
+
templateRef: TemplateRef<SsvViewportMatcherContext>,
|
|
88
|
+
) {
|
|
89
|
+
this._thenTemplateRef = templateRef;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ngOnInit(): void {
|
|
93
|
+
// console.log("ssvViewportMatcher init");
|
|
94
|
+
|
|
95
|
+
this._update$
|
|
96
|
+
.pipe(
|
|
97
|
+
// tap(x => console.log(">>> ssvViewportMatcher - update triggered", x)),
|
|
98
|
+
filter(() => !!this.sizeInfo),
|
|
99
|
+
// tap(x => console.log(">>> ssvViewportMatcher - updating...", x)),
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
101
|
+
tap(() => this._updateView(this.sizeInfo!)),
|
|
102
|
+
tap(() => this.cdr.markForCheck())
|
|
103
|
+
)
|
|
104
|
+
.subscribe();
|
|
105
|
+
|
|
106
|
+
this.sizeType$$ = this.viewport.sizeType$
|
|
107
|
+
.pipe(
|
|
108
|
+
// tap(x => console.log("ssvViewportMatcher - sizeType changed", x)),
|
|
109
|
+
tap(x => this.sizeInfo = x),
|
|
110
|
+
tap(() => this._update$.next(this._context)),
|
|
111
|
+
)
|
|
112
|
+
.subscribe();
|
|
113
|
+
|
|
114
|
+
this.cssClass$$ = this.viewport.sizeType$
|
|
115
|
+
.pipe(
|
|
116
|
+
startWith<ViewportSizeTypeInfo | undefined>(undefined),
|
|
117
|
+
filter(() => !!(this._thenViewRef || this._elseViewRef)),
|
|
118
|
+
pairwise(),
|
|
119
|
+
tap(([prev, curr]) => {
|
|
120
|
+
const el: Element = this._thenViewRef
|
|
121
|
+
? this._thenViewRef.rootNodes[0]
|
|
122
|
+
: this._elseViewRef?.rootNodes[0];
|
|
123
|
+
|
|
124
|
+
if (!el.classList) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (prev) {
|
|
128
|
+
this.renderer.removeClass(el, `ssv-vp-size--${prev.name}`);
|
|
129
|
+
}
|
|
130
|
+
this.renderer.addClass(el, `ssv-vp-size--${curr?.name}`);
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
.subscribe();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ngOnDestroy(): void {
|
|
137
|
+
this.cssClass$$.unsubscribe();
|
|
138
|
+
this.sizeType$$.unsubscribe();
|
|
139
|
+
this._update$.complete();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private _updateView(sizeInfo: ViewportSizeTypeInfo) {
|
|
143
|
+
if (isViewportConditionMatch(sizeInfo, this._context, this.viewport.sizeTypeMap)) {
|
|
144
|
+
if (!this._thenViewRef) {
|
|
145
|
+
this.viewContainer.clear();
|
|
146
|
+
this._elseViewRef = null;
|
|
147
|
+
|
|
148
|
+
if (this._thenTemplateRef) {
|
|
149
|
+
this._thenViewRef = this.viewContainer.createEmbeddedView(
|
|
150
|
+
this._thenTemplateRef,
|
|
151
|
+
this._context,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
if (!this._elseViewRef) {
|
|
157
|
+
this.viewContainer.clear();
|
|
158
|
+
this._thenViewRef = null;
|
|
159
|
+
|
|
160
|
+
if (this._elseTemplateRef) {
|
|
161
|
+
this._elseViewRef = this.viewContainer.createEmbeddedView(
|
|
162
|
+
this._elseTemplateRef,
|
|
163
|
+
this._context,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Injectable, InjectionToken, inject } from "@angular/core";
|
|
2
|
+
|
|
3
|
+
import { DeviceType, ViewportSize } from "./viewport.model";
|
|
4
|
+
|
|
5
|
+
// todo: make this configurable
|
|
6
|
+
/** Viewport size for SSR. */
|
|
7
|
+
const viewportSizeSSR: Record<DeviceType, ViewportSize> = {
|
|
8
|
+
[DeviceType.desktop]: {
|
|
9
|
+
width: 1366,
|
|
10
|
+
height: 768,
|
|
11
|
+
},
|
|
12
|
+
[DeviceType.tablet]: {
|
|
13
|
+
width: 768,
|
|
14
|
+
height: 1024,
|
|
15
|
+
},
|
|
16
|
+
[DeviceType.mobile]: {
|
|
17
|
+
width: 414,
|
|
18
|
+
height: 736,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const VIEWPORT_SSR_DEVICE = new InjectionToken<DeviceType>("UX_VIEWPORT_SSR_DEVICE", {
|
|
23
|
+
factory: () => DeviceType.desktop,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
@Injectable({
|
|
27
|
+
providedIn: "root",
|
|
28
|
+
})
|
|
29
|
+
export class ViewportServerSizeService {
|
|
30
|
+
|
|
31
|
+
private readonly deviceType = inject(VIEWPORT_SSR_DEVICE);
|
|
32
|
+
|
|
33
|
+
get(): ViewportSize {
|
|
34
|
+
return viewportSizeSSR[this.deviceType] || viewportSizeSSR[DeviceType.desktop];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The indices of each breakpoint provided based on the `UX_VIEWPORT_DEFAULT_BREAKPOINTS`.
|
|
3
|
+
* @see UX_VIEWPORT_DEFAULT_BREAKPOINTS
|
|
4
|
+
*/
|
|
5
|
+
export enum ViewportSizeType {
|
|
6
|
+
xsmall = 0,
|
|
7
|
+
small = 1,
|
|
8
|
+
medium = 2,
|
|
9
|
+
large = 3,
|
|
10
|
+
xlarge = 4,
|
|
11
|
+
xxlarge = 5,
|
|
12
|
+
xxlarge1 = 6,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export enum ComparisonOperation {
|
|
16
|
+
equals = "=",
|
|
17
|
+
notEquals = "<>",
|
|
18
|
+
lessThan = "<",
|
|
19
|
+
lessOrEqualThan = "<=",
|
|
20
|
+
greaterThan = ">",
|
|
21
|
+
greaterOrEqualThan = ">=",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ComparisonOperationKeyType = keyof typeof ComparisonOperation;
|
|
25
|
+
export type ComparisonOperationValueType = "=" | "<>" | "<" | "<=" | ">" | ">="; // todo: find a better way to do this
|
|
26
|
+
export type ComparisonOperationLiteral = ComparisonOperationKeyType | ComparisonOperationValueType;
|
|
27
|
+
|
|
28
|
+
export enum DeviceType {
|
|
29
|
+
desktop = "desktop",
|
|
30
|
+
mobile = "mobile",
|
|
31
|
+
tablet = "tablet"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ViewportSize {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ViewportSizeTypeInfo {
|
|
40
|
+
type: number;
|
|
41
|
+
name: string;
|
|
42
|
+
widthThreshold: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ViewportMatchConditions {
|
|
46
|
+
sizeType?: string | string[] | null;
|
|
47
|
+
sizeTypeExclude?: string | string[] | null;
|
|
48
|
+
expression?: ViewportSizeMatcherExpression;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ViewportSizeMatcherExpression {
|
|
52
|
+
size: string;
|
|
53
|
+
operation: ComparisonOperationLiteral;
|
|
54
|
+
}
|