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

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

Shift time string formatting to Util.
Start AudioTimeBar implementation.

File size: 9.0 KB
Line 
1<template>
2<div>
3 <div class="editor-controls">
4 <audio-time-bar v-model="currentPlaybackTime" :audio-length="2" />
5
6 <toggle-button v-model="enableEditing">
7 <span class="material-icons .mdi-m">edit</span>
8 </toggle-button>
9 </div>
10
11 <div class="text-container words-container" :hidden="enableEditing">
12 <ul class="list-view" v-if="!enableEditing">
13 <li v-for="word in words" :key="word.id" class="word-container" @click="playAudio(word.startTime)">
14 <span class="word-highlight word" :class="{ 'word-highlight-applied': word.shouldHighlight }">
15 {{ word.word }}
16 </span>
17 </li>
18 </ul>
19
20 <ul class="list-view" v-if="enableEditing">
21 <li v-for="(word, index) in words" :key="word.id" class="word-container">
22 <input :size="word.word.length <= 1 ? 1 : word.word.length - 1" :ref="word.id"
23 class="editor-word" v-model="word.word" type="text"
24 @input="onEditorInput($event, index)" @focusout="onEditorFocusOut(index)" @keyup="onEditorKeyUp($event, index)" />
25 <span>&nbsp;</span>
26 </li>
27 </ul>
28 </div>
29</div>
30</template>
31
32<style scoped lang="scss">
33.word {
34 padding: 0.25em;
35}
36
37.editor-controls {
38 display: grid;
39 align-items: center;
40 grid-template-columns: 1fr auto;
41 margin: 0.5em 0 0.3em 0;
42 gap: 1em;
43}
44
45.words-container {
46 transition-duration: var(--transition-duration);
47
48 &[hidden] {
49 background-color: inherit;
50 }
51}
52
53.word-container {
54 font: var(--monospace-font);
55 display: inline-block;
56 line-height: 2em;
57}
58
59.word-highlight-applied {
60 background-color: rgba(255, 255, 0, 0.4);
61}
62
63.word-highlight {
64 &:hover {
65 @extend .word-highlight-applied;
66 }
67}
68
69.editor-word {
70 border: 1px solid black;
71 border-radius: 2px;
72 padding: 2px;
73 font-family: inherit;
74 font-size: 1rem;
75}
76</style>
77
78<script>
79import { mapState } from "vuex";
80import { TranscriptionViewModel, PlaybackState } from "../main";
81import AudioTimeBar from "./AudioTimeBar.vue"
82import Util from "../js/Util";
83
84export class Word {
85 /**
86 * Initialises a new instance of the Word class.
87 * @param {String} word The word.
88 * @param {Number} startTime The time within the audio that this word starts being spoken.
89 * @param {Number} endTime The time within the audio that this word finishes being spoken.
90 */
91 constructor(word, startTime, endTime) {
92 /** @type {String} The ID of this word. */
93 this.id = Util.generateUuid();
94
95 /** @type {String} The word. */
96 this.word = word;
97
98 /** @type {Number} The time within the audio that this word starts being spoken. */
99 this.startTime = startTime;
100
101 /** @type {Number} The time within the audio that this word finishes being spoken. */
102 this.endTime = endTime;
103
104 /** @type {Boolean} A value indicating whether this word should be highlighted. */
105 this.shouldHighlight = false;
106
107 /** @type {Number} The number of times the user has tried to delete this word. */
108 this.deletionAttempts = 0;
109 }
110}
111
112export default {
113 name: "TranscriptionItemEditor",
114 components: {
115 AudioTimeBar
116 },
117 props: {
118 transcription: TranscriptionViewModel
119 },
120 data() {
121 return {
122 enableEditing: false,
123 words: [],
124 lastHighlightedWord: 0
125 }
126 },
127 computed: mapState({
128 playbackState: state => state.playbackState,
129 currentPlaybackTime: state => state.playbackState.currentTime,
130 translations: state => state.translations
131 }),
132 methods: {
133 playAudio(startTime) {
134 const pState = new PlaybackState(this.transcription.id, true, startTime);
135 this.$store.commit("setPlaybackState", pState);
136 },
137 /**
138 * Invoked when the value of a word changes.
139 * @param {Number} index The index of the word that has changed.
140 */
141 onEditorInput(event, index) {
142 /** @type {Word} */
143 const word = this.words[index];
144
145 if (event.inputType === "insertText") {
146 // Insert a new word if whitespace is entered on an existing word
147 if (event.data === " " && word.word !== " ") {
148 // TODO: Proper timing metadata
149 const newWord = new Word("", word.startTime, word.endTime);
150 this.words.splice(index + 1, 0, newWord);
151
152 // We have to give the element some time to render, even though the ref is registered immediately
153 setTimeout(() => this.$refs[newWord.id].focus(), 50);
154 }
155
156 word.word = word.word.replace(" ", "");
157 }
158 },
159 onEditorFocusOut(index) {
160 // Remove empty words when they lose focus
161 if (this.words[index].word === "") {
162 this.words.splice(index, 1);
163 }
164 },
165 onEditorKeyUp(event, index) {
166 /** @type {Word} */
167 const word = this.words[index];
168
169 // Remove the word if the user is repetitively hitting backspace/delete on an empty cell
170 if (event.code === "Backspace" || event.code === "Delete") {
171 if (word.word.length < 1) {
172 if (word.deletionAttempts === 1) {
173 this.words.splice(index, 1);
174
175 setFocus(event.code === "Backspace", this);
176 }
177
178 console.log(word.deletionAttempts);
179 word.deletionAttempts++;
180 }
181 }
182 else {
183 word.deletionAttempts = 0;
184 }
185
186 // Focus on the next word if the user is moving right with their caret at the word's end
187 // if (event.code === "ArrowRight") {
188 // const inputElement = this.$refs[word.id];
189
190 // if (inputElement.selectionStart === word.word.length) {
191 // setFocus(false, this);
192 // }
193 // }
194
195 // Focus on the previous word if the user is moving left with their caret at the word's start
196 // if (event.code === "ArrowLeft") {
197 // const inputElement = this.$refs[word.id];
198
199 // if (inputElement.selectionStart === 0) {
200 // setFocus(true, this);
201 // }
202 // }
203
204 /**
205 * Gets the index of an adjacent word to focus on.
206 * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
207 * @param vm The view model.
208 */
209 function getFocusIndex(focusLeft, vm) {
210 let focusIndex = 0;
211
212 if (focusLeft) {
213 focusIndex = index === 0 ? 0 : index - 1;
214 }
215 else {
216 focusIndex = index === vm.words.length - 1 ? vm.words.length - 1 : index + 1;
217 }
218
219 return focusIndex;
220 }
221
222 /**
223 * Sets the focus to an adjacent word.
224 * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
225 * @param vm The view model.
226 */
227 function setFocus(focusLeft, vm) {
228 const focusIndex = getFocusIndex(focusLeft, vm);
229 const focusId = vm.words[focusIndex].id;
230 vm.$refs[focusId].focus();
231
232 if (!focusLeft) {
233 vm.$refs[focusId].setSelectionRange(-1, 0);
234 }
235 }
236 }
237 },
238 watch: {
239 currentPlaybackTime(newValue) {
240 this.words[this.lastHighlightedWord].shouldHighlight = false;
241
242 if (!this.playbackState.isPlaying || this.playbackState.id !== this.transcription.id) {
243 return;
244 }
245
246 for (const word of this.words) {
247 word.shouldHighlight = word.startTime < newValue && word.endTime > newValue;
248 }
249 }
250 },
251 beforeMount() {
252 this.words = getWords(this.transcription);
253 }
254}
255
256/**
257 * Gets the words in a transcription.
258 * @param {TranscriptionViewModel} transcription The transcription.
259 * @returns {Word[]}
260 */
261export function getWords(transcription) {
262 /** @type {Word[]} */
263 const words = [];
264
265 let lastWord = "";
266 let currStartTime = 0;
267
268 for (const metadata of transcription.metadata) {
269 if (metadata.char === " ") {
270 words.push(new Word(lastWord, currStartTime, metadata.start_time));
271
272 lastWord = "";
273 currStartTime = metadata.start_time;
274 }
275 else {
276 lastWord += metadata.char;
277 }
278 }
279
280 // Push the last word, as most transcriptions will not end in a space (hence breaking the above algorithm)
281 if (lastWord.length > 0) {
282 const newWord = new Word(lastWord, currStartTime, transcription.metadata[transcription.metadata.length - 1].start_time)
283 words.push(newWord);
284 }
285
286 return words;
287}
288</script>
Note: See TracBrowser for help on using the repository browser.