ngx-edu-sharing-ui 0.7.0

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.
Files changed (171) hide show
  1. package/.browserslistrc +16 -0
  2. package/.eslintrc.json +44 -0
  3. package/README.md +40 -0
  4. package/assets/scss/mixins.scss +95 -0
  5. package/assets/scss/variables.scss +33 -0
  6. package/karma.conf.js +42 -0
  7. package/ng-package.json +10 -0
  8. package/package.json +19 -0
  9. package/src/lib/actionbar/actionbar.component.html +59 -0
  10. package/src/lib/actionbar/actionbar.component.scss +123 -0
  11. package/src/lib/actionbar/actionbar.component.ts +174 -0
  12. package/src/lib/common/edu-sharing-ui-common.module.ts +80 -0
  13. package/src/lib/directives/border-box-observer.directive.ts +75 -0
  14. package/src/lib/directives/check-text-overflow.directive.ts +61 -0
  15. package/src/lib/directives/drag-nodes/drag-nodes.ts +32 -0
  16. package/src/lib/directives/drag-nodes/nodes-drag-source.directive.ts +79 -0
  17. package/src/lib/directives/drag-nodes/nodes-drag.directive.ts +43 -0
  18. package/src/lib/directives/drag-nodes/nodes-drop-target.directive.ts +116 -0
  19. package/src/lib/directives/focus-state.directive.ts +34 -0
  20. package/src/lib/directives/icon.directive.ts +142 -0
  21. package/src/lib/directives/nodes-drop-target-legacy.directive.ts +155 -0
  22. package/src/lib/dropdown/dropdown.component.html +32 -0
  23. package/src/lib/dropdown/dropdown.component.scss +67 -0
  24. package/src/lib/dropdown/dropdown.component.ts +71 -0
  25. package/src/lib/edu-sharing-ui-configuration.ts +47 -0
  26. package/src/lib/edu-sharing-ui.module.ts +49 -0
  27. package/src/lib/list-items/available-widgets.ts +30 -0
  28. package/src/lib/list-items/format-duration.pipe.ts +17 -0
  29. package/src/lib/list-items/list-base/list-base.component.html +52 -0
  30. package/src/lib/list-items/list-base/list-base.component.ts +44 -0
  31. package/src/lib/list-items/list-collection-info/list-collection-info.component.html +48 -0
  32. package/src/lib/list-items/list-collection-info/list-collection-info.component.scss +8 -0
  33. package/src/lib/list-items/list-collection-info/list-collection-info.component.ts +24 -0
  34. package/src/lib/list-items/list-counts/list-counts.component.html +1 -0
  35. package/src/lib/list-items/list-counts/list-counts.component.scss +3 -0
  36. package/src/lib/list-items/list-counts/list-counts.component.ts +59 -0
  37. package/src/lib/list-items/list-items.module.ts +33 -0
  38. package/src/lib/list-items/list-node-license/list-node-license.component.html +8 -0
  39. package/src/lib/list-items/list-node-license/list-node-license.component.ts +47 -0
  40. package/src/lib/list-items/list-node-replication-source/list-node-replication-source.component.html +11 -0
  41. package/src/lib/list-items/list-node-replication-source/list-node-replication-source.component.ts +60 -0
  42. package/src/lib/list-items/list-node-workflow/list-node-workflow.component.html +3 -0
  43. package/src/lib/list-items/list-node-workflow/list-node-workflow.component.ts +21 -0
  44. package/src/lib/list-items/list-text/list-text.component.html +176 -0
  45. package/src/lib/list-items/list-text/list-text.component.scss +3 -0
  46. package/src/lib/list-items/list-text/list-text.component.ts +107 -0
  47. package/src/lib/list-items/list-widget.ts +52 -0
  48. package/src/lib/list-items/node-row/node-row.component.html +31 -0
  49. package/src/lib/list-items/node-row/node-row.component.scss +50 -0
  50. package/src/lib/list-items/node-row/node-row.component.ts +16 -0
  51. package/src/lib/list-items/node-source.pipe.ts +48 -0
  52. package/src/lib/node-entries/combined-data-source.ts +51 -0
  53. package/src/lib/node-entries/custom-templates-data-source.ts +6 -0
  54. package/src/lib/node-entries/drag-preview/drag-preview.component.html +6 -0
  55. package/src/lib/node-entries/drag-preview/drag-preview.component.scss +35 -0
  56. package/src/lib/node-entries/drag-preview/drag-preview.component.ts +15 -0
  57. package/src/lib/node-entries/entries-model.ts +120 -0
  58. package/src/lib/node-entries/items-cap.ts +54 -0
  59. package/src/lib/node-entries/list-item-label.pipe.ts +28 -0
  60. package/src/lib/node-entries/mixins.scss +23 -0
  61. package/src/lib/node-entries/node-cache.spec.ts +199 -0
  62. package/src/lib/node-entries/node-cache.ts +81 -0
  63. package/src/lib/node-entries/node-data-source-remote.ts +33 -0
  64. package/src/lib/node-entries/node-data-source.ts +148 -0
  65. package/src/lib/node-entries/node-entries-card/node-entries-card.component.html +167 -0
  66. package/src/lib/node-entries/node-entries-card/node-entries-card.component.scss +28 -0
  67. package/src/lib/node-entries/node-entries-card/node-entries-card.component.ts +132 -0
  68. package/src/lib/node-entries/node-entries-card/node-entries-card.main.scss +261 -0
  69. package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.html +205 -0
  70. package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.scss +181 -0
  71. package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.ts +361 -0
  72. package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.html +100 -0
  73. package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.scss +46 -0
  74. package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.ts +40 -0
  75. package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.html +23 -0
  76. package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.scss +58 -0
  77. package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.ts +16 -0
  78. package/src/lib/node-entries/node-entries-global.service.ts +79 -0
  79. package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.html +25 -0
  80. package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.scss +32 -0
  81. package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.ts +31 -0
  82. package/src/lib/node-entries/node-entries-table/node-entries-table.component.html +270 -0
  83. package/src/lib/node-entries/node-entries-table/node-entries-table.component.scss +169 -0
  84. package/src/lib/node-entries/node-entries-table/node-entries-table.component.ts +333 -0
  85. package/src/lib/node-entries/node-entries-templates.service.ts +31 -0
  86. package/src/lib/node-entries/node-entries-wrapper.component.ts +363 -0
  87. package/src/lib/node-entries/node-entries.component.html +33 -0
  88. package/src/lib/node-entries/node-entries.component.scss +13 -0
  89. package/src/lib/node-entries/node-entries.component.ts +151 -0
  90. package/src/lib/node-entries/node-entries.module.ts +93 -0
  91. package/src/lib/node-entries/node-rating/node-rating.component.html +53 -0
  92. package/src/lib/node-entries/node-rating/node-rating.component.scss +31 -0
  93. package/src/lib/node-entries/node-rating/node-rating.component.ts +105 -0
  94. package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.html +39 -0
  95. package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.scss +44 -0
  96. package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.ts +43 -0
  97. package/src/lib/node-entries/node-type-badge/node-type-badge.component.html +31 -0
  98. package/src/lib/node-entries/node-type-badge/node-type-badge.component.scss +5 -0
  99. package/src/lib/node-entries/node-type-badge/node-type-badge.component.ts +36 -0
  100. package/src/lib/node-entries/option-button/option-button.component.ts +42 -0
  101. package/src/lib/node-entries/preview-image/preview-image.component.html +19 -0
  102. package/src/lib/node-entries/preview-image/preview-image.component.scss +31 -0
  103. package/src/lib/node-entries/preview-image/preview-image.component.ts +47 -0
  104. package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.html +27 -0
  105. package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.scss +9 -0
  106. package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.ts +26 -0
  107. package/src/lib/node-url/node-url.component.html +66 -0
  108. package/src/lib/node-url/node-url.component.scss +32 -0
  109. package/src/lib/node-url/node-url.component.ts +136 -0
  110. package/src/lib/pipes/file-size.pipe.ts +24 -0
  111. package/src/lib/pipes/format-date.pipe.ts +39 -0
  112. package/src/lib/pipes/node-icon.pipe.ts +11 -0
  113. package/src/lib/pipes/node-image-size.pipe.ts +18 -0
  114. package/src/lib/pipes/node-image.pipe.ts +71 -0
  115. package/src/lib/pipes/node-person-name.pipe.ts +41 -0
  116. package/src/lib/pipes/node-title.pipe.ts +12 -0
  117. package/src/lib/pipes/option-tooltip.pipe.ts +32 -0
  118. package/src/lib/pipes/replace-chars.pipe.ts +21 -0
  119. package/src/lib/pipes/vcard-name.pipe.ts +11 -0
  120. package/src/lib/services/abstract/app.service.ts +4 -0
  121. package/src/lib/services/abstract/keyboard-shortcuts.service.ts +10 -0
  122. package/src/lib/services/abstract/options-helper.service.ts +29 -0
  123. package/src/lib/services/abstract/toast.service.ts +5 -0
  124. package/src/lib/services/accessibility.service.ts +101 -0
  125. package/src/lib/services/local-events.service.ts +29 -0
  126. package/src/lib/services/node-entries.service.ts +172 -0
  127. package/src/lib/services/node-helper.service.ts +239 -0
  128. package/src/lib/services/nodes-drag-drop.service.ts +165 -0
  129. package/src/lib/services/options-helper-data.service.ts +186 -0
  130. package/src/lib/services/repo-url.service.ts +46 -0
  131. package/src/lib/services/temporary-storage.service.ts +58 -0
  132. package/src/lib/services/ui.service.ts +182 -0
  133. package/src/lib/sort-dropdown/sort-dropdown.component.html +22 -0
  134. package/src/lib/sort-dropdown/sort-dropdown.component.scss +47 -0
  135. package/src/lib/sort-dropdown/sort-dropdown.component.ts +42 -0
  136. package/src/lib/spinner/spinner.component.html +14 -0
  137. package/src/lib/spinner/spinner.component.scss +141 -0
  138. package/src/lib/spinner/spinner.component.ts +12 -0
  139. package/src/lib/translations/README.md +44 -0
  140. package/src/lib/translations/fallback-translation-handler.ts +7 -0
  141. package/src/lib/translations/languages.ts +6 -0
  142. package/src/lib/translations/translation-loader.spec.ts +352 -0
  143. package/src/lib/translations/translation-loader.ts +189 -0
  144. package/src/lib/translations/translation-source.ts +9 -0
  145. package/src/lib/translations/translations.module.ts +49 -0
  146. package/src/lib/translations/translations.service.spec.ts +152 -0
  147. package/src/lib/translations/translations.service.ts +188 -0
  148. package/src/lib/types/accessibillity.ts +15 -0
  149. package/src/lib/types/api-models.ts +4 -0
  150. package/src/lib/types/drag-drop.ts +22 -0
  151. package/src/lib/types/keyboard-shortcuts.ts +29 -0
  152. package/src/lib/types/list-item.ts +67 -0
  153. package/src/lib/types/option-item.ts +247 -0
  154. package/src/lib/types/workflow.ts +35 -0
  155. package/src/lib/util/DateHelper.spec.ts +112 -0
  156. package/src/lib/util/DateHelper.ts +197 -0
  157. package/src/lib/util/VCard.ts +277 -0
  158. package/src/lib/util/color-helper.ts +125 -0
  159. package/src/lib/util/duration-helper.spec.ts +35 -0
  160. package/src/lib/util/duration-helper.ts +98 -0
  161. package/src/lib/util/functions.ts +15 -0
  162. package/src/lib/util/helper.ts +60 -0
  163. package/src/lib/util/isNumeric.ts +13 -0
  164. package/src/lib/util/rest-helper.ts +28 -0
  165. package/src/lib/util/ui-animation.ts +154 -0
  166. package/src/lib/util/ui-constants.ts +20 -0
  167. package/src/module.ts +76 -0
  168. package/src/test.ts +28 -0
  169. package/tsconfig.lib.json +15 -0
  170. package/tsconfig.lib.prod.json +10 -0
  171. package/tsconfig.spec.json +17 -0
@@ -0,0 +1,189 @@
1
+ import { HttpClient } from '@angular/common/http';
2
+ import { TranslateLoader } from '@ngx-translate/core';
3
+ import { ConfigService } from 'ngx-edu-sharing-api';
4
+ import * as rxjs from 'rxjs';
5
+ import { concat, Observable, of } from 'rxjs';
6
+ import { catchError, first, map, reduce } from 'rxjs/operators';
7
+ import { EduSharingUiConfiguration } from '../edu-sharing-ui-configuration';
8
+ import { LANGUAGES } from './languages';
9
+ import { TranslationSource } from './translation-source';
10
+
11
+ export const TRANSLATION_LIST = [
12
+ 'common',
13
+ 'admin',
14
+ 'recycle',
15
+ 'workspace',
16
+ 'search',
17
+ 'collections',
18
+ 'login',
19
+ 'permissions',
20
+ 'oer',
21
+ 'messages',
22
+ 'register',
23
+ 'profiles',
24
+ 'services',
25
+ 'stream',
26
+ 'override',
27
+ ];
28
+
29
+ type Dictionary = { [key: string]: string | Dictionary };
30
+
31
+ export class TranslationLoader implements TranslateLoader {
32
+ static create(
33
+ http: HttpClient,
34
+ configService: ConfigService,
35
+ configuration: EduSharingUiConfiguration,
36
+ ) {
37
+ return new TranslationLoader(http, configService, configuration);
38
+ }
39
+
40
+ private constructor(
41
+ private http: HttpClient,
42
+ private configService: ConfigService,
43
+ private configuration: EduSharingUiConfiguration,
44
+ private prefix: string = 'assets/i18n',
45
+ private suffix: string = '.json',
46
+ ) {}
47
+
48
+ // If you need to configure this, define an injectable configuration object. See
49
+ // https://angular.io/guide/dependency-injection-providers#injecting-a-configuration-object.
50
+ private readonly source: TranslationSource = TranslationSource.Auto;
51
+
52
+ /**
53
+ * Gets the translations from the server
54
+ */
55
+ getTranslation(lang: string): Observable<Dictionary> {
56
+ if (lang === 'none') {
57
+ return of({});
58
+ }
59
+ this.configService.setLocale(LANGUAGES[lang]);
60
+ return rxjs
61
+ .forkJoin({
62
+ originalTranslations: this.getOriginalTranslations(lang).pipe(
63
+ // Default to empty dictionary if we got nothing
64
+ map((translations) => translations || {}),
65
+ ),
66
+ translationOverrides: this.configService
67
+ .observeTranslationOverrides()
68
+ .pipe(first()),
69
+ })
70
+ .pipe(
71
+ map(({ originalTranslations, translationOverrides }) =>
72
+ // FIXME: This will alter the object returned by `getOriginalTranslations`.
73
+ this.applyOverrides(originalTranslations, translationOverrides),
74
+ ),
75
+ map((translations) => this.replaceGenderCharacter(translations)),
76
+ catchError((error, obs) => {
77
+ console.error(error);
78
+ return of(error);
79
+ }),
80
+ );
81
+ }
82
+
83
+ private getOriginalTranslations(lang: string): Observable<Dictionary> {
84
+ switch (this.getSource()) {
85
+ case 'repository':
86
+ return this.configService.observeDefaultTranslations().pipe(first());
87
+ case 'local':
88
+ return this.mergeTranslations(this.fetchTranslations(lang));
89
+ }
90
+ }
91
+
92
+ private getSource(): 'repository' | 'local' {
93
+ if (
94
+ (this.configuration.production && this.source === TranslationSource.Auto) ||
95
+ this.source === TranslationSource.Repository
96
+ ) {
97
+ return 'repository';
98
+ } else {
99
+ return 'local';
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Returns an array of Observables that will each fetch a translations json
105
+ * file.
106
+ */
107
+ private fetchTranslations(lang: string): Observable<Dictionary>[] {
108
+ return TRANSLATION_LIST.map(
109
+ (translation) => `${this.prefix}/${translation}/${lang}${this.suffix}`,
110
+ ).map((url) => this.http.get(url) as Observable<Dictionary>);
111
+ }
112
+
113
+ /**
114
+ * Takes an array as returned by `fetchTranslations` and converts it to an
115
+ * Observable that yields a single Dictionary object.
116
+ */
117
+ private mergeTranslations(translations: Observable<Dictionary>[]): Observable<Dictionary> {
118
+ return concat(...translations).pipe(
119
+ reduce((acc: Dictionary, value: Dictionary) => {
120
+ for (const prop in value) {
121
+ if (value.hasOwnProperty(prop)) {
122
+ acc[prop] = value[prop];
123
+ }
124
+ }
125
+ return acc;
126
+ }, {}),
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Applies `overrides` to `translations` and returns `translations`.
132
+ *
133
+ * Example:
134
+ * translations = { foo: { bar: 'bar' } }
135
+ * overrides = { 'foo.bar': 'baz' }
136
+ * results in
137
+ * translations = { foo: {bar: 'baz' } }
138
+ *
139
+ * @param translations Nested translations object.
140
+ * @param overrides Flat object with dots (.) in keys interpreted as
141
+ * separators.
142
+ */
143
+ private applyOverrides(
144
+ translations: Dictionary,
145
+ overrides: { [key: string]: string },
146
+ ): Dictionary {
147
+ if (overrides) {
148
+ for (const [key, value] of Object.entries<string>(overrides)) {
149
+ let ref = translations;
150
+ const path = key.split('.');
151
+ const pathLast = path.pop();
152
+ for (const item of path) {
153
+ if (!ref[item]) {
154
+ ref[item] = {};
155
+ }
156
+ const refItem = ref[item];
157
+ if (typeof refItem === 'string') {
158
+ throw new Error('Trying to override leave with sub tree: ' + path);
159
+ }
160
+ ref = refItem;
161
+ }
162
+ ref[pathLast] = value;
163
+ }
164
+ }
165
+ return translations;
166
+ }
167
+
168
+ private replaceGenderCharacter(translations: Dictionary, path: string[] = []) {
169
+ for (let key of Object.keys(translations)) {
170
+ if (typeof translations[key] === 'string') {
171
+ // DO NOT REMOVE (required for csv language dumping)
172
+ /*console.log(CsvHelper.fromArray(null, [[
173
+ path.concat(key).join('.'), translations[key]
174
+ ]]));*/
175
+ translations[key] = (translations[key] as string).replace(
176
+ /{{GENDER_SEPARATOR}}/g,
177
+ '*',
178
+ );
179
+ } else {
180
+ translations[key] = this.replaceGenderCharacter(
181
+ translations[key] as Dictionary,
182
+ path.concat(key),
183
+ );
184
+ }
185
+ }
186
+
187
+ return translations;
188
+ }
189
+ }
@@ -0,0 +1,9 @@
1
+ /** The preferred source for language files. */
2
+ export enum TranslationSource {
3
+ /** In dev mode, local files are used and in production, repository files are used (default). */
4
+ Auto,
5
+ /** Local files (assets/i18n) are used. */
6
+ Local,
7
+ /** Repository files are used. */
8
+ Repository,
9
+ }
@@ -0,0 +1,49 @@
1
+ import { HttpClient } from '@angular/common/http';
2
+ import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
3
+ import { MissingTranslationHandler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
4
+ import { ConfigService } from 'ngx-edu-sharing-api';
5
+ import { FallbackTranslationHandler } from './fallback-translation-handler';
6
+ import { TranslationLoader } from './translation-loader';
7
+ import { EduSharingUiConfiguration } from '../edu-sharing-ui-configuration';
8
+
9
+ /**
10
+ * Import this module once in the app module to provide the `TranslateService`.
11
+ *
12
+ * Export `TranslateModule` in the shared module to provide directives and pipes.
13
+ */
14
+ @NgModule({
15
+ declarations: [],
16
+ imports: [
17
+ TranslateModule.forRoot({
18
+ loader: {
19
+ provide: TranslateLoader,
20
+ useFactory: TranslationLoader.create,
21
+ deps: [HttpClient, ConfigService, EduSharingUiConfiguration],
22
+ },
23
+ missingTranslationHandler: {
24
+ provide: MissingTranslationHandler,
25
+ useClass: FallbackTranslationHandler,
26
+ },
27
+ }),
28
+ ],
29
+ })
30
+ export class TranslationsModule {
31
+ static forRoot(): ModuleWithProviders<TranslationsModule> {
32
+ return {
33
+ ngModule: TranslationsModule,
34
+ providers: [],
35
+ };
36
+ }
37
+
38
+ constructor(@Optional() @SkipSelf() parentModule?: TranslationsModule) {
39
+ if (parentModule) {
40
+ console.warn(
41
+ 'TranslationsModule is already loaded. Import it in the AppModule only',
42
+ parentModule,
43
+ );
44
+ /*throw new Error(
45
+ 'TranslationsModule is already loaded. Import it in the AppModule only',
46
+ );*/
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,152 @@
1
+ import { fakeAsync } from '@angular/core/testing';
2
+ import { ActivatedRoute } from '@angular/router';
3
+ import { TranslateService } from '@ngx-translate/core';
4
+ import { ConfigService, SessionStorageService } from 'ngx-edu-sharing-api';
5
+ import * as rxjs from 'rxjs';
6
+ import { TranslationsService } from './translations.service';
7
+
8
+ class BridgeServiceStub {
9
+ isRunningCordova() {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ class ConfigServiceStub {
15
+ get<T>(key: string, defaultValue?: T): Promise<T> {
16
+ return Promise.resolve(defaultValue);
17
+ }
18
+ instant<T>(key: string, defaultValue?: T): T {
19
+ return defaultValue;
20
+ }
21
+ getLocator() {
22
+ return {
23
+ getBridge: () => new BridgeServiceStub(),
24
+ };
25
+ }
26
+ }
27
+
28
+ class SessionStorageServiceStub {
29
+ get(key: string) {
30
+ return rxjs.of(null);
31
+ }
32
+ set(key: string, value: string) {}
33
+ }
34
+
35
+ class ActivatedRouteStub {
36
+ queryParams = rxjs.of({});
37
+ }
38
+
39
+ describe('TranslationsService', () => {
40
+ describe('initialize', () => {
41
+ let translationsService: TranslationsService;
42
+ let translateServiceSpy: any;
43
+ let bridgeServiceStub: BridgeServiceStub;
44
+ let configurationServiceStub: ConfigServiceStub;
45
+ let sessionStorageServiceStub: SessionStorageServiceStub;
46
+ let activatedRouteStub: ActivatedRouteStub;
47
+
48
+ beforeEach(() => {
49
+ translateServiceSpy = jasmine.createSpyObj('TranslateService', [
50
+ 'use',
51
+ 'addLangs',
52
+ 'getBrowserLang',
53
+ 'setDefaultLang',
54
+ 'getTranslation',
55
+ ]);
56
+ translateServiceSpy.getTranslation.and.callFake(() => rxjs.of(null));
57
+ translateServiceSpy.use.and.callFake(() => rxjs.of(null));
58
+ bridgeServiceStub = new BridgeServiceStub();
59
+ configurationServiceStub = new ConfigServiceStub();
60
+ sessionStorageServiceStub = new SessionStorageServiceStub();
61
+ activatedRouteStub = new ActivatedRouteStub();
62
+ translationsService = new TranslationsService(
63
+ configurationServiceStub as unknown as ConfigService,
64
+ activatedRouteStub as any as ActivatedRoute,
65
+ sessionStorageServiceStub as any as SessionStorageService,
66
+ translateServiceSpy as TranslateService,
67
+ null,
68
+ );
69
+ });
70
+
71
+ function callInitialize(): Promise<string> {
72
+ return new Promise((resolve) => {
73
+ fakeAsync(() => {
74
+ translationsService.initialize().subscribe(() => {
75
+ resolve(translationsService.getLanguage());
76
+ }, fail);
77
+ })();
78
+ });
79
+ }
80
+
81
+ it('should use default languages', async () => {
82
+ const configurationServiceSpy = spyOn(
83
+ configurationServiceStub,
84
+ 'get',
85
+ ).and.callThrough();
86
+ await callInitialize();
87
+ expect(configurationServiceSpy.calls.count()).toBe(
88
+ 1,
89
+ 'configurationService.get was called once',
90
+ );
91
+ expect(configurationServiceSpy.calls.mostRecent().args).toEqual([
92
+ 'supportedLanguages',
93
+ ['de', 'de-informal', 'en', 'none'], // Translation.DEFAULT_SUPPORTED_LANGUAGES
94
+ ]);
95
+ });
96
+
97
+ it('should default to "de"', async () => {
98
+ const lang = await callInitialize();
99
+ expect(lang).toBe('de');
100
+ });
101
+
102
+ it('should call translate.use()', async () => {
103
+ await callInitialize();
104
+ expect(translateServiceSpy.use.calls.count()).toBe(1);
105
+ expect(translateServiceSpy.use.calls.mostRecent().args).toEqual(['de']);
106
+ });
107
+
108
+ it('should call translate.setDefaultLang()', async () => {
109
+ await callInitialize();
110
+ expect(translateServiceSpy.setDefaultLang.calls.count()).toBe(1);
111
+ expect(translateServiceSpy.setDefaultLang.calls.mostRecent().args).toEqual(['de']);
112
+ });
113
+
114
+ it('should use browserLang', async () => {
115
+ translateServiceSpy.getBrowserLang.and.returnValue('en');
116
+ const lang = await callInitialize();
117
+ expect(lang).toBe('en');
118
+ });
119
+
120
+ it('should use sessionStorage', async () => {
121
+ // Should be overridden by sessionStorage
122
+ translateServiceSpy.getBrowserLang.and.returnValue('de');
123
+ const storageGetSpy = spyOn(sessionStorageServiceStub, 'get').and.returnValue(
124
+ rxjs.of('en'),
125
+ );
126
+ const lang = await callInitialize();
127
+ expect(storageGetSpy.calls.count()).toBe(1, 'storage.get was called once');
128
+ expect(storageGetSpy.calls.mostRecent().args).toEqual(['language']);
129
+ expect(lang).toBe('en');
130
+ });
131
+
132
+ it('should store used language in sessionStorage', async () => {
133
+ translateServiceSpy.getBrowserLang.and.returnValue('en');
134
+ const storageSetSpy = spyOn(sessionStorageServiceStub, 'set');
135
+ await callInitialize();
136
+ expect(storageSetSpy.calls.count()).toBe(1, 'storage.set was called once');
137
+ expect(storageSetSpy.calls.mostRecent().args).toEqual(['language', 'en']);
138
+ });
139
+
140
+ it('should use queryParams', async () => {
141
+ // Should be overridden by queryParams
142
+ translateServiceSpy.getBrowserLang.and.returnValue('de');
143
+ // Should also be overridden by queryParams
144
+ const storageGetSpy = spyOn(sessionStorageServiceStub, 'get').and.returnValue(
145
+ rxjs.of('de'),
146
+ );
147
+ activatedRouteStub.queryParams = rxjs.of({ locale: 'en' });
148
+ const lang = await callInitialize();
149
+ expect(lang).toBe('en');
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,188 @@
1
+ import { Injectable, Optional } from '@angular/core';
2
+ import { ActivatedRoute } from '@angular/router';
3
+ import { TranslateService } from '@ngx-translate/core';
4
+ import { BehaviorSubject, from, Observable, of as observableOf, of } from 'rxjs';
5
+ import { first, map, switchMap, tap } from 'rxjs/operators';
6
+ import { LANGUAGES } from './languages';
7
+ import { ConfigService, SessionStorageService } from 'ngx-edu-sharing-api';
8
+ import { AppService } from '../services/abstract/app.service';
9
+
10
+ // 'none' means that only labels should be shown (for dev)
11
+ const DEFAULT_SUPPORTED_LANGUAGES = ['de', 'de-informal', 'en', 'none'];
12
+
13
+ @Injectable({ providedIn: 'root' })
14
+ export class TranslationsService {
15
+ private language: string;
16
+ private languageLoaded = new BehaviorSubject(false);
17
+
18
+ constructor(
19
+ private config: ConfigService,
20
+ private route: ActivatedRoute,
21
+ private storage: SessionStorageService,
22
+ private translate: TranslateService,
23
+ @Optional() private appService: AppService,
24
+ ) {}
25
+
26
+ /**
27
+ * Determines and configures the language to use and triggers loading of translations with
28
+ * ngx-translate.
29
+ *
30
+ * Call this once in the app component.
31
+ */
32
+ initialize(): Observable<void> {
33
+ const supportedLanguages$ = from(
34
+ this.config.get('supportedLanguages', DEFAULT_SUPPORTED_LANGUAGES),
35
+ );
36
+ if (this.appService?.isRunningApp()) {
37
+ return supportedLanguages$.pipe(
38
+ switchMap((supportedLanguages: string[]) =>
39
+ this.initializeCordova(supportedLanguages),
40
+ ),
41
+ map(() => void 0),
42
+ );
43
+ }
44
+ supportedLanguages$
45
+ .pipe(
46
+ tap((supportedLanguages) => {
47
+ if (!supportedLanguages.includes('none')) {
48
+ supportedLanguages.push('none');
49
+ }
50
+ }),
51
+ tap((supportedLanguages: string[]) => this.translate.addLangs(supportedLanguages)),
52
+ // Select queryParams.locale if set meaningfully
53
+ switchMap((supportedLanguages: string[]) =>
54
+ this.route.queryParams.pipe(
55
+ first(),
56
+ map((params) => {
57
+ let selectedLanguage: string = null;
58
+ if (supportedLanguages.indexOf(params.locale) !== -1) {
59
+ selectedLanguage = params.locale;
60
+ } else if (params.locale) {
61
+ console.warn(
62
+ `Url requested language ${params.locale}, ` +
63
+ 'but it was not found or is not configured in the allowed languages: ' +
64
+ supportedLanguages,
65
+ );
66
+ }
67
+ return {
68
+ supportedLanguages,
69
+ selectedLanguage,
70
+ };
71
+ }),
72
+ ),
73
+ ),
74
+ // Select storage.get('language') if set meaningfully
75
+ switchMap(({ supportedLanguages, selectedLanguage }) => {
76
+ if (selectedLanguage) {
77
+ return observableOf({
78
+ supportedLanguages,
79
+ selectedLanguage,
80
+ useStored: false,
81
+ });
82
+ } else {
83
+ return this.storage.get('language').pipe(
84
+ map((storageLanguage) => {
85
+ let useStored = false;
86
+ if (supportedLanguages.indexOf(storageLanguage) !== -1) {
87
+ selectedLanguage = storageLanguage;
88
+ useStored = true;
89
+ }
90
+ return {
91
+ supportedLanguages,
92
+ selectedLanguage,
93
+ useStored,
94
+ };
95
+ }),
96
+ );
97
+ }
98
+ }),
99
+ // Use browser language if available, otherwise fall back to the first supported
100
+ // language.
101
+ map(({ supportedLanguages, selectedLanguage, useStored }) => {
102
+ if (selectedLanguage) {
103
+ return {
104
+ supportedLanguages,
105
+ selectedLanguage,
106
+ useStored,
107
+ };
108
+ } else if (
109
+ // Select browser language if set meaningfully
110
+ supportedLanguages.indexOf(this.translate.getBrowserLang()) !== -1
111
+ ) {
112
+ return {
113
+ supportedLanguages,
114
+ selectedLanguage: this.translate.getBrowserLang(),
115
+ useStored,
116
+ };
117
+ } else {
118
+ // Select first supported language
119
+ return {
120
+ supportedLanguages,
121
+ selectedLanguage: supportedLanguages[0],
122
+ useStored,
123
+ };
124
+ }
125
+ }),
126
+ // Set fallback language
127
+ tap(({ supportedLanguages, selectedLanguage, useStored }) => {
128
+ if (!useStored) {
129
+ this.storage.set('language', selectedLanguage);
130
+ }
131
+ if (selectedLanguage === 'none') {
132
+ this.translate.setDefaultLang('none');
133
+ } else if (selectedLanguage === 'de-informal') {
134
+ this.translate.setDefaultLang('de');
135
+ } else {
136
+ this.translate.setDefaultLang(supportedLanguages[0]);
137
+ }
138
+ }),
139
+ // Configure `ngx-translate` to use the determined language and trigger loading of
140
+ // translations.
141
+ switchMap(({ selectedLanguage }) => {
142
+ // console.log('language used: ' + selectedLanguage);
143
+ this.language = selectedLanguage;
144
+ return this.translate.use(selectedLanguage).pipe(map(() => void 0));
145
+ }),
146
+ )
147
+ .subscribe(() => {
148
+ // Notify anyone waiting for translations to be loaded.
149
+ this.languageLoaded.next(true);
150
+ });
151
+ return this.waitForInit();
152
+ }
153
+
154
+ private async initializeCordova(supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES) {
155
+ this.translate.addLangs(supportedLanguages);
156
+ let language = supportedLanguages[0];
157
+ this.translate.setDefaultLang(language);
158
+ this.translate.use(language);
159
+ this.language = language;
160
+ const data = await this.appService.getLanguage();
161
+ if (supportedLanguages.indexOf(data) != -1) {
162
+ language = data;
163
+ }
164
+ this.language = language;
165
+ this.translate.use(language).subscribe(() => {
166
+ this.languageLoaded.next(true);
167
+ });
168
+ // this.translate.getTranslation(language).subscribe(() => {
169
+ // });
170
+ return this.waitForInit();
171
+ }
172
+
173
+ waitForInit(): Observable<void> {
174
+ return this.languageLoaded.pipe(
175
+ first((languageLoaded) => languageLoaded),
176
+ map(() => void 0),
177
+ );
178
+ }
179
+
180
+ /** Same as `translate.currentLang`. */
181
+ getLanguage(): string {
182
+ return this.language;
183
+ }
184
+
185
+ getISOLanguage(): string {
186
+ return LANGUAGES[this.language];
187
+ }
188
+ }
@@ -0,0 +1,15 @@
1
+ export class AccessibilitySettings {
2
+ toastMode: 'important' | 'all' = 'all';
3
+ toastDuration: ToastDuration = ToastDuration.Seconds_5;
4
+ contrastMode = false;
5
+ indicatorIcons = true;
6
+ }
7
+ export enum ToastDuration {
8
+ Seconds_3 = 3,
9
+ Seconds_5 = 5,
10
+ Seconds_8 = 8,
11
+ Seconds_15 = 15,
12
+ Seconds_30 = 30,
13
+ Seconds_60 = 60,
14
+ Infinite = null,
15
+ }
@@ -0,0 +1,4 @@
1
+ import { Node } from 'ngx-edu-sharing-api';
2
+ export interface VirtualNode extends Node {
3
+ virtual: boolean;
4
+ }
@@ -0,0 +1,22 @@
1
+ import { Node } from 'ngx-edu-sharing-api';
2
+
3
+ export type DropAction = 'move' | 'copy' | 'link';
4
+
5
+ export interface DragData<T = unknown> {
6
+ draggedNodes: Node[];
7
+ action: DropAction;
8
+ target: T;
9
+ }
10
+ export interface DropTargetState {
11
+ action: DropAction;
12
+ canDrop: CanDrop;
13
+ }
14
+
15
+ export interface CanDrop {
16
+ /** Whether the target is a valid drop target for the dragged nodes and the given action. */
17
+ accept: boolean;
18
+ /** When denied, whether to explicitly mark the target when hovered. */
19
+ denyExplicit?: boolean;
20
+ /** A message to show when tried to drop on a denied target. */
21
+ denyReason?: string;
22
+ }
@@ -0,0 +1,29 @@
1
+ export type Modifier = 'Ctrl/Cmd' | 'Shift' | 'Alt';
2
+
3
+ export interface KeyboardShortcutCondition {
4
+ modifiers?: Modifier[];
5
+ keyCode: string;
6
+ ignoreWhen?: (event: KeyboardEvent) => boolean;
7
+ }
8
+ export interface KeyboardShortcut extends KeyboardShortcutCondition {
9
+ callback: () => void;
10
+ }
11
+
12
+ export function matchesShortcutCondition(
13
+ event: KeyboardEvent,
14
+ condition: KeyboardShortcutCondition,
15
+ ): boolean {
16
+ return (
17
+ event.code === condition.keyCode &&
18
+ matchesModifiers(event, condition.modifiers) &&
19
+ !condition.ignoreWhen?.(event)
20
+ );
21
+ }
22
+
23
+ function matchesModifiers(event: KeyboardEvent, modifiers: Modifier[] = []): boolean {
24
+ return (
25
+ modifiers.includes('Alt') === event.altKey &&
26
+ modifiers.includes('Shift') === event.shiftKey &&
27
+ modifiers.includes('Ctrl/Cmd') === (event.ctrlKey || event.metaKey)
28
+ );
29
+ }