editor.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. <script>
  2. import { toEmojiName } from "utils";
  3. const exec = (val, command = "insertHTML") => {
  4. document.execCommand(command, false, val);
  5. };
  6. const selection = window.getSelection();
  7. let lastSelectionRange;
  8. let emojiData = [];
  9. export default {
  10. name: "LemonEditor",
  11. components: {},
  12. props: {},
  13. data() {
  14. return {
  15. submitDisabled: true,
  16. accept: ""
  17. };
  18. },
  19. created() {},
  20. mounted() {
  21. //this.$refs.fileInput.addEventListener("change", this._handleChangeFile);
  22. },
  23. computed: {},
  24. watch: {},
  25. render() {
  26. //<a-popover trigger="click" overlay-class-name="lemon-editor__emoji">
  27. return (
  28. <div class="lemon-editor">
  29. <input
  30. style="display:none"
  31. type="file"
  32. multiple="multiple"
  33. ref="fileInput"
  34. accept={this.accept}
  35. onChange={this._handleChangeFile}
  36. />
  37. <div class="lemon-editor__tool">
  38. {emojiData.length > 0 && (
  39. <lemon-popover class="lemon-editor__emoji">
  40. <template slot="content">{this._renderEmojiTabs()}</template>
  41. <div class="lemon-editor__tool-item">
  42. <i class="lemon-icon-emoji" />
  43. </div>
  44. </lemon-popover>
  45. )}
  46. <div
  47. class="lemon-editor__tool-item"
  48. on-click={() => this._handleSelectFile("*")}
  49. >
  50. <i class="lemon-icon-folder" />
  51. </div>
  52. <div
  53. class="lemon-editor__tool-item"
  54. on-click={() => this._handleSelectFile("image/*")}
  55. >
  56. <i class="lemon-icon-image" />
  57. </div>
  58. </div>
  59. <div class="lemon-editor__inner">
  60. <div
  61. class="lemon-editor__input"
  62. ref="textarea"
  63. contenteditable="true"
  64. on-keyup={this._handleKeyup}
  65. on-keydown={this._handleKeydown}
  66. on-paste={this._handlePaste}
  67. on-click={this._handleClick}
  68. on-input={this._handleInput}
  69. spellcheck="false"
  70. />
  71. </div>
  72. <div class="lemon-editor__footer">
  73. <div class="lemon-editor__tip">使用 ctrl + enter 快捷发送消息</div>
  74. <div class="lemon-editor__submit">
  75. <lemon-button
  76. disabled={this.submitDisabled}
  77. on-click={this._handleSend}
  78. >
  79. 发 送
  80. </lemon-button>
  81. </div>
  82. </div>
  83. </div>
  84. );
  85. },
  86. methods: {
  87. _saveLastRange() {
  88. lastSelectionRange = selection.getRangeAt(0);
  89. },
  90. _focusLastRange() {
  91. this.$refs.textarea.focus();
  92. if (lastSelectionRange) {
  93. selection.removeAllRanges();
  94. selection.addRange(lastSelectionRange);
  95. }
  96. },
  97. _handleClick() {
  98. this._saveLastRange();
  99. },
  100. _handleInput() {
  101. this._checkSubmitDisabled();
  102. },
  103. _renderEmojiTabs() {
  104. const renderImageGrid = items => {
  105. return items.map(item => (
  106. <img
  107. src={item.src}
  108. title={item.title}
  109. class="lemon-editor__emoji-item"
  110. on-click={() => this._handleSelectEmoji(item)}
  111. />
  112. ));
  113. };
  114. if (emojiData[0].label) {
  115. const nodes = emojiData.map((item, index) => {
  116. return (
  117. <div slot="tab-pane" index={index} tab={item.label}>
  118. {renderImageGrid(item.children)}
  119. {renderImageGrid(item.children)}
  120. </div>
  121. );
  122. });
  123. return <lemon-tabs style="width: 412px">{nodes}</lemon-tabs>;
  124. } else {
  125. return (
  126. <div class="lemon-tabs-content" style="width:406px">
  127. {renderImageGrid(emojiData)}
  128. </div>
  129. );
  130. }
  131. },
  132. _handleSelectEmoji(item) {
  133. this._focusLastRange();
  134. exec(`<img emoji-name="${item.name}" src="${item.src}"></img>`);
  135. this._saveLastRange();
  136. },
  137. async _handleSelectFile(accept) {
  138. this.accept = accept;
  139. await this.$nextTick();
  140. this.$refs.fileInput.click();
  141. },
  142. _handlePaste(e) {
  143. e.preventDefault();
  144. const { clipboardData } = e;
  145. const text = clipboardData.getData("text");
  146. exec(text, "insertText");
  147. // Array.from(clipboardData.items).forEach(item => {
  148. // console.log(item.type);
  149. // });
  150. //e.target.innerText = text;
  151. },
  152. _handleKeyup(e) {
  153. this._saveLastRange();
  154. //this._checkSubmitDisabled();
  155. },
  156. _handleKeydown(e) {
  157. const { keyCode } = e;
  158. if (keyCode == 13) {
  159. // e.preventDefault();
  160. // document.execCommand("defaultParagraphSeparator", false, false);
  161. // exec("<br>");
  162. }
  163. },
  164. getFormatValue() {
  165. return toEmojiName(
  166. this.$refs.textarea.innerHTML
  167. .replace(/<br>|<\/br>/, "")
  168. .replace(/<div>|<p>/g, "\r\n")
  169. .replace(/<\/div>|<\/p>/g, "")
  170. );
  171. },
  172. _checkSubmitDisabled() {
  173. this.submitDisabled = !this.$refs.textarea.innerHTML.trim();
  174. },
  175. _handleSend(e) {
  176. const text = this.getFormatValue();
  177. this.$emit("send", text);
  178. this.clear();
  179. this._checkSubmitDisabled();
  180. },
  181. _handleChangeFile(e) {
  182. const { fileInput } = this.$refs;
  183. Array.from(fileInput.files).forEach(file => {
  184. this.$emit("upload", file);
  185. });
  186. fileInput.value = "";
  187. },
  188. clear() {
  189. this.$refs.textarea.innerHTML = "";
  190. },
  191. initEmoji(data) {
  192. emojiData = data;
  193. this.$forceUpdate();
  194. // this.emoji = [
  195. // {
  196. // label: "表情",
  197. // name: "face",
  198. // data: [
  199. // {
  200. // name: "wx",
  201. // src: "微笑"
  202. // }
  203. // ]
  204. // },
  205. // {
  206. // label: "武器",
  207. // name: "wa",
  208. // data: [
  209. // {
  210. // name: "wx",
  211. // src: "微笑"
  212. // }
  213. // ]
  214. // }
  215. // ];
  216. }
  217. }
  218. };
  219. </script>
  220. <style lang="stylus">
  221. @import '~styles/utils/index'
  222. gap = 10px;
  223. +b(lemon-editor)
  224. height 200px
  225. flex-column()
  226. +e(tool)
  227. display flex
  228. height 40px
  229. align-items center
  230. padding-left 5px
  231. +e(tool-item)
  232. cursor pointer
  233. padding 4px gap
  234. height 28px
  235. color #999
  236. transition all ease .3s
  237. [class^='lemon-icon-']
  238. line-height 26px
  239. font-size 22px
  240. &:hover
  241. color #333
  242. +e(inner)
  243. flex 1
  244. overflow-x hidden
  245. overflow-y auto
  246. scrollbar-light()
  247. +e(input)
  248. height 100%
  249. box-sizing border-box
  250. border none
  251. outline none
  252. padding 0 gap
  253. scrollbar-light()
  254. p,div
  255. margin 0
  256. img
  257. height 20px
  258. padding 0 2px
  259. pointer-events none
  260. vertical-align middle
  261. +e(footer)
  262. display flex
  263. height 52px
  264. justify-content flex-end
  265. padding 0 gap
  266. align-items center
  267. +e(tip)
  268. margin-right 10px
  269. font-size 12px
  270. color #999
  271. user-select none
  272. +e(emoji)
  273. user-select none
  274. .lemon-popover
  275. background #f6f6f6
  276. .lemon-popover__content
  277. padding 0
  278. .lemon-popover__arrow
  279. background #f6f6f6
  280. .lemon-tabs-content
  281. box-sizing border-box
  282. padding 8px
  283. height 200px
  284. overflow-x hidden
  285. overflow-y auto
  286. scrollbar-light()
  287. margin-bottom 8px
  288. +e(emoji-item)
  289. cursor pointer
  290. width 22px
  291. padding 4px
  292. border-radius 4px
  293. &:hover
  294. background #e9e9e9
  295. </style>