diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts deleted file mode 100644 index 6ae9e77ab9..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts +++ /dev/null @@ -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` -
-
- -
- -
- -
-
-
-
-
Document Types
- -
-
-
Media Types
- -
-
- `; - } -} - -export default UmbSearchElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-search': UmbSearchElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts new file mode 100644 index 0000000000..ce3e8be0fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts @@ -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; +}; +@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 = []; + + 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 = 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); + + this._groups = grouped; + } + + render() { + return html` +
+
+ +
+ +
+ +
+
+ ${this._search + ? html`
+ ${this._groups.length > 0 + ? repeat( + this._groups, + (group) => group.name, + (group) => this.#renderGroup(group.name, group.items) + ) + : html`
Only mock data for now Search for blog
`} +
` + : nothing} + `; + } + + #renderGroup(name: string, items: Array) { + return html` +
+
${name}
+
${repeat(items, (item) => item.name, this.#renderItem.bind(this))}
+
+ `; + } + + #renderItem(item: SearchItem) { + return html` + + + ${item.icon ? html`` : this.#renderHashTag()} + + ${item.name} + > + + `; + } + + #renderHashTag() { + return html` + + + + + `; + } + + #mockData: Array = [ + { + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts index f19a149ac7..51332235b3 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts @@ -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