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.

220 lines
5.2 KiB

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