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.

278 lines
6.6 KiB

  1. <template>
  2. <div v-shortkey="['esc']" @shortkey="cleanUp">
  3. <v-annotator
  4. :dark="$vuetify.theme.dark"
  5. :rtl="rtl"
  6. :text="text"
  7. :entities="entities"
  8. :entity-labels="entityLabels"
  9. :relations="relations"
  10. :relation-labels="relationLabels"
  11. :allow-overlapping="allowOverlapping"
  12. :grapheme-mode="graphemeMode"
  13. :selected-entities="selectedEntities"
  14. @add:entity="handleAddEvent"
  15. @click:entity="onEntityClicked"
  16. @click:relation="onRelationClicked"
  17. @contextmenu:entity="deleteEntity"
  18. @contextmenu:relation="deleteRelation"
  19. />
  20. <labeling-menu
  21. :opened="entityMenuOpened"
  22. :x="x"
  23. :y="y"
  24. :selected-label="currentLabel"
  25. :labels="entityLabels"
  26. @close="cleanUp"
  27. @click:label="addOrUpdateEntity"
  28. />
  29. <labeling-menu
  30. :opened="relationMenuOpened"
  31. :x="x"
  32. :y="y"
  33. :selected-label="currentRelationLabel"
  34. :labels="relationLabels"
  35. @close="cleanUp"
  36. @click:label="addOrUpdateRelation"
  37. />
  38. </div>
  39. </template>
  40. <script lang="ts">
  41. import Vue, { PropType } from 'vue'
  42. import VAnnotator from 'v-annotator'
  43. import LabelingMenu from './LabelingMenu.vue'
  44. import { SpanDTO } from '~/services/application/tasks/sequenceLabeling/sequenceLabelingData'
  45. import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
  46. export default Vue.extend({
  47. components: {
  48. VAnnotator,
  49. LabelingMenu,
  50. },
  51. props: {
  52. dark: {
  53. type: Boolean,
  54. default: false,
  55. },
  56. rtl: {
  57. type: Boolean,
  58. default: false,
  59. },
  60. text: {
  61. type: String,
  62. default: "",
  63. required: true,
  64. },
  65. entities: {
  66. type: Array as PropType<SpanDTO[]>,
  67. default: () => [],
  68. required: true,
  69. },
  70. entityLabels: {
  71. type: Array,
  72. default: () => [],
  73. required: true,
  74. },
  75. relations: {
  76. type: Array,
  77. default: () => [],
  78. },
  79. relationLabels: {
  80. type: Array,
  81. default: () => [],
  82. },
  83. allowOverlapping: {
  84. type: Boolean,
  85. default: false,
  86. required: false,
  87. },
  88. graphemeMode: {
  89. type: Boolean,
  90. default: false,
  91. },
  92. selectedLabel: {
  93. type: Object,
  94. default: null,
  95. required: false,
  96. },
  97. relationMode: {
  98. type: Boolean,
  99. default: false,
  100. },
  101. },
  102. data() {
  103. return {
  104. entityMenuOpened: false,
  105. relationMenuOpened: false,
  106. x: 0,
  107. y: 0,
  108. startOffset: 0,
  109. endOffset: 0,
  110. entity: null as any,
  111. relation: null as any,
  112. selectedEntities: [] as SpanDTO[],
  113. };
  114. },
  115. computed: {
  116. currentLabel(): any {
  117. if (this.entity) {
  118. const label = this.entityLabels.find((label: any) => label.id === this.entity!.label)
  119. return label
  120. } else {
  121. return null
  122. }
  123. },
  124. currentRelationLabel(): any {
  125. if (this.relation) {
  126. const label = this.relationLabels.find((label: any) => label.id === this.relation.labelId)
  127. return label
  128. } else {
  129. return null
  130. }
  131. }
  132. },
  133. methods: {
  134. setOffset(startOffset: number, endOffset: number) {
  135. this.startOffset = startOffset
  136. this.endOffset = endOffset
  137. },
  138. setEntity(entityId: number) {
  139. this.entity = this.entities.find((entity: any) => entity.id === entityId)
  140. },
  141. setRelation(relationId: number) {
  142. this.relation = this.relations.find((relation: any) => relation.id === relationId)
  143. },
  144. setEntityForRelation(e: Event, entityId: number) {
  145. const entity = this.entities.find((entity) => entity.id === entityId)!
  146. const index = this.selectedEntities.findIndex((e) => e.id === entity.id)
  147. if (index === -1) {
  148. this.selectedEntities.push(entity)
  149. } else {
  150. this.selectedEntities.splice(index, 1)
  151. }
  152. if (this.selectedEntities.length === 2) {
  153. if (this.selectedLabel) {
  154. this.addRelation(this.selectedLabel.id)
  155. this.cleanUp()
  156. } else {
  157. this.showRelationLabelMenu(e)
  158. }
  159. }
  160. },
  161. showEntityLabelMenu(e: any) {
  162. e.preventDefault()
  163. this.entityMenuOpened = false
  164. this.x = e.clientX || e.changedTouches[0].clientX
  165. this.y = e.clientY || e.changedTouches[0].clientY
  166. this.$nextTick(() => {
  167. this.entityMenuOpened = true
  168. })
  169. },
  170. showRelationLabelMenu(e: any) {
  171. e.preventDefault()
  172. this.relationMenuOpened = false
  173. this.x = e.clientX || e.changedTouches[0].clientX
  174. this.y = e.clientY || e.changedTouches[0].clientY
  175. this.$nextTick(() => {
  176. this.relationMenuOpened = true
  177. })
  178. },
  179. handleAddEvent(e: any, startOffset: number, endOffset: number) {
  180. this.setOffset(startOffset, endOffset)
  181. if (this.selectedLabel) {
  182. this.addOrUpdateEntity(this.selectedLabel.id)
  183. } else {
  184. this.showEntityLabelMenu(e)
  185. }
  186. },
  187. onEntityClicked(e: any, entityId: number) {
  188. if (this.relationMode) {
  189. this.setEntityForRelation(e, entityId)
  190. } else {
  191. this.setEntity(entityId)
  192. this.showEntityLabelMenu(e)
  193. }
  194. },
  195. onRelationClicked(e: any, relation: any) {
  196. this.setRelation(relation.id)
  197. this.showRelationLabelMenu(e)
  198. },
  199. addOrUpdateEntity(labelId: number) {
  200. if (labelId) {
  201. if (this.entity) {
  202. this.updateEntity(labelId)
  203. } else {
  204. this.addEntity(labelId)
  205. }
  206. } else {
  207. this.deleteEntity(this.entity)
  208. }
  209. this.cleanUp()
  210. },
  211. addOrUpdateRelation(labelId: number) {
  212. if (labelId) {
  213. if (this.relation) {
  214. this.updateRelation(labelId)
  215. } else {
  216. this.addRelation(labelId)
  217. }
  218. } else {
  219. this.deleteRelation(this.relation)
  220. }
  221. this.cleanUp()
  222. },
  223. addEntity(labelId: number) {
  224. this.$emit('addEntity', this.startOffset, this.endOffset, labelId)
  225. },
  226. updateEntity(labelId: number) {
  227. this.$emit('click:entity', this.entity!.id, labelId)
  228. },
  229. deleteEntity(entity: any) {
  230. this.$emit('contextmenu:entity', entity.id)
  231. this.cleanUp()
  232. },
  233. cleanUp() {
  234. this.entityMenuOpened = false
  235. this.relationMenuOpened = false
  236. this.entity = null
  237. this.relation = null
  238. this.startOffset = 0
  239. this.endOffset = 0
  240. this.selectedEntities = []
  241. },
  242. addRelation(labelId: number) {
  243. const [fromEntity, toEntity] = this.selectedEntities
  244. this.$emit('addRelation', fromEntity.id, toEntity.id, labelId)
  245. },
  246. updateRelation(labelId: number) {
  247. this.$emit("click:relation", this.relation.id, labelId)
  248. },
  249. deleteRelation(relation: any) {
  250. this.$emit('contextmenu:relation', relation.id)
  251. }
  252. },
  253. });
  254. </script>