diff --git a/qortal-ui-core/src/functional-components/side-menu-item-style.js b/qortal-ui-core/src/functional-components/side-menu-item-style.js new file mode 100644 index 00000000..aec5bf0b --- /dev/null +++ b/qortal-ui-core/src/functional-components/side-menu-item-style.js @@ -0,0 +1,153 @@ +import { css } from 'lit' + +export const sideMenuItemStyle = css` + :host { + --font-family: "Roboto", sans-serif; + --item-font-size: 1rem; + --sub-item-font-size: 0.85rem; + --item-padding: 1rem; + --item-content-padding: 1rem; + --icon-height: 1.25rem; + --icon-width: 1.25rem; + --item-border-radius: 5px; + --item-selected-color: #dddddd; + --item-selected-color-text: #333333; + --item-color-active: #d1d1d1; + --item-color-hover: #eeeeee; + --item-text-color: #080808; + --item-icon-color: #080808; + --item-border-color: #eeeeee; + --item-border-selected-color: #333333; + + --overlay-box-shadow: 0 2px 4px -1px hsla(214, 53%, 23%, 0.16), 0 3px 12px -1px hsla(214, 50%, 22%, 0.26); + --overlay-background-color: #ffffff; + + --spacing: 4px; + + font-family: var(--font-family); + display: flex; + overflow: hidden; + flex-direction: column; + border-radius: var(--item-border-radius); + } + + #itemLink { + align-items: center; + font-size: var(--item-font-size); + font-weight: 400; + height: var(--icon-height); + transition: background-color 200ms; + padding: var(--item-padding); + cursor: pointer; + display: inline-flex; + flex-grow: 1; + align-items: center; + overflow: hidden; + text-decoration: none; + border-bottom: 1px solid var(--item-border-color); + } + + #itemLink:hover { + background-color: var(--item-color-hover); + } + + #itemLink:active { + background-color: var(--item-color-active); + } + + #content { + padding-left: var(--item-content-padding); + flex: 1; + } + + :host([compact]) #content { + padding-left: 0; + display: none; + } + + :host([selected]) #itemLink { + background-color: var(--item-selected-color); + color: var(--item-selected-color-text); + border-left: 3px solid var(--item-border-selected-color); + } + + :host([selected]) slot[name="icon"]::slotted(*) { + color: var(--item-selected-color-text); + } + + :host(:not([selected])) #itemLink{ + color: var(--item-text-color); + } + + :host([expanded]){ + background-color: var(--item-selected-color); + } + + :host([hasSelectedChild]){ + background-color: var(--item-selected-color); + } + + :host span { + cursor: inherit; + overflow: hidden; + text-overflow: ellipsis; + user-select: none; + -webkit-user-select: none; + white-space: nowrap; + } + + slot[name="icon"]::slotted(*) { + flex-shrink: 0; + color: var(--item-icon-color); + height: var(--icon-height); + width: var(--icon-width); + pointer-events: none; + } + + #collapse-button { + float: right; + } + + :host([compact]) #itemLink[level]:not([level="0"]) { + padding: calc( var(--item-padding) / 2); + } + + :host(:not([compact])) #itemLink[level]:not([level="0"]) { + padding-left: calc(var(--icon-width) + var(--item-content-padding)); + } + + #itemLink[level]:not([level="0"]) #content { + display: block; + visibility: visible; + width: auto; + font-weight: 400; + font-size: var(--sub-item-font-size) + } + + #overlay { + display: block; + left: 101%; + min-width: 200px; + padding: 4px 2px; + background-color: var(--overlay-background-color); + background-image: var(--overlay-background-image, none); + box-shadow: var(--overlay-box-shadow); + border: 1px solid var(--overlay-background-color); + border-left: 0; + border-radius: 0 3px 3px 0; + position: absolute; + z-index: 1; + animation: pop 200ms forwards; + } + + @keyframes pop{ + 0% { + transform: translateX(-5px); + opacity: 0.5; + } + 100% { + transform: translateX(0); + opacity: 1; + } + } +`; diff --git a/qortal-ui-core/src/functional-components/side-menu-item.js b/qortal-ui-core/src/functional-components/side-menu-item.js new file mode 100644 index 00000000..8c869e5c --- /dev/null +++ b/qortal-ui-core/src/functional-components/side-menu-item.js @@ -0,0 +1,210 @@ +import { LitElement, html, css } from 'lit' +import { ifDefined } from 'lit/directives/if-defined.js' +import { sideMenuItemStyle } from './side-menu-item-style.js' +import '@vaadin/icon' +import '@vaadin/icons' +import '@polymer/paper-tooltip' + +export class SideMenuItem extends LitElement { + static get properties() { + return { + selected: { type: Boolean, reflect: true }, + label: { type: String, reflect: true }, + expanded: { type: Boolean, reflect: true }, + compact: { type: Boolean, reflect: true }, + href: { type: String, reflect: true }, + target: { type: String, reflect: true } + } + } + + static get styles() { + return css` + ${sideMenuItemStyle} + ` + } + + constructor() { + super() + this.selected = false + this.expanded = false + } + + render() { + return html` + ${this._itemLinkTemplate()} ${this._tooltipTemplate()} + ${this._childrenTemplate()} + ` + } + + firstUpdated(changedProperties) { + if (!this.hasChildren()) { + return + } + this.collapseExpandIcon = document.createElement("vaadin-icon") + this.collapseExpandIcon.id = "collapse-button" + this.shadowRoot.getElementById("content").appendChild(this.collapseExpandIcon) + this._boundOutsideClickListener = this._outsideClickListener.bind(this) + } + + _itemLinkTemplate() { + return html` + + +
+ ${this.label} +
+
+ ` + } + + _tooltipTemplate() { + return html` + ${this._getLevel === 0 && this.compact ? html` + + ${this.label} + + ` + : undefined} + ` + } + + _childrenTemplate() { + return html` + ${this.expanded ? html` + ${this.compact ? html` +
+ ` + : html` + + `} + ` + : undefined} + ` + } + + updated(changedProperties) { + changedProperties.forEach((oldValue, propName) => { + if (propName === "compact") { + this._onCompactChanged() + } + + if (propName === "expanded") { + this._onExpandedChanged() + } + + if (propName === "selected"){ + if (oldValue === this.selected){ + return + } + + if (this.selected) { + this._changeSelectedState(true) + this._markParentWithSelectedChild() + } + } + }); + } + + _onCompactChanged() { + this.expanded = false; + + if (this.collapseExpandIcon == null) { + return; + } + + if (!this.compact) { + this.collapseExpandIcon["icon"] = "vaadin:chevron-down-small" + } else { + this.collapseExpandIcon["icon"] = "vaadin:chevron-right-small" + } + } + + _onExpandedChanged() { + if (this.collapseExpandIcon == null) { + return; + } + + if (this.expanded) { + this._onHandleExpanded(); + } else { + this._onHandleCollapsed(); + } + } + + _onHandleExpanded() { + if (!this.compact) { + this.collapseExpandIcon["icon"] = "vaadin:chevron-up-small" + } else { + this.collapseExpandIcon["icon"] = "vaadin:chevron-left-small" + document.addEventListener("click", this._boundOutsideClickListener, true) + } + } + + _onHandleCollapsed() { + if (!this.compact) { + this.collapseExpandIcon["icon"] = "vaadin:chevron-down-small" + } else { + this.collapseExpandIcon["icon"] = "vaadin:chevron-right-small" + document.removeEventListener( + "click", + this._boundOutsideClickListener, + true + ) + } + } + + _onClick(e) { + if (!this.hasChildren()) { + this.selected = true + } else { + this.expanded = !this.expanded + e.preventDefault() + } + } + + _outsideClickListener(event) { + const eventPath = event.composedPath() + if (eventPath.indexOf(this) < 0) { + this.expanded = false + } + } + + _changeSelectedState(selected, sourceEvent) { + this.selected = selected + let evt = new CustomEvent("side-menu-item-select", { + bubbles: true, + cancelable: true, + detail: { sourceEvent: sourceEvent } + }); + this.dispatchEvent(evt) + } + + hasChildren() { + return !!this.querySelector("side-menu-item") + } + + _markParentWithSelectedChild() { + let element = this.parentElement; + while (element instanceof SideMenuItem) { + element.setAttribute('hasSelectedChild', true) + element = element.parentElement; + } + } + + get _getLevel() { + let level = 0 + let element = this.parentElement + while (element instanceof SideMenuItem) { + level++; + element = element.parentElement + } + return level + } +} + +window.customElements.define("side-menu-item", SideMenuItem); diff --git a/qortal-ui-core/src/functional-components/side-menu.js b/qortal-ui-core/src/functional-components/side-menu.js new file mode 100644 index 00000000..c3b08c24 --- /dev/null +++ b/qortal-ui-core/src/functional-components/side-menu.js @@ -0,0 +1,78 @@ +import {LitElement, html, css} from 'lit' + +class SideMenu extends LitElement { + static get properties() { + return { + items: {type: Array}, + selectedValue: {type: String, reflect: true}, + compact: {type: Boolean, reflect: true} + } + } + + static get styles() { + return css` + nav { + padding: 0; + } + + :host { + list-style: none; + width: 100%; + position: relative; + } + + :host([compact]) { + width: auto; + } + ` + } + + constructor() { + super() + this.compact = false + } + + render() { + return html` + + ` + } + + firstUpdated(_changedProperties) { + this.items = [...this.querySelectorAll("side-menu-item")] + } + + _handleSelect(event) { + let targetItem = event.target + this._deselectAllItems() + targetItem.selected = true + this.selectedValue = targetItem.label + } + + _deselectAllItems() { + this.items.forEach(element => { + if (this.compact) { + element.expanded = false + } + element.selected = false + element.hasChildren() ? element.removeAttribute('hasSelectedChild') : undefined + }); + } + + updated(changedProperties) { + changedProperties.forEach((oldValue, propName) => { + if (propName === "compact") { + this.items.forEach(item => (item.compact = this.compact)) + let evt = new CustomEvent("side-menu-compact-change", { + bubbles: true, + cancelable: true + }) + this.dispatchEvent(evt) + } + }) + } +} + +window.customElements.define("side-menu", SideMenu);