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.
- package/.browserslistrc +16 -0
- package/.eslintrc.json +44 -0
- package/README.md +40 -0
- package/assets/scss/mixins.scss +95 -0
- package/assets/scss/variables.scss +33 -0
- package/karma.conf.js +42 -0
- package/ng-package.json +10 -0
- package/package.json +19 -0
- package/src/lib/actionbar/actionbar.component.html +59 -0
- package/src/lib/actionbar/actionbar.component.scss +123 -0
- package/src/lib/actionbar/actionbar.component.ts +174 -0
- package/src/lib/common/edu-sharing-ui-common.module.ts +80 -0
- package/src/lib/directives/border-box-observer.directive.ts +75 -0
- package/src/lib/directives/check-text-overflow.directive.ts +61 -0
- package/src/lib/directives/drag-nodes/drag-nodes.ts +32 -0
- package/src/lib/directives/drag-nodes/nodes-drag-source.directive.ts +79 -0
- package/src/lib/directives/drag-nodes/nodes-drag.directive.ts +43 -0
- package/src/lib/directives/drag-nodes/nodes-drop-target.directive.ts +116 -0
- package/src/lib/directives/focus-state.directive.ts +34 -0
- package/src/lib/directives/icon.directive.ts +142 -0
- package/src/lib/directives/nodes-drop-target-legacy.directive.ts +155 -0
- package/src/lib/dropdown/dropdown.component.html +32 -0
- package/src/lib/dropdown/dropdown.component.scss +67 -0
- package/src/lib/dropdown/dropdown.component.ts +71 -0
- package/src/lib/edu-sharing-ui-configuration.ts +47 -0
- package/src/lib/edu-sharing-ui.module.ts +49 -0
- package/src/lib/list-items/available-widgets.ts +30 -0
- package/src/lib/list-items/format-duration.pipe.ts +17 -0
- package/src/lib/list-items/list-base/list-base.component.html +52 -0
- package/src/lib/list-items/list-base/list-base.component.ts +44 -0
- package/src/lib/list-items/list-collection-info/list-collection-info.component.html +48 -0
- package/src/lib/list-items/list-collection-info/list-collection-info.component.scss +8 -0
- package/src/lib/list-items/list-collection-info/list-collection-info.component.ts +24 -0
- package/src/lib/list-items/list-counts/list-counts.component.html +1 -0
- package/src/lib/list-items/list-counts/list-counts.component.scss +3 -0
- package/src/lib/list-items/list-counts/list-counts.component.ts +59 -0
- package/src/lib/list-items/list-items.module.ts +33 -0
- package/src/lib/list-items/list-node-license/list-node-license.component.html +8 -0
- package/src/lib/list-items/list-node-license/list-node-license.component.ts +47 -0
- package/src/lib/list-items/list-node-replication-source/list-node-replication-source.component.html +11 -0
- package/src/lib/list-items/list-node-replication-source/list-node-replication-source.component.ts +60 -0
- package/src/lib/list-items/list-node-workflow/list-node-workflow.component.html +3 -0
- package/src/lib/list-items/list-node-workflow/list-node-workflow.component.ts +21 -0
- package/src/lib/list-items/list-text/list-text.component.html +176 -0
- package/src/lib/list-items/list-text/list-text.component.scss +3 -0
- package/src/lib/list-items/list-text/list-text.component.ts +107 -0
- package/src/lib/list-items/list-widget.ts +52 -0
- package/src/lib/list-items/node-row/node-row.component.html +31 -0
- package/src/lib/list-items/node-row/node-row.component.scss +50 -0
- package/src/lib/list-items/node-row/node-row.component.ts +16 -0
- package/src/lib/list-items/node-source.pipe.ts +48 -0
- package/src/lib/node-entries/combined-data-source.ts +51 -0
- package/src/lib/node-entries/custom-templates-data-source.ts +6 -0
- package/src/lib/node-entries/drag-preview/drag-preview.component.html +6 -0
- package/src/lib/node-entries/drag-preview/drag-preview.component.scss +35 -0
- package/src/lib/node-entries/drag-preview/drag-preview.component.ts +15 -0
- package/src/lib/node-entries/entries-model.ts +120 -0
- package/src/lib/node-entries/items-cap.ts +54 -0
- package/src/lib/node-entries/list-item-label.pipe.ts +28 -0
- package/src/lib/node-entries/mixins.scss +23 -0
- package/src/lib/node-entries/node-cache.spec.ts +199 -0
- package/src/lib/node-entries/node-cache.ts +81 -0
- package/src/lib/node-entries/node-data-source-remote.ts +33 -0
- package/src/lib/node-entries/node-data-source.ts +148 -0
- package/src/lib/node-entries/node-entries-card/node-entries-card.component.html +167 -0
- package/src/lib/node-entries/node-entries-card/node-entries-card.component.scss +28 -0
- package/src/lib/node-entries/node-entries-card/node-entries-card.component.ts +132 -0
- package/src/lib/node-entries/node-entries-card/node-entries-card.main.scss +261 -0
- package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.html +205 -0
- package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.scss +181 -0
- package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.ts +361 -0
- package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.html +100 -0
- package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.scss +46 -0
- package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.ts +40 -0
- package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.html +23 -0
- package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.scss +58 -0
- package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.ts +16 -0
- package/src/lib/node-entries/node-entries-global.service.ts +79 -0
- package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.html +25 -0
- package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.scss +32 -0
- package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.ts +31 -0
- package/src/lib/node-entries/node-entries-table/node-entries-table.component.html +270 -0
- package/src/lib/node-entries/node-entries-table/node-entries-table.component.scss +169 -0
- package/src/lib/node-entries/node-entries-table/node-entries-table.component.ts +333 -0
- package/src/lib/node-entries/node-entries-templates.service.ts +31 -0
- package/src/lib/node-entries/node-entries-wrapper.component.ts +363 -0
- package/src/lib/node-entries/node-entries.component.html +33 -0
- package/src/lib/node-entries/node-entries.component.scss +13 -0
- package/src/lib/node-entries/node-entries.component.ts +151 -0
- package/src/lib/node-entries/node-entries.module.ts +93 -0
- package/src/lib/node-entries/node-rating/node-rating.component.html +53 -0
- package/src/lib/node-entries/node-rating/node-rating.component.scss +31 -0
- package/src/lib/node-entries/node-rating/node-rating.component.ts +105 -0
- package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.html +39 -0
- package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.scss +44 -0
- package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.ts +43 -0
- package/src/lib/node-entries/node-type-badge/node-type-badge.component.html +31 -0
- package/src/lib/node-entries/node-type-badge/node-type-badge.component.scss +5 -0
- package/src/lib/node-entries/node-type-badge/node-type-badge.component.ts +36 -0
- package/src/lib/node-entries/option-button/option-button.component.ts +42 -0
- package/src/lib/node-entries/preview-image/preview-image.component.html +19 -0
- package/src/lib/node-entries/preview-image/preview-image.component.scss +31 -0
- package/src/lib/node-entries/preview-image/preview-image.component.ts +47 -0
- package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.html +27 -0
- package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.scss +9 -0
- package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.ts +26 -0
- package/src/lib/node-url/node-url.component.html +66 -0
- package/src/lib/node-url/node-url.component.scss +32 -0
- package/src/lib/node-url/node-url.component.ts +136 -0
- package/src/lib/pipes/file-size.pipe.ts +24 -0
- package/src/lib/pipes/format-date.pipe.ts +39 -0
- package/src/lib/pipes/node-icon.pipe.ts +11 -0
- package/src/lib/pipes/node-image-size.pipe.ts +18 -0
- package/src/lib/pipes/node-image.pipe.ts +71 -0
- package/src/lib/pipes/node-person-name.pipe.ts +41 -0
- package/src/lib/pipes/node-title.pipe.ts +12 -0
- package/src/lib/pipes/option-tooltip.pipe.ts +32 -0
- package/src/lib/pipes/replace-chars.pipe.ts +21 -0
- package/src/lib/pipes/vcard-name.pipe.ts +11 -0
- package/src/lib/services/abstract/app.service.ts +4 -0
- package/src/lib/services/abstract/keyboard-shortcuts.service.ts +10 -0
- package/src/lib/services/abstract/options-helper.service.ts +29 -0
- package/src/lib/services/abstract/toast.service.ts +5 -0
- package/src/lib/services/accessibility.service.ts +101 -0
- package/src/lib/services/local-events.service.ts +29 -0
- package/src/lib/services/node-entries.service.ts +172 -0
- package/src/lib/services/node-helper.service.ts +239 -0
- package/src/lib/services/nodes-drag-drop.service.ts +165 -0
- package/src/lib/services/options-helper-data.service.ts +186 -0
- package/src/lib/services/repo-url.service.ts +46 -0
- package/src/lib/services/temporary-storage.service.ts +58 -0
- package/src/lib/services/ui.service.ts +182 -0
- package/src/lib/sort-dropdown/sort-dropdown.component.html +22 -0
- package/src/lib/sort-dropdown/sort-dropdown.component.scss +47 -0
- package/src/lib/sort-dropdown/sort-dropdown.component.ts +42 -0
- package/src/lib/spinner/spinner.component.html +14 -0
- package/src/lib/spinner/spinner.component.scss +141 -0
- package/src/lib/spinner/spinner.component.ts +12 -0
- package/src/lib/translations/README.md +44 -0
- package/src/lib/translations/fallback-translation-handler.ts +7 -0
- package/src/lib/translations/languages.ts +6 -0
- package/src/lib/translations/translation-loader.spec.ts +352 -0
- package/src/lib/translations/translation-loader.ts +189 -0
- package/src/lib/translations/translation-source.ts +9 -0
- package/src/lib/translations/translations.module.ts +49 -0
- package/src/lib/translations/translations.service.spec.ts +152 -0
- package/src/lib/translations/translations.service.ts +188 -0
- package/src/lib/types/accessibillity.ts +15 -0
- package/src/lib/types/api-models.ts +4 -0
- package/src/lib/types/drag-drop.ts +22 -0
- package/src/lib/types/keyboard-shortcuts.ts +29 -0
- package/src/lib/types/list-item.ts +67 -0
- package/src/lib/types/option-item.ts +247 -0
- package/src/lib/types/workflow.ts +35 -0
- package/src/lib/util/DateHelper.spec.ts +112 -0
- package/src/lib/util/DateHelper.ts +197 -0
- package/src/lib/util/VCard.ts +277 -0
- package/src/lib/util/color-helper.ts +125 -0
- package/src/lib/util/duration-helper.spec.ts +35 -0
- package/src/lib/util/duration-helper.ts +98 -0
- package/src/lib/util/functions.ts +15 -0
- package/src/lib/util/helper.ts +60 -0
- package/src/lib/util/isNumeric.ts +13 -0
- package/src/lib/util/rest-helper.ts +28 -0
- package/src/lib/util/ui-animation.ts +154 -0
- package/src/lib/util/ui-constants.ts +20 -0
- package/src/module.ts +76 -0
- package/src/test.ts +28 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +10 -0
- 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,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
|
+
}
|