Merge pull request #1656 from umbraco/feature/icons-extension
Feature: icons extension
This commit is contained in:
@@ -20,6 +20,7 @@ import { UmbIconRegistry } from '../src/packages/core/icon-registry/icon.registr
|
||||
import { UmbLitElement } from '../src/packages/core/lit-element';
|
||||
import { umbLocalizationRegistry } from '../src/packages/core/localization';
|
||||
import customElementManifests from '../dist-cms/custom-elements.json';
|
||||
import icons from '../src/packages/core/icon-registry/icons/icons';
|
||||
|
||||
import '../src/libs/context-api/provide/context-provider.element';
|
||||
import '../src/packages/core/components';
|
||||
@@ -36,6 +37,7 @@ class UmbStoryBookElement extends UmbLitElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._umbIconRegistry.setIcons(icons);
|
||||
this._umbIconRegistry.attach(this);
|
||||
this._registerExtensions(documentManifests);
|
||||
new UmbModalManagerContext(this);
|
||||
|
||||
@@ -18,11 +18,10 @@ const run = async () => {
|
||||
var icons = await collectDictionaryIcons();
|
||||
icons = await collectDiskIcons(icons);
|
||||
writeIconsToDisk(icons);
|
||||
generateJSON(icons);
|
||||
generateJS(icons);
|
||||
};
|
||||
|
||||
const collectDictionaryIcons = async () => {
|
||||
|
||||
const rawData = readFileSync(iconMapJson);
|
||||
const fileRaw = rawData.toString();
|
||||
const fileJSON = JSON.parse(fileRaw);
|
||||
@@ -32,11 +31,11 @@ const collectDictionaryIcons = async () => {
|
||||
// Lucide:
|
||||
fileJSON.lucide.forEach((iconDef) => {
|
||||
if (iconDef.file && iconDef.name) {
|
||||
const path = lucideSvgDirectory + "/" + iconDef.file;
|
||||
const path = lucideSvgDirectory + '/' + iconDef.file;
|
||||
|
||||
try {
|
||||
const rawData = readFileSync(path);
|
||||
// For Lucide icons specially we adjust the icons a bit for them to work in our case:
|
||||
// For Lucide icons specially we adjust the icons a bit for them to work in our case: [NL]
|
||||
let svg = rawData.toString().replace(' width="24"\n', '');
|
||||
svg = svg.replace(' height="24"\n', '');
|
||||
svg = svg.replace('stroke-width="2"', 'stroke-width="1.75"');
|
||||
@@ -60,11 +59,11 @@ const collectDictionaryIcons = async () => {
|
||||
// SimpleIcons:
|
||||
fileJSON.simpleIcons.forEach((iconDef) => {
|
||||
if (iconDef.file && iconDef.name) {
|
||||
const path = simpleIconsSvgDirectory + "/" + iconDef.file;
|
||||
const path = simpleIconsSvgDirectory + '/' + iconDef.file;
|
||||
|
||||
try {
|
||||
const rawData = readFileSync(path);
|
||||
let svg = rawData.toString()
|
||||
let svg = rawData.toString();
|
||||
const iconFileName = iconDef.name;
|
||||
|
||||
// SimpleIcons need to use fill="currentColor"
|
||||
@@ -91,11 +90,11 @@ const collectDictionaryIcons = async () => {
|
||||
// Umbraco:
|
||||
fileJSON.umbraco.forEach((iconDef) => {
|
||||
if (iconDef.file && iconDef.name) {
|
||||
const path = umbracoSvgDirectory + "/" + iconDef.file;
|
||||
const path = umbracoSvgDirectory + '/' + iconDef.file;
|
||||
|
||||
try {
|
||||
const rawData = readFileSync(path);
|
||||
const svg = rawData.toString()
|
||||
const svg = rawData.toString();
|
||||
const iconFileName = iconDef.name;
|
||||
|
||||
const icon = {
|
||||
@@ -136,8 +135,7 @@ const collectDiskIcons = async (icons) => {
|
||||
const iconName = iconFileName;
|
||||
|
||||
// Only append not already defined icons:
|
||||
if (!icons.find(x => x.name === iconName)) {
|
||||
|
||||
if (!icons.find((x) => x.name === iconName)) {
|
||||
const icon = {
|
||||
name: iconName,
|
||||
legacy: true,
|
||||
@@ -169,20 +167,20 @@ const writeIconsToDisk = (icons) => {
|
||||
});
|
||||
};
|
||||
|
||||
const generateJSON = (icons) => {
|
||||
const JSONPath = `${iconsOutputDirectory}/icons.json`;
|
||||
const generateJS = (icons) => {
|
||||
const JSPath = `${iconsOutputDirectory}/icons.ts`;
|
||||
|
||||
const iconDescriptors = icons.map((icon) => {
|
||||
return {
|
||||
name: icon.name,
|
||||
legacy: icon.legacy,
|
||||
path: `./icons/${icon.fileName}.js`,
|
||||
};
|
||||
return `{
|
||||
name: "${icon.name}",
|
||||
${icon.legacy ? 'legacy: true,' : ''}
|
||||
path: "./icons/${icon.fileName}.js",
|
||||
}`.replace(/\t/g, ''); // Regex removes white space [NL]
|
||||
});
|
||||
|
||||
const content = `${JSON.stringify(iconDescriptors)}`;
|
||||
const content = `export default [${iconDescriptors.join(',')}];`;
|
||||
|
||||
writeFileWithDir(JSONPath, content, (err) => {
|
||||
writeFileWithDir(JSPath, content, (err) => {
|
||||
if (err) {
|
||||
// eslint-disable-next-line no-undef
|
||||
console.log(err);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@ import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
|
||||
import { UmbAuthContext } from '@umbraco-cms/backoffice/auth';
|
||||
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbIconRegistry } from '@umbraco-cms/backoffice/icon';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { Guard, UmbRoute } from '@umbraco-cms/backoffice/router';
|
||||
import { pathWithoutBasePath } from '@umbraco-cms/backoffice/router';
|
||||
@@ -85,7 +84,6 @@ export class UmbAppElement extends UmbLitElement {
|
||||
new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
|
||||
new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);
|
||||
|
||||
new UmbIconRegistry().attach(this);
|
||||
new UUIIconRegistryEssential().attach(this);
|
||||
|
||||
new UmbContextDebugController(this);
|
||||
|
||||
@@ -10,33 +10,31 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
import './components/index.js';
|
||||
|
||||
// TODO: temp solution to load core packages
|
||||
const CORE_PACKAGES = [
|
||||
import('../../packages/audit-log/umbraco-package.js'),
|
||||
import('../../packages/block/umbraco-package.js'),
|
||||
import('../../packages/data-type/umbraco-package.js'),
|
||||
import('../../packages/dictionary/umbraco-package.js'),
|
||||
import('../../packages/umbraco-news/umbraco-package.js'),
|
||||
import('../../packages/documents/umbraco-package.js'),
|
||||
import('../../packages/dynamic-root/umbraco-package.js'),
|
||||
import('../../packages/health-check/umbraco-package.js'),
|
||||
import('../../packages/language/umbraco-package.js'),
|
||||
import('../../packages/log-viewer/umbraco-package.js'),
|
||||
import('../../packages/markdown-editor/umbraco-package.js'),
|
||||
import('../../packages/data-type/umbraco-package.js'),
|
||||
import('../../packages/media/umbraco-package.js'),
|
||||
import('../../packages/members/umbraco-package.js'),
|
||||
import('../../packages/models-builder/umbraco-package.js'),
|
||||
//import('../../packages/object-type/umbraco-package.js'),// This had nothing to register.
|
||||
import('../../packages/packages/umbraco-package.js'),
|
||||
import('../../packages/relations/umbraco-package.js'),
|
||||
import('../../packages/search/umbraco-package.js'),
|
||||
import('../../packages/settings/umbraco-package.js'),
|
||||
import('../../packages/language/umbraco-package.js'),
|
||||
import('../../packages/static-file/umbraco-package.js'),
|
||||
import('../../packages/dynamic-root/umbraco-package.js'),
|
||||
import('../../packages/block/umbraco-package.js'),
|
||||
import('../../packages/tags/umbraco-package.js'),
|
||||
import('../../packages/templating/umbraco-package.js'),
|
||||
import('../../packages/tiny-mce/umbraco-package.js'),
|
||||
import('../../packages/umbraco-news/umbraco-package.js'),
|
||||
import('../../packages/markdown-editor/umbraco-package.js'),
|
||||
import('../../packages/templating/umbraco-package.js'),
|
||||
import('../../packages/dictionary/umbraco-package.js'),
|
||||
import('../../packages/user/umbraco-package.js'),
|
||||
import('../../packages/health-check/umbraco-package.js'),
|
||||
import('../../packages/audit-log/umbraco-package.js'),
|
||||
import('../../packages/webhook/umbraco-package.js'),
|
||||
import('../../packages/relations/umbraco-package.js'),
|
||||
import('../../packages/models-builder/umbraco-package.js'),
|
||||
import('../../packages/log-viewer/umbraco-package.js'),
|
||||
import('../../packages/packages/umbraco-package.js'),
|
||||
];
|
||||
|
||||
@customElement('umb-backoffice')
|
||||
@@ -55,13 +53,13 @@ export class UmbBackofficeElement extends UmbLitElement {
|
||||
new UmbBackofficeEntryPointExtensionInitializer(this, umbExtensionsRegistry);
|
||||
new UmbEntryPointExtensionInitializer(this, umbExtensionsRegistry);
|
||||
|
||||
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions();
|
||||
|
||||
// So far local packages are this simple to registerer, so no need for a manager to do that:
|
||||
CORE_PACKAGES.forEach(async (packageImport) => {
|
||||
const packageModule = await packageImport;
|
||||
umbExtensionsRegistry.registerMany(packageModule.extensions);
|
||||
});
|
||||
|
||||
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -81,7 +81,7 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
|
||||
#tabs {
|
||||
height: 60px;
|
||||
flex-basis: 100%;
|
||||
font-size: 16px;
|
||||
font-size: 16px; /* specific for the header */
|
||||
--uui-tab-text: var(--uui-color-header-contrast);
|
||||
--uui-tab-text-hover: var(--uui-color-header-contrast-emphasis);
|
||||
--uui-tab-text-active: var(--uui-color-header-contrast-emphasis);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UmbBackofficeContext } from '../backoffice.context.js';
|
||||
import { UMB_BACKOFFICE_CONTEXT } from '../backoffice.context.js';
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbSectionContext, UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section';
|
||||
import type { UmbRoute, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router';
|
||||
import type { ManifestSection, UmbSectionElement } from '@umbraco-cms/backoffice/extension-registry';
|
||||
@@ -99,17 +99,18 @@ export class UmbBackofficeMainElement extends UmbLitElement {
|
||||
render() {
|
||||
return this._routes.length > 0
|
||||
? html`<umb-router-slot .routes=${this._routes} @change=${this._onRouteChange}></umb-router-slot>`
|
||||
: '';
|
||||
: nothing;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
background-color: var(--uui-color-background);
|
||||
display: block;
|
||||
background-color: var(--uui-color-background);
|
||||
width: 100%;
|
||||
height: calc(
|
||||
100% - 60px
|
||||
); // 60 => top header height, TODO: Make sure this comes from somewhere so it is maintainable and eventually responsive.
|
||||
); /* 60 => top header height, TODO: Make sure this comes from somewhere so it is maintainable and eventually responsive. */
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { UmbIconDictionary } from '@umbraco-cms/backoffice/icon';
|
||||
import type { ManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface ManifestIcons extends ManifestPlainJs<{ default: UmbIconDictionary }> {
|
||||
type: 'icons';
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import type { ManifestExternalLoginProvider } from './external-login-provider.mo
|
||||
import type { ManifestGlobalContext } from './global-context.model.js';
|
||||
import type { ManifestHeaderApp, ManifestHeaderAppButtonKind } from './header-app.model.js';
|
||||
import type { ManifestHealthCheck } from './health-check.model.js';
|
||||
import type { ManifestIcons } from './icons.model.js';
|
||||
import type { ManifestLocalization } from './localization.model.js';
|
||||
import type { ManifestMenu } from './menu.model.js';
|
||||
import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.model.js';
|
||||
import type { ManifestModal } from './modal.model.js';
|
||||
@@ -40,7 +42,6 @@ import type { ManifestSectionView } from './section-view.model.js';
|
||||
import type { ManifestStore, ManifestTreeStore, ManifestItemStore } from './store.model.js';
|
||||
import type { ManifestTheme } from './theme.model.js';
|
||||
import type { ManifestTinyMcePlugin } from './tinymce-plugin.model.js';
|
||||
import type { ManifestLocalization } from './localization.model.js';
|
||||
import type { ManifestTree } from './tree.model.js';
|
||||
import type { ManifestTreeItem } from './tree-item.model.js';
|
||||
import type { ManifestUserProfileApp } from './user-profile-app.model.js';
|
||||
@@ -66,6 +67,7 @@ import type { ManifestBackofficeEntryPoint } from './backoffice-entry-point.mode
|
||||
import type { ManifestEntryPoint } from './entry-point.model.js';
|
||||
import type { ManifestBase, ManifestBundle, ManifestCondition } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export type * from './app-entry-point.model.js';
|
||||
export type * from './auth-provider.model.js';
|
||||
export type * from './backoffice-entry-point.model.js';
|
||||
export type * from './block-editor-custom-view.model.js';
|
||||
@@ -84,6 +86,7 @@ export type * from './external-login-provider.model.js';
|
||||
export type * from './global-context.model.js';
|
||||
export type * from './header-app.model.js';
|
||||
export type * from './health-check.model.js';
|
||||
export type * from './icons.model.js';
|
||||
export type * from './localization.model.js';
|
||||
export type * from './menu-item.model.js';
|
||||
export type * from './menu.model.js';
|
||||
@@ -109,7 +112,6 @@ export type * from './workspace-context.model.js';
|
||||
export type * from './workspace-footer-app.model.js';
|
||||
export type * from './workspace-view.model.js';
|
||||
export type * from './workspace.model.js';
|
||||
export type * from './app-entry-point.model.js';
|
||||
|
||||
export type ManifestEntityActions =
|
||||
| ManifestEntityAction
|
||||
@@ -163,6 +165,7 @@ export type ManifestTypes =
|
||||
| ManifestHeaderApp
|
||||
| ManifestHeaderAppButtonKind
|
||||
| ManifestHealthCheck
|
||||
| ManifestIcons
|
||||
| ManifestItemStore
|
||||
| ManifestMenu
|
||||
| ManifestMenuItem
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { UmbIconRegistryContext } from './icon-registry.context.js';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
export const UMB_ICON_REGISTRY_CONTEXT = new UmbContextToken<UmbIconRegistryContext>('UmbIconRegistryContext');
|
||||
@@ -0,0 +1,49 @@
|
||||
import { UmbIconRegistry } from './icon.registry.js';
|
||||
import type { UmbIconDefinition } from './types.js';
|
||||
import { UMB_ICON_REGISTRY_CONTEXT } from './icon-registry.context-token.js';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { loadManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { type ManifestIcons, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export class UmbIconRegistryContext extends UmbContextBase<UmbIconRegistryContext> {
|
||||
#registry: UmbIconRegistry;
|
||||
#manifestMap = new Map();
|
||||
#icons = new UmbArrayState<UmbIconDefinition>([], (x) => x.name);
|
||||
readonly icons = this.#icons.asObservable();
|
||||
readonly approvedIcons = this.#icons.asObservablePart((icons) => icons.filter((x) => x.legacy !== true));
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host, UMB_ICON_REGISTRY_CONTEXT);
|
||||
this.#registry = new UmbIconRegistry();
|
||||
this.#registry.attach(host.getHostElement());
|
||||
|
||||
this.observe(this.icons, (icons) => {
|
||||
//if (icons.length > 0) {
|
||||
this.#registry.setIcons(icons);
|
||||
//}
|
||||
});
|
||||
|
||||
this.observe(umbExtensionsRegistry.byType('icons'), (manifests) => {
|
||||
manifests.forEach((manifest) => {
|
||||
if (this.#manifestMap.has(manifest.alias)) return;
|
||||
this.#manifestMap.set(manifest.alias, manifest);
|
||||
// TODO: Should we unInit a entry point if is removed?
|
||||
this.instantiateEntryPoint(manifest);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async instantiateEntryPoint(manifest: ManifestIcons) {
|
||||
if (manifest.js) {
|
||||
const js = await loadManifestPlainJs<{ default?: any }>(manifest.js);
|
||||
if (!js || !js.default || !Array.isArray(js.default)) {
|
||||
throw new Error('Icon manifest JS-file must export an array of icons as the default export.');
|
||||
}
|
||||
this.#icons.append(js.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbIconRegistryContext as api };
|
||||
@@ -1,5 +1,4 @@
|
||||
import icons from './icons/icons.json' assert { type: 'json' };
|
||||
import { UUIIconRegistry } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { type UUIIconHost, UUIIconRegistry } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
interface UmbIconDescriptor {
|
||||
name: string;
|
||||
@@ -13,26 +12,66 @@ interface UmbIconDescriptor {
|
||||
* @description - Icon Registry. Provides icons from the icon manifest. Icons are loaded on demand. All icons are prefixed with 'icon-'
|
||||
*/
|
||||
export class UmbIconRegistry extends UUIIconRegistry {
|
||||
#initResolve?: () => void;
|
||||
#init: Promise<void> = new Promise((resolve) => {
|
||||
this.#initResolve = resolve;
|
||||
});
|
||||
|
||||
#icons: UmbIconDescriptor[] = [];
|
||||
#unhandledProviders: Map<string, UUIIconHost> = new Map();
|
||||
|
||||
setIcons(icons: UmbIconDescriptor[]) {
|
||||
const oldIcons = this.#icons;
|
||||
this.#icons = icons;
|
||||
if (this.#initResolve) {
|
||||
this.#initResolve();
|
||||
this.#initResolve = undefined;
|
||||
}
|
||||
// Go figure out which of the icons are new.
|
||||
const newIcons = this.#icons.filter((i) => !oldIcons.find((o) => o.name === i.name));
|
||||
newIcons.forEach((icon) => {
|
||||
// Do we already have a request for this one, then lets initiate the load for those:
|
||||
const unhandled = this.#unhandledProviders.get(icon.name);
|
||||
if (unhandled) {
|
||||
this.#loadIcon(icon.name, unhandled).then(() => {
|
||||
this.#unhandledProviders.delete(icon.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
appendIcons(icons: UmbIconDescriptor[]) {
|
||||
this.#icons = [...this.#icons, ...icons];
|
||||
}
|
||||
/**
|
||||
* @param {string} iconName
|
||||
* @return {*} {boolean}
|
||||
* @memberof UmbIconStore
|
||||
*/
|
||||
acceptIcon(iconName: string): boolean {
|
||||
const iconManifest = icons.find((i: UmbIconDescriptor) => i.name === iconName);
|
||||
if (!iconManifest) return false;
|
||||
const iconProvider = this.provideIcon(iconName);
|
||||
this.#loadIcon(iconName, iconProvider);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async #loadIcon(iconName: string, iconProvider: UUIIconHost): Promise<boolean> {
|
||||
await this.#init;
|
||||
const iconManifest = this.#icons.find((i: UmbIconDescriptor) => i.name === iconName);
|
||||
// Icon not found, so lets add it to a list of unhandled requests.
|
||||
if (!iconManifest) {
|
||||
this.#unhandledProviders.set(iconName, iconProvider);
|
||||
return false;
|
||||
}
|
||||
|
||||
const icon = this.provideIcon(iconName);
|
||||
const iconPath = iconManifest.path;
|
||||
|
||||
import(/* @vite-ignore */ iconPath)
|
||||
.then((iconModule) => {
|
||||
icon.svg = iconModule.default;
|
||||
iconProvider.svg = iconModule.default;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Failed to load icon ${iconName} on path ${iconPath}`, err.message);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, Story } from '@storybook/web-components';
|
||||
import icons from './icons/icons.json';
|
||||
import icons from './icons/icons.js';
|
||||
import { html, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
export default {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1 +1,4 @@
|
||||
export * from './icon-registry.context-token.js';
|
||||
export * from './icon-registry.context.js';
|
||||
export * from './icon.registry.js';
|
||||
export * from './types.js';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const manifests = [
|
||||
{
|
||||
type: 'icons',
|
||||
alias: 'Umb.Icons.Backoffice',
|
||||
name: 'Backoffice Icons',
|
||||
js: () => import('./icons/icons.js'),
|
||||
},
|
||||
{
|
||||
type: 'globalContext',
|
||||
alias: 'Umb.GlobalContext.Icons',
|
||||
name: 'Icons Context',
|
||||
api: () => import('./icon-registry.context.js'),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface UmbIconDefinition {
|
||||
name: string;
|
||||
path: string;
|
||||
legacy?: boolean;
|
||||
}
|
||||
|
||||
export type UmbIconDictionary = Array<UmbIconDefinition>;
|
||||
@@ -6,6 +6,7 @@ import { manifests as cultureManifests } from './culture/manifests.js';
|
||||
import { manifests as debugManifests } from './debug/manifests.js';
|
||||
import { manifests as entityActionManifests } from './entity-action/manifests.js';
|
||||
import { manifests as extensionManifests } from './extension-registry/manifests.js';
|
||||
import { manifests as iconRegistryManifests } from './icon-registry/manifests.js';
|
||||
import { manifests as localizationManifests } from './localization/manifests.js';
|
||||
import { manifests as modalManifests } from './modal/common/manifests.js';
|
||||
import { manifests as propertyActionManifests } from './property-action/manifests.js';
|
||||
@@ -23,6 +24,7 @@ import type { ManifestTypes, UmbBackofficeManifestKind } from './extension-regis
|
||||
export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
|
||||
...authManifests,
|
||||
...extensionManifests,
|
||||
...iconRegistryManifests,
|
||||
...cultureManifests,
|
||||
...localizationManifests,
|
||||
...themeManifests,
|
||||
|
||||
@@ -1,81 +1,97 @@
|
||||
import icons from '../../../icon-registry/icons/icons.json' assert { type: 'json' };
|
||||
import type { UUIColorSwatchesEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import type { UUIColorSwatchesEvent, UUIIconElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, state, repeat, query, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
|
||||
import type { UmbIconPickerModalData, UmbIconPickerModalValue } from '@umbraco-cms/backoffice/modal';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
import { extractUmbColorVariable, umbracoColors } from '@umbraco-cms/backoffice/resources';
|
||||
import { umbFocus } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UMB_ICON_REGISTRY_CONTEXT, type UmbIconDefinition } from '@umbraco-cms/backoffice/icon';
|
||||
|
||||
// TODO: Make use of UmbPickerLayoutBase
|
||||
// TODO: to prevent element extension we need to move the Picker logic into a separate class we can reuse across all pickers
|
||||
@customElement('umb-icon-picker-modal')
|
||||
export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPickerModalData, UmbIconPickerModalValue> {
|
||||
private _iconList = icons.filter((icon) => !icon.legacy);
|
||||
#icons?: Array<UmbIconDefinition>;
|
||||
|
||||
@query('#search')
|
||||
private _searchInput?: HTMLInputElement;
|
||||
|
||||
@state()
|
||||
private _iconListFiltered: Array<(typeof icons)[0]> = [];
|
||||
private _iconsFiltered?: Array<UmbIconDefinition>;
|
||||
|
||||
@state()
|
||||
private _colorList = umbracoColors;
|
||||
private _colorList = umbracoColors.filter((color) => !color.legacy);
|
||||
|
||||
@state()
|
||||
private _modalValue?: UmbIconPickerModalValue;
|
||||
private _currentIcon?: string;
|
||||
|
||||
@state()
|
||||
private _currentAlias = 'text';
|
||||
private _currentColor = 'text';
|
||||
|
||||
#changeIcon(e: { target: HTMLInputElement; type: string; key: unknown }) {
|
||||
if (e.type == 'click' || (e.type == 'keyup' && e.key == 'Enter')) {
|
||||
this.modalContext?.updateValue({ icon: e.target.id });
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_ICON_REGISTRY_CONTEXT, (context) => {
|
||||
this.observe(context.approvedIcons, (icons) => {
|
||||
this.#icons = icons;
|
||||
this.#filterIcons();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#filterIcons(e: { target: HTMLInputElement }) {
|
||||
if (e.target.value) {
|
||||
this._iconListFiltered = this._iconList.filter((icon) =>
|
||||
icon.name.toLowerCase().includes(e.target.value.toLowerCase()),
|
||||
);
|
||||
#filterIcons() {
|
||||
if (!this.#icons) return;
|
||||
const value = this._searchInput?.value;
|
||||
if (value) {
|
||||
this._iconsFiltered = this.#icons.filter((icon) => icon.name.toLowerCase().includes(value.toLowerCase()));
|
||||
} else {
|
||||
this._iconListFiltered = this._iconList;
|
||||
this._iconsFiltered = this.#icons;
|
||||
}
|
||||
}
|
||||
|
||||
#onColorChange(e: UUIColorSwatchesEvent) {
|
||||
this.modalContext?.updateValue({ color: e.target.value });
|
||||
this._currentAlias = e.target.value;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._iconListFiltered = this._iconList;
|
||||
this._iconsFiltered = this.#icons;
|
||||
|
||||
if (this.modalContext) {
|
||||
this.observe(
|
||||
this.modalContext?.value,
|
||||
(newValue) => {
|
||||
this._modalValue = newValue;
|
||||
this._currentAlias = newValue?.color ?? 'text';
|
||||
this._currentIcon = newValue?.icon;
|
||||
this._currentColor = newValue?.color ?? 'text';
|
||||
},
|
||||
'_observeModalContextValue',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#changeIcon(e: InputEvent | KeyboardEvent) {
|
||||
if (e.type == 'click' || (e.type == 'keyup' && (e as KeyboardEvent).key == 'Enter')) {
|
||||
const iconName = (e.target as UUIIconElement).name;
|
||||
if (iconName) {
|
||||
this.modalContext?.updateValue({ icon: iconName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#onColorChange(e: UUIColorSwatchesEvent) {
|
||||
const colorAlias = e.target.value;
|
||||
this.modalContext?.updateValue({ color: colorAlias });
|
||||
this._currentColor = colorAlias;
|
||||
}
|
||||
|
||||
render() {
|
||||
// TODO: Missing localization in general. [NL]
|
||||
return html`
|
||||
<umb-body-layout headline="Select Icon">
|
||||
<div id="container">
|
||||
${this.renderSearchbar()}
|
||||
${this.renderSearch()}
|
||||
<hr />
|
||||
<uui-color-swatches
|
||||
.value=${this._currentAlias}
|
||||
.value=${this._currentColor}
|
||||
label="Color switcher for icons"
|
||||
@change=${this.#onColorChange}>
|
||||
${
|
||||
// TODO: Missing translation for the color aliases.
|
||||
// TODO: Missing localization for the color aliases. [NL]
|
||||
this._colorList.map(
|
||||
(color) => html`
|
||||
<uui-color-swatch
|
||||
@@ -88,7 +104,7 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
|
||||
}
|
||||
</uui-color-swatches>
|
||||
<hr />
|
||||
<uui-scroll-container id="icon-selection">${this.renderIconSelection()}</uui-scroll-container>
|
||||
<uui-scroll-container id="icons">${this.renderIcons()}</uui-scroll-container>
|
||||
</div>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
@@ -104,36 +120,37 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
|
||||
`;
|
||||
}
|
||||
|
||||
renderSearchbar() {
|
||||
renderSearch() {
|
||||
return html` <uui-input
|
||||
type="search"
|
||||
placeholder=${this.localize.term('placeholders_filter')}
|
||||
label=${this.localize.term('placeholders_filter')}
|
||||
id="searchbar"
|
||||
id="search"
|
||||
@keyup="${this.#filterIcons}"
|
||||
${umbFocus()}>
|
||||
<uui-icon name="search" slot="prepend" id="searchbar_icon"></uui-icon>
|
||||
<uui-icon name="search" slot="prepend" id="search_icon"></uui-icon>
|
||||
</uui-input>`;
|
||||
}
|
||||
|
||||
renderIconSelection() {
|
||||
return repeat(
|
||||
this._iconListFiltered,
|
||||
(icon) => icon.name,
|
||||
(icon) => html`
|
||||
<uui-icon
|
||||
tabindex="0"
|
||||
style="--uui-icon-color: var(${extractUmbColorVariable(this._currentAlias)})"
|
||||
class="icon ${icon.name === this._modalValue?.icon ? 'selected' : ''}"
|
||||
title="${icon.name}"
|
||||
name="${icon.name}"
|
||||
label="${icon.name}"
|
||||
id="${icon.name}"
|
||||
@click="${this.#changeIcon}"
|
||||
@keyup="${this.#changeIcon}">
|
||||
</uui-icon>
|
||||
`,
|
||||
);
|
||||
renderIcons() {
|
||||
return this._iconsFiltered
|
||||
? repeat(
|
||||
this._iconsFiltered,
|
||||
(icon) => icon.name,
|
||||
(icon) => html`
|
||||
<uui-button
|
||||
label="${icon.name}"
|
||||
class="${icon.name === this._currentIcon ? 'selected' : ''}"
|
||||
@click="${this.#changeIcon}"
|
||||
@keyup="${this.#changeIcon}">
|
||||
<uui-icon
|
||||
style="--uui-icon-color: var(${extractUmbColorVariable(this._currentColor)})"
|
||||
name="${icon.name}">
|
||||
</uui-icon>
|
||||
</uui-button>
|
||||
`,
|
||||
)
|
||||
: nothing;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
@@ -160,15 +177,15 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
#search {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
#searchbar_icon {
|
||||
#search_icon {
|
||||
padding-left: var(--uui-size-space-2);
|
||||
}
|
||||
|
||||
#icon-selection {
|
||||
#icons {
|
||||
line-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(40px, calc((100% / 12) - 10px)));
|
||||
@@ -179,27 +196,17 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#icon-selection .icon {
|
||||
display: inline-block;
|
||||
#icons uui-button {
|
||||
border-radius: var(--uui-border-radius);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--uui-size-space-3);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 16px; /* specific for icons */
|
||||
}
|
||||
|
||||
#icon-selection .icon-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#icon-selection .icon:focus,
|
||||
#icon-selection .icon:hover,
|
||||
#icon-selection .icon.selected {
|
||||
#icons uui-button:focus,
|
||||
#icons uui-button:hover,
|
||||
#icons uui-button.selected {
|
||||
outline: 2px solid var(--uui-color-selected);
|
||||
}
|
||||
|
||||
uui-button {
|
||||
uui-button[slot='actions'] {
|
||||
margin-left: var(--uui-size-space-4);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
export const umbracoColors = [
|
||||
{ alias: 'text', varName: '--uui-color-text' },
|
||||
{ alias: 'black', varName: '--uui-color-text' },
|
||||
{ alias: 'yellow', varName: '--uui-palette-sunglow' },
|
||||
{ alias: 'pink', varName: '--uui-palette-spanish-pink' },
|
||||
{ alias: 'dark', varName: '--uui-palette-gunmetal' },
|
||||
{ alias: 'darkblue', varName: '--uui-palette-space-cadet' },
|
||||
{ alias: 'blue', varName: '--uui-palette-violet-blue' },
|
||||
{ alias: 'light-blue', varName: '--uui-palette-malibu' },
|
||||
{ alias: 'red', varName: '--uui-palette-maroon-flush' },
|
||||
{ alias: 'green', varName: '--uui-palette-jungle-green' },
|
||||
{ alias: 'brown', varName: '--uui-palette-chamoisee' },
|
||||
{ alias: 'grey', varName: '--uui-palette-dusty-grey' },
|
||||
|
||||
{ alias: 'blue-grey', legacy: true, varName: '--uui-palette-dusty-grey' },
|
||||
{ alias: 'indigo', legacy: true, varName: '--uui-palette-malibu' },
|
||||
{ alias: 'purple', legacy: true, varName: '--uui-palette-space-cadet' },
|
||||
{ alias: 'deep-purple', legacy: true, varName: '--uui-palette-space-cadet' },
|
||||
{ alias: 'cyan', legacy: true, varName: '-uui-palette-jungle-green' },
|
||||
{ alias: 'light-green', legacy: true, varName: '-uui-palette-jungle-green' },
|
||||
{ alias: 'lime', legacy: true, varName: '-uui-palette-jungle-green' },
|
||||
{ alias: 'amber', legacy: true, varName: '--uui-palette-chamoisee' },
|
||||
{ alias: 'orange', legacy: true, varName: '--uui-palette-chamoisee' },
|
||||
{ alias: 'deep-orange', legacy: true, varName: '--uui-palette-cocoa-brown' },
|
||||
];
|
||||
|
||||
export function extractUmbColorVariable(colorAlias: string): string | undefined {
|
||||
|
||||
@@ -86,7 +86,7 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
|
||||
(app) => app.component,
|
||||
)}
|
||||
</umb-section-sidebar>
|
||||
`
|
||||
`
|
||||
: nothing}
|
||||
<umb-section-main>
|
||||
${this._routes && this._routes.length > 0
|
||||
@@ -105,10 +105,6 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding: var(--uui-size-4) var(--uui-size-8);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export class UmbSectionMainViewElement extends UmbLitElement {
|
||||
</umb-router-slot>
|
||||
</umb-body-layout>
|
||||
`
|
||||
: html`${nothing}`;
|
||||
: nothing;
|
||||
}
|
||||
|
||||
#renderDashboards() {
|
||||
@@ -117,7 +117,7 @@ export class UmbSectionMainViewElement extends UmbLitElement {
|
||||
})}
|
||||
</uui-tab-group>
|
||||
`
|
||||
: '';
|
||||
: nothing;
|
||||
}
|
||||
|
||||
#renderViews() {
|
||||
@@ -140,7 +140,7 @@ export class UmbSectionMainViewElement extends UmbLitElement {
|
||||
})}
|
||||
</uui-tab-group>
|
||||
`
|
||||
: '';
|
||||
: nothing;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
|
||||
Reference in New Issue
Block a user