// @ts-nocheck import React from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import { Select, Balloon } from "@alifd/next"; import AddressCascader from "./AddressCascader"; import { loadTownData, traceTownParents } from "./util"; import { AddressCompE, setStateAsync } from "./Common"; import { last, zipObject, dropRight } from "./lodash-alt"; import { NodeID, NodeSelection, AddressNode, AddressNodeSet, AddressNodeRoot, AddressNodeInternal, AddressNodeLeaf, AddressDataset, AddressRequestParams, CascaderSelectOnChangeExtraData, NodeIDStrict, LabelChineseSimplified, } from "./Types"; import { dataset, AddressCompProps, AddressCompState, nodeChainFromNode, selectNodeFromChain, selectionToChain, foldNodeChainToSelection, anyValueToSelectionId, CANNOT_REACH, maybeGet, addressEqual, id, } from "./Common"; // qualified import for name conflict import * as C from "./Common"; interface AddressMultiProps extends AddressCompProps { root: NodeSelection | NodeID; value: NodeSelection[]; defaultValue: NodeSelection[]; placeholder: any; onChange: (value: NodeSelection[], nodes: AddressNode[]) => void; size: "small" | "medium" | "large"; // not available for now ... hasClear: boolean; popupContainer: React.ReactNode | Function; onLabelUpdate: (label: string) => void; popupProps: object; popupClassName: string; } interface AddressMultiState extends AddressCompState { value: NodeSelection[]; cascadeRootNodeID: NodeIDStrict | null; uiOverlayVisible: boolean; expandedValue: NodeIDStrict; } export default class AddressMulti extends React.Component< AddressMultiProps, AddressMultiState > { static propTypes = { className: PropTypes.string, style: PropTypes.object, value: PropTypes.arrayOf( PropTypes.oneOfType([ PropTypes.object, PropTypes.number, PropTypes.string, ]) ), defaultValue: PropTypes.arrayOf( PropTypes.oneOfType([ PropTypes.object, PropTypes.number, PropTypes.string, ]) ), root: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.object, ]), level: PropTypes.number, disabled: PropTypes.bool, placeholder: PropTypes.node, requestAddressUrl: PropTypes.string, requestTownUrl: PropTypes.string, requestAddressLevelUrl: PropTypes.string, showItemCount: PropTypes.number, maxWidth: PropTypes.number, size: PropTypes.oneOf(["small", "medium", "large"]), hasClear: PropTypes.bool, popupContainer: PropTypes.func, ignored: PropTypes.arrayOf(PropTypes.string), dataOverride: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), preprocessor: PropTypes.func, hiddenData: PropTypes.arrayOf(PropTypes.string), hasToSelectToLastLevel: PropTypes.oneOfType([ PropTypes.bool, PropTypes.number, ]), onLabelUpdate: PropTypes.func, popupProps: PropTypes.object, popupClassName: PropTypes.string, _IS_CN_ADDRESS_MULTI_: PropTypes.bool, }; static defaultProps = { level: 4, placeholder: "请选择地址", requestAddressUrl: "//division-data.alicdn.com/simple/addr_4_1111_1_0.js", requestTownUrl: "//lsp.wuliu.taobao.com/locationservice/addr/output_address_town.do", requestAddressLevelUrl: "//lsp.wuliu.taobao.com/locationservice/addr/outputParentDivisons.do", root: "0", defaultValue: [], showItemCount: 8, maxWidth: 256, size: "medium", hasClear: false, popupContainer: null, ignored: [], dataOverride: {}, preprocessor: id, hiddenData: [], disabled: false, _IS_CN_ADDRESS_MULTI_: true, }; allNodesSet: AddressNodeSet | null; hiddenMap: { [id: string]: any } = {}; constructor(props: AddressMultiProps) { super(props); this.state = { value: [], cascadeRootNodeID: null, paneRoot: null, addressTree: {} as AddressNodeInternal, uiOverlayVisible: false, expandedValue: "1", }; this.updateHiddenMap(this.props.hiddenData); this.handleCascadeChange = this.handleCascadeChange.bind(this); this.handleLoadData = this.handleLoadData.bind(this); this.handleOverlayVisibleChange = this.handleOverlayVisibleChange.bind(this); this.getOverlayContainer = this.getOverlayContainer.bind(this); this.asyncInitialize(); } asyncInitialize = async () => { // PROBLEM: how to deal with these state + async? const defaultValue: NodeSelection[] = this.props.defaultValue; const initialValue = this.props.value || defaultValue; // props.defaultValue await this.dataset(); await this.normalizeAndUpdateCascadeRoot(this.props.root); const temps = await Promise.all( initialValue.map((v) => this.normalizeValueReturningAddressAndNodeChain(v) ) ); // TODO: fix this equality logic ... if ((this.props.value || defaultValue) == initialValue) { // 过滤无效 const filters = (temps || []).filter((item) => { return Object.keys(item.address || {}).length; }); const value = filters.map((t) => t.address); await this.setStateAsync({ value }); this.triggerLabelUpdate(initialValue); } }; updateHiddenMap(hiddenList: NodeIDStrict[]) { const ret = zipObject(hiddenList, new Array(hiddenList.length).fill(true)); this.hiddenMap = ret; return ret; } async componentWillReceiveProps(nextProps: AddressMultiProps) { if ( "root" in nextProps && (this.state.cascadeRootNodeID == null || !addressEqual(nextProps.root, this.state.cascadeRootNodeID)) ) { this.normalizeAndUpdateCascadeRoot(nextProps.root); } if ("value" in nextProps) { let { value } = nextProps; if (value === undefined || value === null || (value as any) === "") { value = []; } if (!Array.isArray(value)) { console.error("value of AddressMulti must be an array!"); } // console.log(nextProps.value) const temps = await Promise.all( value.map((v) => this.normalizeValueReturningAddressAndNodeChain(v)) ); // TODO: equality testing const valueNew = temps.map((t) => t.address); this.setState({ value: valueNew }); this.triggerLabelUpdate(valueNew, nextProps); } if ( "hiddenData" in nextProps && this.props.hiddenData != nextProps.hiddenData ) { this.updateHiddenMap(nextProps.hiddenData); } } getRequestParams = (props = this.props): AddressRequestParams => ({ addressURL: props.requestAddressUrl, townURL: props.requestTownUrl, parentURL: props.requestAddressLevelUrl, ignored: props.ignored, dataOverride: props.dataOverride, preprocessor: props.preprocessor, }); // TODO: this is not type safe setStateAsync = (updater: object) => new Promise((resolve, reject) => // @ts-ignore this.setState(updater, () => resolve()) ); dataset = ( params: AddressRequestParams = this.getRequestParams() ): Promise => C.dataset(this, this.getRequestParams()); datasetImmediate = (): AddressCompE => this as AddressCompE; loadAndMergeTownList = async ( node: AddressNodeInternal | AddressNodeRoot, params: AddressRequestParams = this.getRequestParams(), allNodesSetNow = this.datasetImmediate().allNodesSet ) => { const townList: AddressNodeLeaf[] = await loadTownData(params, node); if (townList.length) { node.children = townList; node.isLeaf = false; } else { const nodeIsALeaf = node as any as AddressNodeLeaf; nodeIsALeaf.children = undefined; nodeIsALeaf.isLeaf = true; } Object.assign( (await this.dataset()).allNodesSet, zipObject( townList.map((t) => t.id), townList ) ); if (this.datasetImmediate().allNodesSet === allNodesSetNow) { this.setState({ addressTree: this.state.addressTree }); } return townList; }; async normalizeValueReturningNodeChain( val: NodeID | NodeSelection, params = this.getRequestParams() ) { const traceTownParentsAndUpdateIfDataNotChanged = async ( town: NodeID, { addressTree, allNodesSet }: AddressDataset ) => { const chain = await traceTownParents(params, town); const nodeChain = selectNodeFromChain(addressTree, dropRight(1, chain)), node = last(nodeChain); if (node) { await this.loadAndMergeTownList( node as AddressNodeInternal, params, allNodesSet ); } return [...nodeChain, allNodesSet[town.toString()]]; }; if (typeof val == "number" || typeof val == "string") { const dataset = await this.dataset(params); const { addressTree, allNodesSet } = dataset; if (allNodesSet[val]) { return nodeChainFromNode(allNodesSet[val]); } else { return await traceTownParentsAndUpdateIfDataNotChanged(val, dataset); } } else if (typeof val == "object") { const val_ = val as NodeSelection; const dataset = await this.dataset(params); const { addressTree, allNodesSet } = dataset; if ( typeof val_.town == "string" && !this.datasetImmediate().allNodesSet[val_.town] ) { return await traceTownParentsAndUpdateIfDataNotChanged( val_.town, dataset ); } else { const selectionChain = selectionToChain(val_); if (!selectionChain.length) { return []; } return nodeChainFromNode( allNodesSet[maybeGet(last(selectionChain)).toString()] ); } } else { throw `Address normalizeValue(): unknown value of ${typeof val} - ${val}`; } } async normalizeValueReturningAddressAndNodeChain( val: NodeID | NodeSelection, params?: AddressRequestParams ) { const nodeChain = await this.normalizeValueReturningNodeChain(val, params); return { nodeChain, address: foldNodeChainToSelection(nodeChain), }; } normalizeAndUpdateCascadeRoot = async (root: NodeID | NodeSelection) => { if (typeof root == "object" && !("country" in root)) { await this.updateCascadeRoot_("0"); return; } const rootNode = last(await this.normalizeValueReturningNodeChain(root)); // console.log("updating Cascade root", root, rootNode) if (!rootNode || rootNode.isLeaf) { throw new Error(`invalid root node ${root} - ${rootNode}`); } await this.updateCascadeRoot_(rootNode.id); }; updateCascadeRoot_ = async (root: NodeID) => { root = root.toString(); const updater: any = { cascadeRootNodeID: root }; if ( !C.isDescendantOfOne(this.allNodesSet!, this.state.expandedValue, [root]) ) { updater.expandedValue = root; } await this.setStateAsync(updater); }; // TODO: Next 1.x Select compatibility, and a little bug of TS typechecker async handleCascadeChange( values: (NodeIDStrict | { value: NodeIDStrict })[] ) { // console.log(extra) // console.log("select change", arguments) const ids = values.map((v) => typeof v == "object" ? (v as any).value : v ); const normalized = this.normalizeSelection(ids); const nodes = normalized.map( (i) => this.datasetImmediate().allNodesSet[i.toString()] ); const nodeChains = nodes.map(nodeChainFromNode); const selections = nodeChains.map(foldNodeChainToSelection); if (!("value" in this.props)) { await setStateAsync(this, { value: selections }); } this.props.onChange && this.props.onChange(selections, nodes); } async loadTownFromNode( value: AddressNodeRoot | AddressNodeInternal ): Promise { if ( this.props.level >= 4 && !value.isLeaf && (!value.children || !value.children.length) && !(value.levelKey == "town") ) { try { await this.loadAndMergeTownList(value); } catch (e) { console.error(e); } return value; } else { return Promise.resolve(value); } } async handleOverlayVisibleChange(value: boolean) { const arg = { uiOverlayVisible: value }; this.setState(arg); } async loadTownFromNodeId(id: NodeIDStrict): Promise { const value = (await this.dataset()).allNodesSet[id.toString()] as | AddressNodeRoot | AddressNodeInternal; return await this.loadTownFromNode(value); } // the arg value is the one cached inside CascaderSelect, not our data handleLoadData = async (valueClone: AddressNode): Promise => await this.loadTownFromNodeId(valueClone.id); renderCount(displayValues: { label: LabelChineseSimplified }[]) { if (!this.state.value.length) { return null; } const text = displayValues.map((n) => n.label).join(", "); const trigger = ( 共  {this.state.value.length}  项 ); return ( {text} ); } handleExpandChange = async (expandedValue: NodeIDStrict) => { await this.loadTownFromNodeId(expandedValue); this.setState({ expandedValue }); }; getOverlayContainer() { const { popupContainer } = this.props; if (this.props.popupContainer) { if (typeof popupContainer == "function") { return popupContainer(); } return popupContainer; } return this.refs.outerWrapper; } triggerLabelUpdate(nodeValues: NodeSelection[], props = this.props) { if (typeof props.onLabelUpdate == "function") { const nodes = nodeValues .map((v) => anyValueToSelectionId(v)) .map((id) => this.allNodesSet![id.toString()]); props.onLabelUpdate.call( null, nodes.map((node) => node.nameZh).join(", "), nodes ); } } normalizeSelection(ids: NodeIDStrict[]): NodeIDStrict[] { // TODO: optimize this "deduplicate" process const iteration = (src: NodeIDStrict[]): NodeIDStrict[] | false => { const children: Map> = new Map(); const ret: NodeIDStrict[][] = []; src.forEach((id) => { const parentId = this.allNodesSet![id.toString()].parentNode!.id; if (!children.has(parentId)) { children.set(parentId, new Set()); } children.get(parentId)!.add(id); }); let changed: boolean = false; children.forEach((v, k) => { if (this.allNodesSet![k.toString()].children!.length == v.size) { changed = true; ret.push([k]); } else { ret.push(Array.from(v.values())); } }); if (changed) { return Array.prototype.concat.apply([], ret); } else { return false; } }; let idsMerged = ids; while (true) { const t = iteration(idsMerged); if (!t) { break; } else { idsMerged = t; } } const ret = idsMerged.filter( (id) => !C.isDescendantOfOne(this.allNodesSet as AddressNodeSet, id, idsMerged) ); return ret; } renderCascader() { return (
last(selectionToChain(node)))} rootNodeId={anyValueToSelectionId(this.props.root)} level={this.props.level} hiddenMap={this.hiddenMap} />
); } render() { const { className, style, placeholder, requestAddressUrl, requestTownUrl, requestAddressLevelUrl, level, root, onChange, value, hasClear, popupProps, popupClassName, size, ...rest } = this.props; if (!this.allNodesSet) { return ( 加载数据中 ... ); } // TODO: why casting to ReactComponent does not work? const dataSourceRoot = !this.state.cascadeRootNodeID ? [] : this.datasetImmediate().allNodesSet[ this.state.cascadeRootNodeID.toString() ].children; const displayValues = this.state.value .map( (v) => (this.allNodesSet as AddressNodeSet)[ anyValueToSelectionId(v).toString() ] ) .map((n) => ({ label: n.nameZh!, value: n.id })); return ( {/* last(selectionToChain(node)))} onChange={this.handleSelectChange} loadData={this.handleLoadData} /> */}