Merge pull request #1428 from atmire/w2p-85123_add-support-for-themeable-favicons
Add support for themeable favicons
@@ -171,7 +171,8 @@ describe('App component', () => {
|
|||||||
TestBed.configureTestingModule(getDefaultTestBedConf());
|
TestBed.configureTestingModule(getDefaultTestBedConf());
|
||||||
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
|
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
|
||||||
document = TestBed.inject(DOCUMENT);
|
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]);
|
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
|
||||||
fixture = TestBed.createComponent(AppComponent);
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
@@ -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 { hasNoValue, hasValue, 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>
|
||||||
*
|
*
|
||||||
@@ -241,9 +246,13 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
*/
|
*/
|
||||||
private setThemeCss(themeName: string): void {
|
private setThemeCss(themeName: string): void {
|
||||||
const head = this.document.getElementsByTagName('head')[0];
|
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
|
// 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 +274,78 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
head.appendChild(link);
|
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() {
|
private trackIdleModal() {
|
||||||
const isIdle$ = this.authService.isUserIdle();
|
const isIdle$ = this.authService.isUserIdle();
|
||||||
const isAuthenticated$ = this.authService.isAuthenticated();
|
const isAuthenticated$ = this.authService.isAuthenticated();
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 15 KiB |
@@ -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 {
|
||||||
|
@@ -292,7 +292,45 @@ export const environment: GlobalConfig = {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// The default dspace theme
|
// 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").
|
// Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with "image" or "video").
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
<title>DSpace</title>
|
<title>DSpace</title>
|
||||||
<meta name="viewport" content="width=device-width,minimum-scale=1">
|
<meta name="viewport" content="width=device-width,minimum-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
<title>DSpace</title>
|
<title>DSpace</title>
|
||||||
<meta name="viewport" content="width=device-width,minimum-scale=1">
|
<meta name="viewport" content="width=device-width,minimum-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 33 KiB |
BIN
src/themes/dspace/assets/images/favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/themes/dspace/assets/images/favicons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
7
src/themes/dspace/assets/images/favicons/favicon.svg
Normal 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 |
@@ -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"
|
||||||
|
}
|