UNPKG

mdui

Version:

a CSS Framework based on material design

341 lines (340 loc) 11.7 kB
/** * 最终生成的元素结构为: * <select class="mdui-select" mdui-select="{position: 'top'}" style="display: none;"> // $native * <option value="1">State 1</option> * <option value="2">State 2</option> * <option value="3" disabled="">State 3</option> * </select> * <div class="mdui-select mdui-select-position-top" style="" id="88dec0e4-d4a2-c6d0-0e7f-1ba4501e0553"> // $element * <span class="mdui-select-selected">State 1</span> // $selected * <div class="mdui-select-menu" style="transform-origin: center 100% 0px;"> // $menu * <div class="mdui-select-menu-item mdui-ripple" selected="">State 1</div> // $items * <div class="mdui-select-menu-item mdui-ripple">State 2</div> * <div class="mdui-select-menu-item mdui-ripple" disabled="">State 3</div> * </div> * </div> */ import $ from 'mdui.jq/es/$'; import contains from 'mdui.jq/es/functions/contains'; import extend from 'mdui.jq/es/functions/extend'; import 'mdui.jq/es/methods/add'; import 'mdui.jq/es/methods/addClass'; import 'mdui.jq/es/methods/after'; import 'mdui.jq/es/methods/append'; import 'mdui.jq/es/methods/appendTo'; import 'mdui.jq/es/methods/attr'; import 'mdui.jq/es/methods/css'; import 'mdui.jq/es/methods/each'; import 'mdui.jq/es/methods/find'; import 'mdui.jq/es/methods/first'; import 'mdui.jq/es/methods/height'; import 'mdui.jq/es/methods/hide'; import 'mdui.jq/es/methods/index'; import 'mdui.jq/es/methods/innerWidth'; import 'mdui.jq/es/methods/is'; import 'mdui.jq/es/methods/on'; import 'mdui.jq/es/methods/remove'; import 'mdui.jq/es/methods/removeAttr'; import 'mdui.jq/es/methods/removeClass'; import 'mdui.jq/es/methods/show'; import 'mdui.jq/es/methods/text'; import 'mdui.jq/es/methods/trigger'; import 'mdui.jq/es/methods/val'; import mdui from '../../mdui'; import '../../jq_extends/methods/transitionEnd'; import '../../jq_extends/static/guid'; import { componentEvent } from '../../utils/componentEvent'; import { $document, $window } from '../../utils/dom'; const DEFAULT_OPTIONS = { position: 'auto', gutter: 16, }; class Select { constructor(selector, options = {}) { /** * 生成的 `<div class="mdui-select">` 元素的 JQ 对象 */ this.$element = $(); /** * 配置参数 */ this.options = extend({}, DEFAULT_OPTIONS); /** * select 的 size 属性的值,根据该值设置 select 的高度 */ this.size = 0; /** * 占位元素,显示已选中菜单项的文本 */ this.$selected = $(); /** * 菜单项的外层元素的 JQ 对象 */ this.$menu = $(); /** * 菜单项数组的 JQ 对象 */ this.$items = $(); /** * 当前选中的菜单项的索引号 */ this.selectedIndex = 0; /** * 当前选中菜单项的文本 */ this.selectedText = ''; /** * 当前选中菜单项的值 */ this.selectedValue = ''; /** * 当前 select 的状态 */ this.state = 'closed'; this.$native = $(selector).first(); this.$native.hide(); extend(this.options, options); // 为当前 select 生成唯一 ID this.uniqueID = $.guid(); // 生成 select this.handleUpdate(); // 点击 select 外面区域关闭 $document.on('click touchstart', (event) => { const $target = $(event.target); if (this.isOpen() && !$target.is(this.$element) && !contains(this.$element[0], $target[0])) { this.close(); } }); } /** * 调整菜单位置 */ readjustMenu() { const windowHeight = $window.height(); // mdui-select 高度 const elementHeight = this.$element.height(); // 菜单项高度 const $itemFirst = this.$items.first(); const itemHeight = $itemFirst.height(); const itemMargin = parseInt($itemFirst.css('margin-top')); // 菜单高度 const menuWidth = this.$element.innerWidth() + 0.01; // 必须比真实宽度多一点,不然会出现省略号 let menuHeight = itemHeight * this.size + itemMargin * 2; // mdui-select 在窗口中的位置 const elementTop = this.$element[0].getBoundingClientRect().top; let transformOriginY; let menuMarginTop; if (this.options.position === 'bottom') { menuMarginTop = elementHeight; transformOriginY = '0px'; } else if (this.options.position === 'top') { menuMarginTop = -menuHeight - 1; transformOriginY = '100%'; } else { // 菜单高度不能超过窗口高度 const menuMaxHeight = windowHeight - this.options.gutter * 2; if (menuHeight > menuMaxHeight) { menuHeight = menuMaxHeight; } // 菜单的 margin-top menuMarginTop = -(itemMargin + this.selectedIndex * itemHeight + (itemHeight - elementHeight) / 2); const menuMaxMarginTop = -(itemMargin + (this.size - 1) * itemHeight + (itemHeight - elementHeight) / 2); if (menuMarginTop < menuMaxMarginTop) { menuMarginTop = menuMaxMarginTop; } // 菜单不能超出窗口 const menuTop = elementTop + menuMarginTop; if (menuTop < this.options.gutter) { // 不能超出窗口上方 menuMarginTop = -(elementTop - this.options.gutter); } else if (menuTop + menuHeight + this.options.gutter > windowHeight) { // 不能超出窗口下方 menuMarginTop = -(elementTop + menuHeight + this.options.gutter - windowHeight); } // transform 的 Y 轴坐标 transformOriginY = `${this.selectedIndex * itemHeight + itemHeight / 2 + itemMargin}px`; } // 设置样式 this.$element.innerWidth(menuWidth); this.$menu .innerWidth(menuWidth) .height(menuHeight) .css({ 'margin-top': menuMarginTop + 'px', 'transform-origin': 'center ' + transformOriginY + ' 0', }); } /** * select 是否为打开状态 */ isOpen() { return this.state === 'opening' || this.state === 'opened'; } /** * 对原生 select 组件进行了修改后,需要调用该方法 */ handleUpdate() { if (this.isOpen()) { this.close(); } this.selectedValue = this.$native.val(); const itemsData = []; this.$items = $(); // 生成 HTML this.$native.find('option').each((index, option) => { const text = option.textContent || ''; const value = option.value; const disabled = option.disabled; const selected = this.selectedValue === value; itemsData.push({ value, text, disabled, selected, index, }); if (selected) { this.selectedText = text; this.selectedIndex = index; } this.$items = this.$items.add('<div class="mdui-select-menu-item mdui-ripple"' + (disabled ? ' disabled' : '') + (selected ? ' selected' : '') + `>${text}</div>`); }); this.$selected = $(`<span class="mdui-select-selected">${this.selectedText}</span>`); this.$element = $(`<div class="mdui-select mdui-select-position-${this.options.position}" ` + `style="${this.$native.attr('style')}" ` + `id="${this.uniqueID}"></div>`) .show() .append(this.$selected); this.$menu = $('<div class="mdui-select-menu"></div>') .appendTo(this.$element) .append(this.$items); $(`#${this.uniqueID}`).remove(); this.$native.after(this.$element); // 根据 select 的 size 属性设置高度 this.size = parseInt(this.$native.attr('size') || '0'); if (this.size <= 0) { this.size = this.$items.length; if (this.size > 8) { this.size = 8; } } // 点击选项时关闭下拉菜单 // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; this.$items.on('click', function () { if (that.state === 'closing') { return; } const $item = $(this); const index = $item.index(); const data = itemsData[index]; if (data.disabled) { return; } that.$selected.text(data.text); that.$native.val(data.value); that.$items.removeAttr('selected'); $item.attr('selected', ''); that.selectedIndex = data.index; that.selectedValue = data.value; that.selectedText = data.text; that.$native.trigger('change'); that.close(); }); // 点击 $element 时打开下拉菜单 this.$element.on('click', (event) => { const $target = $(event.target); // 在菜单上点击时不打开 if ($target.is('.mdui-select-menu') || $target.is('.mdui-select-menu-item')) { return; } this.toggle(); }); } /** * 动画结束的回调 */ transitionEnd() { this.$element.removeClass('mdui-select-closing'); if (this.state === 'opening') { this.state = 'opened'; this.triggerEvent('opened'); this.$menu.css('overflow-y', 'auto'); } if (this.state === 'closing') { this.state = 'closed'; this.triggerEvent('closed'); // 恢复样式 this.$element.innerWidth(''); this.$menu.css({ 'margin-top': '', height: '', width: '', }); } } /** * 触发组件事件 * @param name */ triggerEvent(name) { componentEvent(name, 'select', this.$native, this); } /** * 切换下拉菜单的打开状态 */ toggle() { this.isOpen() ? this.close() : this.open(); } /** * 打开下拉菜单 */ open() { if (this.isOpen()) { return; } this.state = 'opening'; this.triggerEvent('open'); this.readjustMenu(); this.$element.addClass('mdui-select-open'); this.$menu.transitionEnd(() => this.transitionEnd()); } /** * 关闭下拉菜单 */ close() { if (!this.isOpen()) { return; } this.state = 'closing'; this.triggerEvent('close'); this.$menu.css('overflow-y', ''); this.$element .removeClass('mdui-select-open') .addClass('mdui-select-closing'); this.$menu.transitionEnd(() => this.transitionEnd()); } /** * 获取当前菜单的状态。共包含四种状态:`opening`、`opened`、`closing`、`closed` */ getState() { return this.state; } } mdui.Select = Select;