@vue-skuilder/edit-ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index.css +1 -0
- package/dist/edit-ui.es.js +71090 -0
- package/dist/edit-ui.es.js.map +1 -0
- package/dist/edit-ui.umd.js +83 -0
- package/dist/edit-ui.umd.js.map +1 -0
- package/package.json +67 -0
- package/src/components/BulkImport/CardPreviewList.vue +345 -0
- package/src/components/BulkImportView.vue +633 -0
- package/src/components/CourseEditor.vue +164 -0
- package/src/components/ViewableDataInputForm/DataInputForm.vue +533 -0
- package/src/components/ViewableDataInputForm/FieldInput.types.ts +33 -0
- package/src/components/ViewableDataInputForm/FieldInputs/AudioInput.vue +188 -0
- package/src/components/ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue +79 -0
- package/src/components/ViewableDataInputForm/FieldInputs/FieldInput.css +12 -0
- package/src/components/ViewableDataInputForm/FieldInputs/ImageInput.vue +231 -0
- package/src/components/ViewableDataInputForm/FieldInputs/IntegerInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MarkdownInput.vue +34 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue +246 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MidiInput.vue +113 -0
- package/src/components/ViewableDataInputForm/FieldInputs/NumberInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/OptionsFieldInput.ts +161 -0
- package/src/components/ViewableDataInputForm/FieldInputs/StringInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/typeValidators.ts +49 -0
- package/src/components/index.ts +21 -0
- package/src/index.ts +6 -0
- package/src/stores/useDataInputFormStore.ts +49 -0
- package/src/stores/useFieldInputStore.ts +191 -0
- package/src/vue-shims.d.ts +5 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mr-2 mb-2">
|
|
3
|
+
<v-label class="text-h5">Add media:</v-label>
|
|
4
|
+
<div
|
|
5
|
+
class="drop-zone"
|
|
6
|
+
:class="{ 'drop-zone--over': isDragging }"
|
|
7
|
+
@drop="dropHandler"
|
|
8
|
+
@dragover.prevent="dragOverHandler"
|
|
9
|
+
@dragenter.prevent="dragEnterHandler"
|
|
10
|
+
@dragleave.prevent="dragLeaveHandler"
|
|
11
|
+
>
|
|
12
|
+
<input
|
|
13
|
+
ref="fileInput"
|
|
14
|
+
type="file"
|
|
15
|
+
accept="image/*,audio/*"
|
|
16
|
+
multiple
|
|
17
|
+
style="display: none"
|
|
18
|
+
@change="handleFileInput"
|
|
19
|
+
/>
|
|
20
|
+
<!-- <template> -->
|
|
21
|
+
<div v-for="(item, index) in mediaItems" :key="index" class="media-item">
|
|
22
|
+
<template v-if="item.type === 'image'">
|
|
23
|
+
<img :src="item.thumbnailUrl" alt="Uploaded image thumbnail" class="thumbnail" />
|
|
24
|
+
</template>
|
|
25
|
+
<template v-else-if="item.type === 'audio'">
|
|
26
|
+
<audio controls :src="item.url"></audio>
|
|
27
|
+
</template>
|
|
28
|
+
<v-btn size="small" @click="removeMedia(index)">Remove</v-btn>
|
|
29
|
+
</div>
|
|
30
|
+
<!-- <template> -->
|
|
31
|
+
Drop image or audio files here...
|
|
32
|
+
<v-btn @click="triggerFileInput">Or Click to Upload</v-btn>
|
|
33
|
+
<!-- </template> -->
|
|
34
|
+
<!-- <v-btn @click="addMoreMedia">Add More Media</v-btn> -->
|
|
35
|
+
<!-- </template> -->
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script lang="ts">
|
|
41
|
+
import { defineComponent } from 'vue';
|
|
42
|
+
import FieldInput from './OptionsFieldInput';
|
|
43
|
+
import { Status } from '@vue-skuilder/common';
|
|
44
|
+
import { FieldInputSetupReturn } from './OptionsFieldInput';
|
|
45
|
+
|
|
46
|
+
export interface MediaItem {
|
|
47
|
+
type: 'image' | 'audio';
|
|
48
|
+
file: File;
|
|
49
|
+
url: string;
|
|
50
|
+
thumbnailUrl?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default defineComponent({
|
|
54
|
+
name: 'MediaDragDropUploader',
|
|
55
|
+
extends: FieldInput,
|
|
56
|
+
|
|
57
|
+
setup(props, ctx) {
|
|
58
|
+
// Get the parent setup result
|
|
59
|
+
const parentSetup = FieldInput.setup?.(props, ctx) as FieldInputSetupReturn;
|
|
60
|
+
|
|
61
|
+
// Now you can access fieldStore and other parent setup properties
|
|
62
|
+
const { fieldStore } = parentSetup;
|
|
63
|
+
|
|
64
|
+
// Return both parent and child setup properties
|
|
65
|
+
return {
|
|
66
|
+
...parentSetup,
|
|
67
|
+
fieldStore,
|
|
68
|
+
// Add any additional setup properties specific to this component
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
data() {
|
|
73
|
+
return {
|
|
74
|
+
isDragging: false,
|
|
75
|
+
mediaItems: [] as MediaItem[],
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
computed: {
|
|
80
|
+
hasMedia(): boolean {
|
|
81
|
+
return this.mediaItems.length > 0;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
created() {
|
|
86
|
+
// this.validate();
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
methods: {
|
|
90
|
+
dragOverHandler(event: DragEvent) {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
dragEnterHandler(event: DragEvent) {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
this.isDragging = true;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
dragLeaveHandler(event: DragEvent) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
this.isDragging = false;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
dropHandler(event: DragEvent) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
this.isDragging = false;
|
|
107
|
+
const files = event.dataTransfer?.files;
|
|
108
|
+
if (files) {
|
|
109
|
+
this.processFiles(files);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
triggerFileInput() {
|
|
114
|
+
(this.$refs.fileInput as HTMLInputElement).click();
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
handleFileInput(event: Event) {
|
|
118
|
+
const files = (event.target as HTMLInputElement).files;
|
|
119
|
+
if (files) {
|
|
120
|
+
this.processFiles(files);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
processFiles(files: FileList) {
|
|
125
|
+
Array.from(files).forEach((file) => {
|
|
126
|
+
this.addMediaItem(file);
|
|
127
|
+
});
|
|
128
|
+
this.updateStore();
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
addMediaItem(file: File) {
|
|
132
|
+
const type = file.type.startsWith('image/') ? 'image' : 'audio';
|
|
133
|
+
const item: MediaItem = {
|
|
134
|
+
type,
|
|
135
|
+
file,
|
|
136
|
+
url: URL.createObjectURL(file),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (type === 'image') {
|
|
140
|
+
this.createThumbnail(file).then((thumbnailUrl) => {
|
|
141
|
+
item.thumbnailUrl = thumbnailUrl;
|
|
142
|
+
this.$nextTick(() => {
|
|
143
|
+
this.$forceUpdate();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.mediaItems.push(item);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async createThumbnail(file: File): Promise<string> {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
const reader = new FileReader();
|
|
154
|
+
reader.onload = (e: ProgressEvent<FileReader>) => {
|
|
155
|
+
resolve(e.target?.result as string);
|
|
156
|
+
};
|
|
157
|
+
reader.readAsDataURL(file);
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
removeMedia(index: number) {
|
|
162
|
+
URL.revokeObjectURL(this.mediaItems[index].url);
|
|
163
|
+
this.mediaItems.splice(index, 1);
|
|
164
|
+
this.updateStore();
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
clearData() {
|
|
168
|
+
this.mediaItems.forEach((item) => {
|
|
169
|
+
URL.revokeObjectURL(item.url);
|
|
170
|
+
});
|
|
171
|
+
this.mediaItems = [];
|
|
172
|
+
this.updateStore();
|
|
173
|
+
// this.validate();
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
addMoreMedia() {
|
|
177
|
+
console.log('addMoreMedia');
|
|
178
|
+
this.triggerFileInput();
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
updateStore() {
|
|
182
|
+
// for (let i = 1; i <= 10; i++) {
|
|
183
|
+
// delete this.dataInputForm.dataInputForm[`image-${i}`];
|
|
184
|
+
// delete this.dataInputForm.dataInputForm[`audio-${i}`];
|
|
185
|
+
// }
|
|
186
|
+
|
|
187
|
+
let imageCount = 0;
|
|
188
|
+
let audioCount = 0;
|
|
189
|
+
this.mediaItems.forEach((item) => {
|
|
190
|
+
if (item.type === 'image') {
|
|
191
|
+
imageCount++;
|
|
192
|
+
this.fieldStore.setMedia(`image-${imageCount}`, {
|
|
193
|
+
content_type: item.file.type,
|
|
194
|
+
data: item.file,
|
|
195
|
+
});
|
|
196
|
+
} else if (item.type === 'audio') {
|
|
197
|
+
audioCount++;
|
|
198
|
+
this.fieldStore.setMedia(`audio-${audioCount}`, {
|
|
199
|
+
content_type: item.file.type,
|
|
200
|
+
data: item.file,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// this.validate();
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
getValidators() {
|
|
208
|
+
return [
|
|
209
|
+
() => ({
|
|
210
|
+
status: this.mediaItems.length > 0 ? Status.ok : Status.error,
|
|
211
|
+
msg: this.mediaItems.length > 0 ? '' : 'At least one media item is required',
|
|
212
|
+
}),
|
|
213
|
+
];
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<style scoped>
|
|
220
|
+
@import './FieldInput.css';
|
|
221
|
+
|
|
222
|
+
.drop-zone {
|
|
223
|
+
border: 2px dashed #ccc;
|
|
224
|
+
border-radius: 4px;
|
|
225
|
+
padding: 20px;
|
|
226
|
+
text-align: center;
|
|
227
|
+
transition: all 0.3s ease;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.drop-zone--over {
|
|
231
|
+
border-color: #000;
|
|
232
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.thumbnail {
|
|
236
|
+
max-width: 100px;
|
|
237
|
+
max-height: 100px;
|
|
238
|
+
margin-right: 10px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.media-item {
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
margin-bottom: 10px;
|
|
245
|
+
}
|
|
246
|
+
</style>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="recording">
|
|
4
|
+
<span class="text-h5">
|
|
5
|
+
Now Recording from device:
|
|
6
|
+
<span class="font-weight-black">{{ midi.configuredInput }}</span>
|
|
7
|
+
</span>
|
|
8
|
+
</div>
|
|
9
|
+
<syllable-seq-vis v-if="true" ref="inputVis" :seq="SylSeq" last-t-ssuggestion="5000" />
|
|
10
|
+
<v-btn color="primary" :disabled="hasRecording()" @click="play">
|
|
11
|
+
Preview
|
|
12
|
+
<v-icon end>volume_up</v-icon>
|
|
13
|
+
</v-btn>
|
|
14
|
+
<v-btn color="error" :disabled="hasRecording()" @click="reset">
|
|
15
|
+
Clear and try again
|
|
16
|
+
<v-icon end>close</v-icon>
|
|
17
|
+
</v-btn>
|
|
18
|
+
<v-checkbox v-model="transpositions" label="Include Transpositions" @click.capture="reset"></v-checkbox>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
import { defineComponent } from 'vue';
|
|
24
|
+
import FieldInput from './OptionsFieldInput';
|
|
25
|
+
import {
|
|
26
|
+
SkMidi,
|
|
27
|
+
eventsToSyllableSequence,
|
|
28
|
+
SyllableSequence,
|
|
29
|
+
transposeSyllableSeq,
|
|
30
|
+
SyllableSeqVis,
|
|
31
|
+
} from '@vue-skuilder/courses';
|
|
32
|
+
|
|
33
|
+
export default defineComponent({
|
|
34
|
+
name: 'MidiInput',
|
|
35
|
+
components: {
|
|
36
|
+
SyllableSeqVis,
|
|
37
|
+
},
|
|
38
|
+
extends: FieldInput,
|
|
39
|
+
|
|
40
|
+
data() {
|
|
41
|
+
return {
|
|
42
|
+
midi: null as SkMidi | null,
|
|
43
|
+
recording: false,
|
|
44
|
+
SylSeq: eventsToSyllableSequence([]) as SyllableSequence,
|
|
45
|
+
display: false,
|
|
46
|
+
transpositions: false,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async created() {
|
|
51
|
+
try {
|
|
52
|
+
this.midi = await SkMidi.instance();
|
|
53
|
+
this.record();
|
|
54
|
+
// [ ] this wasn't updated when the dataInputFormStore / fieldInputStore components were created ?
|
|
55
|
+
this.store[this.field.name] = this.getTransposedSeqs;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new Error(`MidiInput.created: ${e}`);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
methods: {
|
|
62
|
+
record() {
|
|
63
|
+
if (!this.midi) return;
|
|
64
|
+
this.midi.record();
|
|
65
|
+
this.midi.addNoteonListenter((e) => {
|
|
66
|
+
this.SylSeq.append(e);
|
|
67
|
+
(this.$refs.inputVis as InstanceType<typeof SyllableSeqVis>).updateBounds();
|
|
68
|
+
});
|
|
69
|
+
this.recording = true;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
getTransposedSeqs() {
|
|
73
|
+
if (!this.midi) return [];
|
|
74
|
+
if (this.transpositions) {
|
|
75
|
+
return [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6].map((shift) => {
|
|
76
|
+
return transposeSyllableSeq(this.midi!.recording, shift);
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
return [this.midi.recording];
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
clearData() {
|
|
84
|
+
if (!this.midi) return;
|
|
85
|
+
console.log('midiInput clearing data...');
|
|
86
|
+
this.midi.stopRecording();
|
|
87
|
+
this.midi.eraseRecording();
|
|
88
|
+
this.SylSeq = eventsToSyllableSequence([]);
|
|
89
|
+
this.record();
|
|
90
|
+
this.recording = true;
|
|
91
|
+
|
|
92
|
+
this.store.convertedInput[this.field.name] = this.midi.recording;
|
|
93
|
+
this.store.validation[this.field.name] = false;
|
|
94
|
+
this.store[this.field.name] = this.getTransposedSeqs;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
hasRecording(): boolean {
|
|
98
|
+
return this.midi?.hasRecording ?? false;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
reset() {
|
|
102
|
+
this.clearData();
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
play() {
|
|
106
|
+
if (!this.midi) return;
|
|
107
|
+
this.midi.play();
|
|
108
|
+
this.display = true;
|
|
109
|
+
this.validate();
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-text-field
|
|
3
|
+
ref="inputField"
|
|
4
|
+
v-model="modelValue"
|
|
5
|
+
variant="filled"
|
|
6
|
+
type="number"
|
|
7
|
+
:name="field.name"
|
|
8
|
+
:label="field.name"
|
|
9
|
+
:rules="vuetifyRules()"
|
|
10
|
+
:hint="validationStatus.msg"
|
|
11
|
+
:autofocus="autofocus"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { defineComponent, computed } from 'vue';
|
|
17
|
+
import { numberValidator } from './typeValidators';
|
|
18
|
+
import FieldInput from './OptionsFieldInput';
|
|
19
|
+
import { ValidatingFunction } from '@vue-skuilder/common';
|
|
20
|
+
|
|
21
|
+
export default defineComponent({
|
|
22
|
+
name: 'NumberInput',
|
|
23
|
+
extends: FieldInput,
|
|
24
|
+
|
|
25
|
+
setup(props, ctx) {
|
|
26
|
+
// Get all the setup logic from parent
|
|
27
|
+
const parentSetup = FieldInput.setup?.(props, ctx);
|
|
28
|
+
|
|
29
|
+
const validators = computed<ValidatingFunction[]>(() => {
|
|
30
|
+
const baseValidators = FieldInput.validators.call(this);
|
|
31
|
+
|
|
32
|
+
if (props.field.validator?.test) {
|
|
33
|
+
baseValidators.push(props.field.validator.test);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (baseValidators) {
|
|
37
|
+
return [numberValidator, ...baseValidators];
|
|
38
|
+
} else {
|
|
39
|
+
return [numberValidator];
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...parentSetup,
|
|
45
|
+
validators,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
ComputedRef,
|
|
4
|
+
defineComponent,
|
|
5
|
+
PropType,
|
|
6
|
+
Ref,
|
|
7
|
+
ref,
|
|
8
|
+
watch,
|
|
9
|
+
WritableComputedRef,
|
|
10
|
+
} from 'vue';
|
|
11
|
+
import {
|
|
12
|
+
ValidatingFunction,
|
|
13
|
+
FieldDefinition,
|
|
14
|
+
ValidationResult,
|
|
15
|
+
validationFunctionToVuetifyRule,
|
|
16
|
+
VuetifyRule,
|
|
17
|
+
Status,
|
|
18
|
+
CourseElo,
|
|
19
|
+
} from '@vue-skuilder/common';
|
|
20
|
+
import { useFieldInputStore } from '../../../stores/useFieldInputStore';
|
|
21
|
+
|
|
22
|
+
export interface FieldInputSetupReturn {
|
|
23
|
+
inputField: Ref<HTMLInputElement | null>;
|
|
24
|
+
fieldStore: ReturnType<typeof useFieldInputStore>;
|
|
25
|
+
modelValue: WritableComputedRef<unknown>;
|
|
26
|
+
validators: ComputedRef<ValidatingFunction[]>;
|
|
27
|
+
focus: () => void;
|
|
28
|
+
userInput: () => unknown;
|
|
29
|
+
clearData: () => void;
|
|
30
|
+
setData: (data: unknown) => void;
|
|
31
|
+
vuetifyRules: () => VuetifyRule[];
|
|
32
|
+
generateTags: () => string[];
|
|
33
|
+
generateELO: () => CourseElo | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default defineComponent({
|
|
37
|
+
name: 'FieldInput',
|
|
38
|
+
|
|
39
|
+
props: {
|
|
40
|
+
autofocus: Boolean,
|
|
41
|
+
field: {
|
|
42
|
+
type: Object as PropType<FieldDefinition>,
|
|
43
|
+
required: true,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
setup(props): FieldInputSetupReturn {
|
|
47
|
+
const fieldStore = useFieldInputStore();
|
|
48
|
+
const inputField = ref<HTMLInputElement | null>(null);
|
|
49
|
+
// [ ] TODO: Implement hint - need richer validation result
|
|
50
|
+
// on the fieldStore to do this.
|
|
51
|
+
//
|
|
52
|
+
// Exose / Retrieve it as a computed property
|
|
53
|
+
//
|
|
54
|
+
// const hint = ref('');
|
|
55
|
+
|
|
56
|
+
// Computed property for v-model binding in child components
|
|
57
|
+
const modelValue = computed({
|
|
58
|
+
get: () => fieldStore.inputs[props.field.name],
|
|
59
|
+
set: (value) => fieldStore.setFieldValue(props.field.name, value),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
watch(modelValue, (newValue: unknown, oldValue: unknown) => {
|
|
63
|
+
console.log('[FieldInput] modelValue changed:', {
|
|
64
|
+
new: newValue,
|
|
65
|
+
old: oldValue,
|
|
66
|
+
currentStoreValue: fieldStore.inputs[props.field.name],
|
|
67
|
+
});
|
|
68
|
+
// Trigger validation when value changes
|
|
69
|
+
// props.uiValidationFunction();
|
|
70
|
+
//
|
|
71
|
+
// validate();
|
|
72
|
+
// fieldStore.setFieldValue(props.field.name, newValue);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const validators = computed(() => {
|
|
76
|
+
const ret = [];
|
|
77
|
+
if (props.field?.validator) {
|
|
78
|
+
ret.push(props.field.validator.test);
|
|
79
|
+
}
|
|
80
|
+
return ret;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const focus = () => {
|
|
84
|
+
if (inputField.value) {
|
|
85
|
+
inputField.value.focus();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const userInput = () => {
|
|
90
|
+
if (!props.field?.name) {
|
|
91
|
+
throw new Error('Field name is required for FieldInput component');
|
|
92
|
+
}
|
|
93
|
+
return fieldStore.inputs[props.field.name];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const setData = (data: unknown) => {
|
|
97
|
+
if (!props.field?.name) {
|
|
98
|
+
throw new Error('Field name is required for FieldInput component');
|
|
99
|
+
}
|
|
100
|
+
fieldStore.setFieldValue(props.field.name, data);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const clearData = () => {
|
|
104
|
+
console.log(
|
|
105
|
+
`[FieldInput] Running generic clearData() for ${props.field?.name} in FieldInput.ts`
|
|
106
|
+
);
|
|
107
|
+
if (inputField.value) {
|
|
108
|
+
// if (inputField.value.type === 'file') {
|
|
109
|
+
inputField.value.value = '';
|
|
110
|
+
// } else if (inputField.value.type === 'text') {
|
|
111
|
+
// inputField.value.value = '';
|
|
112
|
+
// } else if (inputField.value.type === '')
|
|
113
|
+
if (!props.field?.name) return;
|
|
114
|
+
|
|
115
|
+
fieldStore.clearField(props.field.name);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const vuetifyRules = () => {
|
|
120
|
+
if (props.field?.validator) {
|
|
121
|
+
return validators.value.map((f) => {
|
|
122
|
+
return validationFunctionToVuetifyRule(f);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return [];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const generateTags = () => {
|
|
129
|
+
console.log('[FieldInput] Running generic generateTags() in FieldInput.ts');
|
|
130
|
+
return props.field?.tagger ? props.field.tagger(userInput()) : [];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const generateELO = () => {
|
|
134
|
+
console.log('[FieldInput] Running generic generateELO() in FieldInput.ts');
|
|
135
|
+
return props.field?.generateELO ? props.field.generateELO(userInput()) : undefined;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
inputField,
|
|
140
|
+
fieldStore,
|
|
141
|
+
modelValue,
|
|
142
|
+
validators,
|
|
143
|
+
focus,
|
|
144
|
+
userInput,
|
|
145
|
+
clearData,
|
|
146
|
+
setData,
|
|
147
|
+
vuetifyRules,
|
|
148
|
+
generateTags,
|
|
149
|
+
generateELO,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
data() {
|
|
154
|
+
return {
|
|
155
|
+
validationStatus: {
|
|
156
|
+
status: Status.ok,
|
|
157
|
+
msg: '',
|
|
158
|
+
} as ValidationResult,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-text-field
|
|
3
|
+
ref="inputField"
|
|
4
|
+
v-model="modelValue"
|
|
5
|
+
variant="filled"
|
|
6
|
+
type="text"
|
|
7
|
+
:name="field.name"
|
|
8
|
+
:label="field.name"
|
|
9
|
+
:rules="vuetifyRules()"
|
|
10
|
+
:hint="validationStatus.msg"
|
|
11
|
+
:autofocus="autofocus"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { defineComponent, computed } from 'vue';
|
|
17
|
+
import FieldInput from './OptionsFieldInput';
|
|
18
|
+
import { ValidatingFunction } from '../../../../base-course/Interfaces/ValidatingFunction';
|
|
19
|
+
|
|
20
|
+
export default defineComponent({
|
|
21
|
+
name: 'StringInput',
|
|
22
|
+
extends: FieldInput,
|
|
23
|
+
|
|
24
|
+
setup(props, ctx) {
|
|
25
|
+
// Get all the setup logic from parent
|
|
26
|
+
const parentSetup = FieldInput.setup?.(props, ctx);
|
|
27
|
+
|
|
28
|
+
// [ ] Test datashape-field-custom validators
|
|
29
|
+
const validators = computed<ValidatingFunction[]>(() => {
|
|
30
|
+
const baseValidators = FieldInput.validators.call(this);
|
|
31
|
+
|
|
32
|
+
if (props.field.validator?.test) {
|
|
33
|
+
baseValidators.push(props.field.validator.test);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (baseValidators) {
|
|
37
|
+
return baseValidators;
|
|
38
|
+
} else {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...parentSetup,
|
|
45
|
+
validators,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Status } from '@vue-skuilder/common';
|
|
2
|
+
import { ValidationResult } from '@vue-skuilder/common';
|
|
3
|
+
|
|
4
|
+
const okResult: ValidationResult = {
|
|
5
|
+
status: Status.ok,
|
|
6
|
+
msg: '',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function numberValidator(value: string): ValidationResult {
|
|
10
|
+
const errorResult: ValidationResult = {
|
|
11
|
+
status: Status.error,
|
|
12
|
+
msg: 'This input must be a number',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (isNumeric(value)) {
|
|
16
|
+
return okResult;
|
|
17
|
+
} else {
|
|
18
|
+
return errorResult;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function integerValidator(value: string): ValidationResult {
|
|
23
|
+
const errorResult: ValidationResult = {
|
|
24
|
+
status: Status.error,
|
|
25
|
+
msg: 'This input must be an integer (..., -2, -1, 0, 1, 2, ...).',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (numberValidator(value).status === Status.error) {
|
|
29
|
+
return numberValidator(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isInteger(value)) {
|
|
33
|
+
return okResult;
|
|
34
|
+
} else {
|
|
35
|
+
return errorResult;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isNumeric(value: unknown): boolean {
|
|
40
|
+
// pilfered from Angular and assumed to be correctish:
|
|
41
|
+
// https://github.com/angular/angular/blob/4.3.x/packages/common/src/pipes/number_pipe.ts#L172
|
|
42
|
+
|
|
43
|
+
// @ts-expect-error - see above
|
|
44
|
+
return !isNaN(value - parseFloat(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isInteger(value: string) {
|
|
48
|
+
return /^[+,-]?\s?\d+$/.test(value);
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Edit UI Components - Core editing components migrated from platform-ui
|
|
2
|
+
|
|
3
|
+
export { default as CourseEditor } from './CourseEditor.vue';
|
|
4
|
+
export { default as DataInputForm } from './ViewableDataInputForm/DataInputForm.vue';
|
|
5
|
+
export { default as BulkImportView } from './BulkImportView.vue';
|
|
6
|
+
|
|
7
|
+
// Field Input Components
|
|
8
|
+
export { default as StringInput } from './ViewableDataInputForm/FieldInputs/StringInput.vue';
|
|
9
|
+
export { default as MarkdownInput } from './ViewableDataInputForm/FieldInputs/MarkdownInput.vue';
|
|
10
|
+
export { default as NumberInput } from './ViewableDataInputForm/FieldInputs/NumberInput.vue';
|
|
11
|
+
export { default as IntegerInput } from './ViewableDataInputForm/FieldInputs/IntegerInput.vue';
|
|
12
|
+
export { default as MediaDragDropUploader } from './ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue';
|
|
13
|
+
export { default as ChessPuzzleInput } from './ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue';
|
|
14
|
+
export { default as MidiInput } from './ViewableDataInputForm/FieldInputs/MidiInput.vue';
|
|
15
|
+
// export { default as AudioInput } from './ViewableDataInputForm/FieldInputs/AudioInput.vue'; // Commented out - file is deprecated
|
|
16
|
+
// export { default as ImageInput } from './ViewableDataInputForm/FieldInputs/ImageInput.vue'; // Commented out - file is deprecated
|
|
17
|
+
|
|
18
|
+
// Field Input Base and Utilities
|
|
19
|
+
export { default as OptionsFieldInput } from './ViewableDataInputForm/FieldInputs/OptionsFieldInput';
|
|
20
|
+
export * from './ViewableDataInputForm/FieldInputs/typeValidators';
|
|
21
|
+
export * from './ViewableDataInputForm/FieldInput.types';
|
package/src/index.ts
ADDED