add modal layout search

This commit is contained in:
Jesper Møller Jensen
2023-02-23 16:47:06 +13:00
parent 9a6b082cbd
commit 81bba10eeb
3 changed files with 329 additions and 157 deletions

View File

@@ -1,156 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-search')
export class UmbSearchElement extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
height: 100%;
background-color: var(--uui-color-background);
box-sizing: border-box;
color: var(--uui-color-text);
font-size: 1rem;
}
input {
all: unset;
height: 100%;
width: 100%;
}
#search-icon,
#close-icon {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
height: 100%;
}
#close-icon > button {
background: var(--uui-color-surface-alt);
border: 1px solid var(--uui-color-border);
padding: 3px 6px 4px 6px;
line-height: 1;
border-radius: 3px;
color: var(--uui-color-text-alt);
font-weight: 800;
font-size: 12px;
}
#close-icon > button:hover {
border-color: var(--uui-color-focus);
color: var(--uui-color-focus);
}
#top {
background-color: var(--uui-color-surface);
display: flex;
height: 48px;
border-bottom: 1px solid var(--uui-color-border);
}
#main {
display: flex;
flex-direction: column;
padding: 0 32px 16px 32px;
}
.group {
margin-top: var(--uui-size-space-4);
}
.group-name {
font-weight: 600;
margin-bottom: var(--uui-size-space-1);
}
.results {
display: flex;
flex-direction: column;
gap: 8px;
}
.result {
background: var(--uui-color-surface);
border: 1px solid var(--uui-color-border);
padding: var(--uui-size-space-3) var(--uui-size-space-4);
border-radius: var(--uui-border-radius);
color: var(--uui-color-interactive);
cursor: pointer;
justify-content: space-between;
display: flex;
}
.result:hover {
background-color: var(--uui-color-surface-emphasis);
color: var(--uui-color-interactive-emphasis);
}
.result:hover span {
font-weight: unset;
opacity: unset;
}
a {
text-decoration: none;
color: inherit;
}
a span {
opacity: 0.5;
font-weight: 100;
}
`,
];
connectedCallback() {
super.connectedCallback();
requestAnimationFrame(() => {
this.shadowRoot?.querySelector('input')?.focus();
});
}
render() {
return html`
<div id="top">
<div id="search-icon">
<uui-icon name="search"></uui-icon>
</div>
<input type="text" placeholder="Search..." autocomplete="off" />
<div id="close-icon">
<button>esc</button>
</div>
</div>
<div id="main">
<div class="group">
<div class="group-name">Document Types</div>
<div class="results">
<a href="#" class="result">
<div class="result-left">
<span class="result-icon">#</span>
Article Controls
</div>
<span>></span>
</a>
<a href="#" class="result">
Article
<span>></span>
</a>
</div>
</div>
<div class="group">
<div class="group-name">Media Types</div>
<div class="results">
<a href="#" class="result">
Article
<span>></span>
</a>
</div>
</div>
</div>
`;
}
}
export default UmbSearchElement;
declare global {
interface HTMLElementTagNameMap {
'umb-search': UmbSearchElement;
}
}

View File

@@ -0,0 +1,292 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, LitElement, nothing } from 'lit';
import { repeat } from 'lit-html/directives/repeat.js';
import { customElement, query, state } from 'lit/decorators.js';
export type SearchItem = {
name: string;
icon?: string;
href: string;
parent: string;
};
export type SearchGroupItem = {
name: string;
items: Array<SearchItem>;
};
@customElement('umb-modal-layout-search')
export class UmbModalLayoutSearchElement extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
height: 100%;
background-color: var(--uui-color-background);
box-sizing: border-box;
color: var(--uui-color-text);
font-size: 1rem;
}
input {
all: unset;
height: 100%;
width: 100%;
}
#search-icon,
#close-icon {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
height: 100%;
}
#close-icon {
padding: 0 var(--uui-size-space-3);
}
#close-icon > button {
background: var(--uui-color-surface-alt);
border: 1px solid var(--uui-color-border);
padding: 3px 6px 4px 6px;
line-height: 1;
border-radius: 3px;
color: var(--uui-color-text-alt);
font-weight: 800;
font-size: 12px;
cursor: pointer;
}
#close-icon > button:hover {
border-color: var(--uui-color-focus);
color: var(--uui-color-focus);
}
#top {
background-color: var(--uui-color-surface);
display: flex;
height: 48px;
border-bottom: 1px solid var(--uui-color-border);
}
#main {
display: flex;
flex-direction: column;
padding: 0px var(--uui-size-space-6) var(--uui-size-space-5) var(--uui-size-space-6);
height: 100%;
}
.group {
margin-top: var(--uui-size-space-4);
}
.group-name {
font-weight: 600;
margin-bottom: var(--uui-size-space-1);
}
.group-items {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-3);
}
.item {
background: var(--uui-color-surface);
border: 1px solid var(--uui-color-border);
padding: var(--uui-size-space-3) var(--uui-size-space-4);
border-radius: var(--uui-border-radius);
color: var(--uui-color-interactive);
display: grid;
grid-template-columns: var(--uui-size-space-6) 1fr var(--uui-size-space-5);
height: min-content;
align-items: center;
}
.item:hover {
background-color: var(--uui-color-surface-emphasis);
color: var(--uui-color-interactive-emphasis);
}
.item:hover .item-symbol {
font-weight: unset;
opacity: 1;
}
.item-icon,
.item-symbol {
opacity: 0.4;
}
.item-icon > * {
height: 1rem;
display: flex;
width: min-content;
}
.item-symbol {
font-weight: 100;
}
a {
text-decoration: none;
color: inherit;
}
#no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
margin-top: var(--uui-size-space-5);
color: var(--uui-color-text-alt);
}
`,
];
@query('input')
private _input!: HTMLInputElement;
@state()
private _search = '';
@state()
private _groups: Array<SearchGroupItem> = [];
connectedCallback() {
super.connectedCallback();
requestAnimationFrame(() => {
this._input.focus();
});
}
#onSearchChange(event: InputEvent) {
const target = event.target as HTMLInputElement;
this._search = target.value;
this.#updateGroups();
}
#onClearSearch() {
this._search = '';
this._input.value = '';
this._input.focus();
this.#updateGroups();
}
#updateGroups() {
const filtered = this.#mockData.filter((item) => {
return item.name.toLowerCase().includes(this._search.toLowerCase());
});
const grouped: Array<SearchGroupItem> = filtered.reduce((acc, item) => {
const group = acc.find((group) => group.name === item.parent);
if (group) {
group.items.push(item);
} else {
acc.push({
name: item.parent,
items: [item],
});
}
return acc;
}, [] as Array<SearchGroupItem>);
this._groups = grouped;
}
render() {
return html`
<div id="top">
<div id="search-icon">
<uui-icon name="search"></uui-icon>
</div>
<input
value=${this._search}
@input=${this.#onSearchChange}
type="text"
placeholder="Search..."
autocomplete="off" />
<div id="close-icon">
<button @click=${this.#onClearSearch}>clear</button>
</div>
</div>
${this._search
? html`<div id="main">
${this._groups.length > 0
? repeat(
this._groups,
(group) => group.name,
(group) => this.#renderGroup(group.name, group.items)
)
: html`<div id="no-results">Only mock data for now <strong>Search for blog</strong></div>`}
</div>`
: nothing}
`;
}
#renderGroup(name: string, items: Array<SearchItem>) {
return html`
<div class="group">
<div class="group-name">${name}</div>
<div class="group-items">${repeat(items, (item) => item.name, this.#renderItem.bind(this))}</div>
</div>
`;
}
#renderItem(item: SearchItem) {
return html`
<a href="${item.href}" class="item">
<span class="item-icon">
${item.icon ? html`<uui-icon name="${item.icon}"></uui-icon>` : this.#renderHashTag()}
</span>
<span class="item-name">${item.name}</span>
<span class="item-symbol">></span>
</a>
`;
}
#renderHashTag() {
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M7.784 14l.42-4H4V8h4.415l.525-5h2.011l-.525 5h3.989l.525-5h2.011l-.525 5H20v2h-3.784l-.42 4H20v2h-4.415l-.525 5h-2.011l.525-5H9.585l-.525 5H7.049l.525-5H4v-2h3.784zm2.011 0h3.99l.42-4h-3.99l-.42 4z" />
</svg>
`;
}
#mockData: Array<SearchItem> = [
{
name: 'Blog',
href: '#',
icon: 'umb:thumbnail-list',
parent: 'Content',
},
{
name: 'Popular blogs',
href: '#',
icon: 'umb:article',
parent: 'Content',
},
{
name: 'Blog hero',
href: '#',
icon: 'umb:picture',
parent: 'Media',
},
{
name: 'Contact form for blog',
href: '#',
parent: 'Document Types',
},
{
name: 'Blog',
href: '#',
parent: 'Document Types',
},
{
name: 'Blog link item',
href: '#',
parent: 'Document Types',
},
];
}
export default UmbModalLayoutSearchElement;
declare global {
interface HTMLElementTagNameMap {
'umb-modal-layout-search': UmbModalLayoutSearchElement;
}
}

View File

@@ -7,6 +7,7 @@ import './layouts/modal-layout-current-user.element';
import './layouts/icon-picker/modal-layout-icon-picker.element';
import './layouts/link-picker/modal-layout-link-picker.element';
import './layouts/basic/modal-layout-basic.element';
import './layouts/search/modal-layout-search.element.ts';
import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
import { BehaviorSubject } from 'rxjs';
@@ -18,8 +19,9 @@ import type { UmbModalPropertyEditorUIPickerData } from './layouts/property-edit
import type { UmbModalMediaPickerData } from './layouts/media-picker/modal-layout-media-picker.element';
import type { UmbModalLinkPickerData } from './layouts/link-picker/modal-layout-link-picker.element';
import { UmbModalHandler } from './modal-handler';
import type { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element';
import { UmbContextToken } from '@umbraco-cms/context-api';
import { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element';
import { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog';
export type UmbModalType = 'dialog' | 'sidebar';
@@ -139,6 +141,40 @@ export class UmbModalService {
});
}
public search(): UmbModalHandler {
const modalHandler = new UmbModalHandler('umb-modal-layout-search');
//TODO START: This is a hack to get the search modal layout to look like i want it to.
//TODO: Remove from here to END when the modal system is more flexible
const topDistance = '128px';
const margin = '16px';
const maxHeight = '600px';
const maxWidth = '500px';
const dialog = document.createElement('dialog') as HTMLDialogElement;
dialog.style.top = `min(${topDistance}, 10vh)`;
dialog.style.margin = '0 auto';
dialog.style.maxHeight = `min(${maxHeight}, calc(100vh - ${margin}))`;
dialog.style.width = `min(${maxWidth}, calc(100vw - ${margin}))`;
dialog.style.boxSizing = 'border-box';
dialog.style.background = 'none';
dialog.style.border = 'none';
dialog.style.padding = '0';
dialog.style.boxShadow = 'var(--uui-shadow-depth-5)';
dialog.style.borderRadius = '9px';
const search = document.createElement('umb-modal-layout-search');
dialog.appendChild(search);
requestAnimationFrame(() => {
dialog.showModal();
});
modalHandler.element = dialog as unknown as UUIModalDialogElement;
//TODO END
modalHandler.element.addEventListener('close-end', () => this._handleCloseEnd(modalHandler));
this.#modals.next([...this.#modals.getValue(), modalHandler]);
return modalHandler;
}
/**
* Opens a modal or sidebar modal
* @public