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.

218 lines
5.2 KiB

  1. <template>
  2. <div @click="open" @touchend="open" class="highlight-container highlight-container--bottom-labels">
  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 EntityItem from '~/components/molecules/EntityItem'
  46. export default {
  47. components: {
  48. EntityItem
  49. },
  50. props: {
  51. text: {
  52. type: String,
  53. default: '',
  54. required: true
  55. },
  56. labels: {
  57. type: Array,
  58. default: () => ([]),
  59. required: true
  60. },
  61. entities: {
  62. type: Array,
  63. default: () => ([]),
  64. required: true
  65. },
  66. deleteAnnotation: {
  67. type: Function,
  68. default: () => ([]),
  69. required: true
  70. },
  71. updateEntity: {
  72. type: Function,
  73. default: () => ([]),
  74. required: true
  75. },
  76. addEntity: {
  77. type: Function,
  78. default: () => ([]),
  79. required: true
  80. }
  81. },
  82. data() {
  83. return {
  84. showMenu: false,
  85. x: 0,
  86. y: 0,
  87. start: 0,
  88. end: 0
  89. }
  90. },
  91. computed: {
  92. sortedEntities() {
  93. return this.entities.slice().sort((a, b) => a.start_offset - b.start_offset)
  94. },
  95. chunks() {
  96. const chunks = []
  97. const entities = this.sortedEntities
  98. let startOffset = 0
  99. for (const entity of entities) {
  100. // add non-entities to chunks.
  101. chunks.push({
  102. label: null,
  103. color: null,
  104. text: this.text.slice(startOffset, entity.start_offset)
  105. })
  106. startOffset = entity.end_offset
  107. // add entities to chunks.
  108. const label = this.labelObject[entity.label]
  109. chunks.push({
  110. id: entity.id,
  111. label: label.text,
  112. color: label.background_color,
  113. text: this.text.slice(entity.start_offset, entity.end_offset)
  114. })
  115. }
  116. // add the rest of text.
  117. chunks.push({
  118. label: null,
  119. color: null,
  120. text: this.text.slice(startOffset, this.text.length)
  121. })
  122. return chunks
  123. },
  124. labelObject() {
  125. const obj = {}
  126. for (const label of this.labels) {
  127. obj[label.id] = label
  128. }
  129. return obj
  130. }
  131. },
  132. methods: {
  133. show(e) {
  134. e.preventDefault()
  135. this.showMenu = false
  136. this.x = e.clientX || e.changedTouches[0].clientX
  137. this.y = e.clientY || e.changedTouches[0].clientY
  138. this.$nextTick(() => {
  139. this.showMenu = true
  140. })
  141. },
  142. setSpanInfo() {
  143. let selection
  144. // Modern browsers.
  145. if (window.getSelection) {
  146. selection = window.getSelection()
  147. } else if (document.selection) {
  148. selection = document.selection
  149. }
  150. // If nothing is selected.
  151. if (selection.rangeCount <= 0) {
  152. return
  153. }
  154. const range = selection.getRangeAt(0)
  155. const preSelectionRange = range.cloneRange()
  156. preSelectionRange.selectNodeContents(this.$el)
  157. preSelectionRange.setEnd(range.startContainer, range.startOffset)
  158. this.start = [...preSelectionRange.toString()].length
  159. this.end = this.start + [...range.toString()].length
  160. },
  161. validateSpan() {
  162. if ((typeof this.start === 'undefined') || (typeof this.end === 'undefined')) {
  163. return false
  164. }
  165. if (this.start === this.end) {
  166. return false
  167. }
  168. for (const entity of this.entities) {
  169. if ((entity.start_offset <= this.start) && (this.start < entity.end_offset)) {
  170. return false
  171. }
  172. if ((entity.start_offset < this.end) && (this.end <= entity.end_offset)) {
  173. return false
  174. }
  175. if ((this.start < entity.start_offset) && (entity.end_offset < this.end)) {
  176. return false
  177. }
  178. }
  179. return true
  180. },
  181. open(e) {
  182. this.setSpanInfo()
  183. if (this.validateSpan()) {
  184. this.show(e)
  185. }
  186. },
  187. assignLabel(labelId) {
  188. if (this.validateSpan()) {
  189. this.addEntity(this.start, this.end, labelId)
  190. this.showMenu = false
  191. this.start = 0
  192. this.end = 0
  193. }
  194. }
  195. }
  196. }
  197. </script>
  198. <style scoped>
  199. .highlight-container.highlight-container--bottom-labels {
  200. align-items: flex-start;
  201. }
  202. .highlight-container {
  203. line-height: 42px!important;
  204. display: flex;
  205. flex-wrap: wrap;
  206. white-space: pre-wrap;
  207. cursor: default;
  208. }
  209. .highlight-container.highlight-container--bottom-labels .highlight.bottom {
  210. margin-top: 6px;
  211. }
  212. </style>