url-field.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. <template>
  2. <div contenteditable class="url-field" ref="editor" spellcheck="false"></div>
  3. </template>
  4. <style lang="scss">
  5. .highlight-VAR {
  6. @apply font-bold;
  7. @apply text-acColor;
  8. }
  9. .highlight-TEXT {
  10. @apply overflow-auto;
  11. @apply break-all;
  12. height: 22px;
  13. }
  14. .highlight-TEXT::-webkit-scrollbar {
  15. @apply hidden;
  16. }
  17. </style>
  18. <script>
  19. export default {
  20. props: {
  21. value: { type: String, default: "" },
  22. },
  23. data() {
  24. return {
  25. cacheValue: null,
  26. unwatchValue: null,
  27. }
  28. },
  29. mounted() {
  30. this.$refs.editor.addEventListener("input", this.updateEditor)
  31. this.$refs.editor.textContent = this.value || ""
  32. this.cacheValue = this.value || ""
  33. this.unwatchValue = this.$watch(
  34. () => this.value,
  35. (newVal) => {
  36. if (this.$refs.editor && this.cacheValue !== newVal)
  37. this.$refs.editor.textContent = newVal || ""
  38. this.updateEditor()
  39. }
  40. )
  41. this.updateEditor()
  42. },
  43. beforeDestroy() {
  44. this.unwatchValue()
  45. this.$refs.editor.removeEventListener("input", this.updateEditor)
  46. },
  47. methods: {
  48. renderText(text) {
  49. const fixedText = text.replace(/(\r\n|\n|\r)/gm, "").trim()
  50. const parseMap = this.parseURL(fixedText)
  51. const convertSpan = document.createElement("span")
  52. const output = parseMap.map(([start, end, protocol]) => {
  53. convertSpan.textContent = fixedText.substring(start, end + 1)
  54. return `<span class='highlight-${protocol}'>${convertSpan.innerHTML}</span>`
  55. })
  56. return output.join("")
  57. },
  58. parseURL(text) {
  59. const map = []
  60. const regex = /<<\w+>>/
  61. let match
  62. let index = 0
  63. while ((match = text.substring(index).match(regex))) {
  64. map.push([index, index + (match.index - 1), "TEXT"])
  65. map.push([index + match.index, index + match.index + match[0].length - 1, "VAR"])
  66. index += match.index + match[0].length
  67. if (index >= text.length - 1) break
  68. }
  69. if (text.length > index && !text.substring(index).match(regex)) {
  70. map.push([index, text.length, "TEXT"])
  71. }
  72. return map
  73. },
  74. getTextSegments({ childNodes }) {
  75. const textSegments = []
  76. Array.from(childNodes).forEach((node) => {
  77. switch (node.nodeType) {
  78. case Node.TEXT_NODE:
  79. textSegments.push({ text: node.nodeValue, node })
  80. break
  81. case Node.ELEMENT_NODE:
  82. textSegments.splice(textSegments.length, 0, ...this.getTextSegments(node))
  83. break
  84. }
  85. })
  86. return textSegments
  87. },
  88. restoreSelection(absoluteAnchorIndex, absoluteFocusIndex) {
  89. const sel = window.getSelection()
  90. const textSegments = this.getTextSegments(this.$refs.editor)
  91. let anchorNode = this.$refs.editor
  92. let anchorIndex = 0
  93. let focusNode = this.$refs.editor
  94. let focusIndex = 0
  95. let currentIndex = 0
  96. textSegments.forEach(({ text, node }) => {
  97. const startIndexOfNode = currentIndex
  98. const endIndexOfNode = startIndexOfNode + text.length
  99. if (startIndexOfNode <= absoluteAnchorIndex && absoluteAnchorIndex <= endIndexOfNode) {
  100. anchorNode = node
  101. anchorIndex = absoluteAnchorIndex - startIndexOfNode
  102. }
  103. if (startIndexOfNode <= absoluteFocusIndex && absoluteFocusIndex <= endIndexOfNode) {
  104. focusNode = node
  105. focusIndex = absoluteFocusIndex - startIndexOfNode
  106. }
  107. currentIndex += text.length
  108. })
  109. sel.setBaseAndExtent(anchorNode, anchorIndex, focusNode, focusIndex)
  110. },
  111. updateEditor() {
  112. this.cacheValue = this.$refs.editor.textContent
  113. const sel = window.getSelection()
  114. const textSegments = this.getTextSegments(this.$refs.editor)
  115. const textContent = textSegments.map(({ text }) => text).join("")
  116. let anchorIndex = null
  117. let focusIndex = null
  118. let currentIndex = 0
  119. textSegments.forEach(({ text, node }) => {
  120. if (node === sel.anchorNode) {
  121. anchorIndex = currentIndex + sel.anchorOffset
  122. }
  123. if (node === sel.focusNode) {
  124. focusIndex = currentIndex + sel.focusOffset
  125. }
  126. currentIndex += text.length
  127. })
  128. this.$refs.editor.innerHTML = this.renderText(textContent)
  129. this.restoreSelection(anchorIndex, focusIndex)
  130. this.$emit("input", this.$refs.editor.textContent)
  131. },
  132. },
  133. }
  134. </script>