Merge pull request #1428 from atmire/w2p-85123_add-support-for-themeable-favicons

Add support for themeable favicons
This commit is contained in:
Tim Donohue
2021-12-20 11:53:07 -06:00
committed by GitHub
13 changed files with 176 additions and 10 deletions

View File

@@ -171,7 +171,8 @@ describe('App component', () => {
TestBed.configureTestingModule(getDefaultTestBedConf());
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
document = TestBed.inject(DOCUMENT);
headSpy = jasmine.createSpyObj('head', ['appendChild']);
headSpy = jasmine.createSpyObj('head', ['appendChild', 'getElementsByClassName']);
headSpy.getElementsByClassName.and.returnValue([]);
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;

View File

@@ -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 { hasNoValue, hasValue, 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 <head>
*
@@ -241,9 +246,13 @@ export class AppComponent implements OnInit, AfterViewInit {
*/
private setThemeCss(themeName: string): void {
const head = this.document.getElementsByTagName('head')[0];
if (hasNoValue(head)) {
return;
}
// 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 +274,78 @@ export class AppComponent implements OnInit, AfterViewInit {
head.appendChild(link);
}
private setHeadTags(themeName: string): void {
const head = this.document.getElementsByTagName('head')[0];
if (hasNoValue(head)) {
return;
}
// clear head tags
const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag'));
if (hasValue(currentHeadTags)) {
currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove());
}
// create new head tags (not yet added to DOM)
const headTagFragment = this.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 (hasNoValue(headTagConfigs)) {
const parentThemeName = themeConfig?.extends;
if (hasValue(parentThemeName)) {
// inherit the head tags of the parent theme
return this.createHeadTags(parentThemeName);
}
const defaultThemeName = DEFAULT_THEME_CONFIG.name;
if (
hasNoValue(defaultThemeName) ||
themeName === defaultThemeName ||
themeName === BASE_THEME_NAME
) {
// last resort, use fallback favicon.ico
return [
this.createHeadTag({
'tagName': 'link',
'attributes': {
'rel': 'icon',
'href': 'assets/images/favicon.ico',
'sizes': 'any',
}
})
];
}
// inherit the head tags of the default theme
return this.createHeadTags(DEFAULT_THEME_CONFIG.name);
}
return headTagConfigs.map(this.createHeadTag.bind(this));
}
private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement {
const tag = this.document.createElement(headTagConfig.tagName);
if (hasValue(headTagConfig.attributes)) {
Object.entries(headTagConfig.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();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

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.
*/
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 {

View File

@@ -292,7 +292,45 @@ export const environment: GlobalConfig = {
{
// The default dspace theme
name: 'dspace'
name: 'dspace',
// Whenever this theme is active, the following tags will be injected into the <head> of the page.
// Example use case: set the favicon based on the active theme.
headTags: [
{
// Insert <link rel="icon" href="assets/dspace/images/favicons/favicon.ico" sizes="any"/> into the <head> of the page.
tagName: 'link',
attributes: {
'rel': 'icon',
'href': 'assets/dspace/images/favicons/favicon.ico',
'sizes': 'any',
}
},
{
// Insert <link rel="icon" href="assets/dspace/images/favicons/favicon.svg" type="image/svg+xml"/> into the <head> of the page.
tagName: 'link',
attributes: {
'rel': 'icon',
'href': 'assets/dspace/images/favicons/favicon.svg',
'type': 'image/svg+xml',
}
},
{
// Insert <link rel="apple-touch-icon" href="assets/dspace/images/favicons/apple-touch-icon.png"/> into the <head> of the page.
tagName: 'link',
attributes: {
'rel': 'apple-touch-icon',
'href': 'assets/dspace/images/favicons/apple-touch-icon.png',
}
},
{
// Insert <link rel="manifest" href="assets/dspace/images/favicons/manifest.webmanifest"/> into the <head> of the page.
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").

View File

@@ -6,7 +6,6 @@
<base href="/">
<title>DSpace</title>
<meta name="viewport" content="width=device-width,minimum-scale=1">
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
</head>
<body>

View File

@@ -6,7 +6,6 @@
<base href="/">
<title>DSpace</title>
<meta name="viewport" content="width=device-width,minimum-scale=1">
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
</head>
<body>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 98 98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0.019,2.867)">
<path d="M53.561,58.569L53.67,58.563L53.786,58.553L53.892,58.54L54.002,58.53L54.112,58.507L54.221,58.488L54.327,58.465L54.433,58.436L54.538,58.413L54.644,58.38L54.747,58.346L54.844,58.311L54.948,58.271L55.049,58.229L55.149,58.187L55.149,58.186L55.245,58.141L55.34,58.097L55.437,58.048L55.528,57.992L55.528,57.991L55.622,57.941L55.622,57.939L55.712,57.883L55.712,57.882L55.805,57.822L55.888,57.766L55.888,57.765L55.973,57.702L56.061,57.637L56.061,57.635L56.148,57.573L56.148,57.572C56.964,56.889 57.541,55.926 57.709,54.834L57.722,54.73L57.722,54.72L57.736,54.619L57.736,54.608L57.745,54.51L57.745,54.498L57.754,54.397L57.754,54.38L57.759,54.285L57.759,54.269L57.761,54.164L57.761,37.704L57.759,37.599L57.759,37.583L57.754,37.488L57.754,37.475L57.745,37.374L57.745,37.362L57.736,37.264L57.736,37.253L57.722,37.153L57.722,37.143L57.709,37.039C57.542,35.947 56.965,34.982 56.148,34.301L56.148,34.3L56.061,34.237L56.061,34.235L55.973,34.17L55.888,34.107L55.888,34.106L55.805,34.05L55.712,33.989L55.622,33.933L55.622,33.93L55.528,33.88L55.528,33.879L55.437,33.823L55.34,33.774L55.245,33.731L55.245,33.73L55.149,33.685L55.149,33.684L55.049,33.641L54.948,33.599L54.844,33.559L54.747,33.524L54.644,33.493L54.538,33.457L54.433,33.434L54.327,33.406L54.221,33.382L54.112,33.363L54.002,33.34L53.892,33.331L53.786,33.317L53.67,33.307L53.561,33.301L53.447,33.296L45.557,33.296C35.841,33.296 29.699,25.458 29.699,16.146L29.699,6.92C29.699,3.108 26.597,0.005 22.785,0.005L6.92,0.005C3.107,0.005 -0,3.111 -0,6.92L-0,23.602C-0,27.408 3.104,30.511 6.92,30.511L15.334,30.511C24.503,30.511 32.24,36.461 32.48,45.914L32.48,45.954C32.24,55.407 24.502,61.356 15.334,61.356L6.92,61.356C3.105,61.356 -0,64.459 -0,68.265L-0,84.947C-0,88.757 3.106,91.862 6.92,91.862L22.785,91.862C26.597,91.862 29.699,88.758 29.699,84.947L29.699,75.724C29.699,66.412 35.843,58.575 45.557,58.575L53.447,58.575L53.561,58.569ZM87.607,9.956C81.466,3.814 72.985,0 63.651,0L48.627,0L48.627,17.424L63.651,17.424C68.177,17.424 72.298,19.282 75.291,22.273C78.281,25.263 80.14,29.385 80.14,33.912L80.14,57.954C80.14,62.492 78.287,66.619 75.308,69.609L75.291,69.593C72.3,72.584 68.178,74.442 63.651,74.442L48.627,74.442L48.627,91.866L63.651,91.866C72.984,91.866 81.465,88.052 87.607,81.91L87.607,81.877C93.749,75.734 97.562,67.263 97.562,57.954L97.562,33.912C97.562,24.578 93.749,16.097 87.607,9.956Z" style="fill:rgb(146,198,66);fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,19 @@
{
"name": "DSpace",
"short_name": "DSpace",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#091119",
"background_color": "#091119",
"display": "standalone"
}