@lenne.tech/ng-base 0.0.11 → 0.2.2
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/bundles/lenne.tech-ng-base-src-lib-base.umd.js +338 -32
- package/bundles/lenne.tech-ng-base-src-lib-base.umd.js.map +1 -1
- package/bundles/lenne.tech-ng-base.umd.js +338 -32
- package/bundles/lenne.tech-ng-base.umd.js.map +1 -1
- package/esm2015/lib/base/factories/apollo-options.factory.js +43 -4
- package/esm2015/lib/base/interfaces/base-module-config.interface.js +1 -1
- package/esm2015/lib/base/services/graphql-plus.service.js +5 -2
- package/esm2015/lib/base/services/graphql.service.js +37 -6
- package/esm2015/lib/base/services/scroll.service.js +201 -16
- package/esm2015/src/lib/base/factories/apollo-options.factory.js +43 -4
- package/esm2015/src/lib/base/interfaces/base-module-config.interface.js +1 -1
- package/esm2015/src/lib/base/services/graphql-plus.service.js +5 -2
- package/esm2015/src/lib/base/services/graphql.service.js +37 -6
- package/esm2015/src/lib/base/services/scroll.service.js +201 -16
- package/fesm2015/lenne.tech-ng-base-src-lib-base.js +281 -24
- package/fesm2015/lenne.tech-ng-base-src-lib-base.js.map +1 -1
- package/fesm2015/lenne.tech-ng-base.js +281 -24
- package/fesm2015/lenne.tech-ng-base.js.map +1 -1
- package/lib/base/interfaces/base-module-config.interface.d.ts +9 -0
- package/lib/base/services/graphql.service.d.ts +1 -1
- package/lib/base/services/scroll.service.d.ts +66 -3
- package/package.json +1 -1
- package/src/lib/base/interfaces/base-module-config.interface.d.ts +9 -0
- package/src/lib/base/lenne.tech-ng-base-src-lib-base.metadata.json +1 -1
- package/src/lib/base/services/graphql.service.d.ts +1 -1
- package/src/lib/base/services/scroll.service.d.ts +66 -3
|
@@ -4,14 +4,17 @@ import { HttpLink } from 'apollo-angular/http';
|
|
|
4
4
|
import * as i0 from '@angular/core';
|
|
5
5
|
import { InjectionToken, Injectable, Inject, Directive, ElementRef, Input, Output, HostBinding, HostListener, Pipe, NgModule } from '@angular/core';
|
|
6
6
|
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
|
|
7
|
-
import { ApolloLink } from '@apollo/client/core';
|
|
7
|
+
import { ApolloLink, split, concat } from '@apollo/client/core';
|
|
8
8
|
import { InMemoryCache } from '@apollo/client/cache';
|
|
9
|
+
import { WebSocketLink } from '@apollo/client/link/ws';
|
|
10
|
+
import { getMainDefinition } from '@apollo/client/utilities';
|
|
9
11
|
import * as i1$2 from '@angular/platform-browser';
|
|
10
12
|
import { DomSanitizer, Title, Meta } from '@angular/platform-browser';
|
|
11
13
|
import * as i1$1 from '@angular/router';
|
|
12
14
|
import { Router } from '@angular/router';
|
|
13
15
|
import { buildClientSchema, getIntrospectionQuery } from 'graphql';
|
|
14
16
|
import { map } from 'rxjs/operators';
|
|
17
|
+
import { __awaiter } from 'tslib';
|
|
15
18
|
|
|
16
19
|
const BASE_MODULE_CONFIG = new InjectionToken('BASE_MODULE_CONFIG');
|
|
17
20
|
|
|
@@ -208,7 +211,8 @@ AuthService.ctorParameters = () => [
|
|
|
208
211
|
* Factory for apollo-angular options
|
|
209
212
|
*/
|
|
210
213
|
function apolloOptionsFactory(baseModuleConfig, httpLink, authService) {
|
|
211
|
-
const
|
|
214
|
+
const links = [];
|
|
215
|
+
const http = httpLink.create({
|
|
212
216
|
uri: baseModuleConfig.apiUrl,
|
|
213
217
|
});
|
|
214
218
|
const authMiddleware = new ApolloLink((operation, forward) => {
|
|
@@ -222,8 +226,44 @@ function apolloOptionsFactory(baseModuleConfig, httpLink, authService) {
|
|
|
222
226
|
}
|
|
223
227
|
return forward(operation);
|
|
224
228
|
});
|
|
229
|
+
if (baseModuleConfig.wsUrl) {
|
|
230
|
+
let wsClient;
|
|
231
|
+
if (localStorage) {
|
|
232
|
+
const token = authService.token || null;
|
|
233
|
+
wsClient = new WebSocketLink({
|
|
234
|
+
uri: baseModuleConfig.wsUrl,
|
|
235
|
+
options: {
|
|
236
|
+
reconnect: true,
|
|
237
|
+
connectionParams: {
|
|
238
|
+
authToken: token,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
wsClient = new WebSocketLink({
|
|
245
|
+
uri: baseModuleConfig.wsUrl,
|
|
246
|
+
options: {
|
|
247
|
+
reconnect: true,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// using the ability to split links, you can send data to each link
|
|
252
|
+
// depending on what kind of operation is being sent
|
|
253
|
+
links.push(split(
|
|
254
|
+
// split based on operation type
|
|
255
|
+
({ query }) => {
|
|
256
|
+
// @ts-ignore
|
|
257
|
+
const { kind, operation } = getMainDefinition(query);
|
|
258
|
+
return kind === 'OperationDefinition' && operation === 'subscription';
|
|
259
|
+
}, wsClient, concat(authMiddleware, http)));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
links.push(http);
|
|
263
|
+
links.push(authMiddleware);
|
|
264
|
+
}
|
|
225
265
|
return {
|
|
226
|
-
link: ApolloLink.from(
|
|
266
|
+
link: ApolloLink.from(links),
|
|
227
267
|
cache: new InMemoryCache(),
|
|
228
268
|
defaultOptions: {
|
|
229
269
|
query: {
|
|
@@ -1536,13 +1576,29 @@ class GraphQLService {
|
|
|
1536
1576
|
const documentNode = fields
|
|
1537
1577
|
? gql(config.type + '{\n' + graphql + args + fields + '\n}')
|
|
1538
1578
|
: gql(config.type + '{\n' + graphql + args + '\n}');
|
|
1579
|
+
// Log
|
|
1580
|
+
if (config.log) {
|
|
1581
|
+
console.log({ documentNode });
|
|
1582
|
+
}
|
|
1539
1583
|
const request = {};
|
|
1540
1584
|
request[config.type] = documentNode;
|
|
1541
1585
|
// Log
|
|
1542
1586
|
if (config.log) {
|
|
1543
1587
|
console.log({ request });
|
|
1544
1588
|
}
|
|
1545
|
-
|
|
1589
|
+
let func;
|
|
1590
|
+
if (config.type === GraphQLRequestType.MUTATION) {
|
|
1591
|
+
func = 'mutate';
|
|
1592
|
+
request[config.type] = documentNode;
|
|
1593
|
+
}
|
|
1594
|
+
else if (config.type === GraphQLRequestType.SUBSCRIPTION) {
|
|
1595
|
+
func = 'subscribe';
|
|
1596
|
+
request.query = documentNode;
|
|
1597
|
+
}
|
|
1598
|
+
else {
|
|
1599
|
+
func = config.type;
|
|
1600
|
+
request[config.type] = documentNode;
|
|
1601
|
+
}
|
|
1546
1602
|
this.apollo[func](request).subscribe((result) => {
|
|
1547
1603
|
var _a;
|
|
1548
1604
|
const data = ((_a = result === null || result === void 0 ? void 0 : result.data) === null || _a === void 0 ? void 0 : _a[graphql]) !== undefined ? result.data[graphql] : result;
|
|
@@ -1560,8 +1616,10 @@ class GraphQLService {
|
|
|
1560
1616
|
else {
|
|
1561
1617
|
subscriber.next(config.model.map(data));
|
|
1562
1618
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1619
|
+
if (config.type !== GraphQLRequestType.SUBSCRIPTION) {
|
|
1620
|
+
// Done
|
|
1621
|
+
subscriber.complete();
|
|
1622
|
+
}
|
|
1565
1623
|
}, (error) => {
|
|
1566
1624
|
subscriber.error(error);
|
|
1567
1625
|
subscriber.complete();
|
|
@@ -1572,14 +1630,23 @@ class GraphQLService {
|
|
|
1572
1630
|
/**
|
|
1573
1631
|
* Get fields / args from IGraphQLTypeCollection
|
|
1574
1632
|
*/
|
|
1575
|
-
graphQLTypeCollectionToStringArray(collection, current = '', result = []) {
|
|
1633
|
+
graphQLTypeCollectionToStringArray(collection, current = '', result = [], cacheNode = []) {
|
|
1576
1634
|
if (collection) {
|
|
1577
1635
|
if (collection instanceof GraphQLType) {
|
|
1578
1636
|
result.push(current);
|
|
1579
1637
|
}
|
|
1580
1638
|
else {
|
|
1639
|
+
if (current === null || current === void 0 ? void 0 : current.includes('.')) {
|
|
1640
|
+
cacheNode.push(current.split('.')[1]);
|
|
1641
|
+
}
|
|
1642
|
+
else if (current) {
|
|
1643
|
+
cacheNode.push(current);
|
|
1644
|
+
}
|
|
1581
1645
|
for (const key of Object.keys(collection)) {
|
|
1582
|
-
|
|
1646
|
+
if (current === key || cacheNode.includes(key)) {
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
this.graphQLTypeCollectionToStringArray(collection[key], current ? current + '.' + key : key, result, cacheNode);
|
|
1583
1650
|
}
|
|
1584
1651
|
}
|
|
1585
1652
|
}
|
|
@@ -1631,6 +1698,10 @@ class GraphQLService {
|
|
|
1631
1698
|
}
|
|
1632
1699
|
// Process object
|
|
1633
1700
|
else if (typeof args === 'object') {
|
|
1701
|
+
// Check object is empty
|
|
1702
|
+
if (args && Object.keys(args).length === 0 && Object.getPrototypeOf(args) === Object.prototype) {
|
|
1703
|
+
return { argsString: '{}', schemaArgs, usedArgs };
|
|
1704
|
+
}
|
|
1634
1705
|
// Process all object entries
|
|
1635
1706
|
for (const [key, value] of Object.entries(args)) {
|
|
1636
1707
|
// Init data for current entry
|
|
@@ -1869,7 +1940,9 @@ class GraphQLPlusService extends GraphQLService {
|
|
|
1869
1940
|
}
|
|
1870
1941
|
subscriber.next(response);
|
|
1871
1942
|
this.handleLoader('stop', options);
|
|
1872
|
-
|
|
1943
|
+
if (options.type !== GraphQLRequestType.SUBSCRIPTION) {
|
|
1944
|
+
subscriber.complete();
|
|
1945
|
+
}
|
|
1873
1946
|
}, (error) => {
|
|
1874
1947
|
if (!options.ignoreErrors) {
|
|
1875
1948
|
this.handleError(error, options.excludedErrors);
|
|
@@ -1932,31 +2005,73 @@ class ScrollService {
|
|
|
1932
2005
|
this.router = router;
|
|
1933
2006
|
this.moduleConfig = moduleConfig;
|
|
1934
2007
|
/**
|
|
1935
|
-
*
|
|
2008
|
+
* Is a scroll listener set
|
|
2009
|
+
*
|
|
2010
|
+
* @protected
|
|
1936
2011
|
*/
|
|
1937
|
-
this.
|
|
2012
|
+
this.scrollListener = false;
|
|
1938
2013
|
/**
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
2014
|
+
* Current scroll timer;
|
|
2015
|
+
*
|
|
2016
|
+
* @protected
|
|
1941
2017
|
*/
|
|
1942
|
-
this.
|
|
1943
|
-
const config = Object.assign({ scrollDetectionOffset: 200, scrollOffset: 100 }, moduleConfig);
|
|
2018
|
+
this.scrollTimer = -1;
|
|
2019
|
+
const config = Object.assign({ scrollDetectionOffset: 200, scrollOffset: 100, scrollSamplingRate: 100 }, moduleConfig);
|
|
1944
2020
|
this.detectionOffset = config.scrollDetectionOffset;
|
|
1945
2021
|
this.scrollOffset = config.scrollOffset;
|
|
2022
|
+
this.scrollSamplingRate = config.scrollSamplingRate;
|
|
1946
2023
|
}
|
|
1947
2024
|
/**
|
|
1948
2025
|
* Scroll to structureElement with specific ID
|
|
1949
2026
|
*/
|
|
1950
2027
|
scrollTo(id, options = {}) {
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2028
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2029
|
+
// Prepare config
|
|
2030
|
+
const config = Object.assign({ checkUrl: true, loadImagesBefore: true, resetStyles: false, setOverflow: true, stopPropagation: true }, options);
|
|
2031
|
+
// Check current URL
|
|
2032
|
+
if (config.checkUrl && this.router.url !== '/' && !this.router.url.startsWith('/#')) {
|
|
2033
|
+
yield this.router.navigate(['/'], { fragment: id });
|
|
2034
|
+
return !config.stopPropagation;
|
|
2035
|
+
}
|
|
2036
|
+
// Check if target element exits
|
|
2037
|
+
const el = document.getElementById(id);
|
|
2038
|
+
if (!el) {
|
|
2039
|
+
console.error('Missing scroll element with ID:', id);
|
|
2040
|
+
return !config.stopPropagation;
|
|
2041
|
+
}
|
|
2042
|
+
// Check if body overflow is visible
|
|
2043
|
+
// This is necessary for scroll handling
|
|
2044
|
+
const body = document.querySelector('body');
|
|
2045
|
+
let undoOverflowChanges = false;
|
|
2046
|
+
const scrollBodyStyles = JSON.parse(JSON.stringify(window.getComputedStyle(body)));
|
|
2047
|
+
if (config.setOverflow && (scrollBodyStyles.overflowX !== 'visible' || scrollBodyStyles.overflowY !== 'visible')) {
|
|
2048
|
+
// Set body overflow to visible
|
|
2049
|
+
console.warn('Style of body element for overflowX and overflowY will be set to "visible" for scrolling!');
|
|
2050
|
+
body.style.overflowX = 'visible';
|
|
2051
|
+
body.style.overflowY = 'visible';
|
|
2052
|
+
undoOverflowChanges = true;
|
|
2053
|
+
}
|
|
2054
|
+
// Load images before scrolling (once)
|
|
2055
|
+
if (config.loadImagesBefore) {
|
|
2056
|
+
yield this.preLoadImages();
|
|
2057
|
+
}
|
|
2058
|
+
// Get get bounding rect of target element
|
|
2059
|
+
const cords = el.getBoundingClientRect();
|
|
2060
|
+
// Scroll to element
|
|
2061
|
+
window.scrollBy({ top: cords.top - this.scrollOffset, behavior: 'smooth' });
|
|
2062
|
+
yield this.scrollDone({
|
|
2063
|
+
doneFunction: () => {
|
|
2064
|
+
// Undo changes
|
|
2065
|
+
if (undoOverflowChanges && config.resetStyles) {
|
|
2066
|
+
body.style.overflowX = scrollBodyStyles.overflowX;
|
|
2067
|
+
body.style.overflowY = scrollBodyStyles.overflowY;
|
|
2068
|
+
console.warn('Original style of body element for overflowX and overflowY is set.');
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
});
|
|
2072
|
+
// Return stop propagation configuration
|
|
2073
|
+
return !config.stopPropagation;
|
|
2074
|
+
});
|
|
1960
2075
|
}
|
|
1961
2076
|
/**
|
|
1962
2077
|
* Get last active structureElement
|
|
@@ -1986,6 +2101,148 @@ class ScrollService {
|
|
|
1986
2101
|
const lastActiveElement = this.getLastActiveElement(list);
|
|
1987
2102
|
return id === lastActiveElement || (lastActiveElement === null && isFirst);
|
|
1988
2103
|
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Scroll done promise
|
|
2106
|
+
*/
|
|
2107
|
+
scrollDone(options = {}) {
|
|
2108
|
+
// Init configuration
|
|
2109
|
+
const config = Object.assign({ doneFunction: undefined, timeout: undefined }, options);
|
|
2110
|
+
// Return promise for scroll done
|
|
2111
|
+
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
2112
|
+
// Init variables
|
|
2113
|
+
let rejected = false;
|
|
2114
|
+
let timeoutId;
|
|
2115
|
+
// Set timeout handling if necessary
|
|
2116
|
+
if (config.timeout) {
|
|
2117
|
+
timeoutId = window.setTimeout(() => {
|
|
2118
|
+
rejected = true;
|
|
2119
|
+
reject(new Error('Timeout for scrollDone after:' + config.timeout));
|
|
2120
|
+
}, config.timeout);
|
|
2121
|
+
}
|
|
2122
|
+
// Wait for scroll done
|
|
2123
|
+
yield this.scrollDoneDetection();
|
|
2124
|
+
// Resolve promise if not already rejected
|
|
2125
|
+
if (!rejected) {
|
|
2126
|
+
// Stop timeout if set
|
|
2127
|
+
if (timeoutId) {
|
|
2128
|
+
window.clearTimeout(timeoutId);
|
|
2129
|
+
}
|
|
2130
|
+
// Get result (if doneFunction is set)
|
|
2131
|
+
let result;
|
|
2132
|
+
if (config.doneFunction) {
|
|
2133
|
+
result = yield config.doneFunction();
|
|
2134
|
+
}
|
|
2135
|
+
// Scroll done
|
|
2136
|
+
resolve(result);
|
|
2137
|
+
}
|
|
2138
|
+
}));
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Scroll done detecton
|
|
2142
|
+
*
|
|
2143
|
+
* @protected
|
|
2144
|
+
*/
|
|
2145
|
+
scrollDoneDetection() {
|
|
2146
|
+
// Return current scroll promise (Singelton)
|
|
2147
|
+
if (this.scrollPromise) {
|
|
2148
|
+
return this.scrollPromise;
|
|
2149
|
+
}
|
|
2150
|
+
// Create current scroll promise
|
|
2151
|
+
this.scrollPromise = new Promise((resolve) => {
|
|
2152
|
+
this.scrollPromiseResolve = resolve;
|
|
2153
|
+
if (this.scrollTimer !== -1) {
|
|
2154
|
+
window.clearTimeout(this.scrollTimer);
|
|
2155
|
+
}
|
|
2156
|
+
this.scrollTimer = window.setTimeout(() => {
|
|
2157
|
+
window.removeEventListener('scroll', this.scrollDoneDetection.bind(this));
|
|
2158
|
+
this.scrollPromise = undefined;
|
|
2159
|
+
resolve();
|
|
2160
|
+
}, this.scrollSamplingRate);
|
|
2161
|
+
});
|
|
2162
|
+
// Lisitening to scroll event;
|
|
2163
|
+
window.addEventListener('scroll', this.scrollDoneDetection.bind(this));
|
|
2164
|
+
// Return promise
|
|
2165
|
+
return this.scrollPromise;
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Preaload images (for lazy handling)
|
|
2169
|
+
*
|
|
2170
|
+
* @param options
|
|
2171
|
+
* @protected
|
|
2172
|
+
*/
|
|
2173
|
+
preLoadImages(options) {
|
|
2174
|
+
// Init configuration
|
|
2175
|
+
const config = Object.assign({ doneFunnction: undefined, timeout: undefined }, options);
|
|
2176
|
+
return new Promise((resolve) => {
|
|
2177
|
+
// Init variables
|
|
2178
|
+
let resolved = false;
|
|
2179
|
+
let timeoutId;
|
|
2180
|
+
// Init loading
|
|
2181
|
+
const images = Array.from(document.getElementsByTagName('img'));
|
|
2182
|
+
const promises = [];
|
|
2183
|
+
// Prepare images
|
|
2184
|
+
for (const image of images) {
|
|
2185
|
+
const src = image.getAttribute('src');
|
|
2186
|
+
// Check if changes are necessary and possible
|
|
2187
|
+
if ((image.complete && image.naturalHeight !== 0) || image.getAttribute('loading') !== 'lazy' || !src) {
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
const newImage = document.createElement('img');
|
|
2191
|
+
Array.from(image.attributes).forEach((attr) => {
|
|
2192
|
+
if (attr.name !== 'loading' && attr.name !== 'src') {
|
|
2193
|
+
newImage.setAttribute(attr.name, attr.value);
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
image.replaceWith(newImage);
|
|
2197
|
+
promises.push(this.loadImage(newImage, src));
|
|
2198
|
+
}
|
|
2199
|
+
if (config.timeout) {
|
|
2200
|
+
timeoutId = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
|
|
2201
|
+
resolved = true;
|
|
2202
|
+
if (config.doneFunnction) {
|
|
2203
|
+
yield config.doneFunction();
|
|
2204
|
+
}
|
|
2205
|
+
resolve(undefined);
|
|
2206
|
+
}));
|
|
2207
|
+
}
|
|
2208
|
+
// Wait until all images loaded
|
|
2209
|
+
Promise.all(promises)
|
|
2210
|
+
.catch((e) => console.error('Error during image loading', e))
|
|
2211
|
+
.finally(() => __awaiter(this, void 0, void 0, function* () {
|
|
2212
|
+
// Check if already resolved
|
|
2213
|
+
if (resolved) {
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
// Stop timeout
|
|
2217
|
+
if (timeoutId) {
|
|
2218
|
+
clearTimeout(timeoutId);
|
|
2219
|
+
}
|
|
2220
|
+
// Return result
|
|
2221
|
+
let result;
|
|
2222
|
+
if (config.doneFunnction) {
|
|
2223
|
+
result = yield config.doneFunction();
|
|
2224
|
+
}
|
|
2225
|
+
resolve(result);
|
|
2226
|
+
}));
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Load image for image element
|
|
2231
|
+
*
|
|
2232
|
+
* @param imageElement
|
|
2233
|
+
* @param src
|
|
2234
|
+
* @protected
|
|
2235
|
+
*/
|
|
2236
|
+
loadImage(imageElement, src) {
|
|
2237
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2238
|
+
src = src || imageElement.src;
|
|
2239
|
+
return new Promise((resolve, reject) => {
|
|
2240
|
+
imageElement.onload = () => resolve(imageElement);
|
|
2241
|
+
imageElement.onerror = reject;
|
|
2242
|
+
imageElement.src = src;
|
|
2243
|
+
});
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
1989
2246
|
}
|
|
1990
2247
|
ScrollService.ɵprov = i0.ɵɵdefineInjectable({ factory: function ScrollService_Factory() { return new ScrollService(i0.ɵɵinject(i1$1.Router), i0.ɵɵinject(BASE_MODULE_CONFIG)); }, token: ScrollService, providedIn: "root" });
|
|
1991
2248
|
ScrollService.decorators = [
|