// @ts-nocheck import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Grid, Button, Select, Tab, Menu } from '@alifd/next'; import AddressLabel from './AddressLabel'; import MenuWrap from './MenuWrap'; import { PANE_LEVELS, PANE_ATTRIBUTES, PANE_LEVEL_KEYS, loadAddressData, foldNodeChainToSelection, nodeChainFromNode, selectionToChain, loadTownData, traceTownParents, defaultTextSerializer, addressTreeOfSet, delay, id, } from './util'; import { PROVINCE_LETTERS, provinceLetterParititon } from './Common'; import { doSearch } from './search-util'; import { last, zipObject, dropRight } from './lodash-alt'; const { Row, Col } = Grid; const KEY_SELECT_VALUE = 'KEY_SELECT_VALUE'; function selectNodeFromChain(node, chain) { const ret = [node]; return !chain.length ? ret : ret.concat( selectNodeFromChain( node.children.find((n) => n.id === chain[0]), chain.slice(1, chain.length), ), ); } function anyValueToSelectionId(val) { switch (typeof val) { case 'string': case 'number': return val.toString(); case 'object': return last(selectionToChain(val)); default: throw new Error(`invalid address anyValue ${val}`); } } function addressEqual(lhs, rhs) { return anyValueToSelectionId(lhs) === anyValueToSelectionId(rhs); } async function loadAndMergeTownList(params, node, allNodesSet) { const townList = await loadTownData(params, node); /* eslint-disable */ node.children = townList; /* eslint-enable */ Object.assign( allNodesSet, zipObject( townList.map((t) => t.id), townList, ), ); return townList; } function selectedNodeChain(addressTree, value) { const chain = selectionToChain(value); return selectNodeFromChain(addressTree, chain); } function getShownTabs(normalizedValue, allNodesSet, maxLevel) { const nodeChain = selectedNodeChain(addressTreeOfSet(allNodesSet), normalizedValue); return nodeChain .filter((node) => node.children && node.children.length) .slice(1) .filter((node) => PANE_ATTRIBUTES[node.children[0].levelKey].index <= maxLevel); } const selectionLevelCanCommit = async (node, minLevel) => { if (minLevel < 4) { const isCurLevelEnough = node.level >= minLevel; const curLevelTooSmallButNoAvailableChild = node.isLeaf || node.children.length === 0 || node.children[0].level !== node.level + 1; return isCurLevelEnough || curLevelTooSmallButNoAvailableChild; } const isCurLevelEnough = node.level >= minLevel; const curLevelNoAvailableChild = node.isLeaf || node.children.length === 0; // 不存在子节点 return isCurLevelEnough || curLevelNoAvailableChild; }; function displayValueFromNodeChain(nodeChain) { return nodeChain .slice(2) .map((node) => node.nameZh) .join(' / '); } export default class Address extends React.Component { static propTypes = { className: PropTypes.string, style: PropTypes.object, value: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.string]), defaultValue: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.string]), onChange: PropTypes.func, size: PropTypes.oneOf(['small', 'medium', 'large']), overseas: PropTypes.bool, level: PropTypes.number, addressSelectPlaceholder: PropTypes.string, countrySelectPlaceholder: PropTypes.string, disabled: PropTypes.bool, readOnly: PropTypes.bool, // TODO: can do better - componentSerializer textOnly: PropTypes.bool, textSerializer: PropTypes.func, requestAddressUrl: PropTypes.string, requestTownUrl: PropTypes.string, requestAddressLevelUrl: PropTypes.string, fixedWidth: PropTypes.bool, animation: PropTypes.bool, 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]), partialSelectionOverlayClosable: PropTypes.bool, normalizeInitValue: PropTypes.bool, provincePartition: PropTypes.oneOf(['letter']), showSearch: PropTypes.bool, onLabelUpdate: PropTypes.func, popupProps: PropTypes.object, popupClassName: PropTypes.string, countryPopupProps: PropTypes.object, countryPopupClassName: PropTypes.string, _IS_CN_ADDRESS_: PropTypes.bool, container: PropTypes.any, }; static defaultProps = { defaultValue: { country: '1', province: undefined, city: undefined, area: undefined, town: undefined, }, level: 4, addressSelectPlaceholder: '请选择地址', countrySelectPlaceholder: '请选择国家', disabled: false, readOnly: false, textOnly: false, textSerializer: defaultTextSerializer, requestAddressUrl: '//division-data.alicdn.com/simple/addr_4_1111_1_0.js', requestTownUrl: 'https://lsp.wuliu.taobao.com/locationservice/addr/output_address_town.do', requestAddressLevelUrl: 'https://lsp.wuliu.taobao.com/locationservice/addr/outputParentDivisons.do', fixedWidth: true, animation: true, hasClear: false, ignored: [], dataOverride: {}, preprocessor: id, hiddenData: [], hasToSelectToLastLevel: false, partialSelectionOverlayClosable: true, normalizeInitValue: false, showSearch: false, _IS_CN_ADDRESS_: true, }; constructor(props) { super(props); this.state = { addressTree: {}, countryList: [], paneRoot: null, uiOverlayVisible: false, selectionCommitable: false, uiActiveTab: 'province', uiActiveTabPrev: 'province', value: {}, overlayValue: {}, inSearch: false, searchResult: null, searchQuery: '', }; this.selectRef = null; this.updateHiddenMap(props.hiddenData); this.handleSelect = this.handleSelect.bind(this); this.handleSelectCountry = this.handleSelectCountry.bind(this); this.handleSelectChange = this.handleSelectChange.bind(this); this.handleClear = this.handleClear.bind(this); this.handleOverlayVisibleChange = this.handleOverlayVisibleChange.bind(this); this.handleActiveTabChanged = this.handleActiveTabChanged.bind(this); this.getOverlayContainer = this.getOverlayContainer.bind(this); this.handleSearch = this.handleSearch.bind(this); this.handleSearchFocus = this.handleSearchFocus.bind(this); this.handleSelectSearchItem = this.handleSelectSearchItem.bind(this); const { value, defaultValue } = this.props; // TODO: lift address data store into a larger scope (async () => { const initialValue = props.value || props.defaultValue; const { nodeChain, address } = await this.normalizeValueReturningAddressAndNodeChain(initialValue); // TODO: investigate the sequence problem of valuesetting if ((value || defaultValue) === initialValue) { this.triggerLabelUpdate(nodeChain); await this.setStateAsync({ value: address, searchQuery: this.displayValueFromSelection(address), uiActiveTab: this.getNewActiveTab(address), }); } })(); } updateHiddenMap(hiddenList) { const ret = zipObject(hiddenList, new Array(hiddenList.length).fill(true)); this.hiddenMap = ret; return ret; } getMinSelectLevel(props = this.props) { const v = props.hasToSelectToLastLevel; switch (typeof v) { case 'number': return v; case 'boolean': return v ? props.level : 0; default: return 0; } } async componentWillReceiveProps(nextProps) { // TODO: this is still a concurrency bug const { value } = this.state; const { hiddenData, defaultValue, requestAddressUrl } = this.props; if ('requestAddressUrl' in nextProps && requestAddressUrl !== nextProps.requestAddressUrl) { this.setState({ value: await this.normalizeValue(value), }); } if ('hiddenData' in nextProps && hiddenData !== nextProps.hiddenData) { this.updateHiddenMap(nextProps.hiddenData); } const nextValue = (() => { if (typeof nextProps.value === 'undefined' || nextProps.value === null) { // reseting the field should result in ALL value cleared // instead of fallback to defaultValue // but that would not be appropriate for our Address // (when used without overseas) // use { } to state an EMPTY value explicitly // return { } return /* nextProps.defaultValue || */ defaultValue; } return nextProps.value; })(); const { address: normalizedValue, nodeChain } = await this.normalizeValueReturningAddressAndNodeChain(nextValue); // TODO: Address + Field + setValue ... need more work on this later if ('value' in nextProps /* && this.state.value == valueNow */) { this.triggerLabelUpdate(nodeChain); if (!addressEqual(nextValue, value)) { this.setState({ value: normalizedValue, searchQuery: this.displayValueFromSelection(normalizedValue), // 05/12/2017: we need to update activeTab too // or the overlay would be empty // in fact, the active tab may be better implemented // as a view constraint, rather than view state uiActiveTab: this.getNewActiveTab(normalizedValue), }); } } } /** * 设置 defaultValue 包含街道时触发街道列表查询 * @param {array} chain 节点 id([counry, province, city, area, town]) */ async getDefaultValToTownList(chain) { const { level } = this.props; const { addressTree } = this.state; if (level >= 4 && chain && chain.length) { const selectionToChains = selectNodeFromChain(addressTree, chain) || []; const selectionLen = (selectionToChains && selectionToChains.length) || 0; const townNode = selectionToChains[selectionLen - 1]; const node = selectionToChains[selectionLen - 2]; if (!townNode && node.levelKey !== 'town' && node.levelKey !== 'country' && !node.children.length) { await loadAndMergeTownList(this.getRequestParams(), node, this.allNodesSet); } } } async normalizeValueReturningNodeChain(val, params = this.getRequestParams()) { const traceTownParentsAndUpdateIfDataNotChanged = async (town, { addressTree, allNodesSet }) => { const chain = await traceTownParents(params, town); await this.getDefaultValToTownList(chain); const nodeChain = selectNodeFromChain(addressTree, dropRight(1, chain)); if (this.allNodesSet === allNodesSet) { this.setState({ addressTree }); } return [...nodeChain, allNodesSet[town]]; }; if (typeof val === 'number' || typeof val === 'string') { const dataset = await this.dataset(params); const { allNodesSet } = dataset; if (allNodesSet[val]) { return nodeChainFromNode(allNodesSet[val]); } /* eslint-disable */ return await traceTownParentsAndUpdateIfDataNotChanged(val, dataset); } if (typeof val === 'object') { const dataset = await this.dataset(params); const { allNodesSet } = dataset; if (val.town && !this.allNodesSet[val.town]) { return await traceTownParentsAndUpdateIfDataNotChanged(val.town, dataset); /* eslint-disable */ } const selectionChain = selectionToChain(val); if (!selectionChain.length) { return []; } return nodeChainFromNode(allNodesSet[last(selectionChain)]); } throw new Error(`Address normalizeValue(): unknown value of ${typeof val} - ${val}`); } async normalizeValueReturningAddressAndNodeChain(val, params) { const nodeChain = await this.normalizeValueReturningNodeChain(val, params); const address = foldNodeChainToSelection(nodeChain); return { nodeChain, address }; } async normalizeValue(val, params) { const nodeChain = await this.normalizeValueReturningNodeChain(val, params); const address = foldNodeChainToSelection(nodeChain); return address; } setStateAsync(updater) { return new Promise((resolve) => { this.setState(updater, () => resolve()); }); } async loadAndStoreDataset(params = this.getRequestParams()) { const { countryList, allNodesSet, addressTree } = await loadAddressData(params); this.allNodesSet = allNodesSet; await this.setStateAsync({ countryList, addressTree, paneRoot: allNodesSet['1'], }); return { addressTree, allNodesSet, }; } // TODO: this impl. is not exactly right ... async dataset(params) { const { paneRoot, addressTree } = this.state; if (paneRoot) { return { addressTree, allNodesSet: this.allNodesSet, }; } // concurrency issue ... if (!this.futureDatasetTem) { this.futureDatasetTem = this.loadAndStoreDataset(params); } return await this.futureDatasetTem; } getNewActiveTab(newValue) { const { level } = this.props; const tabs = getShownTabs(newValue, this.allNodesSet, level); if (tabs.length) { return last(tabs).children[0].levelKey; } return 'province'; } async handleSelect(node) { const newNodeChain = nodeChainFromNode(node); const newValue = foldNodeChainToSelection(newNodeChain); const { level, onChange } = this.props; const { addressTree, uiOverlayVisible, uiActiveTab } = this.state; // when the user selected an item in the last level, by common sense, // his/her work on the pane is done, specifically: // * if we has selected the value of last level specified in props // * or we are not in the last level, but we got no child // -> close the pane overlay if (node.levelKey === PANE_LEVEL_KEYS[level] || (level < 4 && !node.children.length)) { this.setState({ uiOverlayVisible: false }); if (this.selectRef && this.selectRef.hide && typeof this.selectRef.hide === 'function') { this.selectRef && this.selectRef?.hide(); // 无线影藏selectDrawer弹窗 } } const activeTab = this.getNewActiveTab(newValue); this.setState({ overlayValue: newValue, selectionCommitable: false }); const stateMutation = { uiActiveTab: activeTab }; // do not commit if hasToSelectToLastLevel isn't meet // OR we are clearing (i.e. selecting a country node) // (you're not able to select a country node in tab panes, except clear) if (node.level <= 0 || (await selectionLevelCanCommit(node, this.getMinSelectLevel()))) { stateMutation.value = newValue; stateMutation.selectionCommitable = true; this.setState(stateMutation); if (onChange) { onChange(newValue, newNodeChain, newNodeChain); } } else { stateMutation.selectionCommitable = false; this.setState(stateMutation); } // don't fetch 'town' children, if at least one of the following conditions holds: // * the user does not need 'town' level // * if we are already in the 'town' level // * if the current node already has children // it either does not need to fetch, or already has been fetched beforehand // * if we are in the country level // (or the server would just return a whole bunch of garbage, // THIS SEEMS TO BE A SERVER BUG, and we have to use a hack) const { overlayValue } = this.state; if (level >= 4 && node.levelKey !== 'town' && node.levelKey !== 'country' && !node.children.length) { const townList = await loadAndMergeTownList(this.getRequestParams(), node, this.allNodesSet); await this.setStateAsync({ addressTree }); const nodeChain = this.selectedNodeChain(overlayValue); if (uiOverlayVisible && last(nodeChain) === node) { this.setState({ uiActiveTab: townList.length ? 'town' : uiActiveTab, uiOverlayVisible: !!townList.length, }); } } this.exitSearch(newValue); } async handleSelectCountry(key) { let newValue; let newNodeChain = []; const { onChange } = this.props; const { addressTree } = this.state; if (key) { newValue = await this.normalizeValue({ country: key }); newNodeChain = selectNodeFromChain(addressTree, selectionToChain(newValue)); } await this.setStateAsync({ value: newValue || {}, uiActiveTab: 'province', }); if (onChange) { onChange(newValue || {}, newNodeChain, newNodeChain); } } // this is invoked only for builtin clear of return ( { return 'menu'; }} > {inner} ); } render() { const { className, style, value, size, textOnly, textSerializer, level, addressSelectPlaceholder, countrySelectPlaceholder, requestAddressUrl, requestTownUrl, requestAddressLevelUrl, fixedWidth, overseas, animation, hasClear, container, ignored, dataOverride, preprocessor, hiddenData, hasToSelectToLastLevel, partialSelectionOverlayClosable, normalizeInitValue, provincePartition, showSearch, onLabelUpdate, popupProps, countryPopupProps, popupClassName, countryPopupClassName, _IS_CN_ADDRESS_, ...rest } = this.props; const elements = []; const { value: val, countryList, uiOverlayVisible, overlayValue, searchQuery } = this.state; // if we are loading address data -> show a 'loading' status // if the user has enabled 'overseas' -> show a country select // if the current country has more fine-grained address data, // and the user does not forbid it explicitly -> show address select pane // (just a fancy FizzBuzz ... if (!this.safeToRender()) { elements.push( 加载数据中 … , ); } else { const nodeChain = this.selectedNodeChain(val); if (textOnly) { let text = ''; if (nodeChain.length < 2) { text = textSerializer([]); } else { text = textSerializer(nodeChain.slice(1)); } return ( {text} ); } if (overseas) { elements.push(