UNPKG

lu2

Version:

Simple and flexible UI component library based on native HTML and JavaScript

499 lines (431 loc) 16.8 kB
/* * @Select.js * @author liuwentao * @version * @Created: 2020-06-09 * @edit: 2020-06-09 **/ /**/import UiPopup from '../Popup/index.js'; /**/import '../Layout/list.js'; /**/(async () => { /**/ if (!CSS.supports('overflow-anchor:auto') || !CSS.supports('offset:none')) { /**/ await import('../safari-polyfill.js'); /**/ } class UiSelect extends HTMLSelectElement { static get observedAttributes () { return ['width', 'height', 'radius', 'open', 'color']; } constructor () { super(); // 关联的元素们 if (!this.element) { this.element = { trigger: null, popup: null }; } // 尺寸变化的观察器 this.resizeObserver = null; this.intersectionObserver = null; // 重置原生的属性 this.setProperty(); } set popup (value) { this.toggleAttribute('popup', Boolean(value)); // 弹出选择层的动态处理 if (!this.element.popup) { this.create(); this.events(); } } get popup () { return this.hasAttribute('popup'); } set open (value) { this.toggleAttribute('open', Boolean(value)); } get open () { return this.hasAttribute('open'); } get height () { let height = this.getAttribute('height'); if (!isNaN(height) && !isNaN(parseFloat(height))) { height = height + 'px'; } return height; } set height (val) { this.setAttribute('height', val); } get width () { let width = this.getAttribute('width'); if (!isNaN(width) && !isNaN(parseFloat(width))) { width = width + 'px'; } return width; } set width (val) { this.setAttribute('width', val); } get font () { let fontSize = this.getAttribute('font'); if (!isNaN(fontSize) && !isNaN(parseFloat(fontSize))) { fontSize = fontSize + 'px'; } return fontSize; } set font (val) { this.setAttribute('font', val); } get color () { return this.getAttribute('color'); } set color (val) { this.setAttribute('color', val); } get radius () { let radius = this.getAttribute('radius'); if (!isNaN(radius) && !isNaN(parseFloat(radius))) { radius = radius + 'px'; } return radius; } set radius (val) { this.setAttribute('radius', val); } // 创建必要的元素 create () { // trigger 元素 if (!this.element.trigger && this.hasAttribute('trigger')) { this.element.trigger = document.getElementById(this.getAttribute('trigger')); } if (!this.element.trigger) { // 浮在原生下拉框上的元素 let eleTrigger = document.createElement('ui-select'); eleTrigger.setAttribute('is', 'ui-select'); // 插入到前面 this.insertAdjacentElement('beforebegin', eleTrigger); // 是否是静态定位 if (getComputedStyle(eleTrigger).position == 'static') { eleTrigger.style.position = 'absolute'; } // 层级高一个 let numZIndex = getComputedStyle(this).zIndex; if (typeof numZIndex == 'number') { eleTrigger.style.zIndex = numZIndex + 1; } // 和下拉元素关联 this.element.trigger = eleTrigger; // 尺寸和定位 this.resize(); } // popup 元素创建 if (!this.element.popup) { this.element.popup = new UiPopup({ transition: true, overlay: { touch: 'none' }, radius: true, width: 'calc(100% - 2rem)', style: { maxHeight: 'calc(100% - 10rem)' } }); } // DOM 创建完毕的事件 this.dispatchEvent(new CustomEvent('DOMContentLoaded')); } /** * 下拉内容的刷新 */ refresh () { // 主要的几个元素 let elePopup = this.element.popup; if (!elePopup) { return; } if (!this.options.length) { elePopup.content = '<div class="ui-select-empty">没有选项</div>'; return; } // <select> 元素隐射 // <optgroup> -> <ui-list> // <option> -> <ui-list-item> // 单选还是复选 let type = 'radio'; const isMutiple = this.multiple; if (isMutiple) { type = 'checkbox'; } // 随机 name 值 let name = ('n' + Math.random()).replace('0.', ''); // 索引值 let index = -1; // 最终的内容 let funGetItem = (option) => { let eleList = document.createElement('ui-list-item'); eleList.className = option.className; eleList.setAttribute('space', ''); // 索引值递增 // 作用是,当所有 option 都没有 value 的时候,通过 // index 值进行选中匹配 index++; // 里面的选择框 eleList.innerHTML = `<input type="${type}" name="${name}" value="${option.value}" ${option.selected ? 'checked' : ''} ${option.disabled ? 'disabled' : ''} data-index="${index}" >${option.innerHTML}`; return eleList; }; // 放在 form 元素中 let eleForm = document.createElement('form'); // 遍历处理 [...this.children].forEach((child) => { let eleList = null; if (child.matches('optgroup')) { eleList = document.createElement('ui-list'); if (child.label) { eleList.setAttribute('label', child.label); } if (child.disabled) { eleList.setAttribute('disabled', ''); } eleList.className = child.className; // 子元素处理 child.querySelectorAll('option').forEach(option => { // 禁用覆盖到子元素 if (child.disabled) { option.disabled = true; } eleList.appendChild(funGetItem(option)); }); } else if (child.matches('option')) { eleList = funGetItem(child); } eleForm.appendChild(eleList); }); // 多选增加确定按钮 if (isMutiple) { let eleListBtn = document.createElement('ui-flex'); eleListBtn.innerHTML = `<button type="reset" is="ui-button" blank>重置</button> <hr v> <button type="button" is="ui-button" data-type="primary" blank>确定</button>`; eleForm.append(eleListBtn); } // 内容显示 elePopup.content = eleForm } /** * 下拉的事件处理 */ events () { const eleTrigger = this.element.trigger; const elePopup = this.element.popup; if (eleTrigger && elePopup) { eleTrigger.addEventListener('click', () => { elePopup.show(); }); elePopup.addEventListener('show', () => { this.refresh(); }); // 弹出层的列表选中处理 const eleContainer = elePopup.element.container; eleContainer.addEventListener('click', event => { let eleTarget = event.target; if (!eleTarget || !eleTarget.type) { return; } // 重置,原设想这里代码是不需要的 // 但是 click 执行比 reset 重置要快, // 所以这里插了这个处理 if (eleTarget.type == 'reset') { eleTarget.closest('form').reset(); } if (eleTarget.type === 'radio') { if (eleTarget.value) { this.value = eleTarget.value; } else { this.selectedIndex = Number(eleTarget.dataset.index); } } else if (this.multiple) { this.value = [...eleContainer.querySelectorAll(`[type="checkbox"]:checked`)].map(checkbox => { return checkbox.value; }).join(); } // 触发元素的内容替换 let arrTextSelected = []; [...this.options].forEach(option => { if (option.selected) { arrTextSelected.push(option.innerHTML); } }); let nodeText = document.createTextNode(arrTextSelected.join() || eleTrigger.originText || ''); let isNodeText = [...eleTrigger.childNodes].some(node => { if (node.nodeType === Node.TEXT_NODE) { node.parentNode.replaceChild(nodeText, node); return true; } return false; }); // 如果没有文本节点 if (!isNodeText) { eleTrigger.prepend(nodeText); } // 隐藏弹出层 if (eleTarget.type != 'checkbox') { elePopup.remove(); } }); // 如果 trigger 元素非自主创建 // 则定位什么的都不做观察处理 if (eleTrigger.matches('ui-select[is="ui-select"]')) { // 尺寸变化同步变化 this.resizeObserver = new ResizeObserver(() => { this.resize(); }); this.resizeObserver.observe(this); // 位置变化时候也跟踪 // 发生在进入视区和离开视区 this.intersectionObserver = new IntersectionObserver(() => { this.resize(); }); this.intersectionObserver.observe(this); } eleTrigger.originText = eleTrigger.textContent; } } /** * popup点击浮层根据下拉框的位置偏移 */ resize () { const objElement = this.element; let eleTrigger = objElement.trigger; if (!eleTrigger) { return; } // 按钮的尺寸和位置 let objBoundSelect = this.getBoundingClientRect(); // 尺寸设置 eleTrigger.style.width = objBoundSelect.width + 'px'; eleTrigger.style.height = objBoundSelect.height + 'px'; // 偏移对比 let objBoundTrigger = eleTrigger.getBoundingClientRect(); let objStyleTrigger = getComputedStyle(eleTrigger); // 目前的偏移 let numOffsetX = objStyleTrigger.getPropertyValue('--ui-offset-x') || 0; let numOffsetY = objStyleTrigger.getPropertyValue('--ui-offset-y') || 0; // 偏移误差 let numDistanceX = objBoundSelect.left - objBoundTrigger.left; let numDistanceY = objBoundSelect.top - objBoundTrigger.top; // 设置新的偏移值 eleTrigger.style.setProperty('--ui-offset-x', Number(numOffsetX) + numDistanceX); eleTrigger.style.setProperty('--ui-offset-y', Number(numOffsetY) + numDistanceY); } /** * 重置原生的属性 */ setProperty () { Object.defineProperty(this, 'value', { configurable: true, enumerable: true, writeable: true, get: () => { let valArr = []; [].slice.call(this.options).forEach((option) => { if (option.selected) { valArr.push(option.value); } }); return valArr.join(); }, set: (value) => { let isMatch = false; value = this.multiple ? value.split(',') : [value.toString()]; [].slice.call(this.options).forEach((option) => { // 单选框模式下,如果多个值匹配,让第一个选中 // 如果没有下面这句,会最后一个匹配的选中 if (!this.multiple && isMatch) return; if (value.indexOf(option.value) !== -1) { option.selected = isMatch = true; } else if (this.multiple) { option.selected = false; } }); // 触发 change 事件 this.dispatchEvent(new CustomEvent('change')); } }); const props = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'selectedIndex'); Object.defineProperty(HTMLSelectElement.prototype, 'selectedIndex', { ...props, set (v) { if (this.options[v]) { this.options[v].selected = true; } } }); } /** * <select>属性变化时候的处理 * @param {String} name 变化的属性名称 */ renderAttribute (name, keep) { if (typeof this[name] == 'string') { this.style.setProperty('--ui-select-' + name, this[name]); } else if (!keep) { this.style.removeProperty('--ui-select-' + name); } } render () { UiSelect.observedAttributes.forEach(attr => { this.renderAttribute(attr, true); }); } attributeChangedCallback (name) { this.render(name); } /** * is="ui-select" 元素载入到页面后 */ connectedCallback () { // 渲染 if (this.popup) { this.create(); this.events(); } this.render(); // 全局事件 this.dispatchEvent(new CustomEvent('connected', { detail: { type: 'ui-select' } })); this.isConnectedCallback = true; } /** * is="ui-select" 元素从页面移除后 */ disconnectedCallback () { if (this.element.trigger) { this.element.trigger.remove(); } if (this.element.popup) { this.element.popup.remove(); } if (this.resizeObserver) { this.resizeObserver.disconnect(); } } } // 自定义元素注册 if (!customElements.get('ui-select')) { customElements.define('ui-select', UiSelect, { extends: 'select' }); } /**/})();