mirror of https://github.com/qortal/qortal-ui
AlphaX-Projects
3 years ago
committed by
GitHub
3 changed files with 441 additions and 0 deletions
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
`;
|
@ -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` |
||||||
|
<a id="itemLink"
|
||||||
|
level=${this._getLevel}
|
||||||
|
href=${this.href || '#!'} |
||||||
|
@click="${(e) => this._onClick(e)}" |
||||||
|
target=${ifDefined(this.target)} |
||||||
|
> |
||||||
|
<slot class="icon" name="icon"></slot> |
||||||
|
<div id ="content"> |
||||||
|
<span>${this.label}</span> |
||||||
|
</div> |
||||||
|
</a> |
||||||
|
` |
||||||
|
} |
||||||
|
|
||||||
|
_tooltipTemplate() { |
||||||
|
return html` |
||||||
|
${this._getLevel === 0 && this.compact ? html` |
||||||
|
<paper-tooltip for="itemLink" position="right" animation-delay="0"> |
||||||
|
${this.label} |
||||||
|
</paper-tooltip> |
||||||
|
` |
||||||
|
: undefined} |
||||||
|
` |
||||||
|
} |
||||||
|
|
||||||
|
_childrenTemplate() { |
||||||
|
return html` |
||||||
|
${this.expanded ? html` |
||||||
|
${this.compact ? html` |
||||||
|
<div id="overlay"><slot></slot></div> |
||||||
|
` |
||||||
|
: html` |
||||||
|
<slot></slot> |
||||||
|
`}
|
||||||
|
` |
||||||
|
: 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); |
@ -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` |
||||||
|
<nav @side-menu-item-select=${this._handleSelect}> |
||||||
|
<slot></slot> |
||||||
|
</nav> |
||||||
|
` |
||||||
|
} |
||||||
|
|
||||||
|
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); |
Loading…
Reference in new issue