Commit 2345e665 authored by Benjamin Franzke's avatar Benjamin Franzke Committed by Benni Mack
Browse files

[TASK] Add <typo3-backend-icon> component

The component is provided as replacement for the
current lit-helper `icon()` which cannot provide
support for icon usage inside shadow dom elements.

The component renders all icons as inline SVGs.
FontawesomeIconProvider is therefore adapted
to provide inline rendered SVGs for font-awesome
icons. Not that these inline font-awesome SVGs
will also be used in areas where 'inline' icons are
requested (like information toolbar) but the
fallback to 'default' was used before.

The component supports all properties of the
Icon API via HTML attributes.
Colors and custom sizes can be applied to the
component via CSS color/font-size property:

typo3-backend-icon[identifier=status-dialog-information] {
  color:#6daae0;
  /* An explicit font-size will be used as icon width & height */
  font-size: 1.2em;
}

Resolves: #93473
Releases: master
Change-Id: I3044c325f122eb0085ecb3f45fb9502bfb314d5d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67675


Tested-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent f4d7a7ba
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
import module = require('module');
import {html, css, unsafeCSS, customElement, property, LitElement, TemplateResult, CSSResult} from 'lit-element';
import {unsafeHTML} from 'lit-html/directives/unsafe-html';
import {until} from 'lit-html/directives/until';
import {Sizes, States, MarkupIdentifiers} from '../Enum/IconTypes';
import Icons = require('../Icons');
import 'TYPO3/CMS/Backend/Element/SpinnerElement';
/**
* Module: TYPO3/CMS/Backend/Element/IconElement
*
* @example
* <typo3-backend-icon identifier="data-view-page" size="small"></typo3-backend-icon>
*/
@customElement('typo3-backend-icon')
export class IconElement extends LitElement {
@property({type: String}) identifier: string;
@property({type: String, reflect: true}) size: Sizes = Sizes.default;
@property({type: String}) state: States = States.default;
@property({type: String}) overlay: string = null;
@property({type: String}) markup: MarkupIdentifiers = MarkupIdentifiers.inline;
/**
* @internal Usage of `raw` attribute is discouraged due to security implications.
*
* The `raw` attribute value will be rendered unescaped into DOM as raw html (.innerHTML = raw).
* That means it is the responsibility of the callee to ensure the HTML string does not contain
* user supplied strings.
* This attribute should therefore only be used to preserve backwards compatibility,
* and must not be used in new code or with user supplied strings.
* Use `identifier` attribute if ever possible instead.
*/
@property({type: String}) raw?: string = null;
public static get styles(): CSSResult[]
{
const iconUnifyModifier = 0.86;
const iconSize = (identifier: CSSResult, size: number) => css`
:host([size=${identifier}]),
:host([raw]) .icon-size-${identifier} {
font-size: ${size}px;
}
`;
return [
css`
:host {
display: flex;
font-size: 1em;
width: 1em;
height: 1em;
line-height: 0;
}
typo3-backend-spinner {
font-size: 1em;
}
.icon {
position: relative;
display: block;
overflow: hidden;
white-space: nowrap;
height: 1em;
width: 1em;
line-height: 1;
}
.icon svg,
.icon img {
display: block;
height: 1em;
width: 1em;
transform: translate3d(0, 0, 0);
}
.icon * {
display: block;
line-height: inherit;
}
.icon-markup {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.icon-overlay {
position: absolute;
bottom: 0;
right: 0;
font-size: 0.6875em;
text-align: center;
}
.icon-color {
fill: currentColor;
}
.icon-state-disabled .icon-markup {
opacity: .5;
}
.icon-unify {
font-size: ${iconUnifyModifier}em;
line-height: ${1 / iconUnifyModifier};
}
.icon-spin .icon-markup {
animation: icon-spin 2s infinite linear;
}
@keyframes icon-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`,
iconSize(unsafeCSS(Sizes.small), 16),
iconSize(unsafeCSS(Sizes.default), 32),
iconSize(unsafeCSS(Sizes.large), 48),
iconSize(unsafeCSS(Sizes.mega), 64),
];
}
public render(): TemplateResult {
if (this.raw) {
return html`${unsafeHTML(this.raw)}`;
}
if (!this.identifier) {
return html``;
}
const icon = Icons.getIcon(this.identifier, this.size, this.overlay, this.state, this.markup)
.then((markup: string) => {
return html`
${unsafeHTML(markup)}
`;
});
return html`${until(icon, html`<typo3-backend-spinner></typo3-backend-spinner>`)}`;
}
}
......@@ -12,6 +12,7 @@
*/
import {html, css, customElement, property, LitElement, TemplateResult, CSSResult} from 'lit-element';
import {Sizes} from '../Enum/IconTypes';
/**
* Module: TYPO3/CMS/Backend/Element/SpinnerElement
......@@ -22,36 +23,37 @@ import {html, css, customElement, property, LitElement, TemplateResult, CSSResul
*/
@customElement('typo3-backend-spinner')
export class SpinnerElement extends LitElement {
@property({type: String}) size: string = 'small';
@property({type: String}) size: Sizes = Sizes.default;
public static get styles(): CSSResult
{
return css`
:host {
display: block;
font-size: 32px;
width: 1em;
height: 1em;
display: flex;
justify-content: center;
align-items: center;
}
.spinner {
display: block;
margin: 2px;
border-style: solid;
border-color: #212121 #bababa #bababa;
border-radius: 50%;
width: 0.625em;
height: 0.625em;
border-width: 0.0625em;
animation: spin 1s linear infinite;
}
.spinner.small {
border-width: 2px;
width: 10px;
height: 10px;
:host([size=small]) .spinner {
font-size: 16px;
}
.spinner.medium {
border-width: 3px;
width: 14px;
height: 14px;
:host([size=large]) .spinner {
font-size: 48px;
}
.spinner.large {
border-width: 4px;
width: 20px;
height: 20px;
:host([size=mega]) .spinner {
font-size: 64px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
......@@ -61,6 +63,6 @@ export class SpinnerElement extends LitElement {
}
public render(): TemplateResult {
return html`<div class="spinner ${this.size}"></div>`
return html`<div class="spinner"></div>`
}
}
......@@ -12,7 +12,8 @@
*/
import {html, customElement, property, LitElement, TemplateResult} from 'lit-element';
import {icon, lll} from 'TYPO3/CMS/Core/lit-helper';
import {lll} from 'TYPO3/CMS/Core/lit-helper';
import 'TYPO3/CMS/Backend/Element/IconElement';
/**
* Module: TYPO3/CMS/Backend/Element/TableWizardElement
......@@ -161,7 +162,7 @@ export class TableWizardElement extends LitElement {
<span class="btn-group">
<button class="btn btn-default" type="button" title="${lll('table_smallFields')}"
@click="${(evt: Event) => this.toggleType(evt)}">
${icon(this.type === 'input' ? 'actions-chevron-expand' : 'actions-chevron-contract')}
<typo3-backend-icon identifier="${this.type === 'input' ? 'actions-chevron-expand' : 'actions-chevron-contract'}" size="small"></typo3-backend-icon>
</button>
</span>
`;
......
......@@ -15,6 +15,7 @@ export enum Sizes {
small = 'small',
default = 'default',
large = 'large',
mega = 'mega',
overlay = 'overlay',
}
......
......@@ -14,7 +14,8 @@
import {Tooltip} from 'bootstrap';
import {render} from 'lit-html';
import {html, TemplateResult} from 'lit-element';
import {icon, lll} from 'TYPO3/CMS/Core/lit-helper';
import {lll} from 'TYPO3/CMS/Core/lit-helper';
import 'TYPO3/CMS/Backend/Element/IconElement';
/**
* @exports TYPO3/CMS/Backend/FormEngine/Element/TreeToolbar
......@@ -148,18 +149,20 @@ export class TreeToolbar
return html`
<div class="${this.settings.toolbarSelector}">
<div class="input-group">
<span class="input-group-addon input-group-icon filter">${icon('actions-filter', 'small')}</span>
<span class="input-group-addon input-group-icon filter">
<typo3-backend-icon identifier="actions-filter" size="small"></typo3-backend-icon>
</span>
<input type="text" class="form-control ${this.settings.searchInput}" placeholder="${lll('tcatree.findItem')}" @input="${(evt: InputEvent) => this.search(evt)}">
</div>
<div class="btn-group">
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.expandAllBtn}" title="${lll('tcatree.expandAll')}" @click="${() => this.expandAll()}">
${icon('apps-pagetree-category-expand-all', 'small')}
<typo3-backend-icon identifier="apps-pagetree-category-expand-all" size="small"></typo3-backend-icon>
</button>
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.collapseAllBtn}" title="${lll('tcatree.collapseAll')}" @click="${() => this.collapseAll()}">
${icon('apps-pagetree-category-collapse-all', 'small')}
<typo3-backend-icon identifier="apps-pagetree-category-collapse-all" size="small"></typo3-backend-icon>
</button>
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.toggleHideUnchecked}" title="${lll('tcatree.toggleHideUnchecked')}" @click="${() => this.toggleHideUnchecked()}">
${icon('apps-pagetree-category-toggle-hide-checked', 'small')}
<typo3-backend-icon identifier="apps-pagetree-category-toggle-hide-checked" size="small"></typo3-backend-icon>
</button>
</div>
</div>
......
......@@ -13,7 +13,7 @@
import {render} from 'lit-html';
import {html, TemplateResult} from 'lit-element';
import {icon, lll} from 'TYPO3/CMS/Core/lit-helper';
import {lll} from 'TYPO3/CMS/Core/lit-helper';
import {PageTree} from './PageTree';
import {PageTreeDragDrop, ToolbarDragHandler} from './PageTreeDragDrop';
import viewPort from '../Viewport';
......@@ -22,6 +22,7 @@ import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import {select as d3select} from 'd3-selection';
import DebounceEvent from 'TYPO3/CMS/Core/Event/DebounceEvent';
import {SvgTreeWrapper} from '../SvgTree';
import 'TYPO3/CMS/Backend/Element/IconElement';
/**
* @exports TYPO3/CMS/Backend/PageTree/PageTreeElement
......@@ -72,13 +73,13 @@ export class PageTreeElement {
<div id="typo3-pagetree-treeContainer" class="navigation-tree-container">
<div id="typo3-pagetree-tree" class="svg-tree-wrapper">
<div class="node-loader">
${icon('spinner-circle-light', 'small')}
<typo3-backend-icon identifier="spinner-circle-light" size="small"></typo3-backend-icon>
</div>
</div>
</div>
</div>
<div class="svg-tree-loader">
${icon('spinner-circle-light', 'large')}
<typo3-backend-icon identifier="spinner-circle-light" size="large"></typo3-backend-icon>
</div>
</div>
`;
......@@ -174,18 +175,16 @@ class Toolbar {
<input type="text" class="form-control form-control-sm search-input" placeholder="${lll('tree.searchTermInfo')}">
</div>
<button class="btn btn-default btn-borderless btn-sm" @click="${() => this.refreshTree()}" data-tree-icon="actions-refresh" title="${lll('labels.refresh')}">
${icon('actions-refresh', 'small')}
<typo3-backend-icon identifier="actions-refresh" size="small"></typo3-backend-icon>
</button>
</div>
<div class="svg-toolbar__submenu">
${this.tree.settings.doktypes && this.tree.settings.doktypes.length
? this.tree.settings.doktypes.map((item: any) => {
// @todo Unsure, why this has to be done for doktype icons
this.tree.fetchIcon(item.icon, false);
return html`
<div class="svg-toolbar__drag-node" data-tree-icon="${item.icon}" data-node-type="${item.nodeType}"
title="${item.title}" tooltip="${item.tooltip}">
${icon(item.icon, 'small')}
<typo3-backend-icon identifier="${item.icon}" size="small"></typo3-backend-icon>
</div>
`;
})
......
......@@ -13,11 +13,12 @@
import {render} from 'lit-html';
import {html, TemplateResult} from 'lit-element';
import {icon, lll} from 'TYPO3/CMS/Core/lit-helper';
import {lll} from 'TYPO3/CMS/Core/lit-helper';
import {FileStorageTree} from './FileStorageTree';
import viewPort from '../Viewport';
import DebounceEvent from 'TYPO3/CMS/Core/Event/DebounceEvent';
import {FileStorageTreeActions} from './FileStorageTreeActions';
import 'TYPO3/CMS/Backend/Element/IconElement';
/**
* Responsible for setting up the viewport for the Navigation Component for the File Tree
......@@ -61,13 +62,13 @@ export class FileStorageTreeContainer {
<div class="navigation-tree-container">
<div id="typo3-filestoragetree-tree" class="svg-tree-wrapper">
<div class="node-loader">
${icon('spinner-circle-light', 'small')}
<typo3-backend-icon identifier="spinner-circle-light" size="small"></typo3-backend-icon>
</div>
</div>
</div>
</div>
<div class="svg-tree-loader">
${icon('spinner-circle-light', 'large')}
<typo3-backend-icon identifier="spinner-circle-light" size="large"></typo3-backend-icon>
</div>
</div>
`;
......@@ -146,7 +147,7 @@ class Toolbar
<input type="text" class="form-control form-control-sm search-input" placeholder="${lll('tree.searchTermInfo')}">
</div>
<button class="btn btn-default btn-borderless btn-sm" @click="${() => this.refreshTree()}" data-tree-icon="actions-refresh" title="${lll('labels.refresh')}">
${icon('actions-refresh', 'small')}
<typo3-backend-icon identifier="actions-refresh" size="small"></typo3-backend-icon>
</button>
</div>
</div>`;
......
......@@ -12,12 +12,7 @@
*/
import type {TemplateResult} from 'lit-html';
import {html, render, Part} from 'lit-html';
import {unsafeHTML} from 'lit-html/directives/unsafe-html';
import {until} from 'lit-html/directives/until';
import Icons = require('TYPO3/CMS/Backend/Icons');
import 'TYPO3/CMS/Backend/Element/SpinnerElement';
import {render} from 'lit-html';
/**
* @internal
......@@ -46,12 +41,3 @@ export const lll = (key: string): string => {
}
return window.TYPO3.lang[key];
};
/**
* @internal
*/
export const icon = (identifier: string, size: any = 'small') => {
// @todo Fetched and resolved icons should be stored in a session repository in `Icons`
const icon = Icons.getIcon(identifier, size).then((markup: string) => html`${unsafeHTML(markup)}`);
return html`${until(icon, html`<typo3-backend-spinner size="${size}"></typo3-backend-spinner>`)}`;
};
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
var __decorate=this&&this.__decorate||function(e,t,i,n){var o,r=arguments.length,s=r<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(o=e[a])&&(s=(r<3?o(s):r>3?o(t,i,s):o(t,i))||s);return r>3&&s&&Object.defineProperty(t,i,s),s};define(["require","exports","lit-element","lit-html/directives/unsafe-html","lit-html/directives/until","../Enum/IconTypes","../Icons","TYPO3/CMS/Backend/Element/SpinnerElement"],(function(e,t,i,n,o,r,s){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.IconElement=void 0;let a=class extends i.LitElement{constructor(){super(...arguments),this.size=r.Sizes.default,this.state=r.States.default,this.overlay=null,this.markup=r.MarkupIdentifiers.inline,this.raw=null}static get styles(){const e=(e,t)=>i.css`
:host([size=${e}]),
:host([raw]) .icon-size-${e} {
font-size: ${t}px;
}
`;return[i.css`
:host {
display: flex;
font-size: 1em;
width: 1em;
height: 1em;
line-height: 0;
}
typo3-backend-spinner {
font-size: 1em;
}
.icon {
position: relative;
display: block;
overflow: hidden;
white-space: nowrap;
height: 1em;
width: 1em;
line-height: 1;
}
.icon svg,
.icon img {
display: block;
height: 1em;
width: 1em;
transform: translate3d(0, 0, 0);
}
.icon * {
display: block;
line-height: inherit;
}
.icon-markup {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.icon-overlay {
position: absolute;
bottom: 0;
right: 0;
font-size: 0.6875em;
text-align: center;
}
.icon-color {
fill: currentColor;
}
.icon-state-disabled .icon-markup {
opacity: .5;
}
.icon-unify {
font-size: ${.86}em;
line-height: ${1/.86};
}
.icon-spin .icon-markup {
animation: icon-spin 2s infinite linear;
}
@keyframes icon-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`,e(i.unsafeCSS(r.Sizes.small),16),e(i.unsafeCSS(r.Sizes.default),32),e(i.unsafeCSS(r.Sizes.large),48),e(i.unsafeCSS(r.Sizes.mega),64)]}render(){if(this.raw)return i.html`${n.unsafeHTML(this.raw)}`;if(!this.identifier)return i.html``;const e=s.getIcon(this.identifier,this.size,this.overlay,this.state,this.markup).then(e=>i.html`
${n.unsafeHTML(e)}
`);return i.html`${o.until(e,i.html`<typo3-backend-spinner></typo3-backend-spinner>`)}`}};__decorate([i.property({type:String})],a.prototype,"identifier",void 0),__decorate([i.property({type:String,reflect:!0})],a.prototype,"size",void 0),__decorate([i.property({type:String})],a.prototype,"state",void 0),__decorate([i.property({type:String})],a.prototype,"overlay",void 0),__decorate([i.property({type:String})],a.prototype,"markup",void 0),__decorate([i.property({type:String})],a.prototype,"raw",void 0),a=__decorate([i.customElement("typo3-backend-icon")],a),t.IconElement=a}));
\ No newline at end of file
......@@ -10,35 +10,36 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __decorate=this&&this.__decorate||function(e,t,r,i){var n,s=arguments.length,o=s<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,r,i);else for(var d=e.length-1;d>=0;d--)(n=e[d])&&(o=(s<3?n(o):s>3?n(t,r,o):n(t,r))||o);return s>3&&o&&Object.defineProperty(t,r,o),o};define(["require","exports","lit-element"],(function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SpinnerElement=void 0;let i=class extends r.LitElement{constructor(){super(...arguments),this.size="small"}static get styles(){return r.css`
var __decorate=this&&this.__decorate||function(e,t,r,n){var i,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,r,n);else for(var a=e.length-1;a>=0;a--)(i=e[a])&&(o=(s<3?i(o):s>3?i(t,r,o):i(t,r))||o);return s>3&&o&&Object.defineProperty(t,r,o),o};define(["require","exports","lit-element","../Enum/IconTypes"],(function(e,t,r,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SpinnerElement=void 0;let i=class extends r.LitElement{constructor(){super(...arguments),this.size=n.Sizes.default}static get styles(){return r.css`
:host {
display: block;
font-size: 32px;
width: 1em;
height: 1em;
display: flex;
justify-content: center;
align-items: center;
}
.spinner {
display: block;
margin: 2px;
border-style: solid;
border-color: #212121 #bababa #bababa;
border-radius: 50%;
width: 0.625em;
height: 0.625em;
border-width: 0.0625em;
animation: spin 1s linear infinite;
}
.spinner.small {
border-width: 2px;
width: 10px;