You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

235 lines
5.8 KiB

4 years ago
  1. <template>
  2. <div class="highlight-container highlight-container--bottom-labels" @click="open" @touchend="open">
  3. <entity-item
  4. v-for="(chunk, i) in chunks"
  5. :key="i"
  6. :content="chunk.text"
  7. :newline="chunk.newline"
  8. :label="chunk.label"
  9. :color="chunk.color"
  10. :labels="labels"
  11. @remove="deleteAnnotation(chunk.id)"
  12. @update="updateEntity($event.id, chunk.id)"
  13. />
  14. <v-menu
  15. v-model="showMenu"
  16. :position-x="x"
  17. :position-y="y"
  18. absolute
  19. offset-y
  20. >
  21. <v-list
  22. dense
  23. min-width="150"
  24. max-height="400"
  25. class="overflow-y-auto"
  26. >
  27. <v-list-item
  28. v-for="(label, i) in labels"
  29. :key="i"
  30. v-shortkey="[label.suffixKey]"
  31. @shortkey="assignLabel(label.id)"
  32. @click="assignLabel(label.id)"
  33. >
  34. <v-list-item-content>
  35. <v-list-item-title v-text="label.text" />
  36. </v-list-item-content>
  37. <v-list-item-action>
  38. <v-list-item-action-text v-text="label.suffixKey" />
  39. </v-list-item-action>
  40. </v-list-item>
  41. </v-list>
  42. </v-menu>
  43. </div>
  44. </template>
  45. <script>
  46. import EntityItem from './EntityItem'
  47. export default {
  48. components: {
  49. EntityItem
  50. },
  51. props: {
  52. text: {
  53. type: String,
  54. default: '',
  55. required: true
  56. },
  57. labels: {
  58. type: Array,
  59. default: () => ([]),
  60. required: true
  61. },
  62. entities: {
  63. type: Array,
  64. default: () => ([]),
  65. required: true
  66. },
  67. deleteAnnotation: {
  68. type: Function,
  69. default: () => ([]),
  70. required: true
  71. },
  72. updateEntity: {
  73. type: Function,
  74. default: () => ([]),
  75. required: true
  76. },
  77. addEntity: {
  78. type: Function,
  79. default: () => ([]),
  80. required: true
  81. }
  82. },
  83. data() {
  84. return {
  85. showMenu: false,
  86. x: 0,
  87. y: 0,
  88. start: 0,
  89. end: 0
  90. }
  91. },
  92. computed: {
  93. sortedEntities() {
  94. return this.entities.slice().sort((a, b) => a.startOffset - b.startOffset)
  95. },
  96. chunks() {
  97. let chunks = []
  98. let startOffset = 0
  99. // to count the number of characters correctly.
  100. const characters = [...this.text]
  101. for (const entity of this.sortedEntities) {
  102. // add non-entities to chunks.
  103. let piece = characters.slice(startOffset, entity.startOffset).join('')
  104. chunks = chunks.concat(this.makeChunks(piece))
  105. startOffset = entity.endOffset
  106. // add entities to chunks.
  107. const label = this.labelObject[entity.label]
  108. piece = characters.slice(entity.startOffset, entity.endOffset).join('')
  109. chunks.push({
  110. id: entity.id,
  111. label: label.text,
  112. color: label.backgroundColor,
  113. text: piece
  114. })
  115. }
  116. // add the rest of text.
  117. chunks = chunks.concat(this.makeChunks(characters.slice(startOffset, characters.length).join('')))
  118. return chunks
  119. },
  120. labelObject() {
  121. const obj = {}
  122. for (const label of this.labels) {
  123. obj[label.id] = label
  124. }
  125. return obj
  126. }
  127. },
  128. methods: {
  129. makeChunks(text) {
  130. const chunks = []
  131. const snippets = text.split('\n')
  132. for (const snippet of snippets.slice(0, -1)) {
  133. chunks.push({
  134. label: null,
  135. color: null,
  136. text: snippet + '\n',
  137. newline: false
  138. })
  139. chunks.push({
  140. label: null,
  141. color: null,
  142. text: '',
  143. newline: true
  144. })
  145. }
  146. chunks.push({
  147. label: null,
  148. color: null,
  149. text: snippets.slice(-1)[0],
  150. newline: false
  151. })
  152. return chunks
  153. },
  154. show(e) {
  155. e.preventDefault()
  156. this.showMenu = false
  157. this.x = e.clientX || e.changedTouches[0].clientX
  158. this.y = e.clientY || e.changedTouches[0].clientY
  159. this.$nextTick(() => {
  160. this.showMenu = true
  161. })
  162. },
  163. setSpanInfo() {
  164. let selection
  165. // Modern browsers.
  166. if (window.getSelection) {
  167. selection = window.getSelection()
  168. } else if (document.selection) {
  169. selection = document.selection
  170. }
  171. // If nothing is selected.
  172. if (selection.rangeCount <= 0) {
  173. return
  174. }
  175. const range = selection.getRangeAt(0)
  176. const preSelectionRange = range.cloneRange()
  177. preSelectionRange.selectNodeContents(this.$el)
  178. preSelectionRange.setEnd(range.startContainer, range.startOffset)
  179. this.start = [...preSelectionRange.toString()].length
  180. this.end = this.start + [...range.toString()].length
  181. },
  182. validateSpan() {
  183. if ((typeof this.start === 'undefined') || (typeof this.end === 'undefined')) {
  184. return false
  185. }
  186. if (this.start === this.end) {
  187. return false
  188. }
  189. for (const entity of this.entities) {
  190. if ((entity.startOffset <= this.start) && (this.start < entity.endOffset)) {
  191. return false
  192. }
  193. if ((entity.startOffset < this.end) && (this.end <= entity.endOffset)) {
  194. return false
  195. }
  196. if ((this.start < entity.startOffset) && (entity.endOffset < this.end)) {
  197. return false
  198. }
  199. }
  200. return true
  201. },
  202. open(e) {
  203. this.setSpanInfo()
  204. if (this.validateSpan()) {
  205. this.show(e)
  206. }
  207. },
  208. assignLabel(labelId) {
  209. if (this.validateSpan()) {
  210. this.addEntity(this.start, this.end, labelId)
  211. this.showMenu = false
  212. this.start = 0
  213. this.end = 0
  214. }
  215. }
  216. }
  217. }
  218. </script>
  219. <style scoped>
  220. .highlight-container.highlight-container--bottom-labels {
  221. align-items: flex-start;
  222. }
  223. .highlight-container {
  224. line-height: 42px!important;
  225. display: flex;
  226. flex-wrap: wrap;
  227. white-space: pre-wrap;
  228. cursor: default;
  229. }
  230. .highlight-container.highlight-container--bottom-labels .highlight.bottom {
  231. margin-top: 6px;
  232. }
  233. </style>