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