refactor: migrate to material symbols icon set

This commit is contained in:
Thea Schöbl
2022-08-19 11:48:34 +00:00
parent f3cf3b30e3
commit 68734bfe21
2097 changed files with 26045 additions and 18101 deletions

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* eslint-disable unicorn/no-null */
import {matchPropertyContent, matchTagProperties} from './icon-match';
describe('matchTagProperties', function () {
const regex = matchTagProperties('test');
it('should match html tag content', function () {
expect('<test content></test>'.match(regex)).toEqual([' content']);
});
it('should match all tags', function () {
expect(
'<test content1></test> <test content2></test>'.match(regex),
).toEqual([' content1', ' content2']);
});
it('should not match wrong tags', function () {
expect('<no content></no>'.match(regex)).toEqual(null);
});
});
describe('matchPropertyContent', function () {
const regex = matchPropertyContent(['test1', 'test2']);
it('should match bare literals', function () {
expect(`test1="content" test2="content1"`.match(regex)).toEqual([
'content',
'content1',
]);
});
it('should match angular literals', function () {
expect(`[test1]="'content'" [test2]="'content1'"`.match(regex)).toEqual([
'content',
'content1',
]);
});
it('should not match wrong literals', function () {
expect(`no="content" [no]="'content'"`.match(regex)).toEqual(null);
});
});

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
*
*/
export function matchTagProperties(tag: string) {
return new RegExp(`(?<=<${tag})[\\s\\S]*?(?=><\\/${tag}>)`, 'g');
}
/**
*
*/
export function matchPropertyContent(properties: string[]) {
const names = properties.join('|');
return new RegExp(
`((?<=(${names})=")[\\w-]+(?="))|((?<=\\[(${names})]="')[\\w-]+(?='"))`,
'g',
);
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, HostBinding, Input} from '@angular/core';
@Component({
selector: 'stapps-icon',
templateUrl: 'icon.html',
styleUrls: ['icon.scss'],
})
export class IconComponent {
@HostBinding('style.--size')
@Input()
size?: number;
@HostBinding('style.--weight')
@Input()
weight?: number;
@HostBinding('style.--grade')
@Input()
grade?: number;
@Input()
fill: boolean;
@HostBinding('innerHtml')
@Input()
name: string;
@HostBinding('style.--fill') get fillStyle(): number | undefined {
return this.fill ? 1 : undefined;
}
@HostBinding('class.material-symbols-rounded') hostClass = true;
}

View File

@@ -0,0 +1,15 @@
<!--
~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ng-content></ng-content>

View File

@@ -0,0 +1,26 @@
/*!
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
:host {
font-variation-settings: 'wght' var(--weight), 'GRAD' var(--grade), 'FILL' var(--fill);
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: inherit;
transition: all 0.2s ease-in-out;
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* A noop function to aid parsing icon names
*/
export function SCIcon(
strings: TemplateStringsArray,
..._keys: string[]
): string {
return strings.join('');
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Directive, ElementRef, ViewContainerRef} from '@angular/core';
import {SCIcon} from './icon';
import {IconReplacer} from './replace-util';
@Directive({
selector: 'ion-back-button',
})
export class IonBackButtonDirective extends IconReplacer {
constructor(element: ElementRef, viewContainerRef: ViewContainerRef) {
super(element, viewContainerRef, 'shadow');
}
replace() {
this.replaceIcon(this.host.querySelector('.button-inner'), {
md: SCIcon`arrow_back`,
ios: SCIcon`arrow_back_ios`,
size: 24,
});
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Directive, ElementRef, ViewContainerRef} from '@angular/core';
import {SCIcon} from './icon';
import {IconReplacer} from './replace-util';
@Directive({
selector: 'ion-breadcrumb',
})
export class IonBreadcrumbDirective extends IconReplacer {
constructor(element: ElementRef, viewContainerRef: ViewContainerRef) {
super(element, viewContainerRef, 'shadow');
}
replace() {
this.replaceIcon(
this.host.querySelector('button[part="collapsed-indicator"]'),
{
name: SCIcon`more_horiz`,
size: 24,
},
);
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
ComponentRef,
Directive,
ElementRef,
Host,
Input,
OnChanges,
OnDestroy,
OnInit,
Optional,
Self,
ViewContainerRef,
} from '@angular/core';
import {IconComponent} from './icon.component';
import {IonIcon} from '@ionic/angular';
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
const noopProperty = {
set: noop,
get: noop,
};
@Directive({
selector: 'ion-icon',
})
export class IonIconDirective implements OnInit, OnDestroy, OnChanges {
@Input() name: string;
@Input() md: string;
@Input() ios: string;
@Input() fill = false;
@Input() weight: number;
@Input() size: number;
@Input() grade: number;
private mutationObserver: MutationObserver;
iconComponent?: ComponentRef<IconComponent>;
private static get mode(): 'md' | 'ios' {
return document.querySelector(':root')?.getAttribute('mode') as
| 'md'
| 'ios';
}
constructor(
private element: ElementRef,
private viewContainerRef: ViewContainerRef,
@Host() @Self() @Optional() private ionIcon: IonIcon,
) {}
ngOnInit() {
this.iconComponent = this.viewContainerRef.createComponent(
IconComponent,
{},
);
this.element.nativeElement.insertBefore(
this.iconComponent.location.nativeElement,
this.element.nativeElement.firstChild,
);
this.mutationObserver = new MutationObserver(() => {
const inner =
this.element.nativeElement.shadowRoot.querySelector('.icon-inner');
if (!inner) return;
inner.insertBefore(document.createElement('slot'), inner.firstChild);
});
this.mutationObserver.observe(this.element.nativeElement.shadowRoot, {
childList: true,
});
this.ngOnChanges();
// this will effectively completely disable the ion-icon component
for (const name of ['src', 'name', 'icon', 'md', 'ios']) {
this.disableProperty(name);
}
}
ngOnDestroy() {
this.mutationObserver.disconnect();
}
ngOnChanges() {
if (!this.iconComponent) return;
for (const key of ['name', 'weight', 'fill', 'size', 'grade'] as Array<
keyof IconComponent & keyof IonIconDirective
>) {
// @ts-expect-error type mismatch
this.iconComponent.instance[key] = this[key];
}
for (const mode of ['md', 'ios'] as Array<'md' | 'ios'>) {
if (this[mode] && IonIconDirective.mode === mode) {
this.iconComponent.instance.name = this[mode];
}
}
if (this.size) {
this.element.nativeElement.style.cssText = `font-size: ${this.size}px;`;
}
}
disableProperty(name: string) {
Object.defineProperty(
Object.getPrototypeOf((this.ionIcon as unknown as {el: HTMLElement}).el),
name,
noopProperty,
);
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {NgModule} from '@angular/core';
import {IconComponent} from './icon.component';
import {IonIconDirective} from './ion-icon.directive';
import {IonBackButtonDirective} from './ion-back-button.directive';
import {IonSearchbarDirective} from './ion-searchbar.directive';
import {IonBreadcrumbDirective} from './ion-breadcrumb.directive';
@NgModule({
declarations: [
IconComponent,
IonIconDirective,
IonBackButtonDirective,
IonSearchbarDirective,
IonBreadcrumbDirective,
],
exports: [
IonIconDirective,
IonBackButtonDirective,
IonSearchbarDirective,
IonBreadcrumbDirective,
],
})
export class IonIconModule {}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Directive, ElementRef, ViewContainerRef} from '@angular/core';
import {SCIcon} from './icon';
import {IconReplacer} from './replace-util';
@Directive({
selector: 'ion-searchbar',
})
export class IonSearchbarDirective extends IconReplacer {
constructor(element: ElementRef, viewContainerRef: ViewContainerRef) {
super(element, viewContainerRef, 'light');
}
replace() {
this.replaceIcon(this.host.querySelector('.searchbar-input-container'), {
name: SCIcon`search`,
size: 24,
});
this.replaceIcon(this.host.querySelector('.searchbar-clear-button'), {
name: SCIcon`close`,
size: 24,
});
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
ComponentRef,
Directive,
ElementRef,
OnDestroy,
OnInit,
ViewContainerRef,
} from '@angular/core';
import {IonIcon} from '@ionic/angular';
import {IonIconDirective} from './ion-icon.directive';
export type IconData = Omit<
Partial<IonIconDirective>,
| 'ngOnChanges'
| 'ngOnInit'
| 'viewContainerRef'
| 'ngOnDestroy'
| 'element'
| 'ionIcon'
| 'disableProperty'
>;
/**
* A utility class to replace ion-icons in other ionic components.
*/
@Directive()
export abstract class IconReplacer implements OnInit, OnDestroy {
private mutationObserver: MutationObserver;
protected slotName = 'sc-icon';
/**
* The host element
*
* This will be either element.nativeElement.shadowRoot or element.nativeElement
* depending on the iconDomLocation
*/
protected host: HTMLElement;
/**
* @param element The host element
* @param viewContainerRef The view container ref
* @param iconDomLocation If the icon is placed inside the shadow dom or not
* @protected
*/
protected constructor(
private readonly element: ElementRef,
private readonly viewContainerRef: ViewContainerRef,
private readonly iconDomLocation: 'shadow' | 'light',
) {}
/**
* Replace the icons here
*/
abstract replace(): void;
/**
* If any additional work needs to be done, this
* is called during ngOnInit
*/
init() {
// noop
}
/**
* If you need to do cleanup, this method is called during ngOnDestroy
*/
destroy() {
// noop
}
ngOnInit() {
this.host =
this.iconDomLocation === 'shadow'
? this.element.nativeElement.shadowRoot
: this.element.nativeElement;
this.init();
this.mutationObserver = new MutationObserver(() => this.replace());
this.mutationObserver.observe(this.host, {
childList: true,
});
}
replaceIcon(parent: HTMLElement | null, iconData: IconData) {
if (!parent) return;
const icon = parent.querySelector('ion-icon');
if (!icon) return;
const scIcon = this.createIcon(iconData);
// @ts-expect-error can be spread
scIcon.location.nativeElement.classList.add(...icon.classList);
if (this.iconDomLocation === 'shadow') {
// shadow dom needs to utilize slotting, to put it outside
// the shadow dom, otherwise it won't receive any css data
const slot = document.createElement('slot');
slot.name = this.slotName;
icon.replaceWith(slot);
scIcon.location.nativeElement.slot = this.slotName;
this.element.nativeElement.append(scIcon.location.nativeElement);
} else {
icon.replaceWith(scIcon.location.nativeElement);
}
}
private createIcon(iconData: IconData): ComponentRef<IonIcon> {
const ionIcon = this.viewContainerRef.createComponent(IonIcon, {});
const iconDirective = new IonIconDirective(
ionIcon.location,
this.viewContainerRef,
ionIcon.instance,
);
for (const key in iconData) {
// @ts-expect-error type mismatch
iconDirective[key] = iconData[key];
}
iconDirective.ngOnInit();
return ionIcon;
}
ngOnDestroy() {
this.mutationObserver.disconnect();
this.destroy();
}
}