source: main/trunk/model-interfaces-dev/atea/korero-maori-asr/src/components/TranscriptionItemEditor.vue@ 35517

Last change on this file since 35517 was 35517, checked in by cstephen, 3 years ago

Temporarily remove WordTimingSelector for production

File size: 12.4 KB
Line 
1<template>
2<div>
3 <div class="text-container words-container" :hidden="enableEditing">
4 <ul class="list-view" v-if="!enableEditing">
5 <li v-for="word in words" :key="word.id" class="word-container" @click="playAudio(word.startTime)">
6 <span class="word-highlight word" :class="{ 'word-highlight-applied': word.shouldHighlight }">
7 {{ word.word }}
8 </span>
9 </li>
10 </ul>
11
12 <ul class="list-view" v-else>
13 <li v-for="(word, index) in words" :key="word.id" class="word-container">
14 <input :size="word.word.length === 0 ? 1 : word.word.length" :ref="word.id"
15 class="editor-word" v-model="word.word" type="text" onpaste="return false;" :class="{ 'word-highlight-applied': word.shouldHighlight }"
16 @beforeinput="onEditorBeforeInput($event, index)" @focusout="onEditorFocusOut(index)" @keyup="onEditorKeyUp($event, index)" @focus="onEditorFocus(index)" />
17 <span>&nbsp;</span>
18 </li>
19 </ul>
20 </div>
21
22 <!-- <word-timing-selector class="word-timing-selector" :surroundingWords="surroundingWords" :word="selectedWord" /> -->
23
24 <div class="editor-controls">
25 <audio-time-bar v-model.number="currentPlaybackTime" :audio-length="audioLength" :isDisabled="playbackState.id != transcription.id" />
26
27 <toggle-button v-model="enableEditing" :title="translations.get('TranscriptionItemEditor_ToggleEditTooltip')">
28 <span class="material-icons">edit</span>
29 </toggle-button>
30 </div>
31</div>
32</template>
33
34<style scoped lang="scss">
35.word {
36 padding: 0.25em;
37}
38
39.editor-controls {
40 display: grid;
41 align-items: center;
42 grid-template-columns: 1fr auto;
43 margin: 0.5em 0 0.3em 0;
44 gap: 1em;
45}
46
47.words-container {
48 transition-duration: var(--transition-duration);
49
50 &[hidden] {
51 background-color: inherit;
52 }
53}
54
55.word-container {
56 font: var(--monospace-font);
57 display: inline-block;
58 line-height: 2em;
59}
60
61.word-highlight-applied {
62 background-color: var(--highlighted-word-bg);
63}
64
65.word-highlight {
66 &:hover {
67 @extend .word-highlight-applied;
68 }
69}
70
71.editor-word {
72 border: 1px solid black;
73 border-radius: 2px;
74 padding: 2px;
75 font-family: inherit;
76 font-size: 1rem;
77}
78
79.word-timing-selector {
80 margin: 0.5em auto;
81}
82</style>
83
84<script>
85import { mapState } from "vuex";
86import { TranscriptionViewModel } from "../main";
87import AudioPlayback from "../js/AudioPlaybackModule"
88import AudioTimeBar from "./AudioTimeBar.vue"
89// import WordTimingSelector from "./WordTimingSelector.vue"
90import Util, { log } from "../js/Util";
91
92export class Word {
93 /**
94 * Initialises a new instance of the Word class.
95 * @param {String} word The word.
96 * @param {Number} startTime The time within the audio that this word starts being spoken.
97 * @param {Number} endTime The time within the audio that this word finishes being spoken.
98 */
99 constructor(word, startTime, endTime) {
100 /** @type {String} The ID of this word. */
101 this.id = Util.generateUuid();
102
103 /** @type {String} The word. */
104 this.word = word;
105
106 /** @type {Number} The time within the audio that this word starts being spoken. */
107 this.startTime = startTime;
108
109 /** @type {Number} The time within the audio that this word finishes being spoken. */
110 this.endTime = endTime;
111
112 /** @type {Boolean} A value indicating whether this word should be highlighted. */
113 this.shouldHighlight = false;
114
115 /** @type {Number} The number of times the user has tried to delete this word. */
116 this.deletionAttempts = 0;
117 }
118}
119
120export default {
121 name: "TranscriptionItemEditor",
122 components: {
123 AudioTimeBar//,
124 // WordTimingSelector
125 },
126 props: {
127 transcription: TranscriptionViewModel
128 },
129 data() {
130 return {
131 enableEditing: false,
132 words: [],
133 lastHighlightedWordIndex: 0,
134 selectedIndex: 0
135 }
136 },
137 computed: {
138 currentPlaybackTime: {
139 get() {
140 return this.$store.getters.transcriptionPlaybackTime(this.transcription.id);
141 },
142 set(value) {
143 this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: value });
144 }
145 },
146 audioLength() {
147 return this.$store.getters.transcriptionPlaybackLength(this.transcription.id);
148 },
149 surroundingWords() {
150 return this.getSurroundingWords(this.selectedIndex);
151 },
152 selectedWord() {
153 return this.words[this.selectedIndex];
154 },
155 ...mapState({
156 playbackState: state => state.playbackState,
157 translations: state => state.translations
158 })
159 },
160 methods: {
161 async playAudio(startTime) {
162 await AudioPlayback.play(this.transcription.id, startTime);
163 },
164 getSurroundingWords(index) {
165 const BUFFER = 2; // The number of words to take on each side
166
167 let min = index;
168 for (let i = index; i > index - BUFFER && i > 0; i--) {
169 min--;
170 }
171
172 let max = index + 1;
173 for (let i = index; i < index + BUFFER && i < this.words.length; i++) {
174 max++;
175 }
176
177 return this.words.slice(min, max);
178 },
179 /**
180 * Invoked when the value of a word changes.
181 * @param {InputEvent} event The input event.
182 * @param {Number} index The index of the word that has changed.
183 */
184 onEditorBeforeInput(event, index) {
185 log(event);
186
187 const deletionEvents = [
188 "deleteWordForward", "deleteWordBackward",
189 "deleteSoftLineBackward", "deleteSoftLineForward", "deleteEntireSoftLine",
190 "deleteHardLineBackward", "deleteHardLineForward",
191 "deleteByDrag", "deleteByCut",
192 "deleteContent", "deleteContentBackward", "deleteContentForward"
193 ];
194
195 const insertionEvents = [
196 "insertText", "insertReplacementText",
197 "insertLineBreak", "insertParagraph",
198 "insertOrderedList", "insertUnorderedList", "insertHorizontalRule",
199 "insertFromYank", "insertFromDrop", "insertFromPaste", "insertFromPasteAsQuotation",
200 "insertTranspose", "insertCompositionText", "insertLink"
201 ];
202
203 if (event.inputType === "historyUndo" || event.inputType === "historyRedo") {
204 event.preventDefault();
205 return;
206 }
207
208 /** @type {Word} */
209 const word = this.words[index];
210
211 if (deletionEvents.includes(event.inputType) && word.word.length === 0) {
212 event.preventDefault(); // Prevents the event from being propagated to the newly focused input
213 this.setFocus(index, event.inputType === "deleteContentBackward"); // Shifting focus off an empty input, hence the word will be removed
214 }
215
216 if (insertionEvents.includes(event.inputType)) {
217 if (event.data === " ") {
218 // Only insert if the current word isn't blank, to prevent 'jumping jacks' as the duplicate empty boxes are removed
219 if (word.word !== "") {
220 const newStartTime = word.startTime + ((word.endTime - word.startTime) / 2);
221 const newWord = new Word("", newStartTime, word.endTime);
222 this.words.splice(index + 1, 0, newWord);
223
224 // We have to give the element some time to be mounted before we can focus it
225 setTimeout(() => this.$refs[newWord.id].focus(), 50);
226 }
227
228 event.preventDefault();
229 }
230 }
231 },
232 onEditorFocus(index) {
233 if (this.currentPlaybackTime < this.words[index].startTime || this.currentPlaybackTime > this.words[index].endTime) {
234 const wordStartTime = this.words[index].startTime;
235 this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: wordStartTime });
236 }
237
238 this.selectedIndex = index;
239 },
240 onEditorFocusOut(index) {
241 // Remove empty words when they lose focus
242 if (this.words[index].word === "") {
243 this.words.splice(index, 1);
244 }
245 },
246 onEditorKeyUp(event, index) {
247 /** @type {Word} */
248 const word = this.words[index];
249
250 if (event.code === "Spacebar" && word.length > 0) {
251 // - if at either edge, create new word before/after
252 // - else split word
253 }
254
255 // // Focus on the next word if the user is moving right with their caret at the word's end
256 // if (event.code === "ArrowRight") {
257 // const inputElement = this.$refs[word.id];
258
259 // if (inputElement.selectionStart === word.word.length) {
260 // this.setFocus(index, false);
261 // }
262 // }
263
264 // // Focus on the previous word if the user is moving left with their caret at the word's start
265 // if (event.code === "ArrowLeft") {
266 // const inputElement = this.$refs[word.id];
267
268 // if (inputElement.selectionStart === 0) {
269 // this.setFocus(index, true);
270 // }
271 // }
272 },
273
274 /**
275 * Gets the index of an adjacent word to focus on.
276 * @param {Number} index The index of the currently focused word.
277 * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
278 */
279 getFocusIndex(index, focusLeft) {
280 let focusIndex = 0;
281
282 if (focusLeft) {
283 focusIndex = index === 0 ? 0 : index - 1;
284 }
285 else {
286 focusIndex = index === this.words.length - 1 ? this.words.length - 1 : index + 1;
287 }
288
289 return focusIndex;
290 },
291
292 /**
293 * Sets the focus to an adjacent word.
294 * @param {Number} index The index of the currently focused word.
295 * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
296 */
297 setFocus(index, focusLeft) {
298 const focusIndex = this.getFocusIndex(index, focusLeft);
299 const focusId = this.words[focusIndex].id;
300 this.$refs[focusId].focus();
301
302 if (!focusLeft) {
303 this.$refs[focusId].setSelectionRange(-1, 0);
304 }
305 }
306 },
307 watch: {
308 currentPlaybackTime(newValue) {
309 this.words[this.lastHighlightedWordIndex].shouldHighlight = false;
310
311 if (this.playbackState.id !== this.transcription.id) {
312 return;
313 }
314
315 for (let i = 0; i < this.words.length; i++) {
316 const word = this.words[i];
317
318 if (word.startTime <= newValue && word.endTime > newValue) {
319 word.shouldHighlight = true;
320
321 if (this.$refs[word.id]) {
322 this.$refs[word.id].focus();
323 }
324
325 this.lastHighlightedWordIndex = i;
326 break;
327 }
328 }
329 }
330 },
331 beforeMount() {
332 this.words = getWords(this.transcription);
333 }
334}
335
336/**
337 * Gets the words in a transcription.
338 * @param {TranscriptionViewModel} transcription The transcription.
339 * @returns {Word[]}
340 */
341export function getWords(transcription) {
342 /** @type {Word[]} */
343 const words = [];
344
345 let lastWord = "";
346 let currStartTime = 0;
347
348 for (const metadata of transcription.metadata) {
349 if (metadata.char === " ") {
350 words.push(new Word(lastWord, currStartTime, metadata.start_time));
351
352 lastWord = "";
353 currStartTime = metadata.start_time;
354 }
355 else {
356 lastWord += metadata.char;
357 }
358 }
359
360 // Push the last word, as most transcriptions will not end in a space (hence breaking the above algorithm)
361 if (lastWord.length > 0) {
362 const newWord = new Word(lastWord, currStartTime, transcription.metadata[transcription.metadata.length - 1].start_time)
363 words.push(newWord);
364 }
365
366 return words;
367}
368</script>
Note: See TracBrowser for help on using the repository browser.