add modal layout search
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user