From 99a2cf926a02a0acdbdac1d60ecb2a4dc99dff7b Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 23 Nov 2021 19:36:49 +0100 Subject: [PATCH] 85123: WIP: Support for theme-specific head tags --- src/app/app.component.ts | 62 +++++++++++++++++++++++--- src/config/theme.model.ts | 22 +++++++++ src/environments/environment.common.ts | 34 +++++++++++++- 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 6f06a84144..f138753596 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -31,12 +31,12 @@ import { AuthService } from './core/auth/auth.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { MenuService } from './shared/menu/menu.service'; import { HostWindowService } from './shared/host-window.service'; -import { ThemeConfig } from '../config/theme.model'; +import {HeadTagConfig, ThemeConfig} from '../config/theme.model'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; import { environment } from '../environments/environment'; import { models } from './core/core.module'; import { LocaleService } from './core/locale/locale.service'; -import { hasValue, isNotEmpty } from './shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from './shared/empty.util'; import { KlaroService } from './shared/cookies/klaro.service'; import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; @@ -115,11 +115,11 @@ export class AppComponent implements OnInit, AfterViewInit { this.isThemeCSSLoading$.next(true); } if (hasValue(themeName)) { - this.setThemeCss(themeName); + this.loadGlobalThemeConfig(themeName); } else if (hasValue(DEFAULT_THEME_CONFIG)) { - this.setThemeCss(DEFAULT_THEME_CONFIG.name); + this.loadGlobalThemeConfig(DEFAULT_THEME_CONFIG.name); } else { - this.setThemeCss(BASE_THEME_NAME); + this.loadGlobalThemeConfig(BASE_THEME_NAME); } }); @@ -233,6 +233,11 @@ export class AppComponent implements OnInit, AfterViewInit { } } + private loadGlobalThemeConfig(themeName: string): void { + this.setThemeCss(themeName); + this.setHeadTags(themeName); + } + /** * Update the theme css file in * @@ -243,7 +248,7 @@ export class AppComponent implements OnInit, AfterViewInit { const head = this.document.getElementsByTagName('head')[0]; // Array.from to ensure we end up with an array, not an HTMLCollection, which would be // automatically updated if we add nodes later - const currentThemeLinks = Array.from(this.document.getElementsByClassName('theme-css')); + const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css')); const link = this.document.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); @@ -265,6 +270,51 @@ export class AppComponent implements OnInit, AfterViewInit { head.appendChild(link); } + private setHeadTags(themeName: string): void { + const head = this.document.getElementsByTagName('head')[0]; + + // clear head tags + const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag')); + if (isNotEmpty(currentHeadTags)) { + currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove()); + } + + // create new head tags (not yet added to DOM) + const headTagFragment = document.createDocumentFragment(); + this.createHeadTags(themeName) + .forEach(newHeadTag => headTagFragment.appendChild(newHeadTag)); + + // add new head tags to DOM + head.appendChild(headTagFragment); + } + + private createHeadTags(themeName: string): HTMLElement[] { + const themeConfig = this.themeService.getThemeConfigFor(themeName); + const headTagConfigs = themeConfig?.headTags; + + // if the current theme does not have head tags, we inherit the head tags of the parent + if (isEmpty(headTagConfigs)) { + const parentThemeName = themeConfig.extends; + return isNotEmpty(parentThemeName) ? this.createHeadTags(parentThemeName) : []; + } + + return headTagConfigs.map(this.createHeadTag.bind(this)); + } + + private createHeadTag(themeHeadTag: HeadTagConfig): HTMLElement { + const tag = this.document.createElement(themeHeadTag.tagName); + + if (isNotEmpty(themeHeadTag.attributes)) { + Object.entries(themeHeadTag.attributes) + .forEach(([key, value]) => tag.setAttribute(key, value)); + } + + // 'class' attribute should always be 'theme-head-tag' for removal + tag.setAttribute('class', 'theme-head-tag'); + + return tag; + } + private trackIdleModal() { const isIdle$ = this.authService.isUserIdle(); const isAuthenticated$ = this.authService.isAuthenticated(); diff --git a/src/config/theme.model.ts b/src/config/theme.model.ts index 0130b5ffd8..d65118d5d4 100644 --- a/src/config/theme.model.ts +++ b/src/config/theme.model.ts @@ -12,6 +12,28 @@ export interface NamedThemeConfig extends Config { * its ancestor theme(s) will be checked recursively before falling back to the default theme. */ extends?: string; + + /** + * A list of HTML tags that should be added to the HEAD section of the document, whenever this theme is active. + */ + headTags?: HeadTagConfig[]; +} + +/** + * Interface that represents a single theme-specific HTML tag in the HEAD section of the page. + */ +export interface HeadTagConfig extends Config { + /** + * The name of the HTML tag + */ + tagName: string; + + /** + * The attributes on the HTML tag + */ + attributes?: { + [key: string]: string; + }; } export interface RegExThemeConfig extends NamedThemeConfig { diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index b1cbd699a3..b8b7c31092 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -292,7 +292,39 @@ export const environment: GlobalConfig = { { // The default dspace theme - name: 'dspace' + name: 'dspace', + headTags: [ + { + tagName: 'link', + attributes: { + 'rel': 'icon', + 'href': 'assets/dspace/images/favicons/favicon.ico', + 'sizes': 'any', + } + }, + { + tagName: 'link', + attributes: { + 'rel': 'icon', + 'href': 'assets/dspace/images/favicons/favicon.svg', + 'type': 'image/svg+xml', + } + }, + { + tagName: 'link', + attributes: { + 'rel': 'apple-touch-icon', + 'href': 'assets/dspace/images/favicons/apple-touch-icon.png', + } + }, + { + tagName: 'link', + attributes: { + 'rel': 'manifest', + 'href': 'assets/dspace/images/favicons/manifest.webmanifest', + } + }, + ] }, ], // Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with "image" or "video").