standard-ui/components/dist/select.js
2021-08-06 15:47:03 +05:30

402 lines
11 KiB
JavaScript

const smSelect = document.createElement('template')
smSelect.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
--accent-color: #4d2588;
--text-color: 17, 17, 17;
--background-color: 255, 255, 255;
--max-height: auto;
--min-width: 100%;
}
:host([disabled]) .select{
opacity: 0.6;
cursor: not-allowed;
}
.hide{
display: none !important;
}
.select{
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
cursor: pointer;
width: 100%;
-webkit-tap-highlight-color: transparent;
}
.icon {
height: 1.5rem;
width: 1.5rem;
fill: rgba(var(--text-color), 0.7);
}
.selected-option-text{
font-size: 0.9rem;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
}
.selection{
border-radius: 0.3rem;
display: -ms-grid;
display: grid;
-ms-grid-columns: 1fr auto;
grid-template-columns: 1fr auto;
grid-template-areas: 'heading heading' '. .';
padding: 0.4rem 1rem;
background: rgba(var(--text-color), 0.06);
border: solid 1px rgba(var(--text-color), 0.2);
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
outline: none;
}
.selection:focus{
-webkit-box-shadow: 0 0 0 0.1rem var(--accent-color);
box-shadow: 0 0 0 0.1rem var(--accent-color)
}
.icon{
margin-left: 1rem;
}
:host([align-select="left"]) .options{
left: 0;
}
:host([align-select="right"]) .options{
right: 0;
}
.options{
top: 100%;
margin-top: 0.2rem;
overflow: hidden auto;
position: absolute;
grid-area: options;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
min-width: var(--min-width);
max-height: var(--max-height);
background: rgba(var(--background-color), 1);
border: solid 1px rgba(var(--text-color), 0.2);
border-radius: 0.3rem;
z-index: 2;
-webkit-box-shadow: 0.4rem 0.8rem 1.2rem #00000030;
box-shadow: 0.4rem 0.8rem 1.2rem #00000030;
}
.rotate{
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg)
}
@media (any-hover: hover){
::-webkit-scrollbar{
width: 0.5rem;
height: 0.5rem;
}
::-webkit-scrollbar-thumb{
background: rgba(var(--text-color), 0.3);
border-radius: 1rem;
&:hover{
background: rgba(var(--text-color), 0.5);
}
}
}
</style>
<div class="select" >
<div class="selection">
<div class="selected-option-text"></div>
<svg class="icon toggle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z"/></svg>
</div>
<div part="options" class="options hide">
<slot></slot>
</div>
</div>`;
customElements.define('sm-select', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smSelect.content.cloneNode(true))
this.reset = this.reset.bind(this)
this.open = this.open.bind(this)
this.collapse = this.collapse.bind(this)
this.toggle = this.toggle.bind(this)
this.handleOptionsNavigation = this.handleOptionsNavigation.bind(this)
this.handleOptionSelection = this.handleOptionSelection.bind(this)
this.handleKeydown = this.handleKeydown.bind(this)
this.handleClickOutside = this.handleClickOutside.bind(this)
this.availableOptions
this.previousOption
this.isOpen = false;
this.slideDown = [{
transform: `translateY(-0.5rem)`,
opacity: 0
},
{
transform: `translateY(0)`,
opacity: 1
}
]
this.slideUp = [{
transform: `translateY(0)`,
opacity: 1
},
{
transform: `translateY(-0.5rem)`,
opacity: 0
}
]
this.animationOptions = {
duration: 300,
fill: "forwards",
easing: 'ease'
}
this.optionList = this.shadowRoot.querySelector('.options')
this.chevron = this.shadowRoot.querySelector('.toggle')
this.selection = this.shadowRoot.querySelector('.selection')
this.selectedOptionText = this.shadowRoot.querySelector('.selected-option-text')
}
static get observedAttributes() {
return ['value', 'disabled']
}
get value() {
return this.getAttribute('value')
}
set value(val) {
this.setAttribute('value', val)
}
reset(fire = true) {
if (this.availableOptions[0] && this.previousOption !== this.availableOptions[0]) {
const firstElement = this.availableOptions[0];
if (this.previousOption) {
this.previousOption.classList.remove('check-selected')
}
firstElement.classList.add('check-selected')
this.value = firstElement.getAttribute('value')
this.selectedOptionText.textContent = firstElement.textContent
this.previousOption = firstElement;
if (fire) {
this.fireEvent()
}
}
}
open() {
this.optionList.classList.remove('hide')
this.optionList.animate(this.slideDown, this.animationOptions)
this.chevron.classList.add('rotate')
this.isOpen = true
}
collapse() {
this.chevron.classList.remove('rotate')
this.optionList.animate(this.slideUp, this.animationOptions)
.onfinish = () => {
this.optionList.classList.add('hide')
this.isOpen = false
}
}
toggle() {
if (!this.isOpen && !this.hasAttribute('disabled')) {
this.open()
} else {
this.collapse()
}
}
fireEvent() {
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: {
value: this.value
}
}))
}
handleOptionsNavigation(e) {
if (e.code === 'ArrowUp') {
e.preventDefault()
if (document.activeElement.previousElementSibling) {
document.activeElement.previousElementSibling.focus()
} else {
this.availableOptions[this.availableOptions.length - 1].focus()
}
}
else if (e.code === 'ArrowDown') {
e.preventDefault()
if (document.activeElement.nextElementSibling) {
document.activeElement.nextElementSibling.focus()
} else {
this.availableOptions[0].focus()
}
}
}
handleOptionSelection(e) {
if (this.previousOption !== document.activeElement) {
this.value = document.activeElement.getAttribute('value')
this.selectedOptionText.textContent = document.activeElement.textContent;
this.fireEvent()
if (this.previousOption) {
this.previousOption.classList.remove('check-selected')
}
document.activeElement.classList.add('check-selected')
this.previousOption = document.activeElement
}
}
handleClick(e) {
if (e.target === this) {
this.toggle()
}
else {
this.handleOptionSelection()
this.collapse()
}
}
handleKeydown(e) {
if (e.target === this) {
if (this.isOpen && e.code === 'ArrowDown') {
e.preventDefault()
this.availableOptions[0].focus()
this.handleOptionSelection(e)
}
else if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
this.toggle()
}
}
else {
this.handleOptionsNavigation(e)
this.handleOptionSelection(e)
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
this.collapse()
}
}
}
handleClickOutside(e) {
if (this.isOpen && !this.contains(e.target)) {
this.collapse()
}
}
connectedCallback() {
this.setAttribute('role', 'listbox')
if (!this.hasAttribute('disabled')) {
this.selection.setAttribute('tabindex', '0')
}
let slot = this.shadowRoot.querySelector('slot')
slot.addEventListener('slotchange', e => {
this.availableOptions = slot.assignedElements()
this.reset(false)
});
this.addEventListener('click', this.handleClick)
this.addEventListener('keydown', this.handleKeydown)
document.addEventListener('mousedown', this.handleClickOutside)
}
disconnectedCallback() {
this.removeEventListener('click', this.toggle)
this.removeEventListener('keydown', this.handleKeydown)
document.removeEventListener('mousedown', this.handleClickOutside)
}
attributeChangedCallback(name) {
if (name === "disabled") {
if (this.hasAttribute('disabled')) {
this.selection.removeAttribute('tabindex')
} else {
this.selection.setAttribute('tabindex', '0')
}
}
}
})
// option
const smOption = document.createElement('template')
smOption.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.option{
display: grid;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
min-width: 100%;
gap: 0.5rem;
grid-template-columns: max-content minmax(0, 1fr);
padding: 0.8rem 1.2rem;
cursor: pointer;
overflow-wrap: break-word;
outline: none;
user-select: none;
}
:host(:focus){
outline: none;
background: rgba(var(--text-color), 0.1);
}
.icon {
opacity: 0;
height: 1.2rem;
width: 1.2rem;
fill: rgba(var(--text-color), 0.8);
}
:host(:focus) .option .icon{
opacity: 0.4
}
:host(.check-selected) .icon{
opacity: 1
}
@media (hover: hover){
.option:hover{
background: rgba(var(--text-color), 0.1);
}
:host(:not(.check-selected):hover) .icon{
opacity: 0.4
}
}
</style>
<div class="option">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>
<slot></slot>
</div>`;
customElements.define('sm-option', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smOption.content.cloneNode(true))
}
connectedCallback() {
this.setAttribute('role', 'option')
this.setAttribute('tabindex', '0')
}
})