/* eslint-disable */ import React from "react"; import { AttributeSelect, AttributeValue } from "./AttributeSelect"; import { ValueSelect } from "./valueselect/ValueSelect"; import { withConfig, WithConfigProps } from "../_util/config-context"; import { mergeRefs } from "../_util/merge-refs"; import { Popover } from "../popover"; import { TagSearchBoxContext } from "./TagSearchBoxContext"; import { Input } from "../input"; export interface TagInputProps extends WithConfigProps { /** * 触发标签相关事件 */ dispatchTagEvent: (type: string, payload?: any) => void; /** * 所有属性集合 */ attributes: Array; /** * 是否为 Focus 态 */ isFocused: boolean; /** * 搜索框是否处于展开状态 */ active: boolean; /** * 输入框类型(用于修改标签值的 Input type 为 "edit") */ type?: "edit" | "add"; /** * 是否隐藏 */ hidden?: boolean; /** * 最大宽度 */ maxWidth: number; /** * 处理按键事件 */ handleKeyDown?: (e: any) => void; /** * 位置偏移 */ inputOffset?: number; } export interface InputState { inputWidth: number; inputValue: string; // 包含 Input 输入法未完成是值 fullInputValue: string; attribute: AttributeValue; values: Array; showAttrSelect: boolean; showValueSelect: boolean; valueSelectOffset: number; } const keys = { "8": "backspace", "9": "tab", "13": "enter", "27": "esc", "37": "left", "38": "up", "39": "right", "40": "down", }; const INPUT_MIN_SIZE = 0; const SELECT_MIN_HEIGHT = 242; export class ITagInput extends React.Component { static contextType = TagSearchBoxContext; state: InputState = { inputWidth: INPUT_MIN_SIZE, inputValue: "", fullInputValue: "", attribute: null, values: [], showAttrSelect: false, showValueSelect: false, valueSelectOffset: 0, }; _scheduleUpdate: () => any; wrapperRef: React.RefObject; constructor(props) { super(props); this._scheduleUpdate = () => null; this.wrapperRef = React.createRef(); } componentDidMount() {} /** * 刷新下拉列表位置 */ scheduleUpdate = () => this._scheduleUpdate(); /** * 刷新选择组件显示 */ refreshShow = (): void => { const { inputValue, attribute } = this.state; const input = this["input"] as HTMLInputElement; let start = input?.selectionStart, end = input?.selectionEnd; const { pos } = this.getAttrStrAndValueStr(inputValue); if (pos < 0 || start <= pos) { this.setState({ showAttrSelect: true, showValueSelect: false }); return; } if (attribute && end > pos) { this.setState({ showAttrSelect: false, showValueSelect: true }); } }; focusInput = (): void => { if (!this["input"]) return; const input = this["input"] as HTMLElement; input?.focus(); }; moveToEnd = (): void => { const input = this["input"] as HTMLInputElement; input?.focus(); const value = this.state.inputValue; setTimeout(() => input?.setSelectionRange(value.length, value.length), 0); }; selectValue = (): void => { const input = this["input"] as HTMLInputElement; input?.focus(); const value = this.state.inputValue; let { pos } = this.getAttrStrAndValueStr(value); if (pos < 0) pos = -2; setTimeout(() => { input?.setSelectionRange(pos + 2, value.length); this.refreshShow(); }, 0); }; selectAttr = (): void => { const input = this["input"] as HTMLInputElement; input?.focus(); const value = this.state.inputValue; let { pos } = this.getAttrStrAndValueStr(value); if (pos < 0) pos = 0; setTimeout(() => { input?.setSelectionRange(0, pos); this.refreshShow(); }, 0); }; setInfo(info: any, callback?: Function) { const attribute = info.attr; const values = info.values; this.setState({ attribute, values }, () => { if (attribute) { this.setInputValue( `${attribute.name}: ${values.map(item => item.name).join(" | ")}`, callback ); } else { this.setInputValue( `${values.map(item => item.name).join(" | ")}`, callback ); } }); } setInputValue = (value: string, callback?: Function): void => { if (this.props.type === "edit" && value.trim().length <= 0) { return this.props.dispatchTagEvent("del", "edit"); } const attributes = this.props.attributes; let attribute = null, valueStr = value; const input = this["input"] as HTMLElement; const mirror = this["input-mirror"] as HTMLElement; // attribute 是否存在 for (let i = 0; i < attributes.length; ++i) { if ( value.indexOf(attributes[i].name + ":") === 0 || value.indexOf(attributes[i].name + ":") === 0 ) { // 获取属性/值 attribute = attributes[i]; valueStr = value.substr(attributes[i].name.length + 1); // 计算 offset if (mirror) { mirror.innerText = attribute.name + ": "; let width = mirror.clientWidth; if (this.props.inputOffset) width += this.props.inputOffset; this.setState({ valueSelectOffset: width }, () => this.scheduleUpdate() ); } break; } } // 处理前导空格 if (attribute && valueStr.replace(/^\s+/, "").length > 0) { value = `${attribute.name}: ${valueStr.replace(/^\s+/, "")}`; } else if (attribute) { value = `${attribute.name}:${valueStr}`; } // attribute 变化时刷新 values if (attribute !== this.state.attribute) { this.setState({ values: valueStr.split("|").map(item => ({ name: item.trim() })), }); } this.setState({ attribute }, this.refreshShow); if (this.props.type === "edit") { this.props.dispatchTagEvent("editing", { attr: attribute }); } mirror.innerText = value; const width = mirror.clientWidth > INPUT_MIN_SIZE ? mirror.clientWidth : INPUT_MIN_SIZE; this.setState( { inputValue: value, fullInputValue: value, inputWidth: width }, () => { if (callback) callback(); } ); }; /** * 包含输入法过程的输入值 */ setFullInputValue = (value: string): void => { const attributes = this.props.attributes; let attribute = null, valueStr = value; const mirror = this["input-mirror"] as HTMLElement; // attribute 是否存在 for (let i = 0; i < attributes.length; ++i) { if ( value.indexOf(attributes[i].name + ":") === 0 || value.indexOf(attributes[i].name + ":") === 0 ) { // 获取属性/值 attribute = attributes[i]; valueStr = value.substr(attributes[i].name.length + 1); // 计算 offset if (mirror) { mirror.innerText = attribute.name + ": "; let width = mirror.clientWidth; if (this.props.inputOffset) width += this.props.inputOffset; this.setState({ valueSelectOffset: width }, () => this.scheduleUpdate() ); } break; } } // 处理前导空格 if (attribute && valueStr.replace(/^\s+/, "").length > 0) { value = `${attribute.name}: ${valueStr.replace(/^\s+/, "")}`; } else if (attribute) { value = `${attribute.name}:${valueStr}`; } if (mirror) { mirror.innerText = value; const width = mirror.clientWidth > INPUT_MIN_SIZE ? mirror.clientWidth : INPUT_MIN_SIZE; this.setState({ fullInputValue: value, inputWidth: width }); } }; resetInput = (callback?: Function): void => { this.setInputValue("", callback); this.setState({ inputWidth: INPUT_MIN_SIZE }); }; getInputValue = (): string => { return this.state.inputValue; }; addTagByInputValue = (): boolean => { const { attribute, values, inputValue } = this.state; const type = this.props.type || "add"; // 属性值搜索 if ( attribute && this.props.attributes.filter(item => item.key === attribute.key).length > 0 ) { if (values.length <= 0) { return false; } this.props.dispatchTagEvent(type, { attr: attribute, values: values }); } else { // 关键字搜索 if (inputValue.trim().length <= 0) { return false; } const list = inputValue .split("|") .filter(item => item.trim().length > 0) .map(item => { return { name: item.trim() }; }); this.props.dispatchTagEvent(type, { attr: null, values: list }); } this.setState({ showAttrSelect: false, showValueSelect: false }); if (this.props.type !== "edit") { this.resetInput(); } return true; }; handleInputChange = (value): void => { this.setInputValue(value); }; handleInputClick = (e): void => { this.props.dispatchTagEvent("click-input", this.props.type); e.stopPropagation(); this.focusInput(); }; handleAttrSelect = (attr: AttributeValue): void => { if (attr && attr.key) { const str = `${attr.name}: `; const inputValue = this.state.inputValue; if (inputValue.indexOf(str) >= 0) { this.selectValue(); } else { this.setInputValue(str); } this.setState({ values: [] }); } this.focusInput(); }; handleValueChange = (values: Array): void => { this.setState({ values }, () => { this.setInputValue( `${this.state.attribute.name}: ${values .map(item => item.name) .join(" | ")}` ); this.focusInput(); }); }; /** * 值选择组件完成选择 */ handleValueSelect = (values: Array): void => { this.setState({ values }); const inputValue = this.state.inputValue; if (values.length <= 0) { this.setInputValue(this.state.attribute.name + ": "); return; } if (values.length > 0) { const key = this.state.attribute.key; if (this.props.attributes.filter(item => item.key === key).length > 0) { const type = this.props.type || "add"; this.props.dispatchTagEvent(type, { attr: this.state.attribute, values, }); } this.focusInput(); } if (this.props.type !== "edit") { this.resetInput(); } }; /** * 值选择组件取消选择 */ handleValueCancel = () => { if (this.props.type === "edit") { const { attribute, values } = this.state; this.props.dispatchTagEvent("edit-cancel", { attr: attribute, values: values, }); } else { this.resetInput(() => { this.focusInput(); }); } }; /** * 处理粘贴事件 */ handlePaste = (e): void => { e.stopPropagation(); e.preventDefault(); const { attribute } = this.state; if (!attribute || attribute.type === "input") { let value: string = ""; try { const clipboardData = e.clipboardData; value = clipboardData.getData("Text") || ""; } catch (_) {} if (/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/.test(value)) { value = value.replace(/[\r\n\t,,\s]+/g, "|"); } else { value = value.replace(/[\r\n\t,,]+/g, "|"); } value = value .split("|") .map(item => item.trim()) .filter(item => item.length > 0) .join(" | "); const input = this["input"] as HTMLInputElement; const start = input?.selectionStart, end = input?.selectionEnd; const inputValue = this.state.inputValue; // 覆盖选择区域 const curValue = inputValue.substring(0, start) + value + inputValue.substring(end, inputValue.length); // input 属性情况 if (attribute && attribute.type === "input") { this.setInputValue(curValue, this.focusInput); return; } if (inputValue.length > 0) { this.setInputValue(curValue, this.focusInput); } else { this.setInputValue(curValue, this.addTagByInputValue); } } }; handleKeyDown = (e: React.KeyboardEvent): void => { if (!keys[e.keyCode]) return; if (this.props.hidden) { return this.props.handleKeyDown(e); } const inputValue = this.state.inputValue; if (keys[e.keyCode] === "backspace" && inputValue.length > 0) return; if ( (keys[e.keyCode] === "left" || keys[e.keyCode] === "right") && inputValue.length > 0 ) { setTimeout(this.refreshShow, 0); return; } if (keys[e.keyCode] === "esc") { if (!inputValue) { this.context.close(); } return this.handleValueCancel(); } e.preventDefault(); // 事件下传 if (this["attr-select"]) { if (this["attr-select"].handleKeyDown(e.keyCode) === false) return; } if (this["value-select"]) { this["value-select"].handleKeyDownForRenderMode(e.key); if (this["value-select"].handleKeyDown(e.keyCode) === false) return; } switch (keys[e.keyCode]) { case "enter": case "tab": if (!this.props.isFocused) { this.props.dispatchTagEvent("click-input"); } this.addTagByInputValue(); break; case "backspace": this.props.dispatchTagEvent("del", "keyboard"); break; case "up": break; case "down": break; } }; getAttrStrAndValueStr = (str: string): any => { let attrStr = str, valueStr = "", pos = -1; const attributes = this.props.attributes; for (let i = 0; i < attributes.length; ++i) { if (str.indexOf(attributes[i].name + ":") === 0) { // 获取属性/值 attrStr = attributes[i].name; valueStr = str.substr(attrStr.length + 1); pos = attributes[i].name.length; } } return { attrStr, valueStr, pos }; }; render() { const { inputWidth, inputValue, fullInputValue, showAttrSelect, showValueSelect, attribute, valueSelectOffset, } = this.state; const { config: { classPrefix }, active, attributes, isFocused, hidden, maxWidth, type, } = this.props; const { attrStr, valueStr } = this.getAttrStrAndValueStr(inputValue); const wrapper = this.wrapperRef.current; let maxHeight = SELECT_MIN_HEIGHT; try { if (wrapper) { maxHeight = window.innerHeight - wrapper.getBoundingClientRect().bottom - 60; } } catch (_) {} maxHeight = Math.max(maxHeight, SELECT_MIN_HEIGHT); const input = type !== "edit" ? ( (this["input"] = input)} type="text" className={`${classPrefix}-input--tag`} placeholder="" style={{ width: hidden ? 0 : inputWidth + 6, display: active ? "" : "none", maxWidth: maxWidth ? maxWidth - 36 : 435, }} value={inputValue} onChange={this.handleInputChange} onInput={e => this.setFullInputValue(e.currentTarget.value)} onKeyDown={this.handleKeyDown} onFocus={this.refreshShow} onClick={this.refreshShow} onPaste={this.handlePaste} /> ) : ( ); return ( { if (!visible) { this.context.close(); } }} overlay={({ scheduleUpdate }) => { this._scheduleUpdate = scheduleUpdate; return ( <> {showAttrSelect && ( (this["attr-select"] = select)} attributes={attributes} inputValue={attrStr} onSelect={this.handleAttrSelect} maxHeight={maxHeight} /> )} {showValueSelect && !!attribute && !!attribute.type && ( (this["value-select"] = select)} values={attribute.values} render={attribute.render} inputValue={valueStr.trim()} offset={valueSelectOffset} onChange={this.handleValueChange} onSelect={this.handleValueSelect} onCancel={this.handleValueCancel} maxHeight={maxHeight} /> )} ); }} > ); } } export const TagInput = withConfig(ITagInput);