isu-elements
Version:
Polymer components for building web apps.
682 lines (641 loc) • 19.6 kB
JavaScript
import { html, PolymerElement } from '@polymer/polymer'
import '@webcomponents/shadycss/entrypoints/apply-shim.js'
import { mixinBehaviors } from '@polymer/polymer/lib/legacy/class'
import '@polymer/iron-icon'
import '@polymer/iron-icons'
import '@polymer/paper-dialog'
import { IsuFetch } from './isu-fetch'
import { BaseBehavior } from './behaviors/base-behavior'
import './behaviors/isu-elements-shared-styles.js'
/**
* `isu-cascading`
*
* ```html
* <isu-cascading class="isu-cascading" label="地址" id="cascading" attr-for-label="label" attr-for-value="value" separator=">" required show-all-levels></isu-cascading>
*
* ```
*
* @customElement
* @polymer
* @demo demo/isu-cascading/index.html
*/
class IsuCascading extends mixinBehaviors([BaseBehavior], PolymerElement) {
static get template () {
return html`
<style include="isu-elements-shared-styles">
:host {
font-family: var(--isu-ui-font-family), sans-serif;
font-size: var(--isu-ui-font-size);
display: flex;
height: var(--isu-cascading-height, var(--isu-default-line-height, 34px));
line-height: var(--isu-cascading-height, var(--isu-default-line-height, 34px));
width: var(--isu-cascading-width, 320px);
min-width: 200px;
}
:host .cascade {
position: relative;
}
.cascading__container {
flex: 1;
display: flex;
align-items: center;
line-height: inherit;
min-width: 200px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 2px 5px;
position: relative;
cursor: pointer;
}
:host([readonly]) .cascading__container {
pointer-events: none;
opacity: 0.5;
z-index: 10;
cursor: no-drop;
}
#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;
}
#targetDialog {
position: absolute;
bottom: -5px;
left: 0;
width: inherit;
}
:host([opened]) .caret {
transform: rotate(180deg);
transition: transform .2s ease-in-out;
}
.caret {
transition: transform .2s ease-in-out;
color: var(--isu-ui-color_skyblue);
position: absolute;
right: 0;
top: 0;
height: 34px;
line-height: 34px;
}
#boxDialog {
position: absolute;
height: 206px;
margin: 0;
max-width: initial;
}
.dialog-container {
margin: 0;
padding: 0;
display: flex;
}
.view-container {
flex: 1;
height: 206px;
min-width: 160px;
margin: 0;
padding: 8px 0;
box-sizing: border-box;
border-right: 1px solid #ccc;
background: #fff;
@apply --isu-view-container;
}
.view-container:last-of-type {
border-right: none;
}
.view-list {
height: 190px;
overflow-y: auto;
}
.view-item {
position: relative;
padding: 0 12px;
height: 30px;
line-height: 30px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.view-item:hover, .view-item-active {
color: var(--isu-ui-color_skyblue);
font-weight: bold;
}
.chevron-iron {
position: absolute;
right: 0;
height: 30px;
line-height: 30px;
}
:host([required]) .cascading__container::before {
content: "*";
color: red;
position: absolute;
left: -8px;
line-height: inherit;
@apply --isu-required
}
:host([data-invalid]) .cascading__container {
border-color: var(--isu-ui-color_pink);
}
.icon-clear {
position: absolute;
right: 5px;
width: 12px;
height: 12px;
line-height: 34px;
border: 1px solid #ccc;
border-radius: 50%;
color: #ccc;
display: none;
}
.cascading__container:hover .icon-clear {
display: inline-block;
}
.cascading__container:hover .caret {
display: none;
}
:host([data-invalid]) #innerInput {
border-color: var(--isu-ui-color_pink);
}
</style>
<template is="dom-if" if="[[ toBoolean(label) ]]">
<div class="isu-label-div"><span class$="isu-label [[fontSize]]">[[label]]</span><span class="isu-label-after-extension"></span></div>
</template>
<div class$="cascading__container [[fontSize]]" on-click="__onInputClick">
<div id="placeholder">[[placeholder]]</div>
<div class="box-value">[[showLabel]]</div>
<iron-icon class="caret" icon="icons:expand-more"></iron-icon>
<iron-icon class="icon-clear" icon=icons:clear on-click="clear"></iron-icon>
<div id="targetDialog">
</div>
</div>
<div class$="cascade [[fontSize]]">
<paper-dialog id="boxDialog" no-overlap horizontal-align="auto" vertical-align="auto" on-iron-overlay-closed="__cancelClick">
<div class="dialog-container">
<template is="dom-repeat" items="{{treeItems}}" as="tree" index-as="treeIndex">
<div class="view-container">
<div class="view-list">
<template is="dom-repeat" items="[[tree]]">
<template is="dom-if" if="[[__isHover(expandTrigger)]]">
<div class$="view-item [[__setViewClass(item.__select)]]" on-mouseover="__viewItemClick">
[[getValueByKey(item, attrForLabel)]]
<template is="dom-if" if="[[item.children]]">
<iron-icon class="chevron-iron" icon="icons:chevron-right"></iron-icon>
</template>
</div>
</template>
<template is="dom-if" if="[[!__isHover(expandTrigger)]]">
<div class$="view-item [[__setViewClass(item.__select)]]" on-click="__viewItemClick">
[[getValueByKey(item, attrForLabel)]]
<template is="dom-if" if="[[item.children]]">
<iron-icon class="chevron-iron" icon="icons:chevron-right"></iron-icon>
</template>
</div>
</template>
</template>
</div>
</div>
</template>
</div>
</paper-dialog>
<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>
`
}
static get properties () {
return {
/**
* Label of the cascading element
*
* @type String
* @default
*/
label: {
type: String
},
/**
* Placeholder of the cascading container
*
* @type String
* @default 请选择
*/
placeholder: {
type: String,
value: '请选择'
},
/**
* if is true, the collapse cascading will open.
*
* @type Boolean
* @default false
*/
opened: {
type: Boolean,
value: false,
reflectToAttribute: true
},
/**
* The items of the cascading, is passed in by the customer.
*
* @type Array
* @default []
*/
items: {
type: Array,
value: []
},
/**
* The component that sends the request and simulates the data
*/
_fetchUtil: {
type: Object,
readOnly: true,
value: function () {
return new IsuFetch()
}
},
/**
* The items of the cascading, equals to items. If 'lazy' is true, can also be passed in by the customer.
*
* @type Array
* @default []
*/
treeItems: {
type: Array,
notify: true,
value: []
},
/**
* The attrForValue array selected from the selectedValues. eg: ["zhinan", "shejiyuanze", "fankui"]
*
* @type Array
* @default []
*/
value: {
type: Array,
notify: true,
value: []
},
/**
* The array selected from the treeItems.
*
* @type Array
* @default []
*/
selectedValues: {
type: Array,
notify: true,
value: []
},
/**
* The value showed in the cascading container input. eg: 指南>设计原则>反馈
*
* @type Array
* @default []
*/
valueLabel: {
type: String,
notify: true
},
lazy: Boolean,
/**
* Attribute name for value.
* @type {string}
* @default 'value'
*/
attrForValue: {
type: String,
value: 'value'
},
/**
* The prompt tip to show when input is invalid.
* @type {String}
*/
prompt: {
type: String
},
/**
* The prompt tip's position. top/bottom
* @type String
* @default ''
*/
promptPosition: {
type: String,
value: ''
},
/**
*
* Attribute name for label.
*
* @type {string}
* @default 'label'
*/
attrForLabel: {
type: String,
value: 'label'
},
/**
*
* Separator for the valueLabel
*
* @type String
* @default /
*/
separator: {
type: String,
value: '/'
},
/**
*
* Is required
*
* @type Boolean
* @default false
*/
required: {
type: Boolean,
value: false,
reflectToAttribute: true
},
/**
*
* Is readonly
*
* @type Boolean
* @default false
*/
readonly: {
type: Boolean,
value: false,
reflectToAttribute: true
},
/**
*
* The way the secondary menu is expanded(click/hover)
*
* @type String
* @default click
*/
expandTrigger: {
type: String,
value: 'click'
},
/**
* Show the whole path of the selected value in the input box. Show the last path if it is false.
*
* @type Boolean
* @default false
* */
showAllLevels: {
type: Boolean,
value: false
},
/**
* Values displayed in the input box
* @type String
* @default
* */
showLabel: {
type: String,
notify: true
},
src: {
type: String
},
fetchParam: {
type: Object,
notify: true
},
keywordPath: {
type: String,
value: 'keyword'
},
/**
* Whether to get data dynamically internally
* @type {boolean}
* @default false
* */
isInnnerDynamicAppendData: {
type: Boolean,
value: false
},
/**
* Whether to get data dynamically outside
* @type {boolean}
* @default false
* */
isDynamicAppendData: {
type: Boolean,
value: false
},
/**
* The parent element of the currently clicked option
* @type {object}
* @default
* */
currentClickViewElement: {
type: Object,
notify: true
}
}
}
static get observers () {
return [
'__itemsChanged(items)',
'__valueChanged(value)',
'__treeItemsChanged(treeItems)',
'__srcChanged(src)'
]
}
__itemsChanged () {
if (this.items.length) this.set('treeItems', [this.items])
}
__valueChanged (value) {
this.getInvalidAttribute()
if (this.treeItems && this.treeItems.length && !this.lazy && value) {
const treeItems = [].concat(this.treeItems); const selectedValues = []
value.forEach((item, index) => {
const findIndex = treeItems[index].findIndex(itm => itm[this.attrForValue] === item)
treeItems[index][findIndex].__select = true
selectedValues.push(treeItems[index][findIndex])
if (treeItems[index][findIndex].children) treeItems.push(treeItems[index][findIndex].children)
})
this.set('selectedValues', selectedValues)
this.set('valueLabel', selectedValues.map(itm => itm[this.attrForLabel]).join(this.separator))
this.set('treeItems', treeItems)
const lastLevelValue = (selectedValues.length > 0 && selectedValues[selectedValues.length - 1][this.attrForLabel]) || ''
this.showLabel = this.showAllLevels ? this.valueLabel : lastLevelValue
this.$.placeholder.hidden = lastLevelValue
}
}
__treeItemsChanged (treeItems) {
if (treeItems && treeItems.length && this.lazy && this.value) {
const selectedValues = []
this.value.forEach((item, index) => {
const findIndex = treeItems[index].findIndex(itm => itm[this.attrForValue] === item)
if (treeItems[index] && treeItems[index].length && findIndex >= 0) {
treeItems[index][findIndex].__select = true
selectedValues.push(treeItems[index][findIndex])
}
})
this.set('selectedValues', selectedValues)
this.set('valueLabel', selectedValues.map(itm => itm[this.attrForLabel]).join(this.separator))
this.set('treeItems', treeItems)
const lastLevelValue = selectedValues.length && selectedValues[selectedValues.length - 1][this.attrForLabel]
this.$.placeholder.hidden = lastLevelValue
this.showLabel = this.showAllLevels ? this.valueLabel : lastLevelValue
}
}
_mkRequest (data, src) {
return {
url: src,
method: 'POST',
headers: {
'content-type': 'application/json;charset=utf-8',
'Cache-Control': 'no-cache'
},
credentials: 'include',
body: JSON.stringify(data)
}
}
__srcChanged (src) {
if (!src) return
const request = this._mkRequest(this.fetchParam, src)
this._fetchUtil.fetchIt(request)
.then(res => res.json())
.then(data => {
let items
if (this.resultPath) {
items = this.getValueByPath(data, this.resultPath, [])
} else {
items = data || []
}
const findIndex = items.findIndex(item => item[this.attrForValue] == this.value)
if (findIndex >= 0) {
items = [items[findIndex]].concat(items)
items.splice(findIndex + 1, 1)
}
this.items = items
})
.catch(console.error)
}
__setViewClass (select) {
return select ? 'view-item-active' : ''
}
__onInputClick () {
this.opened = !this.opened
this.$.boxDialog.positionTarget = this.$.targetDialog
this.opened ? this.$.boxDialog.open() : this.$.boxDialog.close()
}
__cancelClick () {
this.opened = !this.opened
}
__viewItemClick (e) {
// 解决外部动态插入数据时loading的显示问题
if (this.isDynamicAppendData) {
this.showLoading(parentElement)
}
const { model } = e
const { index, item, parentModel } = model
const treeItems = this.treeItems.slice(0, parentModel.treeIndex + 1)
const treeItem = parentModel.tree.map((itm, idx) => Object.assign({}, itm, { __select: idx === index }))
treeItems[parentModel.treeIndex] = treeItem
if (item.children) {
parentModel.treeIndex + 1 >= this.treeItems.length ? treeItems.push(item.children) : treeItems.splice(parentModel.treeIndex + 1, 1, item.children)
}
const selectedValues = this.selectedValues.slice(0, parentModel.treeIndex + 1)
selectedValues[parentModel.treeIndex] = item
this.set('selectedValues', selectedValues)
if (!item.children) {
this.set('valueLabel', selectedValues.map(itm => itm[this.attrForLabel]).join(this.separator))
this.set('value', selectedValues.map(itm => itm[this.attrForValue]))
}
this.set('treeItems', treeItems)
const lastLevelValue = selectedValues.length && selectedValues[selectedValues.length - 1][this.attrForLabel]
this.$.placeholder.hidden = lastLevelValue
this.showLabel = this.showAllLevels ? this.valueLabel : lastLevelValue
// 解决外部动态插入数据时loading的隐藏问题
const parentElement = e.currentTarget.parentElement
this.currentClickViewElement = parentElement
// 内部动态获取数据
if (this.isInnnerDynamicAppendData) {
this.__getInnerDynamicAppendData(parentElement)
}
}
/**
* 内部动态获取数据
* */
__getInnerDynamicAppendData (element) {
const self = this
self.showLoading(element)
const requestObj = self.fetchParam
const req = self.setValueByPath(self.mkObject(self.keywordPath, requestObj), self.keywordPath, self.value[self.value.length - 1] + '')
// 拼接url,参数名从外界传入,参数值为当前value数组中的最后一个值
const src = `${self.src}?${self.keywordPath}=${self.value[self.value.length - 1]}`
const request = self._mkRequest(req, src)
self._fetchUtil.fetchIt(request)
.then(res => res.json())
.then(data => {
const treeItems = [].concat(self.treeItems)
if (self.value.length && data && data.length > 0) {
treeItems.push(data)
self.treeItems = treeItems
}
self.hideLoading(element)
})
.catch(e => {
console.error
self.hideLoading(element)
})
}
close () {
this.$.boxDialog.close()
}
clear (e) {
e.stopPropagation()
const treeItems = [].concat(this.treeItems)
const chosedStrs = this.valueLabel.split(this.separator)
if (chosedStrs) {
chosedStrs.forEach((str, index) => {
const findIndex = treeItems[index].findIndex(item => item[this.attrForLabel] === str)
delete treeItems[index][findIndex].__select
})
}
// 解决清空选项之后层级表里面还有选中的选项的样式的问题
Array.prototype.forEach.call(this.root.querySelectorAll('.view-item'), function (item) {
item.classList.remove('view-item-active')
})
this.set('value', [])
this.set('valueLabel', null)
this.set('treeItems', treeItems)
}
/**
* Validate, true if the select is set to be required and this.selectedValues.length > 0, or else false.
* @return {boolean}
*/
validate () {
super.validate()
return this.required ? !!(this.value && this.value.length) : true
}
__isHover (expandTrigger) {
return expandTrigger === 'hover'
}
static get is () {
return 'isu-cascading'
}
}
window.customElements.define(IsuCascading.is, IsuCascading)