@skyux/theme 12.32.1 → 12.34.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.
@@ -1,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Optional, Input, Directive, NgModule, inject, ChangeDetectorRef, HostBinding, NgZone, DestroyRef, makeEnvironmentProviders, provideAppInitializer, RendererFactory2 } from '@angular/core';
2
+ import { Injectable, inject, Optional, Input, Directive, NgModule, ChangeDetectorRef, HostBinding, NgZone, DestroyRef, makeEnvironmentProviders, provideAppInitializer, RendererFactory2 } from '@angular/core';
3
3
  import { getIconManifest } from '@skyux/icons';
4
4
  import FontFaceObserver from 'fontfaceobserver';
5
5
  import { take, takeUntil } from 'rxjs/operators';
6
6
  import { ReplaySubject, Subject } from 'rxjs';
7
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
7
  import { DOCUMENT } from '@angular/common';
8
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
9
 
10
10
  /**
11
11
  * Provides a method for retrieving metadata about the SKY UX icon font.
@@ -28,6 +28,165 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
28
28
  }]
29
29
  }] });
30
30
 
31
+ const BRAND_BLACKBAUD = 'blackbaud';
32
+ // Commonly-used icon configurations. Appending these to the head element will
33
+ // override any existing link elements with the same attribute values. When
34
+ // they are removed, the previous favicon link elements will take effect again.
35
+ const FAVICON_CONFIGS = [
36
+ { rel: 'apple-touch-icon', sizes: '180x180' },
37
+ { rel: 'icon', sizes: '32x32' },
38
+ { rel: 'icon', sizes: '16x16' },
39
+ ];
40
+ /**
41
+ * @internal
42
+ * Provides methods for managing theme branding including brand registration,
43
+ * stylesheet management, and host class updates.
44
+ */
45
+ class SkyThemeBrandService {
46
+ #document = inject(DOCUMENT);
47
+ #stylesheetEl;
48
+ #faviconEls = [];
49
+ #maskIconEl;
50
+ #registeredBrands = new Map();
51
+ /**
52
+ * Registers a brand for use in themes.
53
+ * @param brand The brand to register.
54
+ */
55
+ registerBrand(brand) {
56
+ this.#registeredBrands.set(brand.name, brand);
57
+ }
58
+ /**
59
+ * Unregisters a brand.
60
+ * @param name The name of the brand to unregister.
61
+ */
62
+ unregisterBrand(name) {
63
+ this.#registeredBrands.delete(name);
64
+ }
65
+ /**
66
+ * Updates all brand-related styling and classes for the host element.
67
+ * This consolidates brand stylesheet updates and host class management,
68
+ * and automatically resolves registered brands.
69
+ * @param hostEl The host element to update.
70
+ * @param renderer The renderer to use for DOM manipulation.
71
+ * @param brand The brand to apply (will be resolved if registered version exists).
72
+ * @param previousBrand The previous brand to clean up.
73
+ */
74
+ updateBrand(hostEl, renderer, brand, previousBrand) {
75
+ // Resolve to registered brand if available
76
+ if (brand) {
77
+ brand = this.#registeredBrands.get(brand.name) ?? brand;
78
+ }
79
+ const previousClass = previousBrand?.hostClass;
80
+ const currentClass = brand?.hostClass;
81
+ // Update host classes if they've changed
82
+ if (!previousClass || previousClass !== currentClass) {
83
+ this.#updateBrandHostClass(hostEl, renderer, previousClass, currentClass);
84
+ this.#updateBrandStylesheet(renderer, brand, previousBrand);
85
+ }
86
+ this.#updateFavicon(renderer, brand);
87
+ }
88
+ /**
89
+ * Destroys the brand service, cleaning up any brand stylesheets.
90
+ */
91
+ destroy() {
92
+ this.#removeBrandStylesheet();
93
+ }
94
+ #updateBrandStylesheet(renderer, currentBrand, previousBrand) {
95
+ if (previousBrand && previousBrand.name !== BRAND_BLACKBAUD) {
96
+ this.#removeBrandStylesheet();
97
+ }
98
+ if (currentBrand && currentBrand.name !== BRAND_BLACKBAUD) {
99
+ this.#addBrandStylesheet(renderer, currentBrand);
100
+ }
101
+ }
102
+ #updateBrandHostClass(hostEl, renderer, previousClass, currentClass) {
103
+ if (previousClass) {
104
+ renderer.removeClass(hostEl, previousClass);
105
+ }
106
+ if (currentClass && !previousClass) {
107
+ renderer.addClass(hostEl, 'sky-theme-brand-base');
108
+ }
109
+ else if (!currentClass && previousClass) {
110
+ renderer.removeClass(hostEl, 'sky-theme-brand-base');
111
+ }
112
+ if (currentClass) {
113
+ renderer.addClass(hostEl, currentClass);
114
+ }
115
+ }
116
+ #addBrandStylesheet(renderer, brand) {
117
+ if (brand.name !== BRAND_BLACKBAUD) {
118
+ // Use styleUrl if provided, otherwise build the default URL
119
+ const styleUrl = brand.styleUrl ||
120
+ `https://sky.blackbaudcdn.net/static/skyux-brand-${brand.name}/${brand.version}/assets/scss/${brand.name}.css`;
121
+ this.#stylesheetEl = renderer.createElement('link');
122
+ renderer.setAttribute(this.#stylesheetEl, 'rel', 'stylesheet');
123
+ renderer.setAttribute(this.#stylesheetEl, 'href', styleUrl);
124
+ if (brand.sriHash) {
125
+ renderer.setAttribute(this.#stylesheetEl, 'integrity', brand.sriHash);
126
+ renderer.setAttribute(this.#stylesheetEl, 'crossorigin', 'anonymous');
127
+ }
128
+ this.#appendToHead(renderer, this.#stylesheetEl);
129
+ }
130
+ }
131
+ #removeBrandStylesheet() {
132
+ if (this.#stylesheetEl) {
133
+ this.#stylesheetEl.remove();
134
+ this.#stylesheetEl = undefined;
135
+ }
136
+ }
137
+ #updateFavicon(renderer, brand) {
138
+ if (brand?.faviconUrl) {
139
+ const faviconUrl = brand.faviconUrl;
140
+ // Create favicon elements if they don't exist, or reuse existing ones
141
+ while (this.#faviconEls.length < FAVICON_CONFIGS.length) {
142
+ const faviconEl = renderer.createElement('link');
143
+ this.#faviconEls.push(faviconEl);
144
+ this.#appendToHead(renderer, faviconEl);
145
+ }
146
+ // Update each favicon element with its configuration
147
+ for (let i = 0; i < FAVICON_CONFIGS.length; i++) {
148
+ const config = FAVICON_CONFIGS[i];
149
+ const faviconEl = this.#faviconEls[i];
150
+ renderer.setAttribute(faviconEl, 'rel', config.rel);
151
+ renderer.setAttribute(faviconEl, 'sizes', config.sizes);
152
+ renderer.setAttribute(faviconEl, 'href', faviconUrl);
153
+ }
154
+ }
155
+ else {
156
+ // Remove all favicon elements when no favicon URL is provided
157
+ for (const faviconEl of this.#faviconEls) {
158
+ faviconEl.remove();
159
+ }
160
+ this.#faviconEls = [];
161
+ }
162
+ // Handle mask icon
163
+ if (brand?.maskIcon) {
164
+ if (!this.#maskIconEl) {
165
+ this.#maskIconEl = renderer.createElement('link');
166
+ this.#appendToHead(renderer, this.#maskIconEl);
167
+ }
168
+ renderer.setAttribute(this.#maskIconEl, 'rel', 'mask-icon');
169
+ renderer.setAttribute(this.#maskIconEl, 'href', brand.maskIcon.url);
170
+ renderer.setAttribute(this.#maskIconEl, 'color', brand.maskIcon.color);
171
+ }
172
+ else if (this.#maskIconEl) {
173
+ this.#maskIconEl.remove();
174
+ this.#maskIconEl = undefined;
175
+ }
176
+ }
177
+ #appendToHead(renderer, el) {
178
+ renderer.appendChild(this.#document.head, el);
179
+ }
180
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyThemeBrandService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
181
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyThemeBrandService, providedIn: 'root' }); }
182
+ }
183
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyThemeBrandService, decorators: [{
184
+ type: Injectable,
185
+ args: [{
186
+ providedIn: 'root',
187
+ }]
188
+ }] });
189
+
31
190
  /**
32
191
  * Defines properties of a SKY UX theme mode.
33
192
  */
@@ -243,12 +402,18 @@ class SkyThemeBrand {
243
402
  * adjusting for a specified theme brand. This defaults to `sky-theme-brand-<name>`
244
403
  * @param styleUrl The URL of the stylesheet to load for this brand
245
404
  * @param sriHash The subresource integrity hash for the stylesheet
405
+ * @param title The title to display for this brand
406
+ * @param faviconUrl The URL of the favicon to use for this brand
407
+ * @param maskIcon The mask icon configuration for this brand
246
408
  */
247
- constructor(name, version, hostClass, styleUrl, sriHash) {
409
+ constructor(name, version, hostClass, styleUrl, sriHash, title, faviconUrl, maskIcon) {
248
410
  this.name = name;
249
411
  this.version = version;
250
412
  this.styleUrl = styleUrl;
251
413
  this.sriHash = sriHash;
414
+ this.title = title;
415
+ this.faviconUrl = faviconUrl;
416
+ this.maskIcon = maskIcon;
252
417
  this.hostClass = hostClass;
253
418
  if (!/^(\d+|\d+\.\d+\.\d+)(?:-(?:alpha|beta|rc)\.\d+)*$/.test(version)) {
254
419
  throw new Error(`Invalid version format "${version}" for theme brand "${name}".`);
@@ -268,13 +433,21 @@ class SkyThemeBrand {
268
433
  if (this.hostClass !== defaultHostClass) {
269
434
  result.hostClass = this.hostClass;
270
435
  }
271
- // Include styleUrl and sriHash if they are provided
272
436
  if (this.styleUrl) {
273
437
  result.styleUrl = this.styleUrl;
274
438
  }
275
439
  if (this.sriHash) {
276
440
  result.sriHash = this.sriHash;
277
441
  }
442
+ if (this.title) {
443
+ result.title = this.title;
444
+ }
445
+ if (this.faviconUrl) {
446
+ result.faviconUrl = this.faviconUrl;
447
+ }
448
+ if (this.maskIcon) {
449
+ result.maskIcon = this.maskIcon;
450
+ }
278
451
  return result;
279
452
  }
280
453
  /**
@@ -282,7 +455,7 @@ class SkyThemeBrand {
282
455
  * Deserializes a JSON object to a SkyThemeBrand instance.
283
456
  */
284
457
  static deserialize(data) {
285
- return new SkyThemeBrand(data.name, data.version, data.hostClass, data.styleUrl, data.sriHash);
458
+ return new SkyThemeBrand(data.name, data.version, data.hostClass, data.styleUrl, data.sriHash, data.title, data.faviconUrl, data.maskIcon);
286
459
  }
287
460
  #buildDefaultHostClassName() {
288
461
  return `sky-theme-brand-${this.name}`;
@@ -339,7 +512,6 @@ class SkyThemeSettings {
339
512
  }
340
513
  }
341
514
 
342
- const BRAND_BLACKBAUD = 'blackbaud';
343
515
  function assertCurrentSettings(currentSettings) {
344
516
  if (!currentSettings) {
345
517
  throw new Error('Theme service is not initialized. Call init() first.');
@@ -349,6 +521,7 @@ function assertCurrentSettings(currentSettings) {
349
521
  * Provides methods for updating and handling changes to the current theme.
350
522
  */
351
523
  class SkyThemeService {
524
+ #brandSvc = inject(SkyThemeBrandService);
352
525
  /**
353
526
  * Notifies consumers when the current theme settings have changed.
354
527
  */
@@ -356,11 +529,9 @@ class SkyThemeService {
356
529
  return this.#_settingsObs;
357
530
  }
358
531
  #current;
359
- #brandLinkElement;
360
532
  #hostEl;
361
533
  #renderer;
362
534
  #settings;
363
- #registeredBrands = new Map();
364
535
  #_settingsObs;
365
536
  constructor() {
366
537
  this.#settings = new ReplaySubject(1);
@@ -380,7 +551,7 @@ class SkyThemeService {
380
551
  this.#renderer = renderer;
381
552
  if (registeredBrands) {
382
553
  for (const brand of registeredBrands) {
383
- this.#registeredBrands.set(brand.name, brand);
554
+ this.#brandSvc.registerBrand(brand);
384
555
  }
385
556
  }
386
557
  this.setTheme(theme);
@@ -391,6 +562,7 @@ class SkyThemeService {
391
562
  */
392
563
  destroy() {
393
564
  this.#settings.complete();
565
+ this.#brandSvc.destroy();
394
566
  this.#hostEl = this.#renderer = undefined;
395
567
  }
396
568
  /**
@@ -417,7 +589,7 @@ class SkyThemeService {
417
589
  setTheme(settingsOrTheme) {
418
590
  let settings;
419
591
  if (settingsOrTheme instanceof SkyThemeSettings) {
420
- settings = this.#applyRegisteredBrand(settingsOrTheme);
592
+ settings = settingsOrTheme;
421
593
  }
422
594
  else {
423
595
  const current = this.#current;
@@ -428,7 +600,13 @@ class SkyThemeService {
428
600
  this.#applyThemeClass(previous, settings, 'theme');
429
601
  this.#applyThemeClass(previous, settings, 'mode', 'supportedModes');
430
602
  this.#applyThemeClass(previous, settings, 'spacing', 'supportedSpacing');
431
- this.#applyThemeClass(previous, settings, 'brand');
603
+ if (this.#hostEl) {
604
+ // Validate branding support
605
+ if (settings.brand && !settings.theme.supportsBranding) {
606
+ throw new Error('Branding is not supported for the given theme.');
607
+ }
608
+ this.#brandSvc.updateBrand(this.#hostEl, this.#getRenderer(), settings.brand, previous?.brand);
609
+ }
432
610
  this.#settings.next({
433
611
  currentSettings: settings,
434
612
  previousSettings: previous,
@@ -436,20 +614,10 @@ class SkyThemeService {
436
614
  this.#current = settings;
437
615
  }
438
616
  registerBrand(brand) {
439
- this.#registeredBrands.set(brand.name, brand);
617
+ this.#brandSvc.registerBrand(brand);
440
618
  }
441
619
  unregisterBrand(name) {
442
- this.#registeredBrands.delete(name);
443
- }
444
- #applyRegisteredBrand(settings) {
445
- const brandName = settings.brand?.name;
446
- if (brandName) {
447
- const registeredBrand = this.#registeredBrands.get(brandName);
448
- if (registeredBrand) {
449
- settings = new SkyThemeSettings(settings.theme, settings.mode, settings.spacing, registeredBrand);
450
- }
451
- }
452
- return settings;
620
+ this.#brandSvc.unregisterBrand(name);
453
621
  }
454
622
  #updateThemeProperty(property, value) {
455
623
  const current = this.#current;
@@ -482,62 +650,18 @@ class SkyThemeService {
482
650
  const previousClass = previous?.[prop]?.hostClass;
483
651
  const currentClass = currentSetting?.hostClass;
484
652
  if (!previousClass || previousClass !== currentClass) {
485
- this.#updateHostClass(prop, previousClass, currentClass, currentSetting, current, supportedProp);
486
- if (prop === 'brand') {
487
- if (!current.theme.supportsBranding && currentSetting) {
488
- throw new Error('Branding is not supported for the given theme.');
489
- }
490
- this.#updateBrandStylesheet(current.brand, previous?.brand);
491
- }
653
+ this.#updateHostClass(previousClass, currentClass, currentSetting, current, supportedProp);
492
654
  }
493
655
  }
494
- #updateBrandStylesheet(currentBrand, previousBrand) {
495
- if (previousBrand && previousBrand.name !== BRAND_BLACKBAUD) {
496
- this.#removeBrandStylesheet();
497
- }
498
- if (currentBrand && currentBrand.name !== BRAND_BLACKBAUD) {
499
- this.#addBrandStylesheet(currentBrand);
500
- }
501
- }
502
- #updateHostClass(prop, previousClass, currentClass, currentSetting, current, supportedProp) {
656
+ #updateHostClass(previousClass, currentClass, currentSetting, current, supportedProp) {
503
657
  if (previousClass) {
504
658
  this.#removeHostClass(previousClass);
505
659
  }
506
- if (prop === 'brand') {
507
- if (currentClass && !previousClass) {
508
- this.#addHostClass('sky-theme-brand-base');
509
- }
510
- else if (!currentClass && previousClass) {
511
- this.#removeHostClass('sky-theme-brand-base');
512
- }
513
- }
514
660
  if (currentClass &&
515
661
  this.#isSupportedProperty(currentSetting, current, supportedProp)) {
516
662
  this.#addHostClass(currentClass);
517
663
  }
518
664
  }
519
- #addBrandStylesheet(brand) {
520
- if (brand.name !== BRAND_BLACKBAUD) {
521
- // Use styleUrl if provided, otherwise build the default URL
522
- const styleUrl = brand.styleUrl ||
523
- `https://sky.blackbaudcdn.net/static/skyux-brand-${brand.name}/${brand.version}/assets/scss/${brand.name}.css`;
524
- const renderer = this.#getRenderer();
525
- this.#brandLinkElement = renderer.createElement('link');
526
- renderer.appendChild(this.#hostEl, this.#brandLinkElement);
527
- renderer.setAttribute(this.#brandLinkElement, 'rel', 'stylesheet');
528
- renderer.setAttribute(this.#brandLinkElement, 'href', styleUrl);
529
- if (brand.sriHash) {
530
- renderer.setAttribute(this.#brandLinkElement, 'integrity', brand.sriHash);
531
- renderer.setAttribute(this.#brandLinkElement, 'crossorigin', 'anonymous');
532
- }
533
- }
534
- }
535
- #removeBrandStylesheet() {
536
- if (this.#brandLinkElement) {
537
- this.#getRenderer().removeChild(this.#hostEl, this.#brandLinkElement);
538
- this.#brandLinkElement = undefined;
539
- }
540
- }
541
665
  #addHostClass(className) {
542
666
  this.#getRenderer().addClass(this.#hostEl, className);
543
667
  }