UNPKG

mdui

Version:

a CSS Framework based on material design

479 lines (408 loc) 12.1 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 { JQ } from 'mdui.jq/es/JQ'; 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 Selector from 'mdui.jq/es/types/Selector'; 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'; declare module '../../interfaces/MduiStatic' { interface MduiStatic { /** * 下拉选择组件 * * 请通过 `new mdui.Select()` 调用 */ Select: { /** * 实例化 Select 组件 * @param selector CSS 选择器、或 DOM 元素、或 JQ 对象 * @param options 配置参数 */ new ( selector: Selector | HTMLElement | ArrayLike<HTMLElement>, options?: OPTIONS, ): Select; }; } } type OPTIONS = { /** * 下拉框位置:`auto`、`top`、`bottom` */ position?: 'auto' | 'top' | 'bottom'; /** * 菜单与窗口上下边框至少保持多少间距 */ gutter?: number; }; type STATE = 'closing' | 'closed' | 'opening' | 'opened'; type EVENT = 'open' | 'opened' | 'close' | 'closed'; const DEFAULT_OPTIONS: OPTIONS = { position: 'auto', gutter: 16, }; class Select { /** * 原生 `<select>` 元素的 JQ 对象 */ public $native: JQ<HTMLSelectElement>; /** * 生成的 `<div class="mdui-select">` 元素的 JQ 对象 */ public $element: JQ = $(); /** * 配置参数 */ public options: OPTIONS = extend({}, DEFAULT_OPTIONS); /** * select 的 size 属性的值,根据该值设置 select 的高度 */ private size = 0; /** * 占位元素,显示已选中菜单项的文本 */ private $selected: JQ = $(); /** * 菜单项的外层元素的 JQ 对象 */ private $menu: JQ = $(); /** * 菜单项数组的 JQ 对象 */ private $items: JQ = $(); /** * 当前选中的菜单项的索引号 */ private selectedIndex = 0; /** * 当前选中菜单项的文本 */ private selectedText = ''; /** * 当前选中菜单项的值 */ private selectedValue = ''; /** * 唯一 ID */ private uniqueID: string; /** * 当前 select 的状态 */ private state: STATE = 'closed'; public constructor( selector: Selector | HTMLElement | ArrayLike<HTMLElement>, options: OPTIONS = {}, ) { this.$native = $(selector).first() as JQ<HTMLSelectElement>; this.$native.hide(); extend(this.options, options); // 为当前 select 生成唯一 ID this.uniqueID = $.guid(); // 生成 select this.handleUpdate(); // 点击 select 外面区域关闭 $document.on('click touchstart', (event: Event) => { const $target = $(event.target as HTMLElement); if ( this.isOpen() && !$target.is(this.$element) && !contains(this.$element[0], $target[0]) ) { this.close(); } }); } /** * 调整菜单位置 */ private readjustMenu(): void { 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: string; let menuMarginTop: number; 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 是否为打开状态 */ private isOpen(): boolean { return this.state === 'opening' || this.state === 'opened'; } /** * 对原生 select 组件进行了修改后,需要调用该方法 */ public handleUpdate(): void { if (this.isOpen()) { this.close(); } this.selectedValue = this.$native.val() as string; // 保存菜单项数据的数组 type typeItemsData = { value: string; text: string; disabled: boolean; selected: boolean; index: number; }; const itemsData: typeItemsData[] = []; 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: Event) => { const $target = $(event.target as HTMLElement); // 在菜单上点击时不打开 if ( $target.is('.mdui-select-menu') || $target.is('.mdui-select-menu-item') ) { return; } this.toggle(); }); } /** * 动画结束的回调 */ private transitionEnd(): void { 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 */ private triggerEvent(name: EVENT): void { componentEvent(name, 'select', this.$native, this); } /** * 切换下拉菜单的打开状态 */ public toggle(): void { this.isOpen() ? this.close() : this.open(); } /** * 打开下拉菜单 */ public open(): void { if (this.isOpen()) { return; } this.state = 'opening'; this.triggerEvent('open'); this.readjustMenu(); this.$element.addClass('mdui-select-open'); this.$menu.transitionEnd(() => this.transitionEnd()); } /** * 关闭下拉菜单 */ public close(): void { 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` */ public getState(): STATE { return this.state; } } mdui.Select = Select;