isu-element
Version:
Polymer components for building web apps.
618 lines (548 loc) • 16.1 kB
JavaScript
import {mixinBehaviors} from "@polymer/polymer/lib/legacy/class";
import {html, PolymerElement} from "@polymer/polymer";
import '@polymer/iron-icon/iron-icon';
import '@polymer/iron-icons/iron-icons';
import '@polymer/iron-icons/social-icons';
import '@polymer/iron-selector/iron-selector';
import './behaviors/base-behavior.js';
import {BaseBehavior} from "./behaviors/base-behavior";
import './behaviors/isu-elements-shared-styles.js';
/**
* `isu-select`
*
* Example:
* ```html
* <isu-select label="球员" placeholder="选择球员" items="[[items]]"></isu-select>
* <isu-select label="球员" placeholder="选择球员" multi items="[[items]]" value="1,2"></isu-select>
*
* <script>
* items = [
* {"label": "梅西", "value": 1},
* {"label": "C罗", "value": 2},
* {"label": "苏亚雷斯", "value": 3},
* {"label": "库蒂尼奥", "value": 4},
* {"label": "特尔斯特根", "value": 5},
* {"label": "保利尼奥", "value": 6},
* {"label": "内马尔", "value": 13}
* ];
* ```
* ## Styling
*
* The following custom properties and mixins are available for styling:
*
* |Custom property | Description | Default|
* |----------------|-------------|----------|
* |`--isu-label` | Mixin applied to the select label | {}
* |`--isu-select-tag` | Mixin applied to the selected tag | {}
* |`--isu-select-tag-deleter` | Mixin applied to the deleter of each tag| {}
* |`--isu-select-tag-cursor` | Mixin applied to the cursor of the select | {}
* |`--isu-select-dropdown` | Mixin applied to the dropdown snippet of the select | {}
*
* @customElement
* @polymer
* @demo demo/isu-select/index.html
*/
class IsuSelect extends mixinBehaviors([BaseBehavior], PolymerElement) {
static get template() {
return html`
<style include="isu-elements-shared-styles">
:host {
display: flex;
width: 300px;
height: 34px;
line-height: 32px;
font-family: var(--isu-ui-font-family), sans-serif;
font-size: var(--isu-ui-font-size);
position: relative;
background: white;
}
#select__container {
flex: 1;
display: flex;
/*height: inherit;*/
border: 1px solid #CCC;
border-radius: 4px;
position: relative;
@apply --isu-select__container;
}
.tags__container {
flex: 1;
position: relative;
display: flex;
text-align: left;
}
.select__container__viewer {
flex: 1;
display: flex;
flex-wrap: nowrap;
}
:host([opened]) #caret {
transform: rotate(180deg);
transition: transform .2s ease-in-out;
}
#caret {
height: inherit;
transition: transform .2s ease-in-out;
color: var(--isu-ui-color_skyblue);
}
#tag-content {
flex: 1;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
overflow-y: auto;
padding: 2px;
}
#tag-content::-webkit-scrollbar, #select-collapse::-webkit-scrollbar {
display: none;
}
.tag {
color: #fff;
background: var(--isu-ui-bg);
border-radius: 4px;
margin: 3px 2px;
padding: 0 4px;
height: 22px;
line-height: 22px;
/*max-width: 200px;*/
display: flex;
font-size: 14px;
white-space: nowrap;
cursor: default;
@apply --isu-select-tag;
}
.tag-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
@apply --isu-select-tag-name;
}
.tag-deleter {
margin-left: 6px;
width: 18px;
color: #fff;
cursor: pointer;
@apply --isu-select-tag-deleter;
}
.tag-deleter:hover {
color: var(--isu-ui-red);
}
.tag-cursor {
font-size: 16px;
line-height: 28px;
height: 28px;
@apply --isu-select-tag-cursor;
border: none;
outline: none;
padding: 0;
margin: 0;
width: 1px;
}
#select-collapse {
-webkit-transition: max-height 200ms ease-in;
-moz-transition: max-height 200ms ease-in;
-ms-transition: max-height 200ms ease-in;
-o-transition: max-height 200ms ease-in;
transition: max-height 200ms ease-in;
max-height: 0;
position: fixed;
overflow-y: auto;
z-index: 99;
margin-top: 1px;
font-size: 14px;
text-align: left;
background-color: #fff;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
-moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
@apply --isu-select-dropdown;
}
#select-collapse[data-collapse-open] {
max-height: 300px;
}
/*#select-collapse[data-collapse-open-top] {*/
/*top: 100%;*/
/*}*/
/*#select-collapse[data-collapse-open-bottom] {*/
/*bottom: 100%;*/
/*}*/
.selector-panel {
display: block;
padding: 5px;
}
.candidate-item {
text-align: left;
padding: 0 8px;
margin-bottom: 1px;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 22px;
line-height: 22px;
cursor: pointer;
}
.candidate-item:not([class*='iron-selected']):hover {
background: var(--isu-ui-bg);
color: #fff
}
.iron-selected {
background: var(--isu-ui-bg);
color: #fff;
}
.iron-selected:hover {
opacity: 0.8;
}
#placeholder[hidden] {
display: none;
}
#placeholder {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
color: #999;
opacity: 1;
padding: 0 6px;
overflow: hidden;
white-space: nowrap;
}
:host([required]) #select__container::after {
content: "*";
color: red;
position: absolute;
left: -10px;
line-height: inherit;
}
:host([data-invalid]) #select__container {
border-color: var(--isu-ui-color_pink);
}
:host([show-all]) {
height: auto
}
</style>
<template is="dom-if" if="[[ toBoolean(label) ]]">
<div class="isu-label">[[label]]</div>
</template>
<div id="select__container">
<div class="select__container__viewer" on-click="_onInputClick">
<div class="tags__container">
<div id="placeholder">[[placeholder]]</div>
<div id="tag-content">
<input class="tag-cursor" id="tag-cursor__-1" data-cursor-index="-1" on-keydown="_updatePressed" autocomplete="off">
<template is="dom-repeat" items="[[ selectedValues ]]">
<div class="tag">
<div class="tag-name" title="[[getValueByKey(item, attrForLabel)]]">
[[getValueByKey(item, attrForLabel)]]
</div>
<iron-icon class="tag-deleter" icon="icons:clear" data-args="[[getValueByKey(item, attrForValue)]]" on-click="_deleteTag"></iron-icon>
</div>
<template is="dom-if" if="[[ isFocus ]]">
<input class="tag-cursor" id="tag-cursor__[[index]]" data-cursor-index$="[[index]]" on-keydown="_updatePressed" autocomplete="off">
</template>
</template>
</div>
</div>
<iron-icon id="caret" icon="icons:expand-more"></iron-icon>
</div>
<div id="select-collapse" on-click="__focusOnLast">
<iron-selector class="selector-panel" multi="[[ multi ]]" selected="{{ selectedItem }}" selected-values="{{ selectedValues }}" attr-for-selected="candidate-item">
<template is="dom-repeat" items="[[items]]">
<div class="candidate-item" candidate-item="[[item]]" title="[[getValueByKey(item, attrForLabel)]]">
[[getValueByKey(item, attrForLabel)]]
</div>
</template>
</iron-selector>
</div>
<div class="prompt-tip__container" data-prompt$="[[prompt]]">
<div class="prompt-tip">
<iron-icon class="prompt-tip-icon" icon="social:sentiment-very-dissatisfied"></iron-icon>
[[prompt]]
</div>
</div>
<div class="mask"></div>
</div>
`;
}
static get properties() {
return {
/**
* The selected value of this select, if `multi` is true,
* the value will join with comma ( `selectedValues.map(selected => selected[this.attrForValue]).join(',')` ).
* @type {String}
*/
value: {
type: String,
notify: true
},
/**
* The selected value objects of this select.
* @type {array}
*/
selectedValues: {
type: Array,
notify: true
},
/**
*
* The candidate selection of this select.
*
* @attribute items
* @type {array}
*/
items: {
type: Array,
value: []
},
/**
* The prompt tip to show when input is invalid.
*
* @attribute items
* @type {array}
*/
prompt: {
type: String
},
selectedItem: {
type: Object,
notify: true
},
/**
*
* @attribute label
* @type {String}
*/
label: {
type: String
},
/**
* The placeholder of the select.
* @type {String}
*/
placeholder: {
type: String
},
/**
* If true, multiple selections are allowed.
* @type {boolean}
* @default false
*/
multi: {
type: Boolean,
value: false
},
opened: {
type: Boolean,
value: false,
reflectToAttribute: true
},
/**
* Set to true, if the selection is required.
* @type {boolean}
* @default false
*/
required: {
type: Boolean,
value: false
},
/**
* Set to true, if the select is readonly.
* @type {boolean}
* @default false
*/
readonly: {
type: Boolean,
value: false
},
/**
* Attribute name for value.
* @type {string}
* @default 'value'
*/
attrForValue: {
type: String,
value: "value"
},
/**
*
* Attribute name for label.
*
* @type {string}
* @default 'label'
*/
attrForLabel: {
type: String,
value: "label"
},
/*
* 判断是否需要最后一个虚拟输入框的焦点
* */
isFocus: Boolean,
/**
* 多选限制选择的个数
*/
multiLimit: Number,
};
}
static get is() {
return "isu-select";
}
static get observers() {
return [
'_valueChanged(value, items)',
'_selectedValuesChanged(selectedValues.splices)',
'selectedItemChanged(selectedItem)',
'getInvalidAttribute(required,value)'
];
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('blur', e => {
this.closeCollapse();
});
this.isFocus = !this.classList.contains('size-selector');
let parent = this.offsetParent;
while (parent) {
parent.addEventListener('scroll', e => {
this.refreshElemPos();
});
parent = parent.offsetParent;
}
}
/**
* 点击事件
*/
_onInputClick(e) {
if (this.multiLimit && this.selectedValues && this.multiLimit <= this.selectedValues.length) return
this.refreshElemPos();
const classList = e.target.classList;
if (classList.contains('tag-deleter') || classList.contains('tag-cursor')) {
return;
}
this.toggleCollapse();
}
refreshElemPos(){
const anchor = this.$['select__container'];
const {x: left, y} = anchor.getBoundingClientRect();
const collapseHeight = Math.min(this.items.length * 26, 300);
const totalHeight = y + collapseHeight;
let top;
if(totalHeight > document.documentElement.clientHeight) {
top = y - collapseHeight - 4;
} else {
top = y + this.clientHeight;
}
this.$['select-collapse'].style['left'] = left + 'px';
this.$['select-collapse'].style['top'] = top + 'px';
this.$['select-collapse'].style['width'] = this.$['select__container'].clientWidth + 'px';
}
_valueChanged(value, items = []) {
const values = String(value).split(",").map(str => str.trim());
const flatValues = [...(new Set(values))];
const dirty = (this.selectedValues || []).map(selected => selected[this.attrForValue]).join(',');
if (dirty !== value) {
this.selectedValues =
flatValues.map(val => items.find(item => item[this.attrForValue] == val))
.filter(selected => typeof selected !== 'undefined');
if (!this.multi) {
this.selectedItem = items.find(item => item[this.attrForValue] == flatValues[0]);
}
}
this._displayPlaceholder(this.selectedValues.length === 0)
}
_selectedValuesChanged() {
if (this.selectedValues.length > 0) {
this.value = this.selectedValues.map(selected => selected[this.attrForValue]).join(',');
} else {
this.value = undefined;
}
this.closeCollapse();
}
selectedItemChanged() {
this.selectedValues = this.selectedItem ? [this.selectedItem] : [];
}
/**
* 删除Tag项,事件处理函数
*/
_deleteTag(e) {
let value = e.target.dataArgs;
const ind = this.selectedValues.findIndex(selected => selected[this.attrForValue] == value);
this.splice("selectedValues", ind, 1);
}
/**
* @param event
* @private
*/
_updatePressed(event) {
let cursorIndex = event.target.dataset.cursorIndex;
switch (event.key) {
case "ArrowLeft":
cursorIndex = cursorIndex > 0 ? --cursorIndex : -1;
break;
case "ArrowRight":
const max = this.selectedValues.length - 1;
cursorIndex = cursorIndex < max ? ++cursorIndex : max;
break;
case "Backspace":
if (cursorIndex >= 0) {
this.splice('selectedValues', cursorIndex, 1);
}
cursorIndex = cursorIndex > 0 ? --cursorIndex : -1;
break;
}
const currCursor = this.shadowRoot.querySelector(`#tag-cursor__${cursorIndex}`);
currCursor && currCursor.focus();
}
__focusOnLast() {
const lastCursor = this.shadowRoot.querySelector(`#tag-cursor__${this.selectedValues.length - 1}`);
lastCursor && lastCursor.focus();
}
_displayPlaceholder(display) {
this.$.placeholder.hidden = !display;
}
/**
* Open collapse.
*/
openCollapse() {
this.$["select-collapse"].setAttribute('data-collapse-open', '');
this.opened = true;
}
/**
* Close collapse.
*/
closeCollapse() {
this.$["select-collapse"].removeAttribute('data-collapse-open');
this.opened = false;
}
/**
* Toggle collapse.
*/
toggleCollapse() {
if (this.$["select-collapse"].hasAttribute('data-collapse-open')) {
this.$["select-collapse"].removeAttribute('data-collapse-open');
} else {
this.$["select-collapse"].setAttribute('data-collapse-open', '');
}
this.opened = !this.opened;
this.__focusOnLast();
}
/**
* Set focus to select.
*/
doFocus() {
this.__focusOnLast();
}
/**
* Validate, true if the select is set to be required and this.selectedValues.length > 0, or else false.
* @returns {boolean}
*/
validate() {
return this.required ? (this.selectedValues && this.selectedValues.length > 0) : true;
}
}
window.customElements.define(IsuSelect.is, IsuSelect);