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.

228 lines
6.2 KiB

  1. <template lang="pug">
  2. div(@click="setSelectedRange")
  3. span.text-sequence(
  4. v-for="r in chunksWithLabel"
  5. v-bind:class="getChunkClass(r)"
  6. v-bind:data-tooltip="id2label[r.label].text"
  7. v-bind:style="{ \
  8. color: id2label[r.label].text_color, \
  9. backgroundColor: id2label[r.label].background_color \
  10. }"
  11. )
  12. span
  13. span(
  14. v-for="highlightedChunk in getSearchHighlightedChunks(textPart(r))"
  15. v-bind:class="highlightedChunk.highlight && 'has-background-warning'"
  16. ) {{ highlightedChunk.content }}
  17. button.delete.is-small(v-if="id2label[r.label].text_color", v-on:click="removeLabel(r)")
  18. </template>
  19. <script>
  20. export default {
  21. props: {
  22. labels: {
  23. type: Array, // [{id: Integer, color: String, text: String}]
  24. default: () => [],
  25. },
  26. searchQuery: {
  27. type: String,
  28. default: '',
  29. },
  30. text: {
  31. type: String,
  32. default: '',
  33. },
  34. entityPositions: {
  35. type: Array, // [{'startOffset': 10, 'endOffset': 15, 'label_id': 1}]
  36. default: () => [],
  37. },
  38. },
  39. data: () => ({
  40. startOffset: 0,
  41. endOffset: 0,
  42. }),
  43. computed: {
  44. sortedEntityPositions() {
  45. /* eslint-disable vue/no-side-effects-in-computed-properties */
  46. this.entityPositions = this.entityPositions.sort((a, b) => a.start_offset - b.start_offset);
  47. return this.entityPositions;
  48. /* eslint-enable vue/no-side-effects-in-computed-properties */
  49. },
  50. chunks() {
  51. const res = [];
  52. let left = 0;
  53. for (let i = 0; i < this.sortedEntityPositions.length; i++) {
  54. const e = this.sortedEntityPositions[i];
  55. const l = this.makeLabel(left, e.start_offset);
  56. res.push(l);
  57. res.push(e);
  58. left = e.end_offset;
  59. }
  60. const l = this.makeLabel(left, this.text.length);
  61. res.push(l);
  62. return res;
  63. },
  64. chunksWithLabel() {
  65. return this.chunks.filter(r => this.id2label[r.label]);
  66. },
  67. id2label() {
  68. const id2label = {};
  69. // default value;
  70. id2label[-1] = {
  71. text_color: '',
  72. background_color: '',
  73. };
  74. for (let i = 0; i < this.labels.length; i++) {
  75. const label = this.labels[i];
  76. id2label[label.id] = label;
  77. }
  78. return id2label;
  79. },
  80. },
  81. watch: {
  82. entityPositions() {
  83. this.resetRange();
  84. },
  85. },
  86. methods: {
  87. getSearchHighlightedChunks(text) {
  88. if (this.searchQuery) {
  89. const chunks = [];
  90. let currentText = text;
  91. let nextIndex;
  92. do {
  93. nextIndex = currentText.toLowerCase().indexOf(this.searchQuery.toLowerCase());
  94. if (nextIndex !== -1) {
  95. chunks.push({
  96. content: currentText.substring(0, nextIndex),
  97. highlight: false,
  98. });
  99. chunks.push({
  100. content: currentText.substring(nextIndex, nextIndex + this.searchQuery.length),
  101. highlight: true,
  102. });
  103. nextIndex += this.searchQuery.length;
  104. currentText = currentText.substring(nextIndex);
  105. } else {
  106. chunks.push({
  107. content: currentText.substring(nextIndex),
  108. highlight: false,
  109. });
  110. }
  111. } while (nextIndex !== -1);
  112. return chunks.filter(({ content }) => content);
  113. }
  114. return [{ content: text, highlight: false }];
  115. },
  116. getChunkClass(chunk) {
  117. if (!chunk.id) {
  118. return {};
  119. }
  120. const label = this.id2label[chunk.label];
  121. return [
  122. 'tooltip is-tooltip-bottom',
  123. { tag: label.text_color },
  124. ];
  125. },
  126. setSelectedRange() {
  127. let start;
  128. let end;
  129. if (window.getSelection) {
  130. const range = window.getSelection().getRangeAt(0);
  131. const preSelectionRange = range.cloneRange();
  132. preSelectionRange.selectNodeContents(this.$el);
  133. preSelectionRange.setEnd(range.startContainer, range.startOffset);
  134. start = [...preSelectionRange.toString()].length;
  135. end = start + [...range.toString()].length;
  136. } else if (document.selection && document.selection.type !== 'Control') {
  137. const selectedTextRange = document.selection.createRange();
  138. const preSelectionTextRange = document.body.createTextRange();
  139. preSelectionTextRange.moveToElementText(this.$el);
  140. preSelectionTextRange.setEndPoint('EndToStart', selectedTextRange);
  141. start = [...preSelectionTextRange.text].length;
  142. end = start + [...selectedTextRange.text].length;
  143. }
  144. this.startOffset = start;
  145. this.endOffset = end;
  146. console.log(start, end); // eslint-disable-line no-console
  147. },
  148. validRange() {
  149. if (this.startOffset === this.endOffset) {
  150. return false;
  151. }
  152. if (this.startOffset > this.text.length || this.endOffset > this.text.length) {
  153. return false;
  154. }
  155. if (this.startOffset < 0 || this.endOffset < 0) {
  156. return false;
  157. }
  158. for (let i = 0; i < this.entityPositions.length; i++) {
  159. const e = this.entityPositions[i];
  160. if ((e.start_offset <= this.startOffset) && (this.startOffset < e.end_offset)) {
  161. return false;
  162. }
  163. if ((e.start_offset < this.endOffset) && (this.endOffset < e.end_offset)) {
  164. return false;
  165. }
  166. if ((this.startOffset < e.start_offset) && (e.start_offset < this.endOffset)) {
  167. return false;
  168. }
  169. if ((this.startOffset < e.end_offset) && (e.end_offset < this.endOffset)) {
  170. return false;
  171. }
  172. }
  173. return true;
  174. },
  175. resetRange() {
  176. this.startOffset = 0;
  177. this.endOffset = 0;
  178. },
  179. textPart(r) {
  180. return [...this.text].slice(r.start_offset, r.end_offset).join('');
  181. },
  182. addLabel(labelId) {
  183. if (this.validRange()) {
  184. const label = {
  185. start_offset: this.startOffset,
  186. end_offset: this.endOffset,
  187. label: labelId,
  188. };
  189. this.$emit('add-label', label);
  190. }
  191. },
  192. removeLabel(index) {
  193. this.$emit('remove-label', index);
  194. },
  195. makeLabel(startOffset, endOffset) {
  196. const label = {
  197. id: 0,
  198. label: -1,
  199. start_offset: startOffset,
  200. end_offset: endOffset,
  201. };
  202. return label;
  203. },
  204. },
  205. };
  206. </script>