# Comment

通用评论系统及编辑器

**`version 1.0.4`**

```js
import Comment, { Editor, RenderText } from 'comment';
```


## 使用


### 作为组件使用

```
# npm 安装
$ npm install git+https://git.links123.net/npm/comment.git --save
# yarn 安装
$ yarn add git+https://git.links123.net/npm/comment.git
```

然后在代码里面引入 `comment` 组件：

```jsx
import Comment, { Editor } from 'comment'

// ...

render() {
  return (
    <Comment type={1} businessId="test">
      <Editor />
    </Comment>
  )
}
```

**注意：最后，还需要在 HTML 文件里面引入阿里云 OSS SDK `<script src="http://gosspublic.alicdn.com/aliyun-oss-sdk.min.js"></script>`**


### 在非 React.js 项目中使用 ~~作为静态文件引入~~

**使用`assets` 分支的 `./assets/static` 目录[https://git.links123.net/npm/comment/src/branch/assets/assets/static](https://git.links123.net/npm/comment/src/branch/assets/assets/static)的静态文件**

1. 引入通用评论的 css 和 js 文件。在 `assets` 分支的 `./assets/static` 目录: [https://git.links123.net/npm/comment/src/branch/assets/assets/static](https://git.links123.net/npm/comment/src/branch/assets/assets/static)。
2. 调用 `window.renderComment` 方法并传入对应参数来渲染通用评论。参数列表：
  - `id`: string, 必填, 渲染评论的节点的 ID
  - `type`: number, 必填, 评论的 type    
  - `businessId`: string, 选填, 评论的 businessId，默认 test
  - `API`: string, 选填, 评论的 businessId，默认 http://api.links123.net/comment/v1

具体可参考 `./assets/example.html`。

例子如下：

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>通用评论 demo</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" type="text/css" media="screen" href="./static/css/comment.0.5.13.css" />
</head>
<body>
  <div>
    <h1>在非 React.js 项目中使用通用评论</h1>
    <p>将其渲染到 id 为 `comment` 的节点上</p>
    <div id="comment"></div>
  </div>
  
  <script src="http://gosspublic.alicdn.com/aliyun-oss-sdk.min.js"></script>
  <script type="text/javascript" src="./static/js/comment.0.5.13.js"></script>
  <script type="text/javascript">
    // 调用 renderComment 这个方法，渲染通用评论
    window.renderComment({
      id: 'comment', // required, 渲染通用评论到 comment 这个节点，id 名称可 自定义
      type: 1, // required, 评论的 type
      businessId: 'test' // 评论的 businessId, 默认 test
      API: 'http://api.links123.net/comment/v1', // API 前缀, 默认 http://api.links123.net/comment/v1 
    });
  </script>
</body>
</html>
```

#### 一个页面多次渲染评论

有的场景可能需要在一个页面上根据不同 bid 多次渲染评论，可以参考 `./assets/example/rerender.html`。

其方案就是，每次渲染之前，都清空之前渲染的节点内容，再重新渲染。否则 React 会任务两次 render 是同样的内容，就不渲染。

详解：

1. 首先清空父标签的内容
2. 动态创建渲染评论的标签
3. 渲染评论


```html
<!-- ... -->
  <link rel="stylesheet" type="text/css" media="screen" href="./static/css/xxx.css" />
<!-- ... -->
  <div id="RENDER_ELEMENT_WRAPPER"></div>
<!-- ... -->
  <script type="text/javascript" src="./static/js/xxx.js"></script>
<!-- ... -->
```

```js
var RENDER_ELEMENT_WRAPPER = 'RENDER_ELEMENT_WRAPPER'; // 渲染评论的父标签
var RENDER_ELEMENT_COMMENT = 'comment'; // 渲染评论的标签的 id

/**
 * 根据 id 创建节点
 * @param {string} id id
 */
function createElement(id) {
  var node = document.createElement('div');
  node.setAttribute("id", id);
  document.getElementById(RENDER_ELEMENT_WRAPPER).appendChild(node); 
}

/**
 * 根据 id 删除子节点
 * @param {string} id id
 */
function removeChild(id) {
  var myNode = document.getElementById(id);
  while (myNode.firstChild) {
    myNode.removeChild(myNode.firstChild);
  }
}

function main() {
  document.getElementById('render').addEventListener('click', function() {
    // 首先清空父标签的内容
    removeChild(RENDER_ELEMENT_WRAPPER);

    var id = RENDER_ELEMENT_COMMENT;
    // 动态创建渲染评论的标签
    createElement(id);
    // 渲染评论
    window.renderComment({
      id: id,
      type: 1,
      businessId: 'test',
      API: 'http://api.links123.net/comment/v1',
    })
  })
  
  document.getElementById('re-render').addEventListener('click', function() {
    removeChild(RENDER_ELEMENT_WRAPPER)
    var id = RENDER_ELEMENT_COMMENT;
    createElement(id)
    window.renderComment({
      id: id,
      type: 1,
      businessId: 'test1',
      API: 'http://api.links123.net/comment/v1',
    })
  })
}

main();
```

#### ~~class 样式冲突~~

> assets 分支已修复。

~~`master` 分支的图片预览 `container` 样式和 boostrap 有冲突。在 [`fix/classname`](https://git.links123.net/node/npmcomment/src/branch/fix/classname) 分支对评论的样式名称做了修改，改为了 `comment-image-preview-container`。~~

解决办法就是使用 [`fix/classname`](https://git.links123.net/node/npmcomment/src/branch/fix/classname) 分支中的静态文件：

- [https://git.links123.net/node/npmcomment/src/branch/fix/classname](https://git.links123.net/node/npmcomment/src/branch/fix/classname)
- static: [https://git.links123.net/node/npmcomment/src/branch/fix/classname/assets/static](https://git.links123.net/node/npmcomment/src/branch/fix/classname/assets/static)


> 尝试过使用 CSS Module 进行修改，不过改造量较大，改动的过程中影响了其他样式，所以暂时放弃了。


## Comment

- 标记了`deprecated`的配置项表示不推荐使用，并且可能在将来版本中不再受支持。


| props             | type            | default                            | required | description                                       |                          |
|-------------------|-----------------|------------------------------------|----------|---------------------------------------------------|--------------------------|
| type              | number          |                                    | true     | 评论的 type                                          |                          |
| businessId        | string          |                                    | true     | 评论的 business id                                   |                          |
| API               | string          | http://api.links123.net/comment/v1 | false    | API 前缀                                            |                          |
| showList          | boolean         | true                               | false    | 是否显示评论列表                                          |                          |
| showEditor        | boolean         | true                               | false    | 是否显示评论输入框                                         |                          |
| showAlertComment  | boolean         | false                              | false    | 评论成功之后，是否通过 Antd 的 Message 组件进行提示                 |                          |
| showAlertReply    | boolean         | false                              | false    | 回复成功之后，是否通过 Antd 的 Message 组件进行提示                 |                          |
| showAlertFavor    | boolean         | false                              | false    | 点赞/取消点赞成功之后，是否通过 Antd 的 Message 组件进行提示            |                          |
| showError         | boolean         | true                               | false    | 是否使用Antd的Message组件提示错误信息                          |                          |
| onError           | function(msg)   |                                    | false    | 错误回调, 出错了会被调用                                     |                          |
| userId            | number          |                                    | false    | 用户id, comment内部不维护用户id, 调用组件时传递过来, 目前用于判断是否显示删除按钮 |                          |
| token             | string          |                                    | false    | [deprecated] token，用于身份认证，非必须。默认使用 cookie         |                          |
| pageType          | string          | more                               | false    | 分页类别， more-加载更多 pagination-页码                     |                          |
| page              | number          |                                    | false    | 页码受控模式,如果传递了此参数,则需要通过onPageChange回调手动维护page.      |                          |
| limit             | number          | 10                                 | false    | 一次加载的评论数量                                         |                          |
| onGetMoreBtnClick | function()      |                                    | false    | 点击查看更多按钮的回调                                       |                          |
| onPageChange      | function(page)  |                                    | false    | 页码发生变化时的回调,注意:分页数据获取还是在组件内部处理的                    |                          |
| onDelete          | function(type)  |                                    | false    | 评论或回复删除成功后的回调, type: "comment" \                  | "reply" , 用于区分删除的是评论还是回复 |
| locales           | string          | 语言                                 | false    |                                                   |                          |
| businessUserId    | number          |                                    | false    | 评论目标的用户id, 用于发送通知                                 |                          |
| onCountChange     | function(count) |                                    | false    | 评论数量发生变更时的回调(初次获取也会调用)                            |                          |

## Editor

| props               | type                            | default  | required | description                                                         |
|---------------------|---------------------------------|----------|----------|---------------------------------------------------------------------|
| rows                | number                          | 5        | false    | 编辑器的高度。默认情况下，回复评论/回复回复的编辑器会比评论的编辑器高度小一行                             |
| placeholder         | string                          | 说点什么吧... | false    | 评论的中的提示文字                                                           |
| showEmoji           | boolean                         | true     | false    | 是否显示 Toolbar 中表情工具                                                  |
| showUpload          | boolean                         | true     | false    | 是否显示 Toolbar 中 上传图片工具                                               |
| maxUpload           | number                          | 1        | false    | 最大能够上传的图片数量                                                         |
| value               | string                          |          | false    | 编辑器的值。如果设置了该属性，则编辑器变为受控组件，需要父组件来维护 value                            |
| onChange            | function(value)                 |          | false    | 编辑器内容改变的回调函数                                                        |
| onSubmit            | function({ text, files })       |          | false    | 点击提交按钮的回调函数，text 为编辑器的文本，files 为上传的文件列表                             |
| beforeSubmit        | function ({text,files}):Promise |          | false    | 点击提交按钮后的钩子, 若Promise resolve false则不触发onSubmit函数, 主要用于控制默认的submit行为 |
| onCommentSuccess    | function()                      |          | false    | 提交评论成功后的回调                                                          |
| btnSubmitText       | string                          | 发表       | false    | 提交按钮的文字                                                             |
| btnLoading          | boolean                         | false    | false    | 按钮是否正在加载中                                                           |
| btnDisable          | boolean                         | false    | false    | 按钮是否禁用                                                              |
| button              | ReactNode                       |          | false    | 按钮组件。如果上面几个 btn 相关的属性都无法满足要求，则可以使用 button 来自定义提交编辑器值的按钮             |
| emojiToolIcon       | ReactNode                       |          | false    | Toolbar 中表情的图标                                                      |
| imageToolIcon       | ReactNode                       |          | false    | Toolbar 中上传文件的图标                                                    |
| onRef               | function                        |          | false    | 传递子组件的引用                                                            |
| closeUploadWhenBlur | boolean                         |          | false    | 当 upload 失去焦点（鼠标点击非 Upload 的区域）的时候，是否自动关闭 Popover                   |
| showError           | boolean                         | true     | false    | 是否使用Antd的Message组件提示错误信息, 主要是上传图片出错的情况                              |
| onError             | function(msg,{response})        |          | false    | 错误回调, 出错了会被调用, 主要是上传图片出错的情况                                         |
| maxLength           | number                          | 140      | false    | 限制最大输入字数                                                            |
| autoFocus           | boolean                         | false    | false    | 编辑器自动聚焦                                                             |


### 什么时候不要使用 value/onChange/onSubmit

如果将 `comment` 作为通用评论组件，则不要使用 `value` `onChange` `onSubmit`。因为组件内部，实现了通用评论的业务逻辑。

**可以使用 value/onChange/onSubmit 的情况:**

- 单独使用其中的 `Editor`。即 `import { Editor } from 'comment'`
- 不需要展示评论列表，即设置 `showList: false`

```jsx
// 单独使用 Editor
<Editor value={this.state.value} onChange={(v) => this.setState({ value })} />

// 不需要展示评论列表

<Comment type={1}  showList={false}>
  <Editor value={this.state.value} onChange={(v) => this.setState({ value })} />
</Comment>

```

### button 

如果使用 `button`，则 `btnLoading` `btnDisable` `btnSubmitText` 都会失效。因为这些属性是针对于编辑器默认的提交按钮设置的。

所以如果要提交编辑器的值，需要自己在 `Button` 组件上实现提交功能。编辑器的值，可以通过 `onChange` 方法获取到。

如果使用了 `button` 属性，并且没有为其设置 `onClick` 方法，则 `onClick` 默认为发布评论，即点击按钮会发表评论:

```jsx
// 如下代码所示
// 点击“自定义按钮”的时候，会发表评论。这是由 Comment 组件内部实现的业务逻辑
<Comment type={1} businessId="test" showList={false}>
  <Editor 
    button={(
      <Button
        type="primary"
        ghost
      >
        自定义按钮
      </Button>
    )}
  />
</Comment>
```

如果使用了 `button` 属性，并且设置了 `onClick` 方法，则会覆盖默认的 `onClick` 方法:

```jsx
// 下面的代码，点击的时候，不会提交评论
// 而是会输出 state 的值（
// 即编辑器的值，因为 onChange 将编辑器输入的值通过回调函数传递给了父组件）
<Comment type={1} businessId="test" showList={false}>
  <Editor 
    button={(
      <Button
        type="primary"
        ghost
        onChange={(value) => this.setState({ value })}
        onClick={() => console.log(this.state.value)}
      >
        自定义按钮
      </Button>
    )}
    value={this.state.value}
  />
</Comment>
```

### onRef

如果你需要在父组件里面调用子组件的方法，就可以使用 `onRef`。主要是用于调用 Editor 组件内部的 `resetState` 方法。

如果你需要在父组件里面，手动清空 Editor 里面的数据，则可以使用 `resetState`。因为即使父组件传入了 `value`， Editor 内部也会保存一份 `value` 的值，用于 `onSubmit` 回调将值传递给父组件。所以可能存在，当子组件值没有清除，而父组件的 `value` 为空的情况，导致编辑器中文字和实际预想的不一致。所以当在父组件里面使用了 `value` 并重置 `value` 的时候，最好清空 Editor 的 state。

具体使用方式如下：


```jsx

handleChangeSubmit() {
  // 点击按钮，调用 onSubmit 的时候，清空 Editor 的数据
  this.editor.resetState();
}


<Comment type={1} businessId="test">
  <Editor 
    // 使用 onRef 方法创建一个 Editor 组件的引用，并添加到 this 上面
    onRef={ref => this.editor = ref } 
    onSubmit={() => this.handleChangeSubmit()}
  />
</Comment>
```

**注意上面的 `onRef={ref => this.editor = ref }`**，这是实现父组件调用子组件方法的关键。**


## RenderText

```jsx

import { RenderText } from 'comment';

render() {
  return RenderText('test [呲牙]')
}
```

## 开发

```
$ git clone https://git.links123.net/npm/comment
$ cd comment
$ yarn
$ yarn start
```

- `yarn build` 将项目打包成一个单页应用
- `yarn lib` 将项目打包成一个 es5 组件
- `yarn prettier` 优化代码格式

## TODO

- [x] 前后端统一错误码
- [x] type 和 businessID 的定义
- [x] Editor onSubmit 回调
- [x] 对评论的回复点赞，报错
- [x] oss/sts 接口报错
- [ ] 头像 404 `https://links123-images.oss-cn-hangzhou.aliyuncs.com/avatar/`
- [ ] 上传图片的时候偶尔会出现InvalidPart
- [ ] 上传图片失败时, 提示并且不显示缩略图
- [x] 支持Ente事件(PC端和手机键盘Enter)
