@skyux/theme 13.0.0-alpha.4 → 13.0.0-alpha.5

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Optional, Input, Directive, NgModule, inject, ChangeDetectorRef, HostBinding, DOCUMENT, NgZone, DestroyRef, makeEnvironmentProviders, provideAppInitializer, RendererFactory2 } from '@angular/core';
2
+ import { Injectable, inject, DOCUMENT, 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';
@@ -242,12 +242,18 @@ class SkyThemeBrand {
242
242
  * adjusting for a specified theme brand. This defaults to `sky-theme-brand-<name>`
243
243
  * @param styleUrl The URL of the stylesheet to load for this brand
244
244
  * @param sriHash The subresource integrity hash for the stylesheet
245
+ * @param title The title to display for this brand
246
+ * @param faviconUrl The URL of the favicon to use for this brand
247
+ * @param maskIcon The mask icon configuration for this brand
245
248
  */
246
- constructor(name, version, hostClass, styleUrl, sriHash) {
249
+ constructor(name, version, hostClass, styleUrl, sriHash, title, faviconUrl, maskIcon) {
247
250
  this.name = name;
248
251
  this.version = version;
249
252
  this.styleUrl = styleUrl;
250
253
  this.sriHash = sriHash;
254
+ this.title = title;
255
+ this.faviconUrl = faviconUrl;
256
+ this.maskIcon = maskIcon;
251
257
  this.hostClass = hostClass;
252
258
  if (!/^(\d+|\d+\.\d+\.\d+)(?:-(?:alpha|beta|rc)\.\d+)*$/.test(version)) {
253
259
  throw new Error(`Invalid version format "${version}" for theme brand "${name}".`);
@@ -267,13 +273,21 @@ class SkyThemeBrand {
267
273
  if (this.hostClass !== defaultHostClass) {
268
274
  result.hostClass = this.hostClass;
269
275
  }
270
- // Include styleUrl and sriHash if they are provided
271
276
  if (this.styleUrl) {
272
277
  result.styleUrl = this.styleUrl;
273
278
  }
274
279
  if (this.sriHash) {
275
280
  result.sriHash = this.sriHash;
276
281
  }
282
+ if (this.title) {
283
+ result.title = this.title;
284
+ }
285
+ if (this.faviconUrl) {
286
+ result.faviconUrl = this.faviconUrl;
287
+ }
288
+ if (this.maskIcon) {
289
+ result.maskIcon = this.maskIcon;
290
+ }
277
291
  return result;
278
292
  }
279
293
  /**
@@ -281,13 +295,172 @@ class SkyThemeBrand {
281
295
  * Deserializes a JSON object to a SkyThemeBrand instance.
282
296
  */
283
297
  static deserialize(data) {
284
- return new SkyThemeBrand(data.name, data.version, data.hostClass, data.styleUrl, data.sriHash);
298
+ return new SkyThemeBrand(data.name, data.version, data.hostClass, data.styleUrl, data.sriHash, data.title, data.faviconUrl, data.maskIcon);
285
299
  }
286
300
  #buildDefaultHostClassName() {
287
301
  return `sky-theme-brand-${this.name}`;
288
302
  }
289
303
  }
290
304
 
305
+ const BRAND_BLACKBAUD = 'blackbaud';
306
+ // Commonly-used icon configurations. Appending these to the head element will
307
+ // override any existing link elements with the same attribute values. When
308
+ // they are removed, the previous favicon link elements will take effect again.
309
+ const FAVICON_CONFIGS = [
310
+ { rel: 'apple-touch-icon', sizes: '180x180' },
311
+ { rel: 'icon', sizes: '32x32' },
312
+ { rel: 'icon', sizes: '16x16' },
313
+ ];
314
+ /**
315
+ * @internal
316
+ * Provides methods for managing theme branding including brand registration,
317
+ * stylesheet management, and host class updates.
318
+ */
319
+ class SkyThemeBrandService {
320
+ #document = inject(DOCUMENT);
321
+ #stylesheetEl;
322
+ #faviconEls = [];
323
+ #maskIconEl;
324
+ #registeredBrands = new Map();
325
+ /**
326
+ * Registers a brand for use in themes.
327
+ * @param brand The brand to register.
328
+ */
329
+ registerBrand(brand) {
330
+ this.#registeredBrands.set(brand.name, brand);
331
+ }
332
+ /**
333
+ * Unregisters a brand.
334
+ * @param name The name of the brand to unregister.
335
+ */
336
+ unregisterBrand(name) {
337
+ this.#registeredBrands.delete(name);
338
+ }
339
+ /**
340
+ * Updates all brand-related styling and classes for the host element.
341
+ * This consolidates brand stylesheet updates and host class management,
342
+ * and automatically resolves registered brands.
343
+ * @param hostEl The host element to update.
344
+ * @param renderer The renderer to use for DOM manipulation.
345
+ * @param brand The brand to apply (will be resolved if registered version exists).
346
+ * @param previousBrand The previous brand to clean up.
347
+ */
348
+ updateBrand(hostEl, renderer, brand, previousBrand) {
349
+ // Resolve to registered brand if available
350
+ if (brand) {
351
+ brand = this.#registeredBrands.get(brand.name) ?? brand;
352
+ }
353
+ const previousClass = previousBrand?.hostClass;
354
+ const currentClass = brand?.hostClass;
355
+ // Update host classes if they've changed
356
+ if (!previousClass || previousClass !== currentClass) {
357
+ this.#updateBrandHostClass(hostEl, renderer, previousClass, currentClass);
358
+ this.#updateBrandStylesheet(renderer, brand, previousBrand);
359
+ }
360
+ this.#updateFavicon(renderer, brand);
361
+ }
362
+ /**
363
+ * Destroys the brand service, cleaning up any brand stylesheets.
364
+ */
365
+ destroy() {
366
+ this.#removeBrandStylesheet();
367
+ }
368
+ #updateBrandStylesheet(renderer, currentBrand, previousBrand) {
369
+ if (previousBrand && previousBrand.name !== BRAND_BLACKBAUD) {
370
+ this.#removeBrandStylesheet();
371
+ }
372
+ if (currentBrand && currentBrand.name !== BRAND_BLACKBAUD) {
373
+ this.#addBrandStylesheet(renderer, currentBrand);
374
+ }
375
+ }
376
+ #updateBrandHostClass(hostEl, renderer, previousClass, currentClass) {
377
+ if (previousClass) {
378
+ renderer.removeClass(hostEl, previousClass);
379
+ }
380
+ if (currentClass && !previousClass) {
381
+ renderer.addClass(hostEl, 'sky-theme-brand-base');
382
+ }
383
+ else if (!currentClass && previousClass) {
384
+ renderer.removeClass(hostEl, 'sky-theme-brand-base');
385
+ }
386
+ if (currentClass) {
387
+ renderer.addClass(hostEl, currentClass);
388
+ }
389
+ }
390
+ #addBrandStylesheet(renderer, brand) {
391
+ if (brand.name !== BRAND_BLACKBAUD) {
392
+ // Use styleUrl if provided, otherwise build the default URL
393
+ const styleUrl = brand.styleUrl ||
394
+ `https://sky.blackbaudcdn.net/static/skyux-brand-${brand.name}/${brand.version}/assets/scss/${brand.name}.css`;
395
+ this.#stylesheetEl = renderer.createElement('link');
396
+ renderer.setAttribute(this.#stylesheetEl, 'rel', 'stylesheet');
397
+ renderer.setAttribute(this.#stylesheetEl, 'href', styleUrl);
398
+ if (brand.sriHash) {
399
+ renderer.setAttribute(this.#stylesheetEl, 'integrity', brand.sriHash);
400
+ renderer.setAttribute(this.#stylesheetEl, 'crossorigin', 'anonymous');
401
+ }
402
+ this.#appendToHead(renderer, this.#stylesheetEl);
403
+ }
404
+ }
405
+ #removeBrandStylesheet() {
406
+ if (this.#stylesheetEl) {
407
+ this.#stylesheetEl.remove();
408
+ this.#stylesheetEl = undefined;
409
+ }
410
+ }
411
+ #updateFavicon(renderer, brand) {
412
+ if (brand?.faviconUrl) {
413
+ const faviconUrl = brand.faviconUrl;
414
+ // Create favicon elements if they don't exist, or reuse existing ones
415
+ while (this.#faviconEls.length < FAVICON_CONFIGS.length) {
416
+ const faviconEl = renderer.createElement('link');
417
+ this.#faviconEls.push(faviconEl);
418
+ this.#appendToHead(renderer, faviconEl);
419
+ }
420
+ // Update each favicon element with its configuration
421
+ for (let i = 0; i < FAVICON_CONFIGS.length; i++) {
422
+ const config = FAVICON_CONFIGS[i];
423
+ const faviconEl = this.#faviconEls[i];
424
+ renderer.setAttribute(faviconEl, 'rel', config.rel);
425
+ renderer.setAttribute(faviconEl, 'sizes', config.sizes);
426
+ renderer.setAttribute(faviconEl, 'href', faviconUrl);
427
+ }
428
+ }
429
+ else {
430
+ // Remove all favicon elements when no favicon URL is provided
431
+ for (const faviconEl of this.#faviconEls) {
432
+ faviconEl.remove();
433
+ }
434
+ this.#faviconEls = [];
435
+ }
436
+ // Handle mask icon
437
+ if (brand?.maskIcon) {
438
+ if (!this.#maskIconEl) {
439
+ this.#maskIconEl = renderer.createElement('link');
440
+ this.#appendToHead(renderer, this.#maskIconEl);
441
+ }
442
+ renderer.setAttribute(this.#maskIconEl, 'rel', 'mask-icon');
443
+ renderer.setAttribute(this.#maskIconEl, 'href', brand.maskIcon.url);
444
+ renderer.setAttribute(this.#maskIconEl, 'color', brand.maskIcon.color);
445
+ }
446
+ else if (this.#maskIconEl) {
447
+ this.#maskIconEl.remove();
448
+ this.#maskIconEl = undefined;
449
+ }
450
+ }
451
+ #appendToHead(renderer, el) {
452
+ renderer.appendChild(this.#document.head, el);
453
+ }
454
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: SkyThemeBrandService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
455
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: SkyThemeBrandService, providedIn: 'root' }); }
456
+ }
457
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: SkyThemeBrandService, decorators: [{
458
+ type: Injectable,
459
+ args: [{
460
+ providedIn: 'root',
461
+ }]
462
+ }] });
463
+
291
464
  /**
292
465
  * Specifies the theme and mode to be applied to a host element.
293
466
  */
@@ -338,7 +511,6 @@ class SkyThemeSettings {
338
511
  }
339
512
  }
340
513
 
341
- const BRAND_BLACKBAUD = 'blackbaud';
342
514
  function assertCurrentSettings(currentSettings) {
343
515
  if (!currentSettings) {
344
516
  throw new Error('Theme service is not initialized. Call init() first.');
@@ -348,6 +520,7 @@ function assertCurrentSettings(currentSettings) {
348
520
  * Provides methods for updating and handling changes to the current theme.
349
521
  */
350
522
  class SkyThemeService {
523
+ #brandSvc = inject(SkyThemeBrandService);
351
524
  /**
352
525
  * Notifies consumers when the current theme settings have changed.
353
526
  */
@@ -355,11 +528,9 @@ class SkyThemeService {
355
528
  return this.#_settingsObs;
356
529
  }
357
530
  #current;
358
- #brandLinkElement;
359
531
  #hostEl;
360
532
  #renderer;
361
533
  #settings;
362
- #registeredBrands = new Map();
363
534
  #_settingsObs;
364
535
  constructor() {
365
536
  this.#settings = new ReplaySubject(1);
@@ -379,7 +550,7 @@ class SkyThemeService {
379
550
  this.#renderer = renderer;
380
551
  if (registeredBrands) {
381
552
  for (const brand of registeredBrands) {
382
- this.#registeredBrands.set(brand.name, brand);
553
+ this.#brandSvc.registerBrand(brand);
383
554
  }
384
555
  }
385
556
  this.setTheme(theme);
@@ -390,6 +561,7 @@ class SkyThemeService {
390
561
  */
391
562
  destroy() {
392
563
  this.#settings.complete();
564
+ this.#brandSvc.destroy();
393
565
  this.#hostEl = this.#renderer = undefined;
394
566
  }
395
567
  /**
@@ -416,7 +588,7 @@ class SkyThemeService {
416
588
  setTheme(settingsOrTheme) {
417
589
  let settings;
418
590
  if (settingsOrTheme instanceof SkyThemeSettings) {
419
- settings = this.#applyRegisteredBrand(settingsOrTheme);
591
+ settings = settingsOrTheme;
420
592
  }
421
593
  else {
422
594
  const current = this.#current;
@@ -427,7 +599,16 @@ class SkyThemeService {
427
599
  this.#applyThemeClass(previous, settings, 'theme');
428
600
  this.#applyThemeClass(previous, settings, 'mode', 'supportedModes');
429
601
  this.#applyThemeClass(previous, settings, 'spacing', 'supportedSpacing');
430
- this.#applyThemeClass(previous, settings, 'brand');
602
+ if (this.#hostEl) {
603
+ // Validate branding support
604
+ if (settings.brand && !settings.theme.supportsBranding) {
605
+ throw new Error('Branding is not supported for the given theme.');
606
+ }
607
+ this.#brandSvc.updateBrand(this.#hostEl, this.#getRenderer(), settings.brand ??
608
+ (settings.theme === SkyTheme.presets.modern
609
+ ? new SkyThemeBrand('blackbaud', '1.0.0')
610
+ : undefined), previous?.brand);
611
+ }
431
612
  this.#settings.next({
432
613
  currentSettings: settings,
433
614
  previousSettings: previous,
@@ -435,20 +616,10 @@ class SkyThemeService {
435
616
  this.#current = settings;
436
617
  }
437
618
  registerBrand(brand) {
438
- this.#registeredBrands.set(brand.name, brand);
619
+ this.#brandSvc.registerBrand(brand);
439
620
  }
440
621
  unregisterBrand(name) {
441
- this.#registeredBrands.delete(name);
442
- }
443
- #applyRegisteredBrand(settings) {
444
- const brandName = settings.brand?.name;
445
- if (brandName) {
446
- const registeredBrand = this.#registeredBrands.get(brandName);
447
- if (registeredBrand) {
448
- settings = new SkyThemeSettings(settings.theme, settings.mode, settings.spacing, registeredBrand);
449
- }
450
- }
451
- return settings;
622
+ this.#brandSvc.unregisterBrand(name);
452
623
  }
453
624
  #updateThemeProperty(property, value) {
454
625
  const current = this.#current;
@@ -481,62 +652,18 @@ class SkyThemeService {
481
652
  const previousClass = previous?.[prop]?.hostClass;
482
653
  const currentClass = currentSetting?.hostClass;
483
654
  if (!previousClass || previousClass !== currentClass) {
484
- this.#updateHostClass(prop, previousClass, currentClass, currentSetting, current, supportedProp);
485
- if (prop === 'brand') {
486
- if (!current.theme.supportsBranding && currentSetting) {
487
- throw new Error('Branding is not supported for the given theme.');
488
- }
489
- this.#updateBrandStylesheet(current.brand, previous?.brand);
490
- }
655
+ this.#updateHostClass(previousClass, currentClass, currentSetting, current, supportedProp);
491
656
  }
492
657
  }
493
- #updateBrandStylesheet(currentBrand, previousBrand) {
494
- if (previousBrand && previousBrand.name !== BRAND_BLACKBAUD) {
495
- this.#removeBrandStylesheet();
496
- }
497
- if (currentBrand && currentBrand.name !== BRAND_BLACKBAUD) {
498
- this.#addBrandStylesheet(currentBrand);
499
- }
500
- }
501
- #updateHostClass(prop, previousClass, currentClass, currentSetting, current, supportedProp) {
658
+ #updateHostClass(previousClass, currentClass, currentSetting, current, supportedProp) {
502
659
  if (previousClass) {
503
660
  this.#removeHostClass(previousClass);
504
661
  }
505
- if (prop === 'brand') {
506
- if (currentClass && !previousClass) {
507
- this.#addHostClass('sky-theme-brand-base');
508
- }
509
- else if (!currentClass && previousClass) {
510
- this.#removeHostClass('sky-theme-brand-base');
511
- }
512
- }
513
662
  if (currentClass &&
514
663
  this.#isSupportedProperty(currentSetting, current, supportedProp)) {
515
664
  this.#addHostClass(currentClass);
516
665
  }
517
666
  }
518
- #addBrandStylesheet(brand) {
519
- if (brand.name !== BRAND_BLACKBAUD) {
520
- // Use styleUrl if provided, otherwise build the default URL
521
- const styleUrl = brand.styleUrl ||
522
- `https://sky.blackbaudcdn.net/static/skyux-brand-${brand.name}/${brand.version}/assets/scss/${brand.name}.css`;
523
- const renderer = this.#getRenderer();
524
- this.#brandLinkElement = renderer.createElement('link');
525
- renderer.appendChild(this.#hostEl, this.#brandLinkElement);
526
- renderer.setAttribute(this.#brandLinkElement, 'rel', 'stylesheet');
527
- renderer.setAttribute(this.#brandLinkElement, 'href', styleUrl);
528
- if (brand.sriHash) {
529
- renderer.setAttribute(this.#brandLinkElement, 'integrity', brand.sriHash);
530
- renderer.setAttribute(this.#brandLinkElement, 'crossorigin', 'anonymous');
531
- }
532
- }
533
- }
534
- #removeBrandStylesheet() {
535
- if (this.#brandLinkElement) {
536
- this.#getRenderer().removeChild(this.#hostEl, this.#brandLinkElement);
537
- this.#brandLinkElement = undefined;
538
- }
539
- }
540
667
  #addHostClass(className) {
541
668
  this.#getRenderer().addClass(this.#hostEl, className);
542
669
  }
@@ -580,15 +707,8 @@ class SkyAppStyleLoader {
580
707
  else {
581
708
  themePromise = Promise.resolve();
582
709
  }
583
- const fontAwesome = new FontFaceObserver('FontAwesome');
584
- const skyuxIcons = new FontFaceObserver('skyux-icons');
585
710
  const blackbaudSans = new FontFaceObserver('BLKB Sans');
586
711
  return Promise.all([
587
- // Specify a character for FontAwesome since some browsers will fail to detect
588
- // when the font is loaded unless a known character with a different width
589
- // than the default is not specified.
590
- fontAwesome.load('\uf0fc', SkyAppStyleLoader.LOAD_TIMEOUT),
591
- skyuxIcons.load('\ue999', SkyAppStyleLoader.LOAD_TIMEOUT),
592
712
  blackbaudSans.load(undefined, SkyAppStyleLoader.LOAD_TIMEOUT),
593
713
  themePromise,
594
714
  ])