@skyux/theme 13.0.0-alpha.0 → 13.0.0-alpha.10

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';
@@ -237,14 +237,25 @@ class SkyThemeBrand {
237
237
  /**
238
238
  * Creates a new theme brand.
239
239
  * @param name The name of the theme brand.
240
+ * @param version The version of the theme brand.
240
241
  * @param hostClass The class on the host element which child components should reference when
241
- * adjusting for a specified theme brand. This defaults to `sky-theme-brand-<name>
242
+ * adjusting for a specified theme brand. This defaults to `sky-theme-brand-<name>`
243
+ * @param styleUrl The URL of the stylesheet to load for this brand
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
242
248
  */
243
- constructor(name, version, hostClass) {
249
+ constructor(name, version, hostClass, styleUrl, sriHash, title, faviconUrl, maskIcon) {
244
250
  this.name = name;
245
251
  this.version = version;
252
+ this.styleUrl = styleUrl;
253
+ this.sriHash = sriHash;
254
+ this.title = title;
255
+ this.faviconUrl = faviconUrl;
256
+ this.maskIcon = maskIcon;
246
257
  this.hostClass = hostClass;
247
- if (!/\d+\.\d+\.\d+(?:-(?:alpha|beta|rc)\.\d)*$/.test(version)) {
258
+ if (!/^(\d+|\d+\.\d+\.\d+)(?:-(?:alpha|beta|rc)\.\d+)*$/.test(version)) {
248
259
  throw new Error(`Invalid version format "${version}" for theme brand "${name}".`);
249
260
  }
250
261
  }
@@ -262,6 +273,21 @@ class SkyThemeBrand {
262
273
  if (this.hostClass !== defaultHostClass) {
263
274
  result.hostClass = this.hostClass;
264
275
  }
276
+ if (this.styleUrl) {
277
+ result.styleUrl = this.styleUrl;
278
+ }
279
+ if (this.sriHash) {
280
+ result.sriHash = this.sriHash;
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
+ }
265
291
  return result;
266
292
  }
267
293
  /**
@@ -269,13 +295,172 @@ class SkyThemeBrand {
269
295
  * Deserializes a JSON object to a SkyThemeBrand instance.
270
296
  */
271
297
  static deserialize(data) {
272
- return new SkyThemeBrand(data.name, data.version, data.hostClass);
298
+ return new SkyThemeBrand(data.name, data.version, data.hostClass, data.styleUrl, data.sriHash, data.title, data.faviconUrl, data.maskIcon);
273
299
  }
274
300
  #buildDefaultHostClassName() {
275
301
  return `sky-theme-brand-${this.name}`;
276
302
  }
277
303
  }
278
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
+
279
464
  /**
280
465
  * Specifies the theme and mode to be applied to a host element.
281
466
  */
@@ -335,6 +520,7 @@ function assertCurrentSettings(currentSettings) {
335
520
  * Provides methods for updating and handling changes to the current theme.
336
521
  */
337
522
  class SkyThemeService {
523
+ #brandSvc = inject(SkyThemeBrandService);
338
524
  /**
339
525
  * Notifies consumers when the current theme settings have changed.
340
526
  */
@@ -342,7 +528,6 @@ class SkyThemeService {
342
528
  return this.#_settingsObs;
343
529
  }
344
530
  #current;
345
- #brandLinkElement;
346
531
  #hostEl;
347
532
  #renderer;
348
533
  #settings;
@@ -357,10 +542,17 @@ class SkyThemeService {
357
542
  * @param hostEl The host element under which themed components are rendered.
358
543
  * @param renderer A Renderer2 instance for updating the host element with theme changes.
359
544
  * @param theme The initial theme.
545
+ * @param registeredBrands An initial set of brands to register. Additional brands can
546
+ * be registered later via the registerBrand() method.
360
547
  */
361
- init(hostEl, renderer, theme) {
548
+ init(hostEl, renderer, theme, registeredBrands) {
362
549
  this.#hostEl = hostEl;
363
550
  this.#renderer = renderer;
551
+ if (registeredBrands) {
552
+ for (const brand of registeredBrands) {
553
+ this.#brandSvc.registerBrand(brand);
554
+ }
555
+ }
364
556
  this.setTheme(theme);
365
557
  }
366
558
  /**
@@ -369,6 +561,7 @@ class SkyThemeService {
369
561
  */
370
562
  destroy() {
371
563
  this.#settings.complete();
564
+ this.#brandSvc.destroy();
372
565
  this.#hostEl = this.#renderer = undefined;
373
566
  }
374
567
  /**
@@ -387,7 +580,7 @@ class SkyThemeService {
387
580
  }
388
581
  /**
389
582
  * Updates the current theme brand.
390
- * @param brand The new theme brand to apply.
583
+ * @param brand The new theme brand to apply, or undefined to clear the current brand.
391
584
  */
392
585
  setThemeBrand(brand) {
393
586
  this.#updateThemeProperty('brand', brand);
@@ -406,13 +599,28 @@ class SkyThemeService {
406
599
  this.#applyThemeClass(previous, settings, 'theme');
407
600
  this.#applyThemeClass(previous, settings, 'mode', 'supportedModes');
408
601
  this.#applyThemeClass(previous, settings, 'spacing', 'supportedSpacing');
409
- 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
+ }
410
612
  this.#settings.next({
411
613
  currentSettings: settings,
412
614
  previousSettings: previous,
413
615
  });
414
616
  this.#current = settings;
415
617
  }
618
+ registerBrand(brand) {
619
+ this.#brandSvc.registerBrand(brand);
620
+ }
621
+ unregisterBrand(name) {
622
+ this.#brandSvc.unregisterBrand(name);
623
+ }
416
624
  #updateThemeProperty(property, value) {
417
625
  const current = this.#current;
418
626
  assertCurrentSettings(current);
@@ -427,7 +635,7 @@ class SkyThemeService {
427
635
  break;
428
636
  default:
429
637
  }
430
- if (supportedValues && !supportedValues.includes(value)) {
638
+ if (supportedValues && value && !supportedValues.includes(value)) {
431
639
  throw new Error(`The current theme does not support the specified ${property}.`);
432
640
  }
433
641
  const updatedSettings = {
@@ -444,60 +652,18 @@ class SkyThemeService {
444
652
  const previousClass = previous?.[prop]?.hostClass;
445
653
  const currentClass = currentSetting?.hostClass;
446
654
  if (!previousClass || previousClass !== currentClass) {
447
- this.#updateHostClass(prop, previousClass, currentClass, currentSetting, current, supportedProp);
448
- if (prop === 'brand') {
449
- if (!current.theme.supportsBranding && currentSetting) {
450
- throw new Error('Branding is not supported for the given theme.');
451
- }
452
- this.#updateBrandStylesheet(current.brand, previous?.brand);
453
- }
655
+ this.#updateHostClass(previousClass, currentClass, currentSetting, current, supportedProp);
454
656
  }
455
657
  }
456
- #updateBrandStylesheet(currentBrand, previousBrand) {
457
- if (currentBrand &&
458
- currentBrand.name !== 'blackbaud' &&
459
- previousBrand !== currentBrand) {
460
- this.#addBrandStylesheet(currentBrand);
461
- }
462
- else {
463
- this.#removeBrandStylesheet();
464
- }
465
- }
466
- #updateHostClass(prop, previousClass, currentClass, currentSetting, current, supportedProp) {
658
+ #updateHostClass(previousClass, currentClass, currentSetting, current, supportedProp) {
467
659
  if (previousClass) {
468
660
  this.#removeHostClass(previousClass);
469
661
  }
470
- if (prop === 'brand') {
471
- if (currentClass && !previousClass) {
472
- this.#addHostClass('sky-theme-brand-base');
473
- }
474
- else if (!currentClass && previousClass) {
475
- this.#removeHostClass('sky-theme-brand-base');
476
- }
477
- }
478
662
  if (currentClass &&
479
663
  this.#isSupportedProperty(currentSetting, current, supportedProp)) {
480
664
  this.#addHostClass(currentClass);
481
665
  }
482
666
  }
483
- #addBrandStylesheet(brand) {
484
- if (brand.name !== 'blackbaud') {
485
- const cssPath = `https://sky.blackbaudcdn.net/static/skyux-brand-${brand.name}/${brand.version}/assets/scss/${brand.name}.css`;
486
- // Create a link element via Angular's renderer to avoid SSR troubles
487
- this.#brandLinkElement = this.#getRenderer().createElement('link');
488
- // Add the style to the head section
489
- this.#getRenderer().appendChild(this.#hostEl, this.#brandLinkElement);
490
- // Set type of the link item and path to the css file
491
- this.#getRenderer().setProperty(this.#brandLinkElement, 'rel', 'stylesheet');
492
- this.#getRenderer().setProperty(this.#brandLinkElement, 'href', cssPath);
493
- }
494
- }
495
- #removeBrandStylesheet() {
496
- if (this.#brandLinkElement) {
497
- this.#getRenderer().removeChild(this.#hostEl, this.#brandLinkElement);
498
- this.#brandLinkElement = undefined;
499
- }
500
- }
501
667
  #addHostClass(className) {
502
668
  this.#getRenderer().addClass(this.#hostEl, className);
503
669
  }
@@ -541,15 +707,8 @@ class SkyAppStyleLoader {
541
707
  else {
542
708
  themePromise = Promise.resolve();
543
709
  }
544
- const fontAwesome = new FontFaceObserver('FontAwesome');
545
- const skyuxIcons = new FontFaceObserver('skyux-icons');
546
710
  const blackbaudSans = new FontFaceObserver('BLKB Sans');
547
711
  return Promise.all([
548
- // Specify a character for FontAwesome since some browsers will fail to detect
549
- // when the font is loaded unless a known character with a different width
550
- // than the default is not specified.
551
- fontAwesome.load('\uf0fc', SkyAppStyleLoader.LOAD_TIMEOUT),
552
- skyuxIcons.load('\ue999', SkyAppStyleLoader.LOAD_TIMEOUT),
553
712
  blackbaudSans.load(undefined, SkyAppStyleLoader.LOAD_TIMEOUT),
554
713
  themePromise,
555
714
  ])