// @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
handleSelectChange(value) {
if (value === null || value === undefined) {
this.handleClear();
}
}
async handleClear() {
const valueOverride = {};
const { value, addressTree } = this.state;
PANE_LEVELS.forEach((level) => {
valueOverride[level] = undefined;
});
const newValue = {
...value,
...valueOverride,
};
this.handleSelect(last(selectNodeFromChain(addressTree, selectionToChain(newValue))));
this.exitSearch();
this.setState({ uiOverlayVisible: false });
}
handleActiveTabChanged(key) {
this.setState({ uiActiveTab: key });
}
async handleOverlayVisibleChange(val) {
const arg = { uiOverlayVisible: val };
const { value, uiActiveTab, uiActiveTabPrev, selectionCommitable, partialSelectionOverlayClosable } = this.state;
if (val) {
arg.overlayValue = value;
arg.selectionCommitable = await selectionLevelCanCommit(
last(await this.normalizeValueReturningNodeChain(value)),
this.getMinSelectLevel(),
);
arg.uiActiveTabPrev = uiActiveTab;
this.setState(arg);
} else {
this.exitSearch(value);
if (!partialSelectionOverlayClosable && !selectionCommitable) {
return;
}
this.setState({ uiOverlayVisible: false });
if (!selectionCommitable) {
// we are discarding all selection states
// including UI state
arg.uiActiveTab = uiActiveTabPrev;
}
this.setState(arg);
}
}
exitSearch(selection) {
const searchQuery = selection ? this.displayValueFromSelection(selection) : '';
this.setState({ inSearch: false, searchQuery, searchResult: null });
}
handleSearch(v) {
const val = v.trim();
const { value } = this.state;
this.setState({ searchQuery: val });
if (!val) {
this.exitSearch();
return;
}
this.setState({ inSearch: true });
const queryArgs = {
ofCountry: value.country,
};
const result = doSearch(val, queryArgs, this);
this.setState({ searchResult: result.length ? result : null });
this.handleOverlayVisibleChange(true);
}
handleSearchFocus(e, clickByUser) {
if (clickByUser) {
this.setState({ searchQuery: '' });
this.handleOverlayVisibleChange(true);
}
}
async handleSelectSearchItem(nid) {
this.handleOverlayVisibleChange(false);
await delay(5);
await this.handleSelect(this.allNodesSet[nid]);
}
selectedNodeChain(value) {
const chain = selectionToChain(value);
const { addressTree } = this.state;
return selectNodeFromChain(addressTree, chain);
}
getDisabled() {
const { disabled, readOnly } = this.props;
return disabled || readOnly;
}
safeToRenderForValue(value) {
const { paneRoot } = this.state;
const { level } = this.props;
return !!paneRoot && (level < 4 || !value.town || this.allNodesSet[value.town]);
}
safeToRender() {
const { value } = this.state;
return this.safeToRenderForValue(value);
}
getRequestParams(props = this.props) {
return {
addressURL: props.requestAddressUrl,
townURL: props.requestTownUrl,
parentURL: props.requestAddressLevelUrl,
ignored: props.ignored,
dataOverride: props.dataOverride,
preprocessor: props.preprocessor,
};
}
getOverlayContainer() {
const { container } = this.props;
if (container) {
if (typeof container === 'function') {
return container();
}
return container;
}
return this.outerWrapperRef;
}
displayValueFromSelection(selection) {
return displayValueFromNodeChain(this.selectedNodeChain(selection));
}
triggerLabelUpdate(nodeChain) {
const { onLabelUpdate, overseas } = this.props;
if (typeof onLabelUpdate === 'function') {
const nodes = nodeChain.filter((n) => !!n.parentNode);
const label = !overseas && nodes.length <= 1 ? '' : nodes.map((n) => n.nameZh).join(' / ');
onLabelUpdate.call(null, label, nodeChain);
}
}
renderTab(pane, node) {
const { overlayValue } = this.state;
const { provincePartition } = this.props;
const renderTag = (n) =>
this.hiddenMap[node.id] ? null : (