# 右键菜单

需要在表格列、列表或者其它区域点击右键，进行更多操作时使用

## 案例演示

### ContextMenu 基本使用

使用事件坐标点，作为右键菜单弹出位置。有滚动条时，会出现位置错乱。此时可以设置 `useNodePos=true` 使用元素自身位置即可。

---demo
```js
import { ContextMenu } from 'amos-framework';

const list = [
  { key: '1', name: '数据行1' },
  { key: '2', name: '数据行2' },
  { key: '3', name: '数据行3' },
  { key: '4', name: '数据行4' }
];

class Demo extends Component {

  onItemClick = (type, item) => {
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    const menus = [{
      text: '<i class="aficon aficon-edit"></i>修改名称',
      shortcut: '(ctrl + F1)',
      action: () => this.onItemClick('rename', item),
    }, {
      text: '删除',
      action: () => this.onItemClick('delete', item),
    }];

    return menus;
  }

  renderItem(item){
    return (
      <ContextMenu
        style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }}
        key={item.key}
        menus={this.getCtxMenuItem(item)}
        adjustPosition
      >
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### ContextMenu 动态改变菜单

菜单执行过程中，动态改变菜单内容。此时，只需要将传入的 `menus` 改为 `function` 类型即可。

---demo
```js
import { ContextMenu } from 'amos-framework';

const list = [
  { key: '1', name: '数据行1' },
  { key: '2', name: '数据行2' },
  { key: '3', name: '数据行3' },
  { key: '4', name: '数据行4' }
];

class Demo extends Component {
  state = {
    visible: false
  };

  onItemClick = (type, item) => {
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    const { visible } = this.state;
    const menus = [{
      text: '修改名称',
      action: () => this.onItemClick('rename', item),
    }];

    if (visible){
      menus.push({
        text: '隐藏元素',
        action: () => {
          this.setState({
            visible: false
          });
          this.onItemClick('hide', item);
        },
      });
    } else {
      menus.push({
        text: '显示元素',
        action: () => {
          this.setState({
            visible: true
          });
          this.onItemClick('show', item);
        },
      });
    }

    return menus;
  }

  renderItem(item){
    return (
      <ContextMenu style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }} key={item.key} useNodePos menus={() => this.getCtxMenuItem(item)}>
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### ContextMenu 使用节点位置

---demo
```js
import { ContextMenu } from 'amos-framework';

const list = [
  { key: '1', name: '数据行1' },
  { key: '2', name: '数据行2' },
  { key: '3', name: '数据行3' },
  { key: '4', name: '数据行4' }
];

class Demo extends Component {

  onItemClick = (type, item) => {
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    const menus = [{
      text: '修改名称',
      action: () => this.onItemClick('rename', item),
    }, {
      text: '删除',
      action: () => this.onItemClick('delete', item),
    }];

    return menus;
  }

  renderItem(item){
    return (
      <ContextMenu useNodePos style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }} key={item.key} menus={this.getCtxMenuItem(item)}>
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### ContextMenu 多类型右键

内部区域与外部区域设置不同的右键。

---demo
```js
import { ContextMenu } from 'amos-framework';

const ctxMenuFactory = ContextMenu.ctxMenuFactory;

class Demo extends Component {

  onItemClick = (oper, type) => {
    console.log(oper, type);
  }

  getCtxMenuItem = (type) => {
    let menus = [];

    if (type === 'outer'){
      menus.push({
        text: '修改名称',
        action: () => this.onItemClick('rename', type),
      });
    } else {
      menus.push({
        text: '删除',
        action: () => this.onItemClick('delete', type),
      });
    }
    return menus;
  }

  bindContextMenu = (node, type) => {
    if (node){
      ctxMenuFactory({
        node,
        items: this.getCtxMenuItem(type),
        useNodePos: true
      });
    }
  }

  render() {
    return (
      <div style={{ width: '10em', height: '5em', display: 'inline-block', background: '#dadada' }} ref={node => this.bindContextMenu(node, 'outer')}>
        <div>
          <span style={{ background: 'red' }} ref={node => this.bindContextMenu(node, 'inner')}>内部区域</span>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### ContextMenu 直接使用 ctxMenuFactory

可设置自定义的 context

---demo
```js
import { ContextMenu } from 'amos-framework';

const ctxMenuFactory = ContextMenu.ctxMenuFactory;

const list = [
  { key: '1', name: '数据行1' },
  { key: '2', name: '数据行2' },
  { key: '3', name: '数据行3' },
  { key: '4', name: '数据行4' }
];

class Demo extends Component {

  onItemClick = (type, item, evt) => {
    console.log(type, item, evt);
  }

  getCtxMenuItem = (item) => {
    const menus = [{
      text: '修改名称',
      action: (evt) => this.onItemClick('rename', item, evt),
      shortcut: '(ctrl + F1)'
    }, {
      text: '<i class="aficon aficon-delete"></i>删除',
      action: () => this.onItemClick('delete', item),
    }];

    return menus;
  }

  bindContextMenu = (node, item) => {
    if (node){
      ctxMenuFactory({
        node,
        items: this.getCtxMenuItem(item),
        useNodePos: true,
        context: {
          list
        }
      });
    }
  }

  renderItem(item){
    return (
      <div style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }} key={item.key} ref={(node) => this.bindContextMenu(node, item)}>
        <span>{item.name}</span>
      </div>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend


### ContextMenu 底部自动调整位置

> 将条目放到最底部，然后触发右键，查看菜单显示位置。（确保底部无法显示全菜单时，才能查看效果）

---demo
```js
import { ContextMenu } from 'amos-framework';

const list = [
  { key: '1', name: '数据行1' },
  { key: '2', name: '数据行2' },
  { key: '3', name: '数据行3' },
  { key: '4', name: '数据行4' }
];

class Demo extends Component {

  onItemClick = (type, item) => {
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    const menus = [{
      text: '修改名称',
      action: () => this.onItemClick('rename', item),
    }, {
      text: '删除',
      action: () => this.onItemClick('delete', item),
    }];

    return menus;
  }

  renderItem(item){
    return (
      <ContextMenu
        style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }}
        key={item.key}
        menus={this.getCtxMenuItem(item)}
        adjustPosition
      >
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### ContextMenu 使用 react 组件

使用 react 组件作为 `MenuItem`, since `v1.9.9` 版本之后新增

---demo
```js
import { ContextMenu, NestedMenu } from 'amos-framework';

const MenuItem = NestedMenu.Item;
const Divider = NestedMenu.Divider;

const list = [
  { key: '1', name: '数据行1' },
  { key: '2', name: '数据行2' },
  { key: '3', name: '数据行3' },
  { key: '4', name: '数据行4' }
];

class Demo extends Component {

  onItemClick = (type, item) => {
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    return (
      <NestedMenu>
        <MenuItem icon="retweet" text="刷新" onClick={() => this.onItemClick('refresh', item)} />
        <Divider />
        <MenuItem icon="remove" text="删除" onClick={() => this.onItemClick('del', item)} />
        <MenuItem icon="shuoming" text="帮助" onClick={() => this.onItemClick('help', item)} />
        <Divider />
        <MenuItem icon="link" text="天地图" href="https://map.tianditu.gov.cn/" />
      </NestedMenu>
    );
  }

  renderItem(item){
    return (
      <ContextMenu
        style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }}
        key={item.key}
        menus={this.getCtxMenuItem(item)}
        adjustPosition
      >
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend


### ContextMenu 使用 react 组件 - function 模式

使用 react 组件作为 `MenuItem`, since `v1.9.9` 版本之后新增

---demo
```js
import { ContextMenu, NestedMenu } from 'amos-framework';

const MenuItem = NestedMenu.Item;
const Divider = NestedMenu.Divider;

const list = [
  { key: '1', name: '数据行1' },
  { key: '2', name: '数据行2' },
  { key: '3', name: '数据行3' },
  { key: '4', name: '数据行4' }
];

class Demo extends Component {

  onItemClick = (type, item) => {
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    return (
      <NestedMenu>
        <MenuItem icon="retweet" text="刷新" onClick={() => this.onItemClick('refresh', item)} />
        <Divider />
        <MenuItem icon="remove" text="删除" onClick={() => this.onItemClick('del', item)} />
      </NestedMenu>
    );
  }

  renderItem(item){
    return (
      <ContextMenu
        style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }}
        key={item.key}
        menus={() => this.getCtxMenuItem(item)}
        adjustPosition
      >
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### ContextMenu 使用 react 组件动态改变菜单状态

> 菜单渲染完毕之后，更改状态，注意，为避免初始时可以使用，更新之后不可用的情况，该方式可以初始创建时禁用，组件创建完毕之后更新禁用状态。

---demo
```js
import { ContextMenu, NestedMenu } from 'amos-framework';

const MenuItem = NestedMenu.Item;
const Divider = NestedMenu.Divider;

const list = [
  { key: 'd1', name: '数据行1' },
  { key: 'd2', name: '数据行2' },
  { key: 'd3', name: '数据行3' },
  { key: 'd4', name: '数据行4' }
];

const ctxMapping = {
  d1: { refresh: true, remove: false },
  d2: { refresh: true, remove: true },
  d3: { refresh: false, remove: false },
  d4: { refresh: false, remove: true }
};

function getMenuStatus(dataItem){
  return new Promise((resolve, reject) => {
    // 模拟异步获取右键状态
    setTimeout(() => {
      resolve(ctxMapping[dataItem.key]);
    }, 1000);
  });
}

class MyMenu extends Component {
  constructor(props) {
    super(props);
    // 初始，均为禁用状态
    this.state = {
      refresh: false,
      remove: false
    };
  }

  componentDidMount() {
    const { dataItem } = this.props;
    getMenuStatus(dataItem).then(d => {
      this.setState(d);
    });
  }

  render() {
    const { refresh, remove } = this.state;
    const { dataItem } = this.props;
    return (
      <NestedMenu>
        <MenuItem disabled={!refresh} icon="retweet" text="刷新" onClick={() => this.onItemClick('refresh', dataItem)} />
        <Divider />
        <MenuItem disabled={!remove} icon="remove" text="删除" onClick={() => this.onItemClick('del', dataItem)} />
      </NestedMenu>
    );
  }
}

class Demo extends Component {

  onItemClick = (type, item) => {
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    return (
      <MyMenu dataItem={item} />
    );
  }

  renderItem(item){
    return (
      <ContextMenu
        style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }}
        key={item.key}
        menus={() => this.getCtxMenuItem(item)}
        adjustPosition
      >
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend


### ContextMenu 通用方式更新菜单

> 通过动态方式，给 instance 添加 `checkClick` 方法进行判断点击事件

---demo
```js
import { ContextMenu, CoreUtils } from 'amos-framework';

// 也可以直接使用 `import classlist from 'dt2react/lib/class/classList2';`
const classlist = CoreUtils.classlist;

const list = [
  { key: 'd1', name: '数据行1' },
  { key: 'd2', name: '数据行2' },
  { key: 'd3', name: '数据行3' },
  { key: 'd4', name: '数据行4' }
];

// 值为 true，则表示禁用
const ctxMapping = {
  d1: { edit: true, delete: false },
  d2: { edit: true, delete: true },
  d3: { edit: false, delete: false },
  d4: { edit: false, delete: true }
};

function getMenuStatus(dataKey){
  return new Promise((resolve, reject) => {
    // 模拟异步获取右键状态
    setTimeout(() => {
      resolve(ctxMapping[dataKey]);
    }, 1000);
  });
}

class Demo extends Component {
  constructor(props) {
    super(props);
    this.state = {
      menuItemStats: {}
    };
  }

  onItemClick = (type, item, ctx, evt) => {
    debugger
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    const menus = [{
      key: 'edit',
      text: '<i class="aficon aficon-edit"></i>修改名称',
      shortcut: '(ctrl + F1)',
      action: (ctx, evt) => this.onItemClick('rename', item, ctx, evt),
    }, {
      key: 'delete',
      text: '删除',
      action: (ctx, evt) => this.onItemClick('delete', item, ctx, evt),
    }];

    return menus;
  }

  checkClick = (actionId, menuItemKey) => {
    debugger
    const { menuItemStats } = this.state;
    if (menuItemKey){
      if (menuItemKey in menuItemStats){
        return menuItemStats[menuItemKey];
      }
    }
    return true;
  }

  renderItem(item){
    return (
      <ContextMenu
        style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }}
        key={item.key}
        dataKey={item.key}
        menus={this.getCtxMenuItem(item)}
        adjustPosition
        onAfterShow={this.updateMenuItem}
      >
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  updateMenuItem = (e, instance) => {
    instance.checkClick = this.checkClick;
    if (e && e.target && e.target.dataset.datakey){
      const dk = e.target.dataset.datakey;
      getMenuStatus(dk).then(d => {
        this.setState({
          menuItemStats: d
        });
        (Object.keys(d) || []).forEach(key => {
          const v = d[key];
          instance.update((rootEl) => {
            const elItem = rootEl.querySelector(`[data-itemkey='${key}']`);
            if (elItem){
              //  处理禁用
              classlist(elItem).toggle('ctx-item-disabled', !!v)
              // if (v){
              //   classlist(elItem).add('ctx-item-disabled');
              // } else {
              //   && classlist(elItem).remove('ctx-item-disabled');
              // }
            }
          });
        });
      });
    }
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### ContextMenu 通用方式更新菜单(menus返回值为Promise)

> 注意，该方式实现时，右键菜单可能会出现延迟情况，延迟时间由 `Promise` 耗时决定

改进：如果 getMenuStatus 方法，仅与dataKey 有关，可以将 `menus={() => this.getCtxMenuItem(item)}` 改为 `menus={this.getCtxMenuItem(item)}`

---demo
```js
import { ContextMenu } from 'amos-framework';

const list = [
  { key: 'd1', name: '数据行1' },
  { key: 'd2', name: '数据行2' },
  { key: 'd3', name: '数据行3' },
  { key: 'd4', name: '数据行4' }
];

// 值为 true，则表示禁用
const ctxMapping = {
  d1: { edit: true, delete: false },
  d2: { edit: true, delete: true },
  d3: { edit: false, delete: false },
  d4: { edit: false, delete: true }
};

function getMenuStatus(dataKey){
  return new Promise((resolve, reject) => {
    // 模拟异步获取右键状态
    setTimeout(() => {
      resolve(ctxMapping[dataKey]);
    }, 1000);
  });
}

class Demo extends Component {
  constructor(props) {
    super(props);
  }

  onItemClick = (type, item, ctx, evt) => {
    console.log(type, item);
  }

  getCtxMenuItem = (item) => {
    const menus = [{
      key: 'edit',
      text: '<i class="aficon aficon-edit"></i>修改名称',
      shortcut: '(ctrl + F1)',
      action: (ctx, evt) => this.onItemClick('rename', item, ctx, evt),
    }, {
      key: 'delete',
      text: '删除',
      action: (ctx, evt) => this.onItemClick('delete', item, ctx, evt),
    }];

    return getMenuStatus(item.key).then(d => {
      return menus.map(m => {
        const newItem = {
          ...m,
          disabled: d[m.key],
        };
        if (d[m.key]){
          // 禁用时，设置为空方法
          newItem.action = function(){};
        }
        return newItem;
      })
    });
  }

  renderItem(item){
    return (
      <ContextMenu
        style={{ background: '#dadada', margin: '5px 0', textAlign: 'center' }}
        key={item.key}
        dataKey={item.key}
        // menus={() => this.getCtxMenuItem(item)}
        menus={this.getCtxMenuItem(item)}
        adjustPosition
      >
        <span>{item.name}</span>
      </ContextMenu>
    );
  }

  render() {
    return (
      <div style={{ width: '10em', display: 'inline-block' }}>
        {
          list.map(item => this.renderItem(item))
        }
      </div>
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

## API

| params   |type     |default    | description |
|--------- |-------- |---------- |-------- |
| popupClassName | string | - | 菜单弹出框样式名 |
| className | string | - | 菜单触发容器样式名 |
| style | object | - | 菜单触发容器内联样式 |
| menus | `function or array` | - | 菜单内容，`function` 无参数，动态改变菜单条目时使用。 |
| useNodePos | boolean | - | 设置为 true，则使用触发菜单节点位置作为菜单弹出位置，反之则采用 event 位置 |
| children | ReactNode | - | 触发菜单的容器 |
| onClick | Function | - | 自定义点击事件 |
| onAfterShow | Function | - | 点击右键，右键菜单显示之后的回调 `(evt, menu) => {}` |
| adjustPosition | boolean | true | 自动调整菜单显示位置，当出现滚动区域时，没法显示全菜单时，将会自行调整。`ctxMenuFactory` 创建时，需要自行传入该参数。 |

> 如果需要动态改变 menu item 内容和条目，则需要设置 `menus=func` 方式。如果动态数据未改变，此时可给 `<ContextMenu />` 组件传入 key 值，以确保 `React` 组件的更新。

### menu items

#### 采用数组的方式

数据格式：

* text 菜单内容
* action 按键事件，参数为 `{ ...ctx, position }`,其中，ctx 为 `ctxMenuFactory` 创建右键菜单时，传入的 context 值
* shortcut 快捷键
* itemKey 菜单key (since v1.11.7 新增)
* disabled 是否禁用 (since v1.11.7 新增)

```js
const menus = [{
  text: '修改名称',
  action: (evt) => this.onItemClick('rename', item, evt),
  shortcut: '(ctrl + F1)'
}, {
  text: '<i class="aficon aficon-delete"></i>删除',
  action: () => this.onItemClick('delete', item),
}];
```

#### 采用 Function 的方式

```js
const menus = function(context){
  return [{
    text: '修改名称',
    action: () => this.onItemClick('rename', item),
    shortcut: '(ctrl + F1)'
  }, {
    text: '<i class="aficon aficon-delete"></i>删除',
    action: () => this.onItemClick('delete', item),
  }];
}
```

#### 采用 ReactElement 的方式

```js
import { ContextMenu, NestedMenu } from 'amos-framework';

const MenuItem = NestedMenu.Item;
const Divider = NestedMenu.Divider;

function onItemClick(type, item) {
  console.log(type, item);
}

const menus = (
  <NestedMenu>
    <MenuItem icon="retweet" text="刷新" />
    <Divider />
    <MenuItem icon="remove" text="删除" />
    <MenuItem icon="shuoming" text="帮助" onClick={() => onItemClick('help', item)} />
    <Divider />
    <MenuItem icon="link" text="天地图" href="https://map.tianditu.gov.cn/" />
  </NestedMenu>
);

// func 的返回值，也可以是一个 ReactNode
const menus = function(context){
  return (
    <NestedMenu>
      <MenuItem icon="retweet" text="刷新" />
      <Divider />
      <MenuItem icon="remove" text="删除" />
      <MenuItem icon="shuoming" text="帮助" onClick={() => onItemClick('help', item)} />
      <Divider />
      <MenuItem icon="link" text="天地图" href="https://map.tianditu.gov.cn/" />
    </NestedMenu>
  );
}
```

### ctxMenuFactory 创建自定义的 ContextMenu

```js
import { ContextMenu } from 'amos-framework';

const ctxMenuFactory = ContextMenu.ctxMenuFactory;

class MyCtxMenu extends Component {
  bindContextMenu = (node) => {
    const { menus, popupClassName, useNodePos, adjustPosition } = this.props;
    if (node){
      ctxMenuFactory({
        node,
        items: menus,
        popupClassName,
        useNodePos,
        adjustPosition
      });
    }
  }

  render() {
    const { className, children, style } = this.props;
    return <div className={className} ref={this.bindContextMenu} style={style}>{children}</div>;
  }
}
```

### 有可能出现的错误

text 字段，如果采用根节点包裹的方式，有可能会导致部分节点点击事件失效，如下案例：

```js
{
  // 该方式，点击 icon 时，事件失效，点击文字或者空白处正常
  text: '<div><i class="aficon afcion-delete"></i>删除</div>'
  // 正确写法：
  text: '<i class="aficon afcion-delete"></i>删除'

  // 该方式，点击右侧文字 `特殊`，失效
  text: '<div>新增<span style="color: red;">特殊</span></div>'
  // 正确写法：
  text: '新增<span style="color: red;">特殊</span>'
}
```

> 只需要确保传入的 text 中的元素，无自定义的父节点即可。
