richtext.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <template>
  2. <div ref="textRef" class="input textarea" :class="{ suffix: $slots.icon || maxlength, disabled, right }">
  3. <div
  4. ref="inputRef"
  5. contenteditable="true"
  6. class="ui-text input-div"
  7. :placeholder="props.placeholder"
  8. :readonly="readonly"
  9. v-bind="other"
  10. @input="inputHandler"
  11. @click="emit('click')"
  12. @focus="focusHandler"
  13. @blur="blurHandler"
  14. @paste="pasteHandler"
  15. @compositionstart="compositionstartHandler"
  16. @compositionend="compositionendHandler"
  17. />
  18. <span class="replace" />
  19. <span v-if="$slots.icon || props.maxlength" class="retouch">
  20. <slot name="icon" />
  21. <span v-if="props.maxlength" class="len">
  22. <span>{{ length }}</span> / {{ maxlength }}
  23. </span>
  24. </span>
  25. </div>
  26. </template>
  27. <script setup lang="ts">
  28. import { defineEmits, defineExpose, defineProps, nextTick, ref, watchEffect } from 'vue'
  29. import { richTextProps } from './richtext'
  30. const props = defineProps(richTextProps)
  31. const emit = defineEmits(['update:modelValue', 'focus', 'blur', 'click', ''])
  32. const textRef = ref(null)
  33. const inputRef = ref(null)
  34. const length = ref(0)
  35. const updateContent = html => {
  36. inputRef.value.innerHTML = html
  37. length.value = inputRef.value.textContent.length
  38. }
  39. watchEffect(() => {
  40. if (inputRef.value && props.modelValue !== inputRef.value.innerHTML) {
  41. updateContent(props.modelValue)
  42. }
  43. })
  44. let inComposition = false
  45. const compositionstartHandler = () => {
  46. inComposition = true
  47. }
  48. const compositionendHandler = (ev: any) => {
  49. inComposition = false
  50. inputHandler(ev)
  51. }
  52. const inputHandler = (ev: any) => {
  53. if (inComposition) return
  54. if (!props.maxlength || ev.target.textContent.length <= Number(props.maxlength)) {
  55. length.value = inputRef.value.textContent.length
  56. emit('update:modelValue', ev.target.innerHTML)
  57. } else {
  58. nextTick(() => {
  59. if (ev.target.innerHTML !== props.modelValue.toString()) {
  60. updateContent(props.modelValue.toString())
  61. inputFocus()
  62. }
  63. })
  64. }
  65. }
  66. //获取当前光标位置
  67. const getCursortPosition = function (element = inputRef.value) {
  68. let caretOffset = 0
  69. const doc = element.ownerDocument || element.document
  70. const win = doc.defaultView || doc.parentWindow
  71. let sel
  72. if (typeof win.getSelection != 'undefined') {
  73. //谷歌、火狐
  74. sel = win.getSelection()
  75. if (sel.rangeCount > 0) {
  76. //选中的区域
  77. const range = win.getSelection().getRangeAt(0)
  78. const preCaretRange = range.cloneRange() //克隆一个选中区域
  79. preCaretRange.selectNodeContents(element) //设置选中区域的节点内容为当前节点
  80. preCaretRange.setEnd(range.endContainer, range.endOffset) //重置选中区域的结束位置
  81. caretOffset = preCaretRange.toString().length
  82. }
  83. } else if ((sel = doc.selection) && sel.type != 'Control') {
  84. //IE
  85. const textRange = sel.createRange()
  86. const preCaretTextRange = doc.body.createTextRange()
  87. preCaretTextRange.moveToElementText(element)
  88. preCaretTextRange.setEndPoint('EndToEnd', textRange)
  89. caretOffset = preCaretTextRange.text.length
  90. }
  91. return caretOffset
  92. }
  93. let interval: number
  94. const focusHandler = () => {
  95. clearInterval(interval)
  96. interval = window.setInterval(() => {
  97. console.log(getCursortPosition())
  98. emit('updatePos', getCursortPosition())
  99. }, 100)
  100. emit('focus')
  101. }
  102. const blurHandler = () => {
  103. clearInterval(interval)
  104. emit('blur')
  105. }
  106. const inputFocus = () => {
  107. inputRef.value.focus()
  108. const range = window.getSelection()
  109. range.selectAllChildren(inputRef.value)
  110. range.collapseToEnd()
  111. }
  112. const getPasteText = text => {
  113. if (!props.maxlength) {
  114. return text
  115. }
  116. const $el = document.createElement('div')
  117. $el.innerHTML = text
  118. if ($el.textContent.length > props.maxlength - length.value) {
  119. return $el.textContent.slice(0, Math.max(0, props.maxlength - length.value))
  120. } else {
  121. return text
  122. }
  123. }
  124. const pasteHandler = event => {
  125. event.preventDefault()
  126. let text
  127. const clp = (event.originalEvent || event).clipboardData
  128. // 兼容针对于opera ie等浏览器
  129. if (clp === undefined || clp === null) {
  130. text = window.clipboardData.getData('text') || ''
  131. if (text !== '') {
  132. if (window.getSelection) {
  133. // 针对于ie11 10 9 safari
  134. const newNode = document.createElement('span')
  135. newNode.innerHTML = getPasteText(text)
  136. window.getSelection().getRangeAt(0).insertNode(newNode)
  137. } else {
  138. document.selection.createRange().pasteHTML(text)
  139. }
  140. }
  141. } else {
  142. // 兼容chorme或hotfire
  143. text = clp.getData('text/plain') || ''
  144. if (text !== '') {
  145. document.execCommand('insertText', false, getPasteText(text))
  146. }
  147. }
  148. }
  149. defineExpose({
  150. root: textRef,
  151. input: inputRef,
  152. getCursortPosition,
  153. })
  154. </script>