@ardimedia/angular-portal-azure 0.3.16 → 0.3.18
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, computed, Injectable, inject, makeEnvironmentProviders, APP_INITIALIZER, input, output, Component, ElementRef, Injector, effect, afterNextRender } from '@angular/core';
|
|
3
|
-
import { DOCUMENT } from '@angular/common';
|
|
2
|
+
import { signal, computed, Injectable, inject, makeEnvironmentProviders, APP_INITIALIZER, input, output, Component, ElementRef, Injector, DestroyRef, effect, afterNextRender } from '@angular/core';
|
|
3
|
+
import { DOCUMENT, NgComponentOutlet } from '@angular/common';
|
|
4
4
|
|
|
5
5
|
function clearStatusBar() {
|
|
6
6
|
return { text: '', style: 'none' };
|
|
@@ -36,6 +36,161 @@ function createCommand(key, label, action, icon) {
|
|
|
36
36
|
return { key, label, visible: true, enabled: true, icon, action };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// ── Built-in label sets ─────────────────────────────────────────────
|
|
40
|
+
/** German (Switzerland / Liechtenstein) — default */
|
|
41
|
+
const LABELS_DE_CH = {
|
|
42
|
+
loading: 'Laden...',
|
|
43
|
+
saving: 'Speichern...',
|
|
44
|
+
saved: 'Gespeichert',
|
|
45
|
+
deleting: 'Löschen...',
|
|
46
|
+
deleted: 'Gelöscht',
|
|
47
|
+
loadError: 'Fehler beim Laden',
|
|
48
|
+
saveError: 'Fehler beim Speichern',
|
|
49
|
+
deleteError: 'Fehler beim Löschen',
|
|
50
|
+
cmdNew: 'Neu',
|
|
51
|
+
cmdSave: 'Speichern',
|
|
52
|
+
cmdDelete: 'Löschen',
|
|
53
|
+
cmdCancel: 'Abbrechen',
|
|
54
|
+
search: 'Suchen...',
|
|
55
|
+
close: 'Schliessen',
|
|
56
|
+
noAppsTitle: 'Keine Applikationen zugeordnet',
|
|
57
|
+
noAppsMessage: 'Wende dich bitte an den Administrator, damit die Applikationen zugeordnet werden können.',
|
|
58
|
+
closePanel: 'Schliessen',
|
|
59
|
+
lightMode: 'Hellmodus',
|
|
60
|
+
darkMode: 'Dunkelmodus',
|
|
61
|
+
switchToLight: 'Zum Hellmodus wechseln',
|
|
62
|
+
switchToDark: 'Zum Dunkelmodus wechseln',
|
|
63
|
+
settings: 'Einstellungen',
|
|
64
|
+
language: 'Sprache',
|
|
65
|
+
appearance: 'Darstellung',
|
|
66
|
+
};
|
|
67
|
+
/** German (Germany) — Swiss spelling rules apply (no ß) */
|
|
68
|
+
const LABELS_DE_DE = { ...LABELS_DE_CH };
|
|
69
|
+
/** English */
|
|
70
|
+
const LABELS_EN = {
|
|
71
|
+
loading: 'Loading...',
|
|
72
|
+
saving: 'Saving...',
|
|
73
|
+
saved: 'Saved',
|
|
74
|
+
deleting: 'Deleting...',
|
|
75
|
+
deleted: 'Deleted',
|
|
76
|
+
loadError: 'Error loading data',
|
|
77
|
+
saveError: 'Error saving data',
|
|
78
|
+
deleteError: 'Error deleting data',
|
|
79
|
+
cmdNew: 'New',
|
|
80
|
+
cmdSave: 'Save',
|
|
81
|
+
cmdDelete: 'Delete',
|
|
82
|
+
cmdCancel: 'Cancel',
|
|
83
|
+
search: 'Search...',
|
|
84
|
+
close: 'Close',
|
|
85
|
+
noAppsTitle: 'No applications assigned',
|
|
86
|
+
noAppsMessage: 'Please contact the administrator to have applications assigned.',
|
|
87
|
+
closePanel: 'Close',
|
|
88
|
+
lightMode: 'Light mode',
|
|
89
|
+
darkMode: 'Dark mode',
|
|
90
|
+
switchToLight: 'Switch to light mode',
|
|
91
|
+
switchToDark: 'Switch to dark mode',
|
|
92
|
+
settings: 'Settings',
|
|
93
|
+
language: 'Language',
|
|
94
|
+
appearance: 'Appearance',
|
|
95
|
+
};
|
|
96
|
+
/** French */
|
|
97
|
+
const LABELS_FR = {
|
|
98
|
+
loading: 'Chargement...',
|
|
99
|
+
saving: 'Enregistrement...',
|
|
100
|
+
saved: 'Enregistré',
|
|
101
|
+
deleting: 'Suppression...',
|
|
102
|
+
deleted: 'Supprimé',
|
|
103
|
+
loadError: 'Erreur lors du chargement',
|
|
104
|
+
saveError: 'Erreur lors de l\'enregistrement',
|
|
105
|
+
deleteError: 'Erreur lors de la suppression',
|
|
106
|
+
cmdNew: 'Nouveau',
|
|
107
|
+
cmdSave: 'Enregistrer',
|
|
108
|
+
cmdDelete: 'Supprimer',
|
|
109
|
+
cmdCancel: 'Annuler',
|
|
110
|
+
search: 'Rechercher...',
|
|
111
|
+
close: 'Fermer',
|
|
112
|
+
noAppsTitle: 'Aucune application attribuée',
|
|
113
|
+
noAppsMessage: 'Veuillez contacter l\'administrateur pour que les applications soient attribuées.',
|
|
114
|
+
closePanel: 'Fermer',
|
|
115
|
+
lightMode: 'Mode clair',
|
|
116
|
+
darkMode: 'Mode sombre',
|
|
117
|
+
switchToLight: 'Passer au mode clair',
|
|
118
|
+
switchToDark: 'Passer au mode sombre',
|
|
119
|
+
settings: 'Paramètres',
|
|
120
|
+
language: 'Langue',
|
|
121
|
+
appearance: 'Apparence',
|
|
122
|
+
};
|
|
123
|
+
/** Spanish */
|
|
124
|
+
const LABELS_ES = {
|
|
125
|
+
loading: 'Cargando...',
|
|
126
|
+
saving: 'Guardando...',
|
|
127
|
+
saved: 'Guardado',
|
|
128
|
+
deleting: 'Eliminando...',
|
|
129
|
+
deleted: 'Eliminado',
|
|
130
|
+
loadError: 'Error al cargar',
|
|
131
|
+
saveError: 'Error al guardar',
|
|
132
|
+
deleteError: 'Error al eliminar',
|
|
133
|
+
cmdNew: 'Nuevo',
|
|
134
|
+
cmdSave: 'Guardar',
|
|
135
|
+
cmdDelete: 'Eliminar',
|
|
136
|
+
cmdCancel: 'Cancelar',
|
|
137
|
+
search: 'Buscar...',
|
|
138
|
+
close: 'Cerrar',
|
|
139
|
+
noAppsTitle: 'No hay aplicaciones asignadas',
|
|
140
|
+
noAppsMessage: 'Póngase en contacto con el administrador para que se asignen las aplicaciones.',
|
|
141
|
+
closePanel: 'Cerrar',
|
|
142
|
+
lightMode: 'Modo claro',
|
|
143
|
+
darkMode: 'Modo oscuro',
|
|
144
|
+
switchToLight: 'Cambiar a modo claro',
|
|
145
|
+
switchToDark: 'Cambiar a modo oscuro',
|
|
146
|
+
settings: 'Configuración',
|
|
147
|
+
language: 'Idioma',
|
|
148
|
+
appearance: 'Apariencia',
|
|
149
|
+
};
|
|
150
|
+
/** Italian */
|
|
151
|
+
const LABELS_IT = {
|
|
152
|
+
loading: 'Caricamento...',
|
|
153
|
+
saving: 'Salvataggio...',
|
|
154
|
+
saved: 'Salvato',
|
|
155
|
+
deleting: 'Eliminazione...',
|
|
156
|
+
deleted: 'Eliminato',
|
|
157
|
+
loadError: 'Errore durante il caricamento',
|
|
158
|
+
saveError: 'Errore durante il salvataggio',
|
|
159
|
+
deleteError: 'Errore durante l\'eliminazione',
|
|
160
|
+
cmdNew: 'Nuovo',
|
|
161
|
+
cmdSave: 'Salva',
|
|
162
|
+
cmdDelete: 'Elimina',
|
|
163
|
+
cmdCancel: 'Annulla',
|
|
164
|
+
search: 'Cerca...',
|
|
165
|
+
close: 'Chiudi',
|
|
166
|
+
noAppsTitle: 'Nessuna applicazione assegnata',
|
|
167
|
+
noAppsMessage: 'Contattare l\'amministratore per l\'assegnazione delle applicazioni.',
|
|
168
|
+
closePanel: 'Chiudi',
|
|
169
|
+
lightMode: 'Modalità chiara',
|
|
170
|
+
darkMode: 'Modalità scura',
|
|
171
|
+
switchToLight: 'Passa alla modalità chiara',
|
|
172
|
+
switchToDark: 'Passa alla modalità scura',
|
|
173
|
+
settings: 'Impostazioni',
|
|
174
|
+
language: 'Lingua',
|
|
175
|
+
appearance: 'Aspetto',
|
|
176
|
+
};
|
|
177
|
+
// ── Language preset registry ────────────────────────────────────────
|
|
178
|
+
/** Keep DEFAULT_LABELS as alias for backward compatibility */
|
|
179
|
+
const DEFAULT_LABELS = LABELS_DE_CH;
|
|
180
|
+
/** Built-in language presets. Consumers can add custom presets via registerLanguagePreset(). */
|
|
181
|
+
const LANGUAGE_PRESETS = new Map([
|
|
182
|
+
['de-CH', { code: 'de-CH', displayName: 'Deutsch (CH)', labels: LABELS_DE_CH }],
|
|
183
|
+
['de-DE', { code: 'de-DE', displayName: 'Deutsch (DE)', labels: LABELS_DE_DE }],
|
|
184
|
+
['en', { code: 'en', displayName: 'English', labels: LABELS_EN }],
|
|
185
|
+
['fr', { code: 'fr', displayName: 'Français', labels: LABELS_FR }],
|
|
186
|
+
['es', { code: 'es', displayName: 'Español', labels: LABELS_ES }],
|
|
187
|
+
['it', { code: 'it', displayName: 'Italiano', labels: LABELS_IT }],
|
|
188
|
+
]);
|
|
189
|
+
/** Register a custom language preset. */
|
|
190
|
+
function registerLanguagePreset(preset) {
|
|
191
|
+
LANGUAGE_PRESETS.set(preset.code, preset);
|
|
192
|
+
}
|
|
193
|
+
|
|
39
194
|
/** Creates a data blade definition with sensible defaults.
|
|
40
195
|
* statusBar, item, items use getter/setter pairs backed by signals for zoneless change detection.
|
|
41
196
|
* Note: cannot use ...createBlade() spread here — spread copies getter values, not getter/setter pairs. */
|
|
@@ -66,9 +221,9 @@ function createDataBlade(path, title, width = 315) {
|
|
|
66
221
|
* Execute a load-item operation with lifecycle hooks and status bar updates.
|
|
67
222
|
* Replaces the BladeData.loadItem() template method from v0.2.346.
|
|
68
223
|
*/
|
|
69
|
-
async function executeLoadItem(blade, loadFn) {
|
|
224
|
+
async function executeLoadItem(blade, loadFn, labels = DEFAULT_LABELS) {
|
|
70
225
|
blade.lifecycle.onLoadItem?.();
|
|
71
|
-
blade.statusBar = statusBarInfo(
|
|
226
|
+
blade.statusBar = statusBarInfo(labels.loading);
|
|
72
227
|
try {
|
|
73
228
|
const result = await loadFn();
|
|
74
229
|
blade.item = result;
|
|
@@ -77,17 +232,17 @@ async function executeLoadItem(blade, loadFn) {
|
|
|
77
232
|
return result;
|
|
78
233
|
}
|
|
79
234
|
catch (ex) {
|
|
80
|
-
blade.statusBar = statusBarError(ex.message ||
|
|
235
|
+
blade.statusBar = statusBarError(ex.message || labels.loadError);
|
|
81
236
|
blade.lifecycle.onLoadItemError?.(ex);
|
|
82
237
|
}
|
|
83
238
|
}
|
|
84
239
|
/**
|
|
85
240
|
* Execute a load-items operation with lifecycle hooks and status bar updates.
|
|
86
241
|
*/
|
|
87
|
-
async function executeLoadItems(blade, loadFn) {
|
|
242
|
+
async function executeLoadItems(blade, loadFn, labels = DEFAULT_LABELS) {
|
|
88
243
|
blade.lifecycle.onLoadItems?.();
|
|
89
244
|
blade.loading = true;
|
|
90
|
-
blade.statusBar = statusBarInfo(
|
|
245
|
+
blade.statusBar = statusBarInfo(labels.loading);
|
|
91
246
|
try {
|
|
92
247
|
const result = await loadFn();
|
|
93
248
|
blade.items = result;
|
|
@@ -96,7 +251,7 @@ async function executeLoadItems(blade, loadFn) {
|
|
|
96
251
|
return result;
|
|
97
252
|
}
|
|
98
253
|
catch (ex) {
|
|
99
|
-
blade.statusBar = statusBarError(ex.message ||
|
|
254
|
+
blade.statusBar = statusBarError(ex.message || labels.loadError);
|
|
100
255
|
blade.lifecycle.onLoadItemsError?.(ex);
|
|
101
256
|
}
|
|
102
257
|
finally {
|
|
@@ -106,21 +261,21 @@ async function executeLoadItems(blade, loadFn) {
|
|
|
106
261
|
/**
|
|
107
262
|
* Execute a save-item operation with lifecycle hooks and status bar updates.
|
|
108
263
|
*/
|
|
109
|
-
async function executeSaveItem(blade, saveFn) {
|
|
264
|
+
async function executeSaveItem(blade, saveFn, labels = DEFAULT_LABELS) {
|
|
110
265
|
if (blade.lifecycle.isFormValid && !blade.lifecycle.isFormValid()) {
|
|
111
266
|
return;
|
|
112
267
|
}
|
|
113
268
|
blade.lifecycle.onSaveItem?.();
|
|
114
|
-
blade.statusBar = statusBarInfo(
|
|
269
|
+
blade.statusBar = statusBarInfo(labels.saving);
|
|
115
270
|
try {
|
|
116
271
|
const result = await saveFn();
|
|
117
272
|
blade.item = result;
|
|
118
|
-
blade.statusBar = statusBarSuccess(
|
|
273
|
+
blade.statusBar = statusBarSuccess(labels.saved);
|
|
119
274
|
blade.lifecycle.onSavedItem?.();
|
|
120
275
|
return result;
|
|
121
276
|
}
|
|
122
277
|
catch (ex) {
|
|
123
|
-
blade.statusBar = statusBarError(ex.message ||
|
|
278
|
+
blade.statusBar = statusBarError(ex.message || labels.saveError);
|
|
124
279
|
blade.lifecycle.onSaveItemError?.(ex);
|
|
125
280
|
}
|
|
126
281
|
}
|
|
@@ -128,17 +283,17 @@ async function executeSaveItem(blade, saveFn) {
|
|
|
128
283
|
* Execute a delete-item operation with lifecycle hooks and status bar updates.
|
|
129
284
|
* Returns true if the blade should be closed after deletion.
|
|
130
285
|
*/
|
|
131
|
-
async function executeDeleteItem(blade, deleteFn) {
|
|
286
|
+
async function executeDeleteItem(blade, deleteFn, labels = DEFAULT_LABELS) {
|
|
132
287
|
blade.lifecycle.onDeleteItem?.();
|
|
133
|
-
blade.statusBar = statusBarInfo(
|
|
288
|
+
blade.statusBar = statusBarInfo(labels.deleting);
|
|
134
289
|
try {
|
|
135
290
|
await deleteFn();
|
|
136
|
-
blade.statusBar = statusBarSuccess(
|
|
291
|
+
blade.statusBar = statusBarSuccess(labels.deleted);
|
|
137
292
|
const shouldClose = blade.lifecycle.onDeletedItem?.() ?? true;
|
|
138
293
|
return shouldClose;
|
|
139
294
|
}
|
|
140
295
|
catch (ex) {
|
|
141
|
-
blade.statusBar = statusBarError(ex.message ||
|
|
296
|
+
blade.statusBar = statusBarError(ex.message || labels.deleteError);
|
|
142
297
|
blade.lifecycle.onDeleteItemError?.(ex);
|
|
143
298
|
return false;
|
|
144
299
|
}
|
|
@@ -269,6 +424,13 @@ function getAllStringValues(obj) {
|
|
|
269
424
|
* All state is managed via Angular signals for reactive updates.
|
|
270
425
|
*/
|
|
271
426
|
class PortalService {
|
|
427
|
+
static LANG_STORAGE_KEY = 'apa-language';
|
|
428
|
+
/** Localization labels (defaults to German/de-CH, override via PortalConfig.labels) */
|
|
429
|
+
labels = signal({ ...DEFAULT_LABELS }, ...(ngDevMode ? [{ debugName: "labels" }] : []));
|
|
430
|
+
/** Current language code */
|
|
431
|
+
currentLanguage = signal('de-CH', ...(ngDevMode ? [{ debugName: "currentLanguage" }] : []));
|
|
432
|
+
/** Whether the settings dropdown is open */
|
|
433
|
+
isSettingsOpen = signal(false, ...(ngDevMode ? [{ debugName: "isSettingsOpen" }] : []));
|
|
272
434
|
/** The blade stack — ordered left-to-right */
|
|
273
435
|
blades = signal([], ...(ngDevMode ? [{ debugName: "blades" }] : []));
|
|
274
436
|
/** Panorama (startboard/dashboard) state */
|
|
@@ -290,6 +452,8 @@ class PortalService {
|
|
|
290
452
|
const pano = this.panorama();
|
|
291
453
|
return pano.tiles;
|
|
292
454
|
}, ...(ngDevMode ? [{ debugName: "positionedTiles" }] : []));
|
|
455
|
+
/** Consumer label overrides from PortalConfig — re-applied on every language switch */
|
|
456
|
+
_configLabelOverrides = {};
|
|
293
457
|
/**
|
|
294
458
|
* Initialize the portal with configuration.
|
|
295
459
|
* Called by providePortalAzure() during app bootstrap.
|
|
@@ -311,6 +475,59 @@ class PortalService {
|
|
|
311
475
|
if (config.theme) {
|
|
312
476
|
this.theme.set(config.theme);
|
|
313
477
|
}
|
|
478
|
+
// Store consumer label overrides for re-application on language switch
|
|
479
|
+
if (config.labels) {
|
|
480
|
+
this._configLabelOverrides = config.labels;
|
|
481
|
+
}
|
|
482
|
+
// Determine initial language: localStorage → config → browser → default
|
|
483
|
+
const stored = typeof localStorage !== 'undefined'
|
|
484
|
+
? localStorage.getItem(PortalService.LANG_STORAGE_KEY)
|
|
485
|
+
: null;
|
|
486
|
+
const langCode = stored || config.language || this.detectBrowserLanguage();
|
|
487
|
+
this.setLanguage(langCode);
|
|
488
|
+
}
|
|
489
|
+
// --- Language ---
|
|
490
|
+
/** Switch the active language. Persists to localStorage. */
|
|
491
|
+
setLanguage(code) {
|
|
492
|
+
const preset = LANGUAGE_PRESETS.get(code);
|
|
493
|
+
if (!preset) {
|
|
494
|
+
console.warn(`[PortalService] Unknown language "${code}", falling back to de-CH`);
|
|
495
|
+
this.setLanguage('de-CH');
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.currentLanguage.set(code);
|
|
499
|
+
this.labels.set({ ...preset.labels, ...this._configLabelOverrides });
|
|
500
|
+
if (typeof localStorage !== 'undefined') {
|
|
501
|
+
localStorage.setItem(PortalService.LANG_STORAGE_KEY, code);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/** Detect language from browser, match to closest preset. */
|
|
505
|
+
detectBrowserLanguage() {
|
|
506
|
+
if (typeof navigator === 'undefined')
|
|
507
|
+
return 'de-CH';
|
|
508
|
+
const browserLang = navigator.language; // e.g. 'de-CH', 'en-US', 'fr'
|
|
509
|
+
// Exact match
|
|
510
|
+
if (LANGUAGE_PRESETS.has(browserLang))
|
|
511
|
+
return browserLang;
|
|
512
|
+
// Base language match (e.g. 'en-US' → 'en', 'de-AT' → 'de-CH')
|
|
513
|
+
const base = browserLang.split('-')[0];
|
|
514
|
+
for (const key of LANGUAGE_PRESETS.keys()) {
|
|
515
|
+
if (key === base || key.startsWith(base + '-'))
|
|
516
|
+
return key;
|
|
517
|
+
}
|
|
518
|
+
return 'de-CH';
|
|
519
|
+
}
|
|
520
|
+
// --- Settings dropdown ---
|
|
521
|
+
/** Toggle settings dropdown. Closes avatar menu if opening. */
|
|
522
|
+
toggleSettings() {
|
|
523
|
+
const willOpen = !this.isSettingsOpen();
|
|
524
|
+
this.isSettingsOpen.set(willOpen);
|
|
525
|
+
if (willOpen)
|
|
526
|
+
this.closeAvatarMenu();
|
|
527
|
+
}
|
|
528
|
+
/** Close settings dropdown */
|
|
529
|
+
closeSettings() {
|
|
530
|
+
this.isSettingsOpen.set(false);
|
|
314
531
|
}
|
|
315
532
|
/** Update the portal title */
|
|
316
533
|
setTitle(title) {
|
|
@@ -320,6 +537,10 @@ class PortalService {
|
|
|
320
537
|
setUserAccount(userAccount) {
|
|
321
538
|
this.avatarMenu.update((m) => ({ ...m, userAccount }));
|
|
322
539
|
}
|
|
540
|
+
/** Clear all blades and return to panorama */
|
|
541
|
+
clearBlades() {
|
|
542
|
+
this.blades.set([]);
|
|
543
|
+
}
|
|
323
544
|
/** Set tiles on the startboard */
|
|
324
545
|
setTiles(tiles) {
|
|
325
546
|
this.panorama.update((p) => ({
|
|
@@ -355,9 +576,12 @@ class PortalService {
|
|
|
355
576
|
}));
|
|
356
577
|
}
|
|
357
578
|
// --- Avatar menu ---
|
|
358
|
-
/** Toggle avatar menu open/close */
|
|
579
|
+
/** Toggle avatar menu open/close. Closes settings if opening. */
|
|
359
580
|
toggleAvatarMenu() {
|
|
360
|
-
this.avatarMenu
|
|
581
|
+
const willOpen = !this.avatarMenu().isOpen;
|
|
582
|
+
this.avatarMenu.update((m) => ({ ...m, isOpen: willOpen }));
|
|
583
|
+
if (willOpen)
|
|
584
|
+
this.closeSettings();
|
|
361
585
|
}
|
|
362
586
|
/** Close avatar menu */
|
|
363
587
|
closeAvatarMenu() {
|
|
@@ -508,7 +732,7 @@ class BladeService {
|
|
|
508
732
|
/**
|
|
509
733
|
* Check if a blade with the given path is currently open.
|
|
510
734
|
*/
|
|
511
|
-
|
|
735
|
+
isBladeOpen(path) {
|
|
512
736
|
return this.portal.blades().some((b) => b.path === path.toLowerCase());
|
|
513
737
|
}
|
|
514
738
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -519,6 +743,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
519
743
|
args: [{ providedIn: 'root' }]
|
|
520
744
|
}] });
|
|
521
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Registry for mapping blade paths to Angular components.
|
|
748
|
+
*
|
|
749
|
+
* Allows consumer apps to register components for blade paths,
|
|
750
|
+
* enabling dynamic rendering in BladeHostComponent without
|
|
751
|
+
* manual @switch blocks.
|
|
752
|
+
*
|
|
753
|
+
* Usage in app bootstrap:
|
|
754
|
+
* ```typescript
|
|
755
|
+
* const registry = inject(BladeRegistry);
|
|
756
|
+
* registry.register('customers', CustomerNavComponent);
|
|
757
|
+
* registry.register('customers/list', CustomerListComponent);
|
|
758
|
+
* ```
|
|
759
|
+
*
|
|
760
|
+
* Or register multiple at once:
|
|
761
|
+
* ```typescript
|
|
762
|
+
* registry.registerAll({
|
|
763
|
+
* 'customers': CustomerNavComponent,
|
|
764
|
+
* 'customers/list': CustomerListComponent,
|
|
765
|
+
* });
|
|
766
|
+
* ```
|
|
767
|
+
*/
|
|
768
|
+
class BladeRegistry {
|
|
769
|
+
registry = new Map();
|
|
770
|
+
/** Register a component for a blade path */
|
|
771
|
+
register(path, component) {
|
|
772
|
+
this.registry.set(path.toLowerCase(), component);
|
|
773
|
+
}
|
|
774
|
+
/** Register multiple blade path → component mappings */
|
|
775
|
+
registerAll(mappings) {
|
|
776
|
+
for (const [path, component] of Object.entries(mappings)) {
|
|
777
|
+
this.register(path, component);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/** Get the component registered for a path, if any */
|
|
781
|
+
get(path) {
|
|
782
|
+
return this.registry.get(path.toLowerCase());
|
|
783
|
+
}
|
|
784
|
+
/** Check if a component is registered for a path */
|
|
785
|
+
has(path) {
|
|
786
|
+
return this.registry.has(path.toLowerCase());
|
|
787
|
+
}
|
|
788
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
789
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeRegistry, providedIn: 'root' });
|
|
790
|
+
}
|
|
791
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeRegistry, decorators: [{
|
|
792
|
+
type: Injectable,
|
|
793
|
+
args: [{ providedIn: 'root' }]
|
|
794
|
+
}] });
|
|
795
|
+
|
|
522
796
|
/**
|
|
523
797
|
* Provide the angular-portal-azure library configuration.
|
|
524
798
|
*
|
|
@@ -573,7 +847,14 @@ class TileComponent {
|
|
|
573
847
|
[class.fxs-tilesize-mini]="tile().size === 'mini'"
|
|
574
848
|
[class.fxs-tilesize-herowide]="tile().size === 'herowide'"
|
|
575
849
|
[class.fxs-tilesize-small]="tile().size === 'small'">
|
|
576
|
-
<div class="fxs-part fxs-part-clickable"
|
|
850
|
+
<div class="fxs-part fxs-part-clickable"
|
|
851
|
+
role="button"
|
|
852
|
+
tabindex="0"
|
|
853
|
+
[attr.aria-label]="tile().title"
|
|
854
|
+
(click)="onClick()"
|
|
855
|
+
(keydown.enter)="onClick()"
|
|
856
|
+
(keydown.space)="onClick(); $event.preventDefault()"
|
|
857
|
+
style="cursor:pointer;">
|
|
577
858
|
<header class="fxs-part-title">
|
|
578
859
|
<h2 class="msportalfx-tooltip-overflow">{{ tile().title }}</h2>
|
|
579
860
|
@if (tile().subtitle) {
|
|
@@ -595,7 +876,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
595
876
|
[class.fxs-tilesize-mini]="tile().size === 'mini'"
|
|
596
877
|
[class.fxs-tilesize-herowide]="tile().size === 'herowide'"
|
|
597
878
|
[class.fxs-tilesize-small]="tile().size === 'small'">
|
|
598
|
-
<div class="fxs-part fxs-part-clickable"
|
|
879
|
+
<div class="fxs-part fxs-part-clickable"
|
|
880
|
+
role="button"
|
|
881
|
+
tabindex="0"
|
|
882
|
+
[attr.aria-label]="tile().title"
|
|
883
|
+
(click)="onClick()"
|
|
884
|
+
(keydown.enter)="onClick()"
|
|
885
|
+
(keydown.space)="onClick(); $event.preventDefault()"
|
|
886
|
+
style="cursor:pointer;">
|
|
599
887
|
<header class="fxs-part-title">
|
|
600
888
|
<h2 class="msportalfx-tooltip-overflow">{{ tile().title }}</h2>
|
|
601
889
|
@if (tile().subtitle) {
|
|
@@ -636,9 +924,9 @@ class PanoramaComponent {
|
|
|
636
924
|
@if (panorama().tiles.length === 0 && panorama().isTilesLoaded) {
|
|
637
925
|
<div class="fxs-part fxs-part-clickable" style="background-color:var(--apa-surface-raised); padding:25px; max-width:800px; margin-bottom:15px; height:auto;">
|
|
638
926
|
<header class="fxs-part-title" style="color:var(--apa-text)">
|
|
639
|
-
<h3 class="msportalfx-tooltip-overflow">
|
|
927
|
+
<h3 class="msportalfx-tooltip-overflow">{{ portal.labels().noAppsTitle }}</h3>
|
|
640
928
|
<p class="msportalfx-tooltip-overflow" style="margin:0;padding:0">
|
|
641
|
-
|
|
929
|
+
{{ portal.labels().noAppsMessage }}
|
|
642
930
|
</p>
|
|
643
931
|
</header>
|
|
644
932
|
</div>
|
|
@@ -664,9 +952,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
664
952
|
@if (panorama().tiles.length === 0 && panorama().isTilesLoaded) {
|
|
665
953
|
<div class="fxs-part fxs-part-clickable" style="background-color:var(--apa-surface-raised); padding:25px; max-width:800px; margin-bottom:15px; height:auto;">
|
|
666
954
|
<header class="fxs-part-title" style="color:var(--apa-text)">
|
|
667
|
-
<h3 class="msportalfx-tooltip-overflow">
|
|
955
|
+
<h3 class="msportalfx-tooltip-overflow">{{ portal.labels().noAppsTitle }}</h3>
|
|
668
956
|
<p class="msportalfx-tooltip-overflow" style="margin:0;padding:0">
|
|
669
|
-
|
|
957
|
+
{{ portal.labels().noAppsMessage }}
|
|
670
958
|
</p>
|
|
671
959
|
</header>
|
|
672
960
|
</div>
|
|
@@ -688,7 +976,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
688
976
|
* Root portal shell component.
|
|
689
977
|
* Ported from the fxs-portal structure in home.html (v0.2.346).
|
|
690
978
|
*
|
|
691
|
-
* Provides the top bar (with portal title and avatar menu),
|
|
979
|
+
* Provides the top bar (with portal title, settings gear, and avatar menu),
|
|
692
980
|
* and the main content area. Child components (panorama, blade-host,
|
|
693
981
|
* notification-panel) are projected via content projection.
|
|
694
982
|
*
|
|
@@ -706,7 +994,10 @@ class PortalLayoutComponent {
|
|
|
706
994
|
document = inject(DOCUMENT);
|
|
707
995
|
elementRef = inject(ElementRef);
|
|
708
996
|
injector = inject(Injector);
|
|
997
|
+
destroyRef = inject(DestroyRef);
|
|
709
998
|
isDark = signal(false, ...(ngDevMode ? [{ debugName: "isDark" }] : []));
|
|
999
|
+
/** Available languages from the preset registry */
|
|
1000
|
+
availableLanguages = Array.from(LANGUAGE_PRESETS.values()).map((p) => ({ code: p.code, displayName: p.displayName }));
|
|
710
1001
|
constructor() {
|
|
711
1002
|
const stored = localStorage.getItem(PortalLayoutComponent.STORAGE_KEY);
|
|
712
1003
|
const dark = stored === 'true';
|
|
@@ -721,12 +1012,48 @@ class PortalLayoutComponent {
|
|
|
721
1012
|
this.scrollToLastBlade();
|
|
722
1013
|
}, { injector });
|
|
723
1014
|
});
|
|
1015
|
+
// Click-outside handler for dropdowns
|
|
1016
|
+
afterNextRender(() => {
|
|
1017
|
+
const handler = (event) => {
|
|
1018
|
+
const target = event.target;
|
|
1019
|
+
const settingsEl = this.elementRef.nativeElement.querySelector('.apa-settings-container');
|
|
1020
|
+
const avatarEl = this.elementRef.nativeElement.querySelector('.fxs-avatarmenu-tenant-container');
|
|
1021
|
+
if (settingsEl && !settingsEl.contains(target)) {
|
|
1022
|
+
this.portal.closeSettings();
|
|
1023
|
+
}
|
|
1024
|
+
if (avatarEl && !avatarEl.contains(target)) {
|
|
1025
|
+
this.portal.closeAvatarMenu();
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
this.document.addEventListener('click', handler);
|
|
1029
|
+
// Escape key closes open dropdowns
|
|
1030
|
+
const keyHandler = (event) => {
|
|
1031
|
+
if (event.key === 'Escape') {
|
|
1032
|
+
if (this.portal.isSettingsOpen()) {
|
|
1033
|
+
this.portal.closeSettings();
|
|
1034
|
+
event.preventDefault();
|
|
1035
|
+
}
|
|
1036
|
+
if (this.portal.avatarMenu().isOpen) {
|
|
1037
|
+
this.portal.closeAvatarMenu();
|
|
1038
|
+
event.preventDefault();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
this.document.addEventListener('keydown', keyHandler);
|
|
1043
|
+
this.destroyRef.onDestroy(() => {
|
|
1044
|
+
this.document.removeEventListener('click', handler);
|
|
1045
|
+
this.document.removeEventListener('keydown', keyHandler);
|
|
1046
|
+
});
|
|
1047
|
+
}, { injector: this.injector });
|
|
724
1048
|
}
|
|
725
1049
|
toggleDarkMode() {
|
|
726
1050
|
const dark = !this.isDark();
|
|
727
1051
|
localStorage.setItem(PortalLayoutComponent.STORAGE_KEY, String(dark));
|
|
728
1052
|
this.applyTheme(dark);
|
|
729
1053
|
}
|
|
1054
|
+
switchLanguage(code) {
|
|
1055
|
+
this.portal.setLanguage(code);
|
|
1056
|
+
}
|
|
730
1057
|
applyTheme(dark) {
|
|
731
1058
|
this.isDark.set(dark);
|
|
732
1059
|
this.document.documentElement.classList.toggle('apa-dark', dark);
|
|
@@ -748,8 +1075,7 @@ class PortalLayoutComponent {
|
|
|
748
1075
|
}
|
|
749
1076
|
onHomeClick(event) {
|
|
750
1077
|
event.preventDefault();
|
|
751
|
-
|
|
752
|
-
this.portal.blades.set([]);
|
|
1078
|
+
this.portal.clearBlades();
|
|
753
1079
|
}
|
|
754
1080
|
scrollToLastBlade() {
|
|
755
1081
|
const scrollContainer = this.elementRef.nativeElement.querySelector('.fxs-portal-content');
|
|
@@ -788,35 +1114,71 @@ class PortalLayoutComponent {
|
|
|
788
1114
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: PortalLayoutComponent, isStandalone: true, selector: "apa-portal-layout", ngImport: i0, template: `
|
|
789
1115
|
<div class="fxs-portal fxs-theme-blue">
|
|
790
1116
|
<!-- Top bar -->
|
|
791
|
-
<
|
|
1117
|
+
<header class="fxs-topbar" role="banner">
|
|
792
1118
|
<div style="padding-left:25px;">
|
|
793
1119
|
<a href="#" class="fxs-topbar-home fxs-has-hover" (click)="onHomeClick($event)">
|
|
794
1120
|
{{ portal.panorama().title }}
|
|
795
1121
|
</a>
|
|
796
1122
|
</div>
|
|
797
1123
|
<div style="display:flex; align-items:center; gap:12px; padding-right:10px;">
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1124
|
+
<!-- Settings gear -->
|
|
1125
|
+
<div class="apa-settings-container" style="position:relative;">
|
|
1126
|
+
<button class="apa-settings-trigger fxs-has-hover"
|
|
1127
|
+
(click)="portal.toggleSettings()"
|
|
1128
|
+
[attr.aria-label]="portal.labels().settings"
|
|
1129
|
+
[attr.title]="portal.labels().settings"
|
|
1130
|
+
[attr.aria-expanded]="portal.isSettingsOpen()"
|
|
1131
|
+
aria-haspopup="true">
|
|
1132
|
+
<i class="ti ti-settings" aria-hidden="true"></i>
|
|
1133
|
+
</button>
|
|
1134
|
+
@if (portal.isSettingsOpen()) {
|
|
1135
|
+
<div class="apa-settings-dropdown" role="menu">
|
|
1136
|
+
<!-- Appearance -->
|
|
1137
|
+
<div class="apa-settings-section-header">{{ portal.labels().appearance }}</div>
|
|
1138
|
+
<button class="apa-settings-item" role="menuitem" (click)="toggleDarkMode()">
|
|
1139
|
+
<i [class]="isDark() ? 'ti ti-sun' : 'ti ti-moon'" aria-hidden="true"></i>
|
|
1140
|
+
<span>{{ isDark() ? portal.labels().lightMode : portal.labels().darkMode }}</span>
|
|
1141
|
+
</button>
|
|
1142
|
+
<!-- Language -->
|
|
1143
|
+
<div class="apa-settings-section-header">{{ portal.labels().language }}</div>
|
|
1144
|
+
@for (lang of availableLanguages; track lang.code) {
|
|
1145
|
+
<button class="apa-settings-item" role="menuitem"
|
|
1146
|
+
[class.apa-settings-item-active]="lang.code === portal.currentLanguage()"
|
|
1147
|
+
(click)="switchLanguage(lang.code)">
|
|
1148
|
+
<span>{{ lang.displayName }}</span>
|
|
1149
|
+
@if (lang.code === portal.currentLanguage()) {
|
|
1150
|
+
<i class="ti ti-check" aria-hidden="true"></i>
|
|
1151
|
+
}
|
|
1152
|
+
</button>
|
|
1153
|
+
}
|
|
1154
|
+
</div>
|
|
1155
|
+
}
|
|
1156
|
+
</div>
|
|
1157
|
+
<!-- Avatar menu -->
|
|
804
1158
|
<div class="fxs-avatarmenu-tenant-container" style="position:relative;">
|
|
805
|
-
<a class="apa-avatar-trigger fxs-has-hover"
|
|
806
|
-
|
|
1159
|
+
<a class="apa-avatar-trigger fxs-has-hover"
|
|
1160
|
+
role="button"
|
|
1161
|
+
tabindex="0"
|
|
1162
|
+
[attr.aria-expanded]="portal.avatarMenu().isOpen"
|
|
1163
|
+
aria-haspopup="true"
|
|
1164
|
+
(click)="portal.toggleAvatarMenu()"
|
|
1165
|
+
(keydown.enter)="portal.toggleAvatarMenu()"
|
|
1166
|
+
(keydown.space)="portal.toggleAvatarMenu()">
|
|
1167
|
+
<span class="apa-avatar-initials" aria-hidden="true">{{ initials() }}</span>
|
|
807
1168
|
<span class="apa-avatar-info">
|
|
808
1169
|
<span class="apa-avatar-name">{{ displayName() }}</span>
|
|
809
1170
|
<span class="apa-avatar-email">{{ portal.avatarMenu().userAccount.userName }}</span>
|
|
810
1171
|
</span>
|
|
811
1172
|
<i class="ti" [class.ti-chevron-down]="!portal.avatarMenu().isOpen"
|
|
812
|
-
[class.ti-chevron-up]="portal.avatarMenu().isOpen"
|
|
1173
|
+
[class.ti-chevron-up]="portal.avatarMenu().isOpen"
|
|
1174
|
+
aria-hidden="true"></i>
|
|
813
1175
|
</a>
|
|
814
1176
|
@if (portal.avatarMenu().isOpen && portal.avatarMenu().items.length > 0) {
|
|
815
|
-
<div class="apa-avatar-dropdown">
|
|
1177
|
+
<div class="apa-avatar-dropdown" role="menu">
|
|
816
1178
|
@for (item of portal.avatarMenu().items; track item.href) {
|
|
817
|
-
<a class="apa-avatar-dropdown-item" [href]="item.href">
|
|
1179
|
+
<a class="apa-avatar-dropdown-item" role="menuitem" [href]="item.href">
|
|
818
1180
|
@if (item.icon) {
|
|
819
|
-
<i [class]="item.icon"></i>
|
|
1181
|
+
<i [class]="item.icon" aria-hidden="true"></i>
|
|
820
1182
|
}
|
|
821
1183
|
<span>{{ item.label }}</span>
|
|
822
1184
|
</a>
|
|
@@ -825,12 +1187,12 @@ class PortalLayoutComponent {
|
|
|
825
1187
|
}
|
|
826
1188
|
</div>
|
|
827
1189
|
</div>
|
|
828
|
-
</
|
|
1190
|
+
</header>
|
|
829
1191
|
<!-- Main content area -->
|
|
830
|
-
<
|
|
831
|
-
|
|
1192
|
+
<main class="fxs-portal-content fxs-panorama"
|
|
1193
|
+
[style.margin-right.px]="notificationMargin()">
|
|
832
1194
|
<ng-content />
|
|
833
|
-
</
|
|
1195
|
+
</main>
|
|
834
1196
|
<!-- Footer -->
|
|
835
1197
|
<div class="fxs-portal-footer"></div>
|
|
836
1198
|
</div>
|
|
@@ -844,35 +1206,71 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
844
1206
|
template: `
|
|
845
1207
|
<div class="fxs-portal fxs-theme-blue">
|
|
846
1208
|
<!-- Top bar -->
|
|
847
|
-
<
|
|
1209
|
+
<header class="fxs-topbar" role="banner">
|
|
848
1210
|
<div style="padding-left:25px;">
|
|
849
1211
|
<a href="#" class="fxs-topbar-home fxs-has-hover" (click)="onHomeClick($event)">
|
|
850
1212
|
{{ portal.panorama().title }}
|
|
851
1213
|
</a>
|
|
852
1214
|
</div>
|
|
853
1215
|
<div style="display:flex; align-items:center; gap:12px; padding-right:10px;">
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1216
|
+
<!-- Settings gear -->
|
|
1217
|
+
<div class="apa-settings-container" style="position:relative;">
|
|
1218
|
+
<button class="apa-settings-trigger fxs-has-hover"
|
|
1219
|
+
(click)="portal.toggleSettings()"
|
|
1220
|
+
[attr.aria-label]="portal.labels().settings"
|
|
1221
|
+
[attr.title]="portal.labels().settings"
|
|
1222
|
+
[attr.aria-expanded]="portal.isSettingsOpen()"
|
|
1223
|
+
aria-haspopup="true">
|
|
1224
|
+
<i class="ti ti-settings" aria-hidden="true"></i>
|
|
1225
|
+
</button>
|
|
1226
|
+
@if (portal.isSettingsOpen()) {
|
|
1227
|
+
<div class="apa-settings-dropdown" role="menu">
|
|
1228
|
+
<!-- Appearance -->
|
|
1229
|
+
<div class="apa-settings-section-header">{{ portal.labels().appearance }}</div>
|
|
1230
|
+
<button class="apa-settings-item" role="menuitem" (click)="toggleDarkMode()">
|
|
1231
|
+
<i [class]="isDark() ? 'ti ti-sun' : 'ti ti-moon'" aria-hidden="true"></i>
|
|
1232
|
+
<span>{{ isDark() ? portal.labels().lightMode : portal.labels().darkMode }}</span>
|
|
1233
|
+
</button>
|
|
1234
|
+
<!-- Language -->
|
|
1235
|
+
<div class="apa-settings-section-header">{{ portal.labels().language }}</div>
|
|
1236
|
+
@for (lang of availableLanguages; track lang.code) {
|
|
1237
|
+
<button class="apa-settings-item" role="menuitem"
|
|
1238
|
+
[class.apa-settings-item-active]="lang.code === portal.currentLanguage()"
|
|
1239
|
+
(click)="switchLanguage(lang.code)">
|
|
1240
|
+
<span>{{ lang.displayName }}</span>
|
|
1241
|
+
@if (lang.code === portal.currentLanguage()) {
|
|
1242
|
+
<i class="ti ti-check" aria-hidden="true"></i>
|
|
1243
|
+
}
|
|
1244
|
+
</button>
|
|
1245
|
+
}
|
|
1246
|
+
</div>
|
|
1247
|
+
}
|
|
1248
|
+
</div>
|
|
1249
|
+
<!-- Avatar menu -->
|
|
860
1250
|
<div class="fxs-avatarmenu-tenant-container" style="position:relative;">
|
|
861
|
-
<a class="apa-avatar-trigger fxs-has-hover"
|
|
862
|
-
|
|
1251
|
+
<a class="apa-avatar-trigger fxs-has-hover"
|
|
1252
|
+
role="button"
|
|
1253
|
+
tabindex="0"
|
|
1254
|
+
[attr.aria-expanded]="portal.avatarMenu().isOpen"
|
|
1255
|
+
aria-haspopup="true"
|
|
1256
|
+
(click)="portal.toggleAvatarMenu()"
|
|
1257
|
+
(keydown.enter)="portal.toggleAvatarMenu()"
|
|
1258
|
+
(keydown.space)="portal.toggleAvatarMenu()">
|
|
1259
|
+
<span class="apa-avatar-initials" aria-hidden="true">{{ initials() }}</span>
|
|
863
1260
|
<span class="apa-avatar-info">
|
|
864
1261
|
<span class="apa-avatar-name">{{ displayName() }}</span>
|
|
865
1262
|
<span class="apa-avatar-email">{{ portal.avatarMenu().userAccount.userName }}</span>
|
|
866
1263
|
</span>
|
|
867
1264
|
<i class="ti" [class.ti-chevron-down]="!portal.avatarMenu().isOpen"
|
|
868
|
-
[class.ti-chevron-up]="portal.avatarMenu().isOpen"
|
|
1265
|
+
[class.ti-chevron-up]="portal.avatarMenu().isOpen"
|
|
1266
|
+
aria-hidden="true"></i>
|
|
869
1267
|
</a>
|
|
870
1268
|
@if (portal.avatarMenu().isOpen && portal.avatarMenu().items.length > 0) {
|
|
871
|
-
<div class="apa-avatar-dropdown">
|
|
1269
|
+
<div class="apa-avatar-dropdown" role="menu">
|
|
872
1270
|
@for (item of portal.avatarMenu().items; track item.href) {
|
|
873
|
-
<a class="apa-avatar-dropdown-item" [href]="item.href">
|
|
1271
|
+
<a class="apa-avatar-dropdown-item" role="menuitem" [href]="item.href">
|
|
874
1272
|
@if (item.icon) {
|
|
875
|
-
<i [class]="item.icon"></i>
|
|
1273
|
+
<i [class]="item.icon" aria-hidden="true"></i>
|
|
876
1274
|
}
|
|
877
1275
|
<span>{{ item.label }}</span>
|
|
878
1276
|
</a>
|
|
@@ -881,12 +1279,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
881
1279
|
}
|
|
882
1280
|
</div>
|
|
883
1281
|
</div>
|
|
884
|
-
</
|
|
1282
|
+
</header>
|
|
885
1283
|
<!-- Main content area -->
|
|
886
|
-
<
|
|
887
|
-
|
|
1284
|
+
<main class="fxs-portal-content fxs-panorama"
|
|
1285
|
+
[style.margin-right.px]="notificationMargin()">
|
|
888
1286
|
<ng-content />
|
|
889
|
-
</
|
|
1287
|
+
</main>
|
|
890
1288
|
<!-- Footer -->
|
|
891
1289
|
<div class="fxs-portal-footer"></div>
|
|
892
1290
|
</div>
|
|
@@ -912,21 +1310,30 @@ class CommandBarComponent {
|
|
|
912
1310
|
}
|
|
913
1311
|
onCommand(cmd) {
|
|
914
1312
|
if (cmd.enabled && cmd.action) {
|
|
915
|
-
cmd.action();
|
|
1313
|
+
const result = cmd.action();
|
|
1314
|
+
if (result instanceof Promise) {
|
|
1315
|
+
result.catch((err) => console.error(`[CommandBar] Command "${cmd.key}" failed:`, err));
|
|
1316
|
+
}
|
|
916
1317
|
}
|
|
917
1318
|
}
|
|
918
1319
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
919
1320
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: CommandBarComponent, isStandalone: true, selector: "apa-command-bar", inputs: { commands: { classPropertyName: "commands", publicName: "commands", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
920
|
-
<
|
|
921
|
-
<ul class="fxs-commandBar-itemList">
|
|
1321
|
+
<nav class="fxs-commandBar" role="toolbar" aria-label="Commands">
|
|
1322
|
+
<ul class="fxs-commandBar-itemList" role="list">
|
|
922
1323
|
@for (cmd of visibleCommands(); track cmd.key) {
|
|
923
|
-
<li>
|
|
1324
|
+
<li role="listitem">
|
|
924
1325
|
<a class="fxs-commandBar-item"
|
|
1326
|
+
role="button"
|
|
1327
|
+
[tabindex]="cmd.enabled ? 0 : -1"
|
|
925
1328
|
[class.apa-disable-click]="!cmd.enabled"
|
|
926
|
-
|
|
1329
|
+
[attr.aria-disabled]="!cmd.enabled"
|
|
1330
|
+
[attr.aria-label]="cmd.label"
|
|
1331
|
+
(click)="onCommand(cmd)"
|
|
1332
|
+
(keydown.enter)="onCommand(cmd)"
|
|
1333
|
+
(keydown.space)="onCommand(cmd)">
|
|
927
1334
|
<span class="fxs-commandBar-item-text">{{ cmd.label }}</span>
|
|
928
1335
|
@if (cmd.icon) {
|
|
929
|
-
<span class="fxs-commandBar-item-icon apa-commandbar-icon">
|
|
1336
|
+
<span class="fxs-commandBar-item-icon apa-commandbar-icon" aria-hidden="true">
|
|
930
1337
|
<span [class]="cmd.icon"></span>
|
|
931
1338
|
</span>
|
|
932
1339
|
}
|
|
@@ -934,22 +1341,28 @@ class CommandBarComponent {
|
|
|
934
1341
|
</li>
|
|
935
1342
|
}
|
|
936
1343
|
</ul>
|
|
937
|
-
</
|
|
1344
|
+
</nav>
|
|
938
1345
|
`, isInline: true, styles: [":host{display:block}\n"] });
|
|
939
1346
|
}
|
|
940
1347
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandBarComponent, decorators: [{
|
|
941
1348
|
type: Component,
|
|
942
1349
|
args: [{ selector: 'apa-command-bar', standalone: true, template: `
|
|
943
|
-
<
|
|
944
|
-
<ul class="fxs-commandBar-itemList">
|
|
1350
|
+
<nav class="fxs-commandBar" role="toolbar" aria-label="Commands">
|
|
1351
|
+
<ul class="fxs-commandBar-itemList" role="list">
|
|
945
1352
|
@for (cmd of visibleCommands(); track cmd.key) {
|
|
946
|
-
<li>
|
|
1353
|
+
<li role="listitem">
|
|
947
1354
|
<a class="fxs-commandBar-item"
|
|
1355
|
+
role="button"
|
|
1356
|
+
[tabindex]="cmd.enabled ? 0 : -1"
|
|
948
1357
|
[class.apa-disable-click]="!cmd.enabled"
|
|
949
|
-
|
|
1358
|
+
[attr.aria-disabled]="!cmd.enabled"
|
|
1359
|
+
[attr.aria-label]="cmd.label"
|
|
1360
|
+
(click)="onCommand(cmd)"
|
|
1361
|
+
(keydown.enter)="onCommand(cmd)"
|
|
1362
|
+
(keydown.space)="onCommand(cmd)">
|
|
950
1363
|
<span class="fxs-commandBar-item-text">{{ cmd.label }}</span>
|
|
951
1364
|
@if (cmd.icon) {
|
|
952
|
-
<span class="fxs-commandBar-item-icon apa-commandbar-icon">
|
|
1365
|
+
<span class="fxs-commandBar-item-icon apa-commandbar-icon" aria-hidden="true">
|
|
953
1366
|
<span [class]="cmd.icon"></span>
|
|
954
1367
|
</span>
|
|
955
1368
|
}
|
|
@@ -957,7 +1370,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
957
1370
|
</li>
|
|
958
1371
|
}
|
|
959
1372
|
</ul>
|
|
960
|
-
</
|
|
1373
|
+
</nav>
|
|
961
1374
|
`, styles: [":host{display:block}\n"] }]
|
|
962
1375
|
}], propDecorators: { commands: [{ type: i0.Input, args: [{ isSignal: true, alias: "commands", required: false }] }] } });
|
|
963
1376
|
|
|
@@ -978,6 +1391,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
978
1391
|
class BladeComponent {
|
|
979
1392
|
blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : []));
|
|
980
1393
|
bladeClose = output();
|
|
1394
|
+
portal = inject(PortalService);
|
|
981
1395
|
bladeService = inject(BladeService);
|
|
982
1396
|
onClose() {
|
|
983
1397
|
const b = this.blade();
|
|
@@ -987,12 +1401,16 @@ class BladeComponent {
|
|
|
987
1401
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
988
1402
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BladeComponent, isStandalone: true, selector: "apa-blade", inputs: { blade: { classPropertyName: "blade", publicName: "blade", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { bladeClose: "bladeClose" }, ngImport: i0, template: `
|
|
989
1403
|
<section class="fxs-blade-locked fxs-blade fx-rightClick fxs-bladesize-small"
|
|
1404
|
+
role="region"
|
|
1405
|
+
[attr.aria-label]="blade().title"
|
|
990
1406
|
[style.width.px]="blade().width">
|
|
991
1407
|
<!-- Header -->
|
|
992
1408
|
<header class="fxs-blade-header">
|
|
993
1409
|
<!-- Status bar -->
|
|
994
1410
|
<div class="fxs-blade-statusbar-wrapper">
|
|
995
1411
|
<div class="fxs-blade-statusbar"
|
|
1412
|
+
role="status"
|
|
1413
|
+
aria-live="polite"
|
|
996
1414
|
[class.apa-statusbar-info]="blade().statusBar.style === 'info'"
|
|
997
1415
|
[class.apa-statusbar-error]="blade().statusBar.style === 'error' || blade().statusBar.style === 'warning'"
|
|
998
1416
|
[class.apa-statusbar-success]="blade().statusBar.style === 'success'">
|
|
@@ -1002,7 +1420,9 @@ class BladeComponent {
|
|
|
1002
1420
|
|
|
1003
1421
|
<!-- Action buttons -->
|
|
1004
1422
|
<div class="fxs-blade-actions">
|
|
1005
|
-
<button (click)="onClose()"
|
|
1423
|
+
<button (click)="onClose()"
|
|
1424
|
+
[title]="portal.labels().close"
|
|
1425
|
+
[attr.aria-label]="portal.labels().close + ' ' + blade().title">
|
|
1006
1426
|
<svg viewBox="0 0 11 11" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
|
1007
1427
|
<g>
|
|
1008
1428
|
<polygon class="msportal-fx-svg-placeholder" points="10.4,1.4 9.6,0.6 5.5,4.7 1.4,0.6 0.6,1.4 4.7,5.5 0.6,9.6 1.4,10.4 5.5,6.3 9.6,10.4 10.4,9.6 6.3,5.5"/>
|
|
@@ -1042,12 +1462,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1042
1462
|
type: Component,
|
|
1043
1463
|
args: [{ selector: 'apa-blade', standalone: true, imports: [CommandBarComponent], template: `
|
|
1044
1464
|
<section class="fxs-blade-locked fxs-blade fx-rightClick fxs-bladesize-small"
|
|
1465
|
+
role="region"
|
|
1466
|
+
[attr.aria-label]="blade().title"
|
|
1045
1467
|
[style.width.px]="blade().width">
|
|
1046
1468
|
<!-- Header -->
|
|
1047
1469
|
<header class="fxs-blade-header">
|
|
1048
1470
|
<!-- Status bar -->
|
|
1049
1471
|
<div class="fxs-blade-statusbar-wrapper">
|
|
1050
1472
|
<div class="fxs-blade-statusbar"
|
|
1473
|
+
role="status"
|
|
1474
|
+
aria-live="polite"
|
|
1051
1475
|
[class.apa-statusbar-info]="blade().statusBar.style === 'info'"
|
|
1052
1476
|
[class.apa-statusbar-error]="blade().statusBar.style === 'error' || blade().statusBar.style === 'warning'"
|
|
1053
1477
|
[class.apa-statusbar-success]="blade().statusBar.style === 'success'">
|
|
@@ -1057,7 +1481,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1057
1481
|
|
|
1058
1482
|
<!-- Action buttons -->
|
|
1059
1483
|
<div class="fxs-blade-actions">
|
|
1060
|
-
<button (click)="onClose()"
|
|
1484
|
+
<button (click)="onClose()"
|
|
1485
|
+
[title]="portal.labels().close"
|
|
1486
|
+
[attr.aria-label]="portal.labels().close + ' ' + blade().title">
|
|
1061
1487
|
<svg viewBox="0 0 11 11" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
|
1062
1488
|
<g>
|
|
1063
1489
|
<polygon class="msportal-fx-svg-placeholder" points="10.4,1.4 9.6,0.6 5.5,4.7 1.4,0.6 0.6,1.4 4.7,5.5 0.6,9.6 1.4,10.4 5.5,6.3 9.6,10.4 10.4,9.6 6.3,5.5"/>
|
|
@@ -1101,8 +1527,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1101
1527
|
* Each blade in the stack is rendered horizontally. When a new blade
|
|
1102
1528
|
* is added, the portal layout scrolls to show it.
|
|
1103
1529
|
*
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1530
|
+
* If a component is registered via BladeRegistry for a blade path,
|
|
1531
|
+
* it is rendered dynamically via ngComponentOutlet. Otherwise,
|
|
1532
|
+
* the blade path is shown as fallback text.
|
|
1106
1533
|
*
|
|
1107
1534
|
* Usage:
|
|
1108
1535
|
* ```html
|
|
@@ -1111,6 +1538,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1111
1538
|
*/
|
|
1112
1539
|
class BladeHostComponent {
|
|
1113
1540
|
portal = inject(PortalService);
|
|
1541
|
+
registry = inject(BladeRegistry);
|
|
1542
|
+
getComponent(path) {
|
|
1543
|
+
return this.registry.get(path) ?? null;
|
|
1544
|
+
}
|
|
1114
1545
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1115
1546
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BladeHostComponent, isStandalone: true, selector: "apa-blade-host", ngImport: i0, template: `
|
|
1116
1547
|
<div id="apa-blade-area" class="fxs-journey-target fxs-journey">
|
|
@@ -1118,25 +1549,31 @@ class BladeHostComponent {
|
|
|
1118
1549
|
@for (blade of portal.blades(); track blade.path) {
|
|
1119
1550
|
<div class="azureportalblade fxs-stacklayout-child">
|
|
1120
1551
|
<apa-blade [blade]="blade">
|
|
1121
|
-
|
|
1122
|
-
|
|
1552
|
+
@if (getComponent(blade.path); as component) {
|
|
1553
|
+
<ng-container *ngComponentOutlet="component" />
|
|
1554
|
+
} @else {
|
|
1555
|
+
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
1556
|
+
}
|
|
1123
1557
|
</apa-blade>
|
|
1124
1558
|
</div>
|
|
1125
1559
|
}
|
|
1126
1560
|
</div>
|
|
1127
1561
|
</div>
|
|
1128
|
-
`, isInline: true, styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "component", type: BladeComponent, selector: "apa-blade", inputs: ["blade"], outputs: ["bladeClose"] }] });
|
|
1562
|
+
`, isInline: true, styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "component", type: BladeComponent, selector: "apa-blade", inputs: ["blade"], outputs: ["bladeClose"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }] });
|
|
1129
1563
|
}
|
|
1130
1564
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeHostComponent, decorators: [{
|
|
1131
1565
|
type: Component,
|
|
1132
|
-
args: [{ selector: 'apa-blade-host', standalone: true, imports: [BladeComponent], template: `
|
|
1566
|
+
args: [{ selector: 'apa-blade-host', standalone: true, imports: [BladeComponent, NgComponentOutlet], template: `
|
|
1133
1567
|
<div id="apa-blade-area" class="fxs-journey-target fxs-journey">
|
|
1134
1568
|
<div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
|
|
1135
1569
|
@for (blade of portal.blades(); track blade.path) {
|
|
1136
1570
|
<div class="azureportalblade fxs-stacklayout-child">
|
|
1137
1571
|
<apa-blade [blade]="blade">
|
|
1138
|
-
|
|
1139
|
-
|
|
1572
|
+
@if (getComponent(blade.path); as component) {
|
|
1573
|
+
<ng-container *ngComponentOutlet="component" />
|
|
1574
|
+
} @else {
|
|
1575
|
+
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
1576
|
+
}
|
|
1140
1577
|
</apa-blade>
|
|
1141
1578
|
</div>
|
|
1142
1579
|
}
|
|
@@ -1178,22 +1615,25 @@ class BladeNavComponent {
|
|
|
1178
1615
|
}
|
|
1179
1616
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1180
1617
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BladeNavComponent, isStandalone: true, selector: "apa-blade-nav", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, senderPath: { classPropertyName: "senderPath", publicName: "senderPath", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
1181
|
-
<table class="azc-grid-full azc-grid-multiselectable">
|
|
1618
|
+
<table class="azc-grid-full azc-grid-multiselectable" role="grid" aria-label="Navigation">
|
|
1182
1619
|
<colgroup>
|
|
1183
1620
|
<col class="col0" style="width:28px;">
|
|
1184
1621
|
<col class="col1">
|
|
1185
1622
|
</colgroup>
|
|
1186
1623
|
<tbody class="azc-grid-groupdata" role="rowgroup">
|
|
1187
1624
|
@for (item of visibleItems(); track item.bladePath) {
|
|
1188
|
-
<tr role="row" style="cursor:pointer"
|
|
1189
|
-
|
|
1625
|
+
<tr role="row" style="cursor:pointer"
|
|
1626
|
+
[attr.aria-label]="item.title"
|
|
1627
|
+
(click)="onItemClick(item)"
|
|
1628
|
+
(keydown.enter)="onItemClick(item)">
|
|
1629
|
+
<td class="msportalfx-gridcolumn-asseticon" role="gridcell" aria-hidden="true">
|
|
1190
1630
|
@if (item.cssClass) {
|
|
1191
1631
|
<i [class]="item.cssClass"></i>
|
|
1192
1632
|
}
|
|
1193
1633
|
</td>
|
|
1194
1634
|
<td tabindex="0" role="gridcell">
|
|
1195
1635
|
@if (item.hrefPath) {
|
|
1196
|
-
<a [href]="item.hrefPath" target="_blank">{{ item.title }}</a>
|
|
1636
|
+
<a [href]="item.hrefPath" target="_blank" rel="noopener noreferrer">{{ item.title }}</a>
|
|
1197
1637
|
} @else {
|
|
1198
1638
|
<span>{{ item.title }}</span>
|
|
1199
1639
|
}
|
|
@@ -1207,22 +1647,25 @@ class BladeNavComponent {
|
|
|
1207
1647
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeNavComponent, decorators: [{
|
|
1208
1648
|
type: Component,
|
|
1209
1649
|
args: [{ selector: 'apa-blade-nav', standalone: true, template: `
|
|
1210
|
-
<table class="azc-grid-full azc-grid-multiselectable">
|
|
1650
|
+
<table class="azc-grid-full azc-grid-multiselectable" role="grid" aria-label="Navigation">
|
|
1211
1651
|
<colgroup>
|
|
1212
1652
|
<col class="col0" style="width:28px;">
|
|
1213
1653
|
<col class="col1">
|
|
1214
1654
|
</colgroup>
|
|
1215
1655
|
<tbody class="azc-grid-groupdata" role="rowgroup">
|
|
1216
1656
|
@for (item of visibleItems(); track item.bladePath) {
|
|
1217
|
-
<tr role="row" style="cursor:pointer"
|
|
1218
|
-
|
|
1657
|
+
<tr role="row" style="cursor:pointer"
|
|
1658
|
+
[attr.aria-label]="item.title"
|
|
1659
|
+
(click)="onItemClick(item)"
|
|
1660
|
+
(keydown.enter)="onItemClick(item)">
|
|
1661
|
+
<td class="msportalfx-gridcolumn-asseticon" role="gridcell" aria-hidden="true">
|
|
1219
1662
|
@if (item.cssClass) {
|
|
1220
1663
|
<i [class]="item.cssClass"></i>
|
|
1221
1664
|
}
|
|
1222
1665
|
</td>
|
|
1223
1666
|
<td tabindex="0" role="gridcell">
|
|
1224
1667
|
@if (item.hrefPath) {
|
|
1225
|
-
<a [href]="item.hrefPath" target="_blank">{{ item.title }}</a>
|
|
1668
|
+
<a [href]="item.hrefPath" target="_blank" rel="noopener noreferrer">{{ item.title }}</a>
|
|
1226
1669
|
} @else {
|
|
1227
1670
|
<span>{{ item.title }}</span>
|
|
1228
1671
|
}
|
|
@@ -1262,6 +1705,7 @@ class BladeGridComponent {
|
|
|
1262
1705
|
itemClick = output();
|
|
1263
1706
|
searchText = '';
|
|
1264
1707
|
bladeService = inject(BladeService);
|
|
1708
|
+
portal = inject(PortalService);
|
|
1265
1709
|
filteredItems() {
|
|
1266
1710
|
return filterItems(this.items(), this.searchText);
|
|
1267
1711
|
}
|
|
@@ -1282,22 +1726,26 @@ class BladeGridComponent {
|
|
|
1282
1726
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BladeGridComponent, isStandalone: true, selector: "apa-blade-grid", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, senderPath: { classPropertyName: "senderPath", publicName: "senderPath", isSignal: true, isRequired: false, transformFunction: null }, displayField: { classPropertyName: "displayField", publicName: "displayField", isSignal: true, isRequired: false, transformFunction: null }, bladePathField: { classPropertyName: "bladePathField", publicName: "bladePathField", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, ngImport: i0, template: `
|
|
1283
1727
|
@if (searchable()) {
|
|
1284
1728
|
<div style="padding:0 0 10px 0;">
|
|
1285
|
-
<input type="
|
|
1729
|
+
<input type="search"
|
|
1286
1730
|
class="form-control"
|
|
1287
|
-
placeholder="
|
|
1731
|
+
[placeholder]="portal.labels().search"
|
|
1732
|
+
[attr.aria-label]="portal.labels().search"
|
|
1288
1733
|
[value]="searchText"
|
|
1289
1734
|
(input)="onSearchInput($event)" />
|
|
1290
1735
|
</div>
|
|
1291
1736
|
}
|
|
1292
|
-
<table class="azc-grid-full azc-grid-multiselectable">
|
|
1737
|
+
<table class="azc-grid-full azc-grid-multiselectable" role="grid" aria-label="Items">
|
|
1293
1738
|
<colgroup>
|
|
1294
1739
|
<col class="col0" style="width:41px;">
|
|
1295
1740
|
<col class="col1">
|
|
1296
1741
|
</colgroup>
|
|
1297
1742
|
<tbody class="azc-grid-groupdata" role="rowgroup">
|
|
1298
1743
|
@for (item of filteredItems(); track $index) {
|
|
1299
|
-
<tr role="row" style="cursor:pointer"
|
|
1300
|
-
|
|
1744
|
+
<tr role="row" style="cursor:pointer"
|
|
1745
|
+
[attr.aria-label]="getDisplayValue(item)"
|
|
1746
|
+
(click)="onRowClick(item)"
|
|
1747
|
+
(keydown.enter)="onRowClick(item)">
|
|
1748
|
+
<td class="msportalfx-gridcolumn-asseticon" role="gridcell" aria-hidden="true">
|
|
1301
1749
|
<svg xmlns="http://www.w3.org/2000/svg" class="msportal-fx-svg-placeholder" viewBox="0 0 50 50" focusable="false" style="height:21px;width:21px;">
|
|
1302
1750
|
<rect class="msportalfx-svg-c04" x="19.8" y="39.4" width="10.6" height="3.4"/>
|
|
1303
1751
|
<polygon class="msportalfx-svg-c04" points="23.1,50 27,50 30.3,46.5 19.8,46.5"/>
|
|
@@ -1318,22 +1766,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1318
1766
|
args: [{ selector: 'apa-blade-grid', standalone: true, template: `
|
|
1319
1767
|
@if (searchable()) {
|
|
1320
1768
|
<div style="padding:0 0 10px 0;">
|
|
1321
|
-
<input type="
|
|
1769
|
+
<input type="search"
|
|
1322
1770
|
class="form-control"
|
|
1323
|
-
placeholder="
|
|
1771
|
+
[placeholder]="portal.labels().search"
|
|
1772
|
+
[attr.aria-label]="portal.labels().search"
|
|
1324
1773
|
[value]="searchText"
|
|
1325
1774
|
(input)="onSearchInput($event)" />
|
|
1326
1775
|
</div>
|
|
1327
1776
|
}
|
|
1328
|
-
<table class="azc-grid-full azc-grid-multiselectable">
|
|
1777
|
+
<table class="azc-grid-full azc-grid-multiselectable" role="grid" aria-label="Items">
|
|
1329
1778
|
<colgroup>
|
|
1330
1779
|
<col class="col0" style="width:41px;">
|
|
1331
1780
|
<col class="col1">
|
|
1332
1781
|
</colgroup>
|
|
1333
1782
|
<tbody class="azc-grid-groupdata" role="rowgroup">
|
|
1334
1783
|
@for (item of filteredItems(); track $index) {
|
|
1335
|
-
<tr role="row" style="cursor:pointer"
|
|
1336
|
-
|
|
1784
|
+
<tr role="row" style="cursor:pointer"
|
|
1785
|
+
[attr.aria-label]="getDisplayValue(item)"
|
|
1786
|
+
(click)="onRowClick(item)"
|
|
1787
|
+
(keydown.enter)="onRowClick(item)">
|
|
1788
|
+
<td class="msportalfx-gridcolumn-asseticon" role="gridcell" aria-hidden="true">
|
|
1337
1789
|
<svg xmlns="http://www.w3.org/2000/svg" class="msportal-fx-svg-placeholder" viewBox="0 0 50 50" focusable="false" style="height:21px;width:21px;">
|
|
1338
1790
|
<rect class="msportalfx-svg-c04" x="19.8" y="39.4" width="10.6" height="3.4"/>
|
|
1339
1791
|
<polygon class="msportalfx-svg-c04" points="23.1,50 27,50 30.3,46.5 19.8,46.5"/>
|
|
@@ -1393,19 +1845,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1393
1845
|
* Convenience function for setting up typical detail/edit blade commands
|
|
1394
1846
|
* matching the v0.2.346 BladeDetail default commands.
|
|
1395
1847
|
*/
|
|
1396
|
-
function createDetailCommands(handlers) {
|
|
1848
|
+
function createDetailCommands(handlers, labels = DEFAULT_LABELS) {
|
|
1397
1849
|
const commands = [];
|
|
1398
1850
|
if (handlers.onNew) {
|
|
1399
|
-
commands.push(createCommand('new',
|
|
1851
|
+
commands.push(createCommand('new', labels.cmdNew, handlers.onNew, 'ti ti-plus'));
|
|
1400
1852
|
}
|
|
1401
1853
|
if (handlers.onSave) {
|
|
1402
|
-
commands.push(createCommand('save',
|
|
1854
|
+
commands.push(createCommand('save', labels.cmdSave, handlers.onSave, 'ti ti-device-floppy'));
|
|
1403
1855
|
}
|
|
1404
1856
|
if (handlers.onDelete) {
|
|
1405
|
-
commands.push(createCommand('delete',
|
|
1857
|
+
commands.push(createCommand('delete', labels.cmdDelete, handlers.onDelete, 'ti ti-trash'));
|
|
1406
1858
|
}
|
|
1407
1859
|
if (handlers.onCancel) {
|
|
1408
|
-
commands.push(createCommand('cancel',
|
|
1860
|
+
commands.push(createCommand('cancel', labels.cmdCancel, handlers.onCancel, 'ti ti-x'));
|
|
1409
1861
|
}
|
|
1410
1862
|
return commands;
|
|
1411
1863
|
}
|
|
@@ -1430,8 +1882,12 @@ class NotificationPanelComponent {
|
|
|
1430
1882
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: NotificationPanelComponent, isStandalone: true, selector: "apa-notification-panel", ngImport: i0, template: `
|
|
1431
1883
|
@if (portal.notification().isVisible) {
|
|
1432
1884
|
<div class="apa-notification-panel"
|
|
1885
|
+
role="complementary"
|
|
1886
|
+
aria-live="polite"
|
|
1433
1887
|
[style.width.px]="portal.notification().width">
|
|
1434
|
-
<button class="apa-notification-close" (click)="onClose()"
|
|
1888
|
+
<button class="apa-notification-close" (click)="onClose()"
|
|
1889
|
+
[title]="portal.labels().closePanel"
|
|
1890
|
+
[attr.aria-label]="portal.labels().closePanel">
|
|
1435
1891
|
<svg viewBox="0 0 11 11" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
|
1436
1892
|
<polygon class="msportal-fx-svg-placeholder" points="10.4,1.4 9.6,0.6 5.5,4.7 1.4,0.6 0.6,1.4 4.7,5.5 0.6,9.6 1.4,10.4 5.5,6.3 9.6,10.4 10.4,9.6 6.3,5.5"/>
|
|
1437
1893
|
</svg>
|
|
@@ -1446,8 +1902,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1446
1902
|
args: [{ selector: 'apa-notification-panel', standalone: true, template: `
|
|
1447
1903
|
@if (portal.notification().isVisible) {
|
|
1448
1904
|
<div class="apa-notification-panel"
|
|
1905
|
+
role="complementary"
|
|
1906
|
+
aria-live="polite"
|
|
1449
1907
|
[style.width.px]="portal.notification().width">
|
|
1450
|
-
<button class="apa-notification-close" (click)="onClose()"
|
|
1908
|
+
<button class="apa-notification-close" (click)="onClose()"
|
|
1909
|
+
[title]="portal.labels().closePanel"
|
|
1910
|
+
[attr.aria-label]="portal.labels().closePanel">
|
|
1451
1911
|
<svg viewBox="0 0 11 11" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
|
1452
1912
|
<polygon class="msportal-fx-svg-placeholder" points="10.4,1.4 9.6,0.6 5.5,4.7 1.4,0.6 0.6,1.4 4.7,5.5 0.6,9.6 1.4,10.4 5.5,6.3 9.6,10.4 10.4,9.6 6.3,5.5"/>
|
|
1453
1913
|
</svg>
|
|
@@ -1534,11 +1994,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1534
1994
|
* Usage:
|
|
1535
1995
|
* ```html
|
|
1536
1996
|
* <apa-sidebar [items]="sidebarItems" [collapsed]="false" />
|
|
1997
|
+
* <apa-sidebar [items]="sidebarItems" [width]="240" [collapsedWidth]="60" />
|
|
1537
1998
|
* ```
|
|
1538
1999
|
*/
|
|
1539
2000
|
class SidebarComponent {
|
|
1540
2001
|
items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1541
2002
|
collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
|
|
2003
|
+
width = input(200, ...(ngDevMode ? [{ debugName: "width" }] : []));
|
|
2004
|
+
collapsedWidth = input(50, ...(ngDevMode ? [{ debugName: "collapsedWidth" }] : []));
|
|
1542
2005
|
bladeService = inject(BladeService);
|
|
1543
2006
|
visibleItems() {
|
|
1544
2007
|
return this.items().filter((item) => item.isVisible);
|
|
@@ -1550,42 +2013,62 @@ class SidebarComponent {
|
|
|
1550
2013
|
item.callback?.();
|
|
1551
2014
|
}
|
|
1552
2015
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1553
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: SidebarComponent, isStandalone: true, selector: "apa-sidebar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
1554
|
-
<nav class="apa-sidebar"
|
|
2016
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: SidebarComponent, isStandalone: true, selector: "apa-sidebar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
2017
|
+
<nav class="apa-sidebar" aria-label="Sidebar"
|
|
2018
|
+
[class.apa-sidebar-collapsed]="collapsed()"
|
|
2019
|
+
[style.width.px]="collapsed() ? collapsedWidth() : width()">
|
|
1555
2020
|
@for (item of visibleItems(); track item.bladePath) {
|
|
1556
2021
|
<a class="apa-sidebar-item"
|
|
2022
|
+
role="button"
|
|
2023
|
+
tabindex="0"
|
|
2024
|
+
[attr.aria-label]="item.title"
|
|
1557
2025
|
(click)="onItemClick(item)"
|
|
2026
|
+
(keydown.enter)="onItemClick(item)"
|
|
2027
|
+
(keydown.space)="onItemClick(item)"
|
|
1558
2028
|
style="cursor:pointer;">
|
|
1559
2029
|
@if (item.cssClass) {
|
|
1560
|
-
<i [class]="item.cssClass" class="apa-sidebar-icon"></i>
|
|
2030
|
+
<i [class]="item.cssClass" class="apa-sidebar-icon" aria-hidden="true"></i>
|
|
1561
2031
|
}
|
|
1562
2032
|
@if (!collapsed()) {
|
|
1563
2033
|
<span class="apa-sidebar-label">{{ item.title }}</span>
|
|
2034
|
+
@if (item.badge) {
|
|
2035
|
+
<span class="apa-sidebar-badge" aria-label="badge">{{ item.badge }}</span>
|
|
2036
|
+
}
|
|
1564
2037
|
}
|
|
1565
2038
|
</a>
|
|
1566
2039
|
}
|
|
1567
2040
|
</nav>
|
|
1568
|
-
`, isInline: true, styles: [".apa-sidebar{display:flex;flex-direction:column;background-color:var(--apa-chrome);
|
|
2041
|
+
`, isInline: true, styles: [".apa-sidebar{display:flex;flex-direction:column;background-color:var(--apa-chrome);height:100%;padding-top:10px;transition:width .2s ease}.apa-sidebar-item{display:flex;align-items:center;padding:10px 15px;color:var(--apa-chrome-text);text-decoration:none;font-size:13px;transition:background-color .15s ease}.apa-sidebar-item:hover{background-color:var(--apa-chrome-hover)}.apa-sidebar-icon{margin-right:10px;width:20px;text-align:center}.apa-sidebar-collapsed .apa-sidebar-icon{margin-right:0}.apa-sidebar-label{flex:1}.apa-sidebar-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;background-color:var(--apa-accent);color:#fff;font-size:11px;font-weight:600;line-height:1}\n"] });
|
|
1569
2042
|
}
|
|
1570
2043
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SidebarComponent, decorators: [{
|
|
1571
2044
|
type: Component,
|
|
1572
2045
|
args: [{ selector: 'apa-sidebar', standalone: true, template: `
|
|
1573
|
-
<nav class="apa-sidebar"
|
|
2046
|
+
<nav class="apa-sidebar" aria-label="Sidebar"
|
|
2047
|
+
[class.apa-sidebar-collapsed]="collapsed()"
|
|
2048
|
+
[style.width.px]="collapsed() ? collapsedWidth() : width()">
|
|
1574
2049
|
@for (item of visibleItems(); track item.bladePath) {
|
|
1575
2050
|
<a class="apa-sidebar-item"
|
|
2051
|
+
role="button"
|
|
2052
|
+
tabindex="0"
|
|
2053
|
+
[attr.aria-label]="item.title"
|
|
1576
2054
|
(click)="onItemClick(item)"
|
|
2055
|
+
(keydown.enter)="onItemClick(item)"
|
|
2056
|
+
(keydown.space)="onItemClick(item)"
|
|
1577
2057
|
style="cursor:pointer;">
|
|
1578
2058
|
@if (item.cssClass) {
|
|
1579
|
-
<i [class]="item.cssClass" class="apa-sidebar-icon"></i>
|
|
2059
|
+
<i [class]="item.cssClass" class="apa-sidebar-icon" aria-hidden="true"></i>
|
|
1580
2060
|
}
|
|
1581
2061
|
@if (!collapsed()) {
|
|
1582
2062
|
<span class="apa-sidebar-label">{{ item.title }}</span>
|
|
2063
|
+
@if (item.badge) {
|
|
2064
|
+
<span class="apa-sidebar-badge" aria-label="badge">{{ item.badge }}</span>
|
|
2065
|
+
}
|
|
1583
2066
|
}
|
|
1584
2067
|
</a>
|
|
1585
2068
|
}
|
|
1586
2069
|
</nav>
|
|
1587
|
-
`, styles: [".apa-sidebar{display:flex;flex-direction:column;background-color:var(--apa-chrome);
|
|
1588
|
-
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }] } });
|
|
2070
|
+
`, styles: [".apa-sidebar{display:flex;flex-direction:column;background-color:var(--apa-chrome);height:100%;padding-top:10px;transition:width .2s ease}.apa-sidebar-item{display:flex;align-items:center;padding:10px 15px;color:var(--apa-chrome-text);text-decoration:none;font-size:13px;transition:background-color .15s ease}.apa-sidebar-item:hover{background-color:var(--apa-chrome-hover)}.apa-sidebar-icon{margin-right:10px;width:20px;text-align:center}.apa-sidebar-collapsed .apa-sidebar-icon{margin-right:0}.apa-sidebar-label{flex:1}.apa-sidebar-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;background-color:var(--apa-accent);color:#fff;font-size:11px;font-weight:600;line-height:1}\n"] }]
|
|
2071
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], collapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedWidth", required: false }] }] } });
|
|
1589
2072
|
|
|
1590
2073
|
/*
|
|
1591
2074
|
* Public API Surface of @ardimedia/angular-portal-azure
|
|
@@ -1596,5 +2079,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1596
2079
|
* Generated bundle index. Do not edit.
|
|
1597
2080
|
*/
|
|
1598
2081
|
|
|
1599
|
-
export { AvatarMenuComponent, BladeComponent, BladeDetailComponent, BladeGridComponent, BladeHostComponent, BladeNavComponent, BladeService, CommandBarComponent, NotificationPanelComponent, PanoramaComponent, PortalLayoutComponent, PortalService, SidebarComponent, TILE_DIMENSIONS, TileComponent, TileSize, clearStatusBar, createAvatarMenu, createBlade, createCommand, createDataBlade, createDetailCommands, createNavItem, createNotificationPanel, createPanorama, createTile, executeDeleteItem, executeLoadItem, executeLoadItems, executeSaveItem, filterItems, getUserDisplayName, layoutTiles, providePortalAzure, statusBarError, statusBarInfo, statusBarSuccess };
|
|
2082
|
+
export { AvatarMenuComponent, BladeComponent, BladeDetailComponent, BladeGridComponent, BladeHostComponent, BladeNavComponent, BladeRegistry, BladeService, CommandBarComponent, DEFAULT_LABELS, LABELS_DE_CH, LABELS_DE_DE, LABELS_EN, LABELS_ES, LABELS_FR, LABELS_IT, LANGUAGE_PRESETS, NotificationPanelComponent, PanoramaComponent, PortalLayoutComponent, PortalService, SidebarComponent, TILE_DIMENSIONS, TileComponent, TileSize, clearStatusBar, createAvatarMenu, createBlade, createCommand, createDataBlade, createDetailCommands, createNavItem, createNotificationPanel, createPanorama, createTile, executeDeleteItem, executeLoadItem, executeLoadItems, executeSaveItem, filterItems, getUserDisplayName, layoutTiles, providePortalAzure, registerLanguagePreset, statusBarError, statusBarInfo, statusBarSuccess };
|
|
1600
2083
|
//# sourceMappingURL=ardimedia-angular-portal-azure.mjs.map
|