editor.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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. </div>
  120. );
  121. });
  122. return <lemon-tabs style="width: 412px">{nodes}</lemon-tabs>;
  123. } else {
  124. return (
  125. <div class="lemon-tabs-content" style="width:406px">
  126. {renderImageGrid(emojiData)}
  127. </div>
  128. );
  129. }
  130. },
  131. _handleSelectEmoji(item) {
  132. this._focusLastRange();
  133. exec(`<img emoji-name="${item.name}" src="${item.src}"></img>`);
  134. this._saveLastRange();
  135. },
  136. async _handleSelectFile(accept) {
  137. this.accept = accept;
  138. await this.$nextTick();
  139. this.$refs.fileInput.click();
  140. },
  141. _handlePaste(e) {
  142. e.preventDefault();
  143. const { clipboardData } = e;
  144. const text = clipboardData.getData("text");
  145. exec(text, "insertText");
  146. // Array.from(clipboardData.items).forEach(item => {
  147. // console.log(item.type);
  148. // });
  149. //e.target.innerText = text;
  150. },
  151. _handleKeyup(e) {
  152. this._saveLastRange();
  153. //this._checkSubmitDisabled();
  154. },
  155. _handleKeydown(e) {
  156. const { keyCode } = e;
  157. if (keyCode == 13) {
  158. // e.preventDefault();
  159. // document.execCommand("defaultParagraphSeparator", false, false);
  160. // exec("<br>");
  161. }
  162. },
  163. getFormatValue() {
  164. return toEmojiName(
  165. this.$refs.textarea.innerHTML
  166. .replace(/<br>|<\/br>/, "")
  167. .replace(/<div>|<p>/g, "\r\n")
  168. .replace(/<\/div>|<\/p>/g, "")
  169. );
  170. },
  171. _checkSubmitDisabled() {
  172. this.submitDisabled = !this.$refs.textarea.innerHTML.trim();
  173. },
  174. _handleSend(e) {
  175. const text = this.getFormatValue();
  176. this.$emit("send", text);
  177. this.clear();
  178. this._checkSubmitDisabled();
  179. },
  180. _handleChangeFile(e) {
  181. const { fileInput } = this.$refs;
  182. Array.from(fileInput.files).forEach(file => {
  183. this.$emit("upload", file);
  184. });
  185. fileInput.value = "";
  186. },
  187. clear() {
  188. this.$refs.textarea.innerHTML = "";
  189. },
  190. initEmoji(data) {
  191. emojiData = data;
  192. this.$forceUpdate();
  193. }
  194. }
  195. };
  196. </script>
  197. <style lang="stylus">
  198. @import '~styles/utils/index'
  199. gap = 10px;
  200. +b(lemon-editor)
  201. height 200px
  202. flex-column()
  203. +e(tool)
  204. display flex
  205. height 40px
  206. align-items center
  207. padding-left 5px
  208. +e(tool-item)
  209. cursor pointer
  210. padding 4px gap
  211. height 28px
  212. color #999
  213. transition all ease .3s
  214. [class^='lemon-icon-']
  215. line-height 26px
  216. font-size 22px
  217. &:hover
  218. color #333
  219. +e(inner)
  220. flex 1
  221. overflow-x hidden
  222. overflow-y auto
  223. scrollbar-light()
  224. +e(input)
  225. height 100%
  226. box-sizing border-box
  227. border none
  228. outline none
  229. padding 0 gap
  230. scrollbar-light()
  231. p,div
  232. margin 0
  233. img
  234. height 20px
  235. padding 0 2px
  236. pointer-events none
  237. vertical-align middle
  238. +e(footer)
  239. display flex
  240. height 52px
  241. justify-content flex-end
  242. padding 0 gap
  243. align-items center
  244. +e(tip)
  245. margin-right 10px
  246. font-size 12px
  247. color #999
  248. user-select none
  249. +e(emoji)
  250. user-select none
  251. .lemon-popover
  252. background #f6f6f6
  253. .lemon-popover__content
  254. padding 0
  255. .lemon-popover__arrow
  256. background #f6f6f6
  257. .lemon-tabs-content
  258. box-sizing border-box
  259. padding 8px
  260. height 200px
  261. overflow-x hidden
  262. overflow-y auto
  263. scrollbar-light()
  264. margin-bottom 8px
  265. +e(emoji-item)
  266. cursor pointer
  267. width 22px
  268. padding 4px
  269. border-radius 4px
  270. &:hover
  271. background #e9e9e9
  272. </style>