85123: WIP: Support for theme-specific head tags

This commit is contained in:
Bruno Roemers
2021-11-23 19:36:49 +01:00
parent a5a91d5139
commit 99a2cf926a
3 changed files with 111 additions and 7 deletions

View File

@@ -31,12 +31,12 @@ import { AuthService } from './core/auth/auth.service';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service'; import { MenuService } from './shared/menu/menu.service';
import { HostWindowService } from './shared/host-window.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 { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { models } from './core/core.module'; import { models } from './core/core.module';
import { LocaleService } from './core/locale/locale.service'; 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 { KlaroService } from './shared/cookies/klaro.service';
import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { GoogleAnalyticsService } from './statistics/google-analytics.service';
import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { DOCUMENT, isPlatformBrowser } from '@angular/common';
@@ -115,11 +115,11 @@ export class AppComponent implements OnInit, AfterViewInit {
this.isThemeCSSLoading$.next(true); this.isThemeCSSLoading$.next(true);
} }
if (hasValue(themeName)) { if (hasValue(themeName)) {
this.setThemeCss(themeName); this.loadGlobalThemeConfig(themeName);
} else if (hasValue(DEFAULT_THEME_CONFIG)) { } else if (hasValue(DEFAULT_THEME_CONFIG)) {
this.setThemeCss(DEFAULT_THEME_CONFIG.name); this.loadGlobalThemeConfig(DEFAULT_THEME_CONFIG.name);
} else { } 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 <head> * Update the theme css file in <head>
* *
@@ -243,7 +248,7 @@ export class AppComponent implements OnInit, AfterViewInit {
const head = this.document.getElementsByTagName('head')[0]; const head = this.document.getElementsByTagName('head')[0];
// Array.from to ensure we end up with an array, not an HTMLCollection, which would be // Array.from to ensure we end up with an array, not an HTMLCollection, which would be
// automatically updated if we add nodes later // 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'); const link = this.document.createElement('link');
link.setAttribute('rel', 'stylesheet'); link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css'); link.setAttribute('type', 'text/css');
@@ -265,6 +270,51 @@ export class AppComponent implements OnInit, AfterViewInit {
head.appendChild(link); 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() { private trackIdleModal() {
const isIdle$ = this.authService.isUserIdle(); const isIdle$ = this.authService.isUserIdle();
const isAuthenticated$ = this.authService.isAuthenticated(); const isAuthenticated$ = this.authService.isAuthenticated();

View File

@@ -12,6 +12,28 @@ export interface NamedThemeConfig extends Config {
* its ancestor theme(s) will be checked recursively before falling back to the default theme. * its ancestor theme(s) will be checked recursively before falling back to the default theme.
*/ */
extends?: string; 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 { export interface RegExThemeConfig extends NamedThemeConfig {

View File

@@ -292,7 +292,39 @@ export const environment: GlobalConfig = {
{ {
// The default dspace theme // 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"). // Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with "image" or "video").