narro 5 years ago
parent
commit
1b1ab25660

+ 4
- 0
CHANGELOG.md View File

@@ -1,5 +1,9 @@
1 1
 # CHANGELOG
2 2
 
3
+## 0.5.6
4
+- [x] Editor增加maxLength属性
5
+- [x] RenderText增加换行支持 
6
+
3 7
 ## 0.5.5
4 8
 
5 9
 - [x] 增加删除评论、回复功能

+ 2
- 2
README.md View File

@@ -2,7 +2,7 @@
2 2
 
3 3
 通用评论系统及编辑器
4 4
 
5
-**`version 0.5.5`**
5
+**`version 0.5.6`**
6 6
 
7 7
 ```js
8 8
 import Comment, { Editor, RenderText } from 'comment';
@@ -53,7 +53,7 @@ import Comment, { Editor, RenderText } from 'comment';
53 53
 | closeUploadWhenBlur | boolean                         |               | false    | 当 upload 失去焦点(鼠标点击非 Upload 的区域)的时候,是否自动关闭 Popover                        |
54 54
 | showError           | boolean                         | true          | false    | 是否使用Antd的Message组件提示错误信息, 主要是上传图片出错的情况                                   |
55 55
 | onError             | function(msg,{response})        |               | false    | 错误回调, 出错了会被调用, 主要是上传图片出错的情况                                                |
56
-
56
+| maxLength           | number                          | 140           | false    | 限制最大输入字数                                                                                  |
57 57
 
58 58
 
59 59
 

+ 8
- 4
lib/App.js View File

@@ -126,10 +126,14 @@ var App = function (_Component) {
126 126
     key: "errorHandler",
127 127
     value: function errorHandler(error) {
128 128
       if (error.response && error.response.data && error.response.data.msg) {
129
-        this.error(_lang2.default[error.response.data.msg] || _constant.ERROR_DEFAULT, { response: error.response });
129
+        this.error(_lang2.default[error.response.data.msg] || _constant.ERROR_DEFAULT, {
130
+          response: error.response
131
+        });
130 132
         return;
131 133
       }
132
-      this.error(_lang2.default[error.message] || _constant.ERROR_DEFAULT, { response: error.response });
134
+      this.error(_lang2.default[error.message] || _constant.ERROR_DEFAULT, {
135
+        response: error.response
136
+      });
133 137
     }
134 138
 
135 139
     /**
@@ -373,8 +377,8 @@ var App = function (_Component) {
373 377
 
374 378
     /**
375 379
      * 删除回复
376
-     * @param {*} replyId 
377
-     * @param {*} commentId 
380
+     * @param {*} replyId
381
+     * @param {*} commentId
378 382
      */
379 383
 
380 384
   }, {

+ 1
- 1
lib/App.js.map
File diff suppressed because it is too large
View File


+ 8
- 5
lib/components/ContentItem/index.js View File

@@ -209,18 +209,21 @@ var CommentItem = function (_Component) {
209 209
             ) : null,
210 210
             app.userId === content.user_id && _react2.default.createElement(
211 211
               _popconfirm2.default,
212
-              { title: "\u786E\u5B9A\u8981\u5220\u9664\u5417?", onConfirm: function onConfirm() {
212
+              {
213
+                title: "\u786E\u5B9A\u8981\u5220\u9664\u5417?",
214
+                onConfirm: function onConfirm() {
213 215
                   if (replyId) {
214 216
                     app.sDeleteReply(content.id, commentId);
215 217
                     return;
216 218
                   }
217 219
                   app.sDeleteComment(content.id);
218
-                }, okText: "\u786E\u5B9A", cancelText: "\u53D6\u6D88" },
220
+                },
221
+                okText: "\u786E\u5B9A",
222
+                cancelText: "\u53D6\u6D88"
223
+              },
219 224
               _react2.default.createElement(
220 225
                 "a",
221
-                {
222
-                  className: "comment-item-bottom-right"
223
-                },
226
+                { className: "comment-item-bottom-right" },
224 227
                 "\xA0 \u5220\u9664"
225 228
               )
226 229
             ),

+ 1
- 1
lib/components/ContentItem/index.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
lib/components/Editor/Upload.js.map
File diff suppressed because it is too large
View File


+ 67
- 45
lib/components/Editor/index.css View File

@@ -1,76 +1,98 @@
1
+.comment-editor-toolbar {
2
+  text-align: right;
3
+  padding: 0 20px 10px 0;
4
+}
5
+
6
+.comment-editor-toolbar-error {
7
+  color: red;
8
+}
9
+
1 10
 .comment-editor {
2
-  box-sizing: border-box;
3
-  margin: 0;
4
-  padding: 0;
5
-  width: 100%;
6
-  max-width: 100%;
7
-  list-style: none;
8
-  position: relative;
9
-  display: block;
10
-  font-size: 14px;
11
-  line-height: 1.5;
12
-  color: rgba(0, 0, 0, 0.65);
13
-  background-color: #fff;
14
-  background-image: none;
15
-  border: 1px solid #d9d9d9;
16
-  border-radius: 4px;
17
-  transition: all 0.3s, height 0s;
11
+    box-sizing: border-box;
12
+    margin: 0;
13
+    padding: 0;
14
+    width: 100%;
15
+    max-width: 100%;
16
+    list-style: none;
17
+    position: relative;
18
+    display: block;
19
+    font-size: 14px;
20
+    line-height: 1.5;
21
+    color: rgba(0, 0, 0, 0.65);
22
+    background-color: #fff;
23
+    background-image: none;
24
+    border: 1px solid #d9d9d9;
25
+    border-radius: 4px;
26
+    transition: all 0.3s, height 0s;
18 27
 }
28
+
19 29
 .comment-editor textarea.ant-input {
20
-  border: none;
21
-  border-bottom: 1px solid #eee;
22
-  border-bottom-right-radius: 0;
23
-  border-bottom-left-radius: 0;
30
+    border: none;
31
+    border-bottom: 1px solid #eee;
32
+    border-bottom-right-radius: 0;
33
+    border-bottom-left-radius: 0;
24 34
 }
35
+
25 36
 .comment-editor textarea.ant-input:hover {
26
-  border: none;
27
-  border-bottom: 1px solid #eee;
37
+    border: none;
38
+    border-bottom: 1px solid #eee;
28 39
 }
40
+
29 41
 .comment-editor textarea.ant-input:focus {
30
-  box-shadow: none;
31
-  border-bottom: 1px solid #eee;
42
+    box-shadow: none;
43
+    border-bottom: 1px solid #eee;
32 44
 }
45
+
33 46
 .comment-editor [contentEditable="true"]:empty:not(:focus):before {
34
-  content: attr(data-text);
35
-  color: #bfbfbf;
47
+    content: attr(data-text);
48
+    color: #bfbfbf;
36 49
 }
50
+
37 51
 .comment-editor:focus,
38 52
 .comment-editor:hover {
39
-  border-color: #40a9ff;
40
-  outline: 0;
41
-  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
53
+    border-color: #40a9ff;
54
+    outline: 0;
55
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
42 56
 }
57
+
43 58
 .comment-toolbar {
44
-  display: inline-block;
45
-  width: 100%;
46
-  margin: 5px 0 0 0;
59
+    display: inline-block;
60
+    width: 100%;
61
+    margin: 5px 0 0 0;
47 62
 }
63
+
48 64
 .comment-toolbar .comment-toolbar-icon {
49
-  font-size: 23px;
50
-  cursor: pointer;
51
-  transition: color 0.3s;
65
+    font-size: 23px;
66
+    cursor: pointer;
67
+    transition: color 0.3s;
52 68
 }
69
+
53 70
 .comment-toolbar .comment-toolbar-icon:hover {
54
-  color: #40a9ff;
71
+    color: #40a9ff;
55 72
 }
73
+
56 74
 .comment-toolbar-left {
57
-  float: left;
58
-  margin: 8px 11px;
75
+    float: left;
76
+    margin: 8px 11px;
59 77
 }
78
+
60 79
 .comment-toolbar-right {
61
-  float: right;
62
-  margin: 5px 11px;
80
+    float: right;
81
+    margin: 5px 11px;
63 82
 }
64 83
 
65 84
 .comment-emoji-popover .ant-popover-inner-content {
66
-  padding: 12px 16px 20px 16px;
85
+    padding: 12px 16px 20px 16px;
67 86
 }
87
+
68 88
 .comment-emoji-popover .ant-carousel .slick-dots {
69
-  bottom: -10px;
89
+    bottom: -10px;
70 90
 }
91
+
71 92
 .comment-emoji-popover .ant-carousel .slick-dots li.slick-active button {
72
-  background-color: #7b868a;
93
+    background-color: #7b868a;
73 94
 }
95
+
74 96
 .comment-emoji-popover .ant-carousel .slick-dots li button {
75
-  background-color: #a2aeb5;
76
-}
97
+    background-color: #a2aeb5;
98
+}

+ 147
- 112
lib/components/Editor/index.js View File

@@ -16,6 +16,10 @@ var _icon = require("antd/es/icon");
16 16
 
17 17
 var _icon2 = _interopRequireDefault(_icon);
18 18
 
19
+var _message2 = require("antd/es/message");
20
+
21
+var _message3 = _interopRequireDefault(_message2);
22
+
19 23
 var _input = require("antd/es/input");
20 24
 
21 25
 var _input2 = _interopRequireDefault(_input);
@@ -28,6 +32,8 @@ require("antd/es/popover/style/css");
28 32
 
29 33
 require("antd/es/icon/style/css");
30 34
 
35
+require("antd/es/message/style/css");
36
+
31 37
 require("antd/es/input/style/css");
32 38
 
33 39
 var _react = require("react");
@@ -38,6 +44,10 @@ var _propTypes = require("prop-types");
38 44
 
39 45
 var _propTypes2 = _interopRequireDefault(_propTypes);
40 46
 
47
+var _classnames = require("classnames");
48
+
49
+var _classnames2 = _interopRequireDefault(_classnames);
50
+
41 51
 var _constant = require("../../constant");
42 52
 
43 53
 var _helper = require("../../helper");
@@ -187,11 +197,16 @@ var Editor = function (_React$Component) {
187 197
     value: function handleSubmit() {
188 198
       var _this2 = this;
189 199
 
200
+      var maxLength = this.props.maxLength;
190 201
       var _state = this.state,
191 202
           value = _state.value,
192 203
           fileMap = _state.fileMap,
193 204
           fileList = _state.fileList;
194 205
 
206
+      if (value.length > maxLength) {
207
+        _message3.default.error("\u5B57\u6570\u4E0D\u5F97\u8D85\u8FC7" + maxLength + "\u5B57");
208
+        return;
209
+      }
195 210
       var files = [];
196 211
       if (fileList.length) {
197 212
         fileList.forEach(function (item) {
@@ -249,128 +264,146 @@ var Editor = function (_React$Component) {
249 264
           btnDisabled = _props.btnDisabled,
250 265
           button = _props.button,
251 266
           emojiToolIcon = _props.emojiToolIcon,
252
-          imageToolIcon = _props.imageToolIcon;
267
+          imageToolIcon = _props.imageToolIcon,
268
+          maxLength = _props.maxLength;
253 269
 
254 270
       var handleSubmit = this.handleSubmit;
255 271
       var disabledSubmit = btnDisabled || !this.props.value && !this.state.value && !this.state.fileList.length;
272
+      var inputValue = value || this.state.value;
256 273
       return _react2.default.createElement(
257 274
         "div",
258
-        { className: "comment-editor" },
259
-        _react2.default.createElement(TextArea, {
260
-          value: value || this.state.value,
261
-          onChange: function onChange(e) {
262
-            return _this3.handleChange(e.target.value);
263
-          },
264
-          rows: rows,
265
-          placeholder: placeholder
266
-        }),
275
+        null,
276
+        _react2.default.createElement(
277
+          "div",
278
+          { className: (0, _classnames2.default)({
279
+              "comment-editor-toolbar": true,
280
+              "comment-editor-toolbar-error": inputValue.length > maxLength
281
+            }) },
282
+          "\u5DF2\u8F93\u5165 ",
283
+          inputValue.length,
284
+          " / ",
285
+          maxLength,
286
+          " \u5B57"
287
+        ),
267 288
         _react2.default.createElement(
268 289
           "div",
269
-          { className: "comment-toolbar" },
290
+          { className: "comment-editor" },
291
+          _react2.default.createElement(TextArea, {
292
+            value: inputValue,
293
+            onChange: function onChange(e) {
294
+              return _this3.handleChange(e.target.value);
295
+            },
296
+            rows: rows,
297
+            placeholder: placeholder
298
+          }),
270 299
           _react2.default.createElement(
271 300
             "div",
272
-            { className: "comment-toolbar-left" },
273
-            showEmoji && _react2.default.createElement(
274
-              _popover2.default,
275
-              {
276
-                trigger: "click",
277
-                placement: "bottomLeft",
278
-                autoAdjustOverflow: false,
279
-                content: _react2.default.createElement(
280
-                  "div",
281
-                  { style: { width: 200 } },
282
-                  _react2.default.createElement(_Emoji2.default, { onClick: this.handleClickEmoji })
283
-                ),
284
-                overlayClassName: "comment-emoji-popover"
285
-              },
286
-              emojiToolIcon || _react2.default.createElement(_icon2.default, { type: "smile-o", className: "comment-toolbar-icon" })
287
-            ),
288
-            showUpload ? _react2.default.createElement(
289
-              _popover2.default,
290
-              {
291
-                trigger: "click",
292
-                visible: this.state.showUpload,
293
-                overlayStyle: { zIndex: 999 },
294
-                onVisibleChange: closeUploadWhenBlur ? function (visible) {
295
-                  _this3.handleShowUpload(visible);
296
-                } : null,
297
-                content: _react2.default.createElement(
298
-                  "div",
299
-                  {
300
-                    style: {
301
-                      width: 112 * maxUpload,
302
-                      minHeight: 100,
303
-                      margin: "0 auto"
304
-                    }
305
-                  },
306
-                  _react2.default.createElement(_Upload2.default, {
307
-                    onChangeFileList: this.handleChangeFileList,
308
-                    onUpload: this.handleUpload,
309
-                    maxUpload: maxUpload,
310
-                    fileList: this.state.fileList,
311
-                    showError: this.props.showError,
312
-                    onError: this.props.onError
313
-                  })
314
-                ),
315
-                placement: "bottomLeft",
316
-                title: _react2.default.createElement(
317
-                  "div",
318
-                  { style: { margin: "5px auto" } },
319
-                  _react2.default.createElement(
320
-                    "span",
321
-                    null,
322
-                    "\u4E0A\u4F20\u56FE\u7247",
323
-                    maxUpload >= 2 ? _react2.default.createElement(
324
-                      "span",
325
-                      { style: { color: "#666", fontWeight: 400 } },
326
-                      "(\u60A8\u8FD8\u80FD\u4E0A\u4F20",
327
-                      maxUpload - this.state.fileList.length,
328
-                      "\u5F20\u56FE\u7247)"
329
-                    ) : null
301
+            { className: "comment-toolbar" },
302
+            _react2.default.createElement(
303
+              "div",
304
+              { className: "comment-toolbar-left" },
305
+              showEmoji && _react2.default.createElement(
306
+                _popover2.default,
307
+                {
308
+                  trigger: "click",
309
+                  placement: "bottomLeft",
310
+                  autoAdjustOverflow: false,
311
+                  content: _react2.default.createElement(
312
+                    "div",
313
+                    { style: { width: 200 } },
314
+                    _react2.default.createElement(_Emoji2.default, { onClick: this.handleClickEmoji })
330 315
                   ),
331
-                  _react2.default.createElement(_icon2.default, {
332
-                    type: "close",
333
-                    onClick: function onClick() {
334
-                      return _this3.handleShowUpload(false);
316
+                  overlayClassName: "comment-emoji-popover"
317
+                },
318
+                emojiToolIcon || _react2.default.createElement(_icon2.default, { type: "smile-o", className: "comment-toolbar-icon" })
319
+              ),
320
+              showUpload ? _react2.default.createElement(
321
+                _popover2.default,
322
+                {
323
+                  trigger: "click",
324
+                  visible: this.state.showUpload,
325
+                  overlayStyle: { zIndex: 999 },
326
+                  onVisibleChange: closeUploadWhenBlur ? function (visible) {
327
+                    _this3.handleShowUpload(visible);
328
+                  } : null,
329
+                  content: _react2.default.createElement(
330
+                    "div",
331
+                    {
332
+                      style: {
333
+                        width: 112 * maxUpload,
334
+                        minHeight: 100,
335
+                        margin: "0 auto"
336
+                      }
335 337
                     },
336
-                    style: {
337
-                      float: "right",
338
-                      cursor: "pointer",
339
-                      marginTop: 4
340
-                    }
341
-                  })
342
-                )
343
-              },
344
-              imageToolIcon ? _react2.default.cloneElement(imageToolIcon, {
345
-                onClick: function onClick() {
346
-                  return _this3.handleShowUpload(true);
347
-                }
348
-              }) : _react2.default.createElement(_icon2.default, {
349
-                type: "picture",
350
-                className: "comment-toolbar-icon",
351
-                style: { marginLeft: 10 },
352
-                onClick: function onClick() {
353
-                  return _this3.handleShowUpload(true);
354
-                }
355
-              })
356
-            ) : null
357
-          ),
358
-          _react2.default.createElement(
359
-            "div",
360
-            { className: "comment-toolbar-right" },
361
-            button ? _react2.default.cloneElement(button, {
362
-              onClick: button.props.onClick || handleSubmit
363
-            }) : _react2.default.createElement(
364
-              _button2.default,
365
-              {
366
-                onClick: function onClick() {
367
-                  return _this3.handleSubmit();
338
+                    _react2.default.createElement(_Upload2.default, {
339
+                      onChangeFileList: this.handleChangeFileList,
340
+                      onUpload: this.handleUpload,
341
+                      maxUpload: maxUpload,
342
+                      fileList: this.state.fileList,
343
+                      showError: this.props.showError,
344
+                      onError: this.props.onError
345
+                    })
346
+                  ),
347
+                  placement: "bottomLeft",
348
+                  title: _react2.default.createElement(
349
+                    "div",
350
+                    { style: { margin: "5px auto" } },
351
+                    _react2.default.createElement(
352
+                      "span",
353
+                      null,
354
+                      "\u4E0A\u4F20\u56FE\u7247",
355
+                      maxUpload >= 2 ? _react2.default.createElement(
356
+                        "span",
357
+                        { style: { color: "#666", fontWeight: 400 } },
358
+                        "(\u60A8\u8FD8\u80FD\u4E0A\u4F20",
359
+                        maxUpload - this.state.fileList.length,
360
+                        "\u5F20\u56FE\u7247)"
361
+                      ) : null
362
+                    ),
363
+                    _react2.default.createElement(_icon2.default, {
364
+                      type: "close",
365
+                      onClick: function onClick() {
366
+                        return _this3.handleShowUpload(false);
367
+                      },
368
+                      style: {
369
+                        float: "right",
370
+                        cursor: "pointer",
371
+                        marginTop: 4
372
+                      }
373
+                    })
374
+                  )
375
+                },
376
+                imageToolIcon ? _react2.default.cloneElement(imageToolIcon, {
377
+                  onClick: function onClick() {
378
+                    return _this3.handleShowUpload(true);
379
+                  }
380
+                }) : _react2.default.createElement(_icon2.default, {
381
+                  type: "picture",
382
+                  className: "comment-toolbar-icon",
383
+                  style: { marginLeft: 10 },
384
+                  onClick: function onClick() {
385
+                    return _this3.handleShowUpload(true);
386
+                  }
387
+                })
388
+              ) : null
389
+            ),
390
+            _react2.default.createElement(
391
+              "div",
392
+              { className: "comment-toolbar-right" },
393
+              button ? _react2.default.cloneElement(button, {
394
+                onClick: button.props.onClick || handleSubmit
395
+              }) : _react2.default.createElement(
396
+                _button2.default,
397
+                {
398
+                  onClick: function onClick() {
399
+                    return _this3.handleSubmit();
400
+                  },
401
+                  type: "primary",
402
+                  loading: btnLoading,
403
+                  disabled: disabledSubmit
368 404
                 },
369
-                type: "primary",
370
-                loading: btnLoading,
371
-                disabled: disabledSubmit
372
-              },
373
-              btnSubmitText
405
+                btnSubmitText
406
+              )
374 407
             )
375 408
           )
376 409
         )
@@ -399,7 +432,8 @@ Editor.propTypes = {
399 432
   emojiToolIcon: _propTypes2.default.node,
400 433
   imageToolIcon: _propTypes2.default.node,
401 434
   showError: _propTypes2.default.bool,
402
-  onError: _propTypes2.default.func
435
+  onError: _propTypes2.default.func,
436
+  maxLength: _propTypes2.default.number
403 437
 };
404 438
 
405 439
 Editor.defaultProps = {
@@ -412,7 +446,8 @@ Editor.defaultProps = {
412 446
   btnSubmitText: "发表",
413 447
   btnLoading: false,
414 448
   btnDisabled: false,
415
-  showError: true
449
+  showError: true,
450
+  maxLength: 140
416 451
 };
417 452
 
418 453
 exports.default = Editor;

+ 1
- 1
lib/components/Editor/index.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
lib/helper.js View File

@@ -76,6 +76,6 @@ function renderContent(content, onClick) {
76 76
     }
77 77
     var value = emojiObejct[src] ? emojiObejct[src].value : src;
78 78
     return "<img src=\"" + _emoji.prefixUrl + value + "." + _emoji.ext + "\" alt=\"" + value + "\" />";
79
-  });
79
+  }).replace(/\n/g, '<br />');
80 80
 }
81 81
 //# sourceMappingURL=helper.js.map

+ 1
- 1
lib/helper.js.map View File

@@ -1 +1 @@
1
-{"version":3,"sources":["../src/helper.js"],"names":["isFunction","isUrl","arrayToObject","htmlEncode","renderContent","emojiObejct","emoji","functionToCheck","toString","call","userInput","regexp","res","match","array","keyField","reduce","obj","item","str","replace","i","charCodeAt","content","onClick","newContent","indexOf","IMAGE_SPLIT","split","pop","join","REGEXP","a","b","src","slice","value","prefixUrl","ext"],"mappings":";;;;;QAKgBA,U,GAAAA,U;QAMAC,K,GAAAA,K;QAYAC,a,GAAAA,a;QAYAC,U,GAAAA,U;QAYAC,a,GAAAA,a;;AA/ChB;;AACA;;;;;;AAEA,IAAMC,cAAcH,cAAcI,eAAd,EAAqB,OAArB,CAApB;;AAEO,SAASN,UAAT,CAAoBO,eAApB,EAAqC;AAC1C,SACEA,mBAAmB,GAAGC,QAAH,CAAYC,IAAZ,CAAiBF,eAAjB,MAAsC,mBAD3D;AAGD;;AAEM,SAASN,KAAT,CAAeS,SAAf,EAA0B;AAC/B,MAAMC,SAAS,kGAAf;AACA,MAAIC,MAAMF,UAAUG,KAAV,CAAgBF,MAAhB,CAAV;AACA,MAAIC,QAAQ,IAAZ,EAAkB,OAAO,KAAP,CAAlB,KACK,OAAO,IAAP;AACN;;AAED;;;;;AAKO,SAASV,aAAT,CAAuBY,KAAvB,EAA8BC,QAA9B,EAAwC;AAC7C,SAAOD,MAAME,MAAN,CAAa,UAACC,GAAD,EAAMC,IAAN,EAAe;AACjCD,QAAIC,KAAKH,QAAL,CAAJ,IAAsBG,IAAtB;AACA,WAAOD,GAAP;AACD,GAHM,EAGJ,EAHI,CAAP;AAID;;AAED;;;;;AAKO,SAASd,UAAT,CAAoBgB,GAApB,EAAyB;AAC9B,MAAI,CAACA,GAAL,EAAU,OAAO,EAAP;AACV,SAAOA,IAAIC,OAAJ,CAAY,SAAZ,EAAuB,UAASC,CAAT,EAAY;AACxC,WAAO,OAAOA,EAAEC,UAAF,CAAa,CAAb,CAAP,GAAyB,GAAhC;AACD,GAFM,CAAP;AAGD;;AAED;;;;;AAKO,SAASlB,aAAT,CAAuBmB,OAAvB,EAAgCC,OAAhC,EAAyC;AAC9C,MAAIC,aAAaF,OAAjB;AACA,MAAIE,WAAWC,OAAX,CAAmBC,qBAAnB,MAAoC,CAAC,CAAzC,EAA4C;AAC1CF,iBAAaA,WAAWG,KAAX,CAAiBD,qBAAjB,CAAb;AACAF,eAAWI,GAAX;AACAJ,iBAAaA,WAAWK,IAAX,CAAgB,EAAhB,CAAb;AACD;;AAED,SAAO3B,WAAWsB,UAAX,EAAuBL,OAAvB,CAA+BW,gBAA/B,EAAuC,UAASC,CAAT,EAAYC,CAAZ,EAAe;AAC3D,QAAMC,MAAMF,EAAEG,KAAF,CAAQ,CAAR,EAAW,CAAC,CAAZ,CAAZ;;AAEA;AACA;AACA,QAAIlC,MAAMiC,GAAN,CAAJ,EAAgB;AACd,kCAAyBA,GAAzB,iBAAsCA,GAAtC;AACD;AACD,QAAME,QAAQ/B,YAAY6B,GAAZ,IAAmB7B,YAAY6B,GAAZ,EAAiBE,KAApC,GAA4CF,GAA1D;AACA,2BAAoBG,gBAApB,GAAgCD,KAAhC,SAAyCE,UAAzC,iBAAsDF,KAAtD;AACD,GAVM,CAAP;AAWD","file":"helper.js","sourcesContent":["import { REGEXP, IMAGE_SPLIT } from \"./constant\";\r\nimport emoji, { prefixUrl, ext } from \"./emoji\";\r\n\r\nconst emojiObejct = arrayToObject(emoji, \"title\");\r\n\r\nexport function isFunction(functionToCheck) {\r\n  return (\r\n    functionToCheck && {}.toString.call(functionToCheck) === \"[object Function]\"\r\n  );\r\n}\r\n\r\nexport function isUrl(userInput) {\r\n  const regexp = /(http(s)?:\\/\\/.)?(www\\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;\r\n  var res = userInput.match(regexp);\r\n  if (res === null) return false;\r\n  else return true;\r\n}\r\n\r\n/**\r\n * 将对象数组转换为对象\r\n * @param {array} array Array of Objects\r\n * @param {string} keyField string\r\n */\r\nexport function arrayToObject(array, keyField) {\r\n  return array.reduce((obj, item) => {\r\n    obj[item[keyField]] = item;\r\n    return obj;\r\n  }, {});\r\n}\r\n\r\n/**\r\n * HTML 编码\r\n * 将 < > 等字符串进行编码\r\n * @param {string} str 文本\r\n */\r\nexport function htmlEncode(str) {\r\n  if (!str) return \"\";\r\n  return str.replace(/[<>]/gim, function(i) {\r\n    return \"&#\" + i.charCodeAt(0) + \";\";\r\n  });\r\n}\r\n\r\n/**\r\n * 渲染编辑器\r\n * [x] => <img src=\"x\" />\r\n * @param {strig} content\r\n */\r\nexport function renderContent(content, onClick) {\r\n  let newContent = content;\r\n  if (newContent.indexOf(IMAGE_SPLIT) !== -1) {\r\n    newContent = newContent.split(IMAGE_SPLIT);\r\n    newContent.pop();\r\n    newContent = newContent.join(\"\");\r\n  }\r\n\r\n  return htmlEncode(newContent).replace(REGEXP, function(a, b) {\r\n    const src = a.slice(1, -1);\r\n\r\n    // 兼容旧的评\r\n    // 因为旧的评论用 [img url] 方式存储的\r\n    if (isUrl(src)) {\r\n      return `<br/><img src=\"${src}\" alt=\"${src}\" style=\"max-width: 100%\" />`;\r\n    }\r\n    const value = emojiObejct[src] ? emojiObejct[src].value : src;\r\n    return `<img src=\"${prefixUrl}${value}.${ext}\" alt=\"${value}\" />`;\r\n  });\r\n}\r\n"]}
1
+{"version":3,"sources":["../src/helper.js"],"names":["isFunction","isUrl","arrayToObject","htmlEncode","renderContent","emojiObejct","emoji","functionToCheck","toString","call","userInput","regexp","res","match","array","keyField","reduce","obj","item","str","replace","i","charCodeAt","content","onClick","newContent","indexOf","IMAGE_SPLIT","split","pop","join","REGEXP","a","b","src","slice","value","prefixUrl","ext"],"mappings":";;;;;QAKgBA,U,GAAAA,U;QAMAC,K,GAAAA,K;QAYAC,a,GAAAA,a;QAYAC,U,GAAAA,U;QAYAC,a,GAAAA,a;;AA/ChB;;AACA;;;;;;AAEA,IAAMC,cAAcH,cAAcI,eAAd,EAAqB,OAArB,CAApB;;AAEO,SAASN,UAAT,CAAoBO,eAApB,EAAqC;AAC1C,SACEA,mBAAmB,GAAGC,QAAH,CAAYC,IAAZ,CAAiBF,eAAjB,MAAsC,mBAD3D;AAGD;;AAEM,SAASN,KAAT,CAAeS,SAAf,EAA0B;AAC/B,MAAMC,SAAS,kGAAf;AACA,MAAIC,MAAMF,UAAUG,KAAV,CAAgBF,MAAhB,CAAV;AACA,MAAIC,QAAQ,IAAZ,EAAkB,OAAO,KAAP,CAAlB,KACK,OAAO,IAAP;AACN;;AAED;;;;;AAKO,SAASV,aAAT,CAAuBY,KAAvB,EAA8BC,QAA9B,EAAwC;AAC7C,SAAOD,MAAME,MAAN,CAAa,UAACC,GAAD,EAAMC,IAAN,EAAe;AACjCD,QAAIC,KAAKH,QAAL,CAAJ,IAAsBG,IAAtB;AACA,WAAOD,GAAP;AACD,GAHM,EAGJ,EAHI,CAAP;AAID;;AAED;;;;;AAKO,SAASd,UAAT,CAAoBgB,GAApB,EAAyB;AAC9B,MAAI,CAACA,GAAL,EAAU,OAAO,EAAP;AACV,SAAOA,IAAIC,OAAJ,CAAY,SAAZ,EAAuB,UAAUC,CAAV,EAAa;AACzC,WAAO,OAAOA,EAAEC,UAAF,CAAa,CAAb,CAAP,GAAyB,GAAhC;AACD,GAFM,CAAP;AAGD;;AAED;;;;;AAKO,SAASlB,aAAT,CAAuBmB,OAAvB,EAAgCC,OAAhC,EAAyC;AAC9C,MAAIC,aAAaF,OAAjB;AACA,MAAIE,WAAWC,OAAX,CAAmBC,qBAAnB,MAAoC,CAAC,CAAzC,EAA4C;AAC1CF,iBAAaA,WAAWG,KAAX,CAAiBD,qBAAjB,CAAb;AACAF,eAAWI,GAAX;AACAJ,iBAAaA,WAAWK,IAAX,CAAgB,EAAhB,CAAb;AACD;;AAED,SAAO3B,WAAWsB,UAAX,EAAuBL,OAAvB,CAA+BW,gBAA/B,EAAuC,UAAUC,CAAV,EAAaC,CAAb,EAAgB;AAC5D,QAAMC,MAAMF,EAAEG,KAAF,CAAQ,CAAR,EAAW,CAAC,CAAZ,CAAZ;;AAEA;AACA;AACA,QAAIlC,MAAMiC,GAAN,CAAJ,EAAgB;AACd,kCAAyBA,GAAzB,iBAAsCA,GAAtC;AACD;AACD,QAAME,QAAQ/B,YAAY6B,GAAZ,IAAmB7B,YAAY6B,GAAZ,EAAiBE,KAApC,GAA4CF,GAA1D;AACA,2BAAoBG,gBAApB,GAAgCD,KAAhC,SAAyCE,UAAzC,iBAAsDF,KAAtD;AACD,GAVM,EAUJhB,OAVI,CAUI,KAVJ,EAUW,QAVX,CAAP;AAWD","file":"helper.js","sourcesContent":["import { REGEXP, IMAGE_SPLIT } from \"./constant\";\r\nimport emoji, { prefixUrl, ext } from \"./emoji\";\r\n\r\nconst emojiObejct = arrayToObject(emoji, \"title\");\r\n\r\nexport function isFunction(functionToCheck) {\r\n  return (\r\n    functionToCheck && {}.toString.call(functionToCheck) === \"[object Function]\"\r\n  );\r\n}\r\n\r\nexport function isUrl(userInput) {\r\n  const regexp = /(http(s)?:\\/\\/.)?(www\\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;\r\n  var res = userInput.match(regexp);\r\n  if (res === null) return false;\r\n  else return true;\r\n}\r\n\r\n/**\r\n * 将对象数组转换为对象\r\n * @param {array} array Array of Objects\r\n * @param {string} keyField string\r\n */\r\nexport function arrayToObject(array, keyField) {\r\n  return array.reduce((obj, item) => {\r\n    obj[item[keyField]] = item;\r\n    return obj;\r\n  }, {});\r\n}\r\n\r\n/**\r\n * HTML 编码\r\n * 将 < > 等字符串进行编码\r\n * @param {string} str 文本\r\n */\r\nexport function htmlEncode(str) {\r\n  if (!str) return \"\";\r\n  return str.replace(/[<>]/gim, function (i) {\r\n    return \"&#\" + i.charCodeAt(0) + \";\";\r\n  });\r\n}\r\n\r\n/**\r\n * 渲染编辑器\r\n * [x] => <img src=\"x\" />\r\n * @param {strig} content\r\n */\r\nexport function renderContent(content, onClick) {\r\n  let newContent = content;\r\n  if (newContent.indexOf(IMAGE_SPLIT) !== -1) {\r\n    newContent = newContent.split(IMAGE_SPLIT);\r\n    newContent.pop();\r\n    newContent = newContent.join(\"\");\r\n  }\r\n\r\n  return htmlEncode(newContent).replace(REGEXP, function (a, b) {\r\n    const src = a.slice(1, -1);\r\n\r\n    // 兼容旧的评\r\n    // 因为旧的评论用 [img url] 方式存储的\r\n    if (isUrl(src)) {\r\n      return `<br/><img src=\"${src}\" alt=\"${src}\" style=\"max-width: 100%\" />`;\r\n    }\r\n    const value = emojiObejct[src] ? emojiObejct[src].value : src;\r\n    return `<img src=\"${prefixUrl}${value}.${ext}\" alt=\"${value}\" />`;\r\n  }).replace(/\\n/g, '<br />');\r\n}\r\n"]}

+ 1
- 1
lib/index.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
package.json View File

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "comment",
3
-  "version": "0.5.5",
3
+  "version": "0.5.6",
4 4
   "main": "lib/App.js",
5 5
   "description": "通用评论",
6 6
   "keywords": [

+ 22
- 0
src/components/Editor/index.css View File

@@ -1,3 +1,12 @@
1
+.comment-editor-toolbar {
2
+  text-align: right;
3
+  padding: 0 20px 10px 0;
4
+}
5
+
6
+.comment-editor-toolbar-error {
7
+  color: red;
8
+}
9
+
1 10
 .comment-editor {
2 11
   box-sizing: border-box;
3 12
   margin: 0;
@@ -16,47 +25,57 @@
16 25
   border-radius: 4px;
17 26
   transition: all 0.3s, height 0s;
18 27
 }
28
+
19 29
 .comment-editor textarea.ant-input {
20 30
   border: none;
21 31
   border-bottom: 1px solid #eee;
22 32
   border-bottom-right-radius: 0;
23 33
   border-bottom-left-radius: 0;
24 34
 }
35
+
25 36
 .comment-editor textarea.ant-input:hover {
26 37
   border: none;
27 38
   border-bottom: 1px solid #eee;
28 39
 }
40
+
29 41
 .comment-editor textarea.ant-input:focus {
30 42
   box-shadow: none;
31 43
   border-bottom: 1px solid #eee;
32 44
 }
45
+
33 46
 .comment-editor [contentEditable="true"]:empty:not(:focus):before {
34 47
   content: attr(data-text);
35 48
   color: #bfbfbf;
36 49
 }
50
+
37 51
 .comment-editor:focus,
38 52
 .comment-editor:hover {
39 53
   border-color: #40a9ff;
40 54
   outline: 0;
41 55
   box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
42 56
 }
57
+
43 58
 .comment-toolbar {
44 59
   display: inline-block;
45 60
   width: 100%;
46 61
   margin: 5px 0 0 0;
47 62
 }
63
+
48 64
 .comment-toolbar .comment-toolbar-icon {
49 65
   font-size: 23px;
50 66
   cursor: pointer;
51 67
   transition: color 0.3s;
52 68
 }
69
+
53 70
 .comment-toolbar .comment-toolbar-icon:hover {
54 71
   color: #40a9ff;
55 72
 }
73
+
56 74
 .comment-toolbar-left {
57 75
   float: left;
58 76
   margin: 8px 11px;
59 77
 }
78
+
60 79
 .comment-toolbar-right {
61 80
   float: right;
62 81
   margin: 5px 11px;
@@ -65,12 +84,15 @@
65 84
 .comment-emoji-popover .ant-popover-inner-content {
66 85
   padding: 12px 16px 20px 16px;
67 86
 }
87
+
68 88
 .comment-emoji-popover .ant-carousel .slick-dots {
69 89
   bottom: -10px;
70 90
 }
91
+
71 92
 .comment-emoji-popover .ant-carousel .slick-dots li.slick-active button {
72 93
   background-color: #7b868a;
73 94
 }
95
+
74 96
 .comment-emoji-popover .ant-carousel .slick-dots li button {
75 97
   background-color: #a2aeb5;
76 98
 }

+ 130
- 110
src/components/Editor/index.js View File

@@ -1,6 +1,7 @@
1 1
 import React from "react";
2 2
 import PropTypes from "prop-types";
3
-import { Icon, Button, Popover, Input } from "antd";
3
+import { Icon, Button, Popover, Input, message } from "antd";
4
+import classnames from "classnames";
4 5
 import { OSS_LINK } from "../../constant";
5 6
 import { isFunction } from "../../helper";
6 7
 import Upload from "./Upload";
@@ -102,7 +103,12 @@ class Editor extends React.Component {
102 103
    * 需要父组件传入 onSubmit
103 104
    */
104 105
   handleSubmit() {
106
+    const { maxLength } = this.props;
105 107
     let { value, fileMap, fileList } = this.state;
108
+    if (value.length > maxLength) {
109
+      message.error(`字数不得超过${maxLength}字`);
110
+      return;
111
+    }
106 112
     const files = [];
107 113
     if (fileList.length) {
108 114
       fileList.forEach(item => {
@@ -158,125 +164,137 @@ class Editor extends React.Component {
158 164
       btnDisabled,
159 165
       button,
160 166
       emojiToolIcon,
161
-      imageToolIcon
167
+      imageToolIcon,
168
+      maxLength
162 169
     } = this.props;
163 170
     const handleSubmit = this.handleSubmit;
164 171
     const disabledSubmit =
165 172
       btnDisabled ||
166 173
       (!this.props.value && !this.state.value && !this.state.fileList.length);
174
+    const inputValue = value || this.state.value;
167 175
     return (
168
-      <div className="comment-editor">
169
-        <TextArea
170
-          value={value || this.state.value}
171
-          onChange={e => this.handleChange(e.target.value)}
172
-          rows={rows}
173
-          placeholder={placeholder}
174
-        />
176
+      <div>
177
+        <div
178
+          className={classnames({
179
+            "comment-editor-toolbar": true,
180
+            "comment-editor-toolbar-error": inputValue.length > maxLength
181
+          })}
182
+        >
183
+          已输入 {inputValue.length} / {maxLength} 字
184
+        </div>
185
+        <div className="comment-editor">
186
+          <TextArea
187
+            value={inputValue}
188
+            onChange={e => this.handleChange(e.target.value)}
189
+            rows={rows}
190
+            placeholder={placeholder}
191
+          />
175 192
 
176
-        <div className="comment-toolbar">
177
-          <div className="comment-toolbar-left">
178
-            {showEmoji && (
179
-              <Popover
180
-                trigger="click"
181
-                placement="bottomLeft"
182
-                autoAdjustOverflow={false}
183
-                content={
184
-                  <div style={{ width: 200 }}>
185
-                    <Emoji onClick={this.handleClickEmoji} />
186
-                  </div>
187
-                }
188
-                overlayClassName="comment-emoji-popover"
189
-              >
190
-                {emojiToolIcon || (
191
-                  <Icon type="smile-o" className="comment-toolbar-icon" />
192
-                )}
193
-              </Popover>
194
-            )}
193
+          <div className="comment-toolbar">
194
+            <div className="comment-toolbar-left">
195
+              {showEmoji && (
196
+                <Popover
197
+                  trigger="click"
198
+                  placement="bottomLeft"
199
+                  autoAdjustOverflow={false}
200
+                  content={
201
+                    <div style={{ width: 200 }}>
202
+                      <Emoji onClick={this.handleClickEmoji} />
203
+                    </div>
204
+                  }
205
+                  overlayClassName="comment-emoji-popover"
206
+                >
207
+                  {emojiToolIcon || (
208
+                    <Icon type="smile-o" className="comment-toolbar-icon" />
209
+                  )}
210
+                </Popover>
211
+              )}
195 212
 
196
-            {showUpload ? (
197
-              <Popover
198
-                trigger="click"
199
-                visible={this.state.showUpload}
200
-                overlayStyle={{ zIndex: 999 }}
201
-                onVisibleChange={
202
-                  closeUploadWhenBlur
203
-                    ? visible => {
204
-                        this.handleShowUpload(visible);
205
-                      }
206
-                    : null
207
-                }
208
-                content={
209
-                  <div
210
-                    style={{
211
-                      width: 112 * maxUpload,
212
-                      minHeight: 100,
213
-                      margin: "0 auto"
214
-                    }}
215
-                  >
216
-                    <Upload
217
-                      onChangeFileList={this.handleChangeFileList}
218
-                      onUpload={this.handleUpload}
219
-                      maxUpload={maxUpload}
220
-                      fileList={this.state.fileList}
221
-                      showError={this.props.showError}
222
-                      onError={this.props.onError}
223
-                    />
224
-                  </div>
225
-                }
226
-                placement="bottomLeft"
227
-                title={
228
-                  <div style={{ margin: "5px auto" }}>
229
-                    <span>
230
-                      上传图片
231
-                      {maxUpload >= 2 ? (
232
-                        <span style={{ color: "#666", fontWeight: 400 }}>
233
-                          (您还能上传{maxUpload - this.state.fileList.length}张图片)
234
-                        </span>
235
-                      ) : null}
236
-                    </span>
237
-                    <Icon
238
-                      type="close"
239
-                      onClick={() => this.handleShowUpload(false)}
213
+              {showUpload ? (
214
+                <Popover
215
+                  trigger="click"
216
+                  visible={this.state.showUpload}
217
+                  overlayStyle={{ zIndex: 999 }}
218
+                  onVisibleChange={
219
+                    closeUploadWhenBlur
220
+                      ? visible => {
221
+                          this.handleShowUpload(visible);
222
+                        }
223
+                      : null
224
+                  }
225
+                  content={
226
+                    <div
240 227
                       style={{
241
-                        float: "right",
242
-                        cursor: "pointer",
243
-                        marginTop: 4
228
+                        width: 112 * maxUpload,
229
+                        minHeight: 100,
230
+                        margin: "0 auto"
244 231
                       }}
232
+                    >
233
+                      <Upload
234
+                        onChangeFileList={this.handleChangeFileList}
235
+                        onUpload={this.handleUpload}
236
+                        maxUpload={maxUpload}
237
+                        fileList={this.state.fileList}
238
+                        showError={this.props.showError}
239
+                        onError={this.props.onError}
240
+                      />
241
+                    </div>
242
+                  }
243
+                  placement="bottomLeft"
244
+                  title={
245
+                    <div style={{ margin: "5px auto" }}>
246
+                      <span>
247
+                        上传图片
248
+                        {maxUpload >= 2 ? (
249
+                          <span style={{ color: "#666", fontWeight: 400 }}>
250
+                            (您还能上传{maxUpload - this.state.fileList.length}张图片)
251
+                          </span>
252
+                        ) : null}
253
+                      </span>
254
+                      <Icon
255
+                        type="close"
256
+                        onClick={() => this.handleShowUpload(false)}
257
+                        style={{
258
+                          float: "right",
259
+                          cursor: "pointer",
260
+                          marginTop: 4
261
+                        }}
262
+                      />
263
+                    </div>
264
+                  }
265
+                >
266
+                  {imageToolIcon ? (
267
+                    React.cloneElement(imageToolIcon, {
268
+                      onClick: () => this.handleShowUpload(true)
269
+                    })
270
+                  ) : (
271
+                    <Icon
272
+                      type="picture"
273
+                      className="comment-toolbar-icon"
274
+                      style={{ marginLeft: 10 }}
275
+                      onClick={() => this.handleShowUpload(true)}
245 276
                     />
246
-                  </div>
247
-                }
248
-              >
249
-                {imageToolIcon ? (
250
-                  React.cloneElement(imageToolIcon, {
251
-                    onClick: () => this.handleShowUpload(true)
252
-                  })
253
-                ) : (
254
-                  <Icon
255
-                    type="picture"
256
-                    className="comment-toolbar-icon"
257
-                    style={{ marginLeft: 10 }}
258
-                    onClick={() => this.handleShowUpload(true)}
259
-                  />
260
-                )}
261
-              </Popover>
262
-            ) : null}
263
-          </div>
277
+                  )}
278
+                </Popover>
279
+              ) : null}
280
+            </div>
264 281
 
265
-          <div className="comment-toolbar-right">
266
-            {button ? (
267
-              React.cloneElement(button, {
268
-                onClick: button.props.onClick || handleSubmit
269
-              })
270
-            ) : (
271
-              <Button
272
-                onClick={() => this.handleSubmit()}
273
-                type="primary"
274
-                loading={btnLoading}
275
-                disabled={disabledSubmit}
276
-              >
277
-                {btnSubmitText}
278
-              </Button>
279
-            )}
282
+            <div className="comment-toolbar-right">
283
+              {button ? (
284
+                React.cloneElement(button, {
285
+                  onClick: button.props.onClick || handleSubmit
286
+                })
287
+              ) : (
288
+                <Button
289
+                  onClick={() => this.handleSubmit()}
290
+                  type="primary"
291
+                  loading={btnLoading}
292
+                  disabled={disabledSubmit}
293
+                >
294
+                  {btnSubmitText}
295
+                </Button>
296
+              )}
297
+            </div>
280 298
           </div>
281 299
         </div>
282 300
       </div>
@@ -302,7 +320,8 @@ Editor.propTypes = {
302 320
   emojiToolIcon: PropTypes.node,
303 321
   imageToolIcon: PropTypes.node,
304 322
   showError: PropTypes.bool,
305
-  onError: PropTypes.func
323
+  onError: PropTypes.func,
324
+  maxLength: PropTypes.number
306 325
 };
307 326
 
308 327
 Editor.defaultProps = {
@@ -315,7 +334,8 @@ Editor.defaultProps = {
315 334
   btnSubmitText: "发表",
316 335
   btnLoading: false,
317 336
   btnDisabled: false,
318
-  showError: true
337
+  showError: true,
338
+  maxLength: 140
319 339
 };
320 340
 
321 341
 export default Editor;

+ 12
- 10
src/helper.js View File

@@ -53,15 +53,17 @@ export function renderContent(content, onClick) {
53 53
     newContent = newContent.join("");
54 54
   }
55 55
 
56
-  return htmlEncode(newContent).replace(REGEXP, function(a, b) {
57
-    const src = a.slice(1, -1);
56
+  return htmlEncode(newContent)
57
+    .replace(REGEXP, function(a, b) {
58
+      const src = a.slice(1, -1);
58 59
 
59
-    // 兼容旧的评
60
-    // 因为旧的评论用 [img url] 方式存储的
61
-    if (isUrl(src)) {
62
-      return `<br/><img src="${src}" alt="${src}" style="max-width: 100%" />`;
63
-    }
64
-    const value = emojiObejct[src] ? emojiObejct[src].value : src;
65
-    return `<img src="${prefixUrl}${value}.${ext}" alt="${value}" />`;
66
-  });
60
+      // 兼容旧的评
61
+      // 因为旧的评论用 [img url] 方式存储的
62
+      if (isUrl(src)) {
63
+        return `<br/><img src="${src}" alt="${src}" style="max-width: 100%" />`;
64
+      }
65
+      const value = emojiObejct[src] ? emojiObejct[src].value : src;
66
+      return `<img src="${prefixUrl}${value}.${ext}" alt="${value}" />`;
67
+    })
68
+    .replace(/\n/g, "<br />");
67 69
 }