UNPKG

isu-elements

Version:

Polymer components for building web apps.

488 lines (457 loc) 14.7 kB
import { html, PolymerElement } from '@polymer/polymer' import './behaviors/isu-tree-shared-styles.js' import '@polymer/paper-styles/default-theme.js' import '@polymer/paper-checkbox' import { BaseBehavior } from './behaviors/base-behavior' import { mixinBehaviors } from '@polymer/polymer/lib/legacy/class' import '@webcomponents/shadycss/entrypoints/apply-shim.js' import '@polymer/paper-radio-button' /** * * `isu-tree-node` * * Example: * ```html * * ``` * * @customElement * @polymer * @demo demo/isu-tree/index.html */ class IsuTreeNode extends mixinBehaviors([BaseBehavior], PolymerElement) { static get template () { return html` <style include="isu-tree-shared-styles"> .black-span { display: inline-block; width: 24px; height: 24px; } paper-checkbox { --paper-checkbox-unchecked-color: #B1B6BF; position: relative; @apply --paper-checkbox } paper-checkbox.half{ --paper-checkbox-unchecked-background-color: #3F51B5; --paper-checkbox-unchecked-color: #3F51B5; @apply --paper-checkbox-half-choose } paper-checkbox.half::after { content: '-'; display:inline-block; font-size: 26px; font-weight: bold; width: 24px; height: 24px; color: white; position: absolute; top: 8.5px; left: 4px; @apply --paper-checkbox-half-after } .trigger__icon { color: #B1B6BF; @apply --trigger-icon-color } .pitch-on { background: #FCE9BB; text-decoration: underline; } .ellipsis { white-space: nowrap; overflow: hidden; -o-text-overflow: ellipsis; text-overflow: ellipsis; } .ellipsis.pointer{ cursor: pointer; } .high-lighted-color{ color: var(--high-lighted-color, #f106d2); } </style> <template is="dom-if" if="[[data.visible]]"> <div class="dht-tree-twig-one"> <div class$="dht-tree-node-content [[pitchOn]]" style$="[[_getIndentStyle(data.level, indent)]]" on-click="_clickCheckOnClickNode"> <!--箭头--> <template is="dom-if" if="[[data.children.length]]"> <iron-icon class="trigger__icon" icon="[[icon]]" style$="[[_getRotateStyle(rotate)]]" on-click="showNode"></iron-icon> </template> <!--没有子元素的时候箭头用空span代替--> <template is="dom-if" if="[[!data.children.length]]"> <span class="black-span"></span> </template> <!--多选框--> <template is="dom-if" if="[[multi]]"> <paper-checkbox class$="checkbox-item [[getHalfClass(data.indeterminate)]]" checked="{{ data.checked }}" disabled="{{ data.disabled }}" on-change="__checkedChangeHandler" on-click="__checkedClickedHandler" value="[[ getValueByKey(data, attrForValue) ]]"> <span class="ellipsis">[[ getValueByKey(data, attrForLabel) ]]</span> </paper-checkbox> </template> <!--单选框--> <template is="dom-if" if="[[showRadio]]"> <paper-radio-button class="checkbox-item" name="radio" checked="{{ data.checked }}" disabled="{{ data.disabled }}" on-click="__checkedRadioClickedHandler" value="[[ getValueByKey(data, attrForValue) ]]"> <span class="ellipsis">[[ getValueByKey(data, attrForLabel) ]]</span> </paper-radio-button> </template> <!--可自定义部分--> <slot name="before-label"></slot> <template is="dom-if" if="[[isAllFalse(showRadio, multi)]]"> <span class="ellipsis pointer">[[ getValueByKey(data, attrForLabel) ]]</span> </template> <!--可自定义部分--> <slot name="after-label"></slot> </div> <template is="dom-repeat" items="{{data.children}}" index-as="index" initial-count="5"> <isu-tree-node data="{{item}}" multi="[[multi]]" show-radio="[[showRadio]]" search-word="[[searchWord]]" default-expand-all="[[defaultExpandAll]]" indent="[[indent]]" selected-items="[[selectedItems]]" attr-for-value="[[attrForValue]]" attr-for-label="[[attrForLabel]]" hidden="[[!isShow]]" icon="[[icon]]" > <slot name="before-label"></slot> </isu-tree-node> </template> </div> </template> ` } static get properties () { return { // 存储Node节点data data: { type: Object }, /** * 选择的树节点集合 * * @type {array} * */ selectedItems: { type: Array, notify: true }, /** * 节点作为value值的属性,默认取id作为value值 * @type {String} * @default 'children' */ attrForValue: { type: String, value: 'id' }, attrForLabel: { type: String, value: 'label' }, /** * The angle at which a triangle rotates * * @type {number} * @default -90 */ rotate: { type: Number, value: -90 }, // 三角形标记 /** * Whether to show checkbox or not * * @type {boolean} * @default false * */ multi: { type: Boolean, value: false }, /** * Whether to show radio or not * * @type {boolean} * @default false * */ showRadio: { type: Boolean, value: false }, /** * Accordion mode, showing only one sibling node at a time * * @type {boolean} * @default false * */ accordion: { type: Boolean, value: false }, /** * Whether to expand all nodes by default * * @type {boolean} * @default false * */ defaultExpandAll: { type: Boolean, value: false }, /** * Operate the child element is closed or opened * */ isShow: { type: Boolean, value: false }, /** * How many pixels are indented per layer. * * @type {number} * @default 18 * */ indent: { type: Number, value: 18 }, isFirst: { type: Boolean, value: false }, searchWord: { // 有搜索框时输入的关键字 type: String }, /** * 是否高亮显示开关,如果是,搜索时候,匹配上的文本会高亮显示 */ highlightedLabelFlag: { type: Boolean, value: false }, /** * checkOnClickNode 为true时候,选中的节点添加选中样式 */ pitchOn: { type: String, computed: '__pitchOn(multi, showRadio, selectedItems)' }, icon: { type: String, value: 'icons:arrow-drop-down' } } } static get is () { return 'isu-tree-node' } static get observers () { return [ '_defaultExpandAllChanged(defaultExpandAll)', '_searchWordChanged(searchWord)', '_isFirst(isFirst)', '__selectedItemsChanged(selectedItems)', '__dataChanged(data.*)' ] } connectedCallback () { super.connectedCallback() this.addEventListener('children-status-changed', this._childrenStatusChangedHandle) } __dataChanged (data) { const { path } = data if (path.includes('indeterminate') || path.includes('checked')) { this.debounce('childrenStatusChanged', () => { this.dispatchEvent(new CustomEvent('children-status-changed', { detail: { data: this.data }, bubbles: true, composed: true })) }, 200) } } _childrenStatusChangedHandle (e) { e.stopPropagation() this.debounce('childrenStatusChangedHandle', this.queryCurNodeIsChecked.bind(this, this.selectedItems), 100) } _isFirst (isFirst) { if (!this.defaultExpandAll) { this.set('isShow', isFirst) this.set('rotate', isFirst ? 0 : -90) } } getHalfClass (indeterminate) { const result = indeterminate ? 'half' : '' return result } _searchWordChanged (searchWord) { this._showNodeFilter() this._highlightedLabel(searchWord) } _highlightedLabel (searchWord) { if (searchWord === null || searchWord === undefined) return const eles = this.root.querySelectorAll('.ellipsis') if (!searchWord) { Array.from(eles).forEach(e => { const label = this.data[this.attrForLabel || 'label'] e.innerHTML = label }) } else { Array.from(eles).forEach(e => { const searchWordRegExp = new RegExp(`(${searchWord})`, 'gi') if (searchWordRegExp.test(e.textContent)) { e.innerHTML = e.textContent.replace(searchWordRegExp, '<span class="high-lighted-color">$1</span>') } }) } } _showNodeFilter () { const visible = this.filterNode(this.searchWord, this.data) this.set('data.visible', visible) } filterNode (searchWord, data) { if (!data) { return false } if (!searchWord) { return true } const self = this const { children } = data const searchWordRegExp = new RegExp(`(${searchWord})`, 'gi') const selfVisible = searchWordRegExp.test(data[this.attrForLabel] || data.label) const childVisilbe = (children || []).some(childrenData => { return self.filterNode(searchWord, childrenData) }) return selfVisible || childVisilbe } _defaultExpandAllChanged (defaultExpandAll) { if (defaultExpandAll) { this.isShow = true this.rotate = 0 } } _clickCheckOnClickNode () { if (this.multi || this.showRadio) return this.dispatchEvent(new CustomEvent('single-checked-changed', { detail: { data: this.data }, bubbles: true, composed: true })) this.dispatchEvent(new CustomEvent('select-tree-close-collapse', { detail: { data: true }, bubbles: true, composed: true })) } showNode (e) { e.stopPropagation() if (this.data.children.length <= 0) return false // this.dispatchEvent(new CustomEvent('arrow-check', { detail: { data: null }, bubbles: true, composed: true })) const parent = this.domHost if (this.isShow) { this.isShow = false this.rotate = -90 return } // 如果开启了手风琴模式,一次展示一个层级 if (this.accordion) { parent.shadowRoot.querySelectorAll('isu-tree-node').forEach(el => { el.isShow = false el.rotate = -90 }) } // 操作子元素方式开启关闭 this.isShow = true this.rotate = 0 } _getIndentStyle (level, indent) { return `padding-left: ${level * indent}px` } _getRotateStyle (rotate) { return `transform: rotate(${rotate}deg)` } __checkedChangeHandler (e) { e.stopPropagation() // fix 注意假的checked 其实是半选的状态 const isChecked = e.target.checked && !this.data.indeterminate setTimeout(() => { if (this.multi) { // 如果是多选状态 this.dispatchEvent(new CustomEvent('multiple-checked-changed', { detail: { data: this.data, isChecked: isChecked }, bubbles: true, composed: true })) } }, 0) } __checkedRadioClickedHandler (e) { e.stopPropagation() if (this.multi) { // 如果是多选,跳过 return } this.dispatchEvent(new CustomEvent('single-checked-changed', { detail: { data: this.data }, bubbles: true, composed: true })) } __checkedClickedHandler (e) { e.stopPropagation() if (this.multi) { // 如果是多选状态 // fix 注意假的checked 其实是半选的状态 const isChecked = e.target.checked && !this.data.indeterminate this.dispatchEvent(new CustomEvent('multiple-click-checked-changed', { detail: { data: this.data, isChecked: isChecked }, bubbles: true, composed: true })) } } __pitchOn (multi, showRadio, selectedItems) { const flag = !multi && !showRadio && this.queryCurNodeIsChecked(selectedItems) const result = flag ? 'pitch-on' : '' return result } /** * 根据selectedItems 的变化,来判断当前节点是否显示 * @param selectedItems * @private */ __selectedItemsChanged (selectedItems) { // 判断当前是否选中状态 this.queryCurNodeIsChecked(selectedItems) } /** * 判断当前节点是否中间状态 * @return {boolean|boolean} */ queryCurNodeIsIndeterminate () { let isIndeterminate = false // 默认不是中间状态 const children = this.data.children || [] if (children.length > 0 && this.multi) { // 多选且存在子节点时候,才有中间态 if (children.findIndex(item => item.indeterminate) > -1) { // 存在子节点为中间态,当前为中间态 isIndeterminate = true } else { const isAllChecked = children.every(item => item.checked) // 子节点都选中 const isAllUnchecked = children.every(item => !item.checked) // 子节点都没有选中 isIndeterminate = !isAllChecked && !isAllUnchecked } } this.set('data.indeterminate', isIndeterminate) return isIndeterminate } /** * 判断当前节点是否选中状态 * @param selectedItems * @return {boolean} */ queryCurNodeIsChecked (selectedItems = []) { let isChecked = false if (this.multi) { // 多选时候 const isIndeterminate = this.queryCurNodeIsIndeterminate() if (!isIndeterminate) { const children = (this.data && this.data.children) || [] const flag1 = children.length > 0 && children.every(item => item.checked) // 1、存在子节点,所有的子节点都是选中状态,则为选中状态 const flag2 = children.length === 0 && (selectedItems || []).findIndex(item => item[this.attrForValue] === this.data[this.attrForValue]) > -1 // 2、不存在子节点,且当前节点在选中节点列表中 isChecked = flag1 || flag2 } } else { // 不是多选,当前节点在选中列表中则为选中 isChecked = (selectedItems || []).findIndex(item => item[this.attrForValue] === this.data[this.attrForValue]) > -1 } this.set('data.checked', isChecked) return isChecked } } window.customElements.define(IsuTreeNode.is, IsuTreeNode)