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.

236 lines
5.7 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.suffix_key]"
  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.suffix_key" />
  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 '~/components/molecules/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.start_offset - b.start_offset)
  95. },
  96. chunks() {
  97. let chunks = []
  98. const entities = this.sortedEntities
  99. let startOffset = 0
  100. for (const entity of entities) {
  101. // add non-entities to chunks.
  102. chunks = chunks.concat(this.makeChunks(this.text.slice(startOffset, entity.start_offset)))
  103. startOffset = entity.end_offset
  104. // add entities to chunks.
  105. const label = this.labelObject[entity.label]
  106. chunks.push({
  107. id: entity.id,
  108. label: label.text,
  109. color: label.background_color,
  110. text: this.text.slice(entity.start_offset, entity.end_offset)
  111. })
  112. }
  113. // add the rest of text.
  114. chunks = chunks.concat(this.makeChunks(this.text.slice(startOffset, this.text.length)))
  115. return chunks
  116. },
  117. labelObject() {
  118. const obj = {}
  119. for (const label of this.labels) {
  120. obj[label.id] = label
  121. }
  122. return obj
  123. }
  124. },
  125. methods: {
  126. makeChunks(text) {
  127. const chunks = []
  128. const snippets = text.split('\n')
  129. for (const snippet of snippets.slice(0, -1)) {
  130. chunks.push({
  131. label: null,
  132. color: null,
  133. text: snippet + '\n',
  134. newline: false
  135. })
  136. chunks.push({
  137. label: null,
  138. color: null,
  139. text: '',
  140. newline: true
  141. })
  142. }
  143. chunks.push({
  144. label: null,
  145. color: null,
  146. text: snippets.slice(-1)[0],
  147. newline: false
  148. })
  149. return chunks
  150. },
  151. show(e) {
  152. e.preventDefault()
  153. this.showMenu = false
  154. this.x = e.clientX || e.changedTouches[0].clientX
  155. this.y = e.clientY || e.changedTouches[0].clientY
  156. this.$nextTick(() => {
  157. this.showMenu = true
  158. })
  159. },
  160. setSpanInfo() {
  161. let selection
  162. // Modern browsers.
  163. if (window.getSelection) {
  164. selection = window.getSelection()
  165. } else if (document.selection) {
  166. selection = document.selection
  167. }
  168. // If nothing is selected.
  169. if (selection.rangeCount <= 0) {
  170. return
  171. }
  172. const range = selection.getRangeAt(0)
  173. const preSelectionRange = range.cloneRange()
  174. preSelectionRange.selectNodeContents(this.$el)
  175. preSelectionRange.setEnd(range.startContainer, range.startOffset)
  176. this.start = [...preSelectionRange.toString()].length
  177. this.end = this.start + [...range.toString()].length
  178. },
  179. validateSpan() {
  180. if ((typeof this.start === 'undefined') || (typeof this.end === 'undefined')) {
  181. return false
  182. }
  183. if (this.start === this.end) {
  184. return false
  185. }
  186. for (const entity of this.entities) {
  187. if ((entity.start_offset <= this.start) && (this.start < entity.end_offset)) {
  188. return false
  189. }
  190. if ((entity.start_offset < this.end) && (this.end <= entity.end_offset)) {
  191. return false
  192. }
  193. if ((this.start < entity.start_offset) && (entity.end_offset < this.end)) {
  194. return false
  195. }
  196. }
  197. return true
  198. },
  199. open(e) {
  200. this.setSpanInfo()
  201. if (this.validateSpan()) {
  202. this.show(e)
  203. }
  204. },
  205. assignLabel(labelId) {
  206. if (this.validateSpan()) {
  207. this.addEntity(this.start, this.end, labelId)
  208. this.showMenu = false
  209. this.start = 0
  210. this.end = 0
  211. }
  212. }
  213. }
  214. }
  215. </script>
  216. <style scoped>
  217. .highlight-container.highlight-container--bottom-labels {
  218. align-items: flex-start;
  219. }
  220. .highlight-container {
  221. line-height: 42px!important;
  222. display: flex;
  223. flex-wrap: wrap;
  224. white-space: pre-wrap;
  225. cursor: default;
  226. }
  227. .highlight-container.highlight-container--bottom-labels .highlight.bottom {
  228. margin-top: 6px;
  229. }
  230. </style>