@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,33 @@
|
|
|
1
|
+
import { CourseElo, ValidatingFunction, ValidationResult } from '@vue-skuilder/common';
|
|
2
|
+
import { ComponentPublicInstance } from 'vue';
|
|
3
|
+
|
|
4
|
+
export interface FieldInputInterface {
|
|
5
|
+
$refs: {
|
|
6
|
+
inputField: HTMLInputElement;
|
|
7
|
+
};
|
|
8
|
+
validationStatus: ValidationResult;
|
|
9
|
+
validators: ValidatingFunction[];
|
|
10
|
+
focus: () => void;
|
|
11
|
+
userInput: () => unknown;
|
|
12
|
+
setData: (data: unknown) => void;
|
|
13
|
+
clearData: () => void;
|
|
14
|
+
vuetifyRules: () => Array<(value: unknown) => boolean | string>;
|
|
15
|
+
generateTags: () => string[];
|
|
16
|
+
generateELO: () => CourseElo | undefined;
|
|
17
|
+
validate: () => ValidationResult;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Type guard
|
|
21
|
+
export function isFieldInput(component: unknown): component is FieldInputInstance {
|
|
22
|
+
return (
|
|
23
|
+
component !== null &&
|
|
24
|
+
typeof component === 'object' &&
|
|
25
|
+
'clearData' in component &&
|
|
26
|
+
'validate' in component &&
|
|
27
|
+
typeof (component as Record<string, unknown>).clearData === 'function' &&
|
|
28
|
+
typeof (component as Record<string, unknown>).validate === 'function'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// This combines the Vue component instance type with our interface
|
|
33
|
+
export type FieldInputInstance = ComponentPublicInstance & FieldInputInterface;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
|
|
3
|
+
image and audio inputs are semi deprecated - not in use right now -
|
|
4
|
+
superceded by the generic fillIn type that allows images and audio from the
|
|
5
|
+
general mediaDragDropUploader
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div>
|
|
9
|
+
<label class="text-h5" :for="field.name">{{ title }}: </label>
|
|
10
|
+
|
|
11
|
+
<div>
|
|
12
|
+
<v-btn-toggle mandatory multiple elevation-5>
|
|
13
|
+
<v-btn variant="flat">
|
|
14
|
+
<v-icon :color="recording ? 'red' : null" @click="record">mic</v-icon>
|
|
15
|
+
</v-btn>
|
|
16
|
+
<v-btn variant="flat">
|
|
17
|
+
<v-icon @click="stop">stop</v-icon>
|
|
18
|
+
</v-btn>
|
|
19
|
+
<v-btn variant="flat">
|
|
20
|
+
<v-icon @click="play">play_arrow</v-icon>
|
|
21
|
+
</v-btn>
|
|
22
|
+
|
|
23
|
+
<v-btn variant="flat"
|
|
24
|
+
><label>
|
|
25
|
+
<input
|
|
26
|
+
:id="blobInputID"
|
|
27
|
+
ref="inputField"
|
|
28
|
+
:name="field.name"
|
|
29
|
+
type="file"
|
|
30
|
+
:class="validationStatus.status"
|
|
31
|
+
@change="processInput"
|
|
32
|
+
/>
|
|
33
|
+
<span>Upload</span><v-icon>folder</v-icon>
|
|
34
|
+
</label>
|
|
35
|
+
</v-btn>
|
|
36
|
+
</v-btn-toggle>
|
|
37
|
+
<div :id="waveSurferId"></div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { defineComponent, PropType } from 'vue';
|
|
44
|
+
import { ValidatingFunction } from '@vue-skuilder/common';
|
|
45
|
+
import WaveSurfer from 'wavesurfer.js';
|
|
46
|
+
import FieldInput from '../OptionsFieldInput';
|
|
47
|
+
import MediaStreamRecorder from 'msr';
|
|
48
|
+
import { FieldDefinition } from '../../../../base-course/Interfaces/FieldDefinition';
|
|
49
|
+
|
|
50
|
+
export default defineComponent({
|
|
51
|
+
extends: FieldInput,
|
|
52
|
+
props: {
|
|
53
|
+
field: {
|
|
54
|
+
type: Object as PropType<FieldDefinition>,
|
|
55
|
+
required: true,
|
|
56
|
+
},
|
|
57
|
+
store: {
|
|
58
|
+
type: Object as PropType<object>,
|
|
59
|
+
required: true,
|
|
60
|
+
},
|
|
61
|
+
uiValidationFunction: {
|
|
62
|
+
type: Function as PropType<() => boolean>,
|
|
63
|
+
required: true,
|
|
64
|
+
},
|
|
65
|
+
autofocus: Boolean,
|
|
66
|
+
},
|
|
67
|
+
data() {
|
|
68
|
+
return {
|
|
69
|
+
recording: false as boolean,
|
|
70
|
+
blob: null as Blob | null,
|
|
71
|
+
blobURL: 'f' as string,
|
|
72
|
+
mediaRecorder: null as any,
|
|
73
|
+
wavesurfer: null as WaveSurfer | null,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
computed: {
|
|
77
|
+
title(): string {
|
|
78
|
+
return this.field.name;
|
|
79
|
+
},
|
|
80
|
+
blobInputID(): string {
|
|
81
|
+
return 'blobInput' + this.field.name;
|
|
82
|
+
},
|
|
83
|
+
waveSurferId(): string {
|
|
84
|
+
return `ws-${this.field.name}`;
|
|
85
|
+
},
|
|
86
|
+
blobInputElement(): HTMLInputElement {
|
|
87
|
+
return document.getElementById(this.blobInputID) as HTMLInputElement;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
methods: {
|
|
91
|
+
getValidators(): ValidatingFunction[] {
|
|
92
|
+
if (this.field.validator) {
|
|
93
|
+
return [this.field.validator.test];
|
|
94
|
+
} else {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
async processInput() {
|
|
99
|
+
if (this.blobInputElement.files) {
|
|
100
|
+
const file = this.blobInputElement.files[0];
|
|
101
|
+
console.log(`
|
|
102
|
+
Processing input file:
|
|
103
|
+
Filename: ${file.name}
|
|
104
|
+
File size: ${file.size}
|
|
105
|
+
File type: ${file.type}
|
|
106
|
+
`);
|
|
107
|
+
this.setData({
|
|
108
|
+
content_type: file.type,
|
|
109
|
+
data: file.slice(),
|
|
110
|
+
} as PouchDB.Core.FullAttachment);
|
|
111
|
+
this.validate();
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
blobHandler(blob: Blob | null): void {
|
|
115
|
+
if (blob === null) {
|
|
116
|
+
alert('nullBlob');
|
|
117
|
+
} else {
|
|
118
|
+
this.store[this.field.name] = {
|
|
119
|
+
content_type: 'image/png',
|
|
120
|
+
data: blob,
|
|
121
|
+
};
|
|
122
|
+
this.validate();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
play() {
|
|
126
|
+
console.log(this.blobURL);
|
|
127
|
+
this.wavesurfer?.playPause();
|
|
128
|
+
},
|
|
129
|
+
stop() {
|
|
130
|
+
if (this.mediaRecorder) {
|
|
131
|
+
this.mediaRecorder.stop();
|
|
132
|
+
this.recording = false;
|
|
133
|
+
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
this.setData({
|
|
136
|
+
content_type: 'audio',
|
|
137
|
+
data: this.blob,
|
|
138
|
+
} as PouchDB.Core.FullAttachment);
|
|
139
|
+
}, 100);
|
|
140
|
+
this.validate();
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
reset() {
|
|
144
|
+
this.wavesurfer?.destroy();
|
|
145
|
+
},
|
|
146
|
+
record() {
|
|
147
|
+
this.recording = true;
|
|
148
|
+
this.wavesurfer = WaveSurfer.create({
|
|
149
|
+
container: `#${this.waveSurferId}`,
|
|
150
|
+
barWidth: 2,
|
|
151
|
+
barHeight: 1,
|
|
152
|
+
barGap: 0,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const mediaConstraints = {
|
|
156
|
+
audio: true,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
navigator.mediaDevices
|
|
160
|
+
.getUserMedia(mediaConstraints)
|
|
161
|
+
.then((stream) => {
|
|
162
|
+
console.log(`stream ${JSON.stringify(stream)} found...`);
|
|
163
|
+
this.mediaRecorder = new MediaStreamRecorder(stream);
|
|
164
|
+
this.mediaRecorder.mimeType = 'audio/webm';
|
|
165
|
+
this.mediaRecorder.ondataavailable = (blob: Blob) => {
|
|
166
|
+
this.blob = blob;
|
|
167
|
+
this.blobURL = URL.createObjectURL(blob);
|
|
168
|
+
this.wavesurfer?.load(this.blobURL);
|
|
169
|
+
};
|
|
170
|
+
this.mediaRecorder.start(0);
|
|
171
|
+
})
|
|
172
|
+
.catch((e) => {
|
|
173
|
+
console.error('media error', e);
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<style scoped>
|
|
181
|
+
@import './FieldInput.css';
|
|
182
|
+
|
|
183
|
+
input[type='file'] {
|
|
184
|
+
display: none;
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
187
|
+
|
|
188
|
+
-->
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-text-field
|
|
3
|
+
ref="inputField"
|
|
4
|
+
v-model="modelValue"
|
|
5
|
+
variant="filled"
|
|
6
|
+
:name="field.name"
|
|
7
|
+
:label="field.name"
|
|
8
|
+
:rules="vuetifyRules()"
|
|
9
|
+
:autofocus="autofocus"
|
|
10
|
+
/>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import { defineComponent } from 'vue';
|
|
15
|
+
import FieldInput from './OptionsFieldInput';
|
|
16
|
+
import { CourseElo } from '@vue-skuilder/common';
|
|
17
|
+
// import { FieldDefinition } from '../../../../base-course/Interfaces/FieldDefinition';
|
|
18
|
+
|
|
19
|
+
export default defineComponent({
|
|
20
|
+
name: 'ChessPuzzleInput',
|
|
21
|
+
extends: FieldInput,
|
|
22
|
+
|
|
23
|
+
setup(props, ctx) {
|
|
24
|
+
// Get all the setup logic from parent
|
|
25
|
+
const parentSetup = FieldInput.setup?.(props, ctx);
|
|
26
|
+
|
|
27
|
+
const generateELO = () => {
|
|
28
|
+
if (!parentSetup) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const split = (parentSetup.modelValue.value as string).split(',');
|
|
33
|
+
const elo = parseInt(split[3]);
|
|
34
|
+
const count = parseInt(split[6]);
|
|
35
|
+
|
|
36
|
+
const crsElo: CourseElo = {
|
|
37
|
+
global: {
|
|
38
|
+
score: elo,
|
|
39
|
+
count,
|
|
40
|
+
},
|
|
41
|
+
tags: {},
|
|
42
|
+
misc: {},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const tags = generateTags();
|
|
46
|
+
tags.forEach((t) => {
|
|
47
|
+
crsElo.tags[t] = {
|
|
48
|
+
score: elo,
|
|
49
|
+
count,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log('generateELO', JSON.stringify(crsElo));
|
|
54
|
+
|
|
55
|
+
return crsElo;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const generateTags = () => {
|
|
59
|
+
if (!parentSetup) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`CPI.generateTags: modelValue: ${parentSetup.modelValue.value}`);
|
|
64
|
+
|
|
65
|
+
const split = (parentSetup.modelValue.value as string).split(',');
|
|
66
|
+
const themes = split[7].split(' ');
|
|
67
|
+
const openingTags = split[9].split(' ');
|
|
68
|
+
|
|
69
|
+
return themes.map((t) => `theme-${t}`).concat(openingTags.map((t) => `opening-${t}`));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...parentSetup,
|
|
74
|
+
generateELO,
|
|
75
|
+
generateTags,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
|
|
3
|
+
image and audio inputs are semi deprecated - not in use right now -
|
|
4
|
+
superceded by the generic fillIn type that allows images and audio from the
|
|
5
|
+
general mediaDragDropUploader
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div>
|
|
9
|
+
<label :for="field.name">{{ field.name }}: </label>
|
|
10
|
+
<div
|
|
11
|
+
class="drop-zone"
|
|
12
|
+
:class="{ 'drop-zone--over': isDragging }"
|
|
13
|
+
@drop="dropHandler"
|
|
14
|
+
@dragover.prevent="dragOverHandler"
|
|
15
|
+
@dragenter.prevent="dragEnterHandler"
|
|
16
|
+
@dragleave.prevent="dragLeaveHandler"
|
|
17
|
+
>
|
|
18
|
+
<template v-if="!thumbnailUrl">
|
|
19
|
+
Drop a file here...
|
|
20
|
+
<input
|
|
21
|
+
:id="blobInputID"
|
|
22
|
+
ref="inputField"
|
|
23
|
+
:name="field.name"
|
|
24
|
+
type="file"
|
|
25
|
+
:class="validationStatus.status"
|
|
26
|
+
accept="image/*"
|
|
27
|
+
@change="processInput"
|
|
28
|
+
@click.stop
|
|
29
|
+
/>
|
|
30
|
+
</template>
|
|
31
|
+
<template v-else>
|
|
32
|
+
<img :src="thumbnailUrl" alt="Uploaded image thumbnail" class="thumbnail" />
|
|
33
|
+
<button @click="clearImage">Clear Image</button>
|
|
34
|
+
</template>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script lang="ts">
|
|
40
|
+
import { defineComponent } from 'vue';
|
|
41
|
+
import { ValidatingFunction } from '@vue-skuilder/common';
|
|
42
|
+
import FieldInput from '../OptionsFieldInput';
|
|
43
|
+
|
|
44
|
+
// [ ] delete this file ? (Jan 6, 2025)
|
|
45
|
+
|
|
46
|
+
export default defineComponent({
|
|
47
|
+
name: 'ImageInput',
|
|
48
|
+
extends: FieldInput,
|
|
49
|
+
|
|
50
|
+
data() {
|
|
51
|
+
return {
|
|
52
|
+
isDragging: false,
|
|
53
|
+
dragCounter: 0,
|
|
54
|
+
thumbnailUrl: null as string | null,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
computed: {
|
|
59
|
+
blobInputID(): string {
|
|
60
|
+
return 'blobInput' + this.field.name;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
blobInputElement(): HTMLInputElement {
|
|
64
|
+
return document.getElementById(this.blobInputID) as HTMLInputElement;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
methods: {
|
|
69
|
+
dragOverHandler(ev: DragEvent) {
|
|
70
|
+
ev.preventDefault();
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
dragEnterHandler(ev: DragEvent) {
|
|
74
|
+
ev.preventDefault();
|
|
75
|
+
this.dragCounter++;
|
|
76
|
+
this.isDragging = true;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
dragLeaveHandler(ev: DragEvent) {
|
|
80
|
+
ev.preventDefault();
|
|
81
|
+
this.dragCounter--;
|
|
82
|
+
if (this.dragCounter === 0) {
|
|
83
|
+
this.isDragging = false;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
dropHandler(ev: DragEvent) {
|
|
88
|
+
if (ev) {
|
|
89
|
+
ev.preventDefault();
|
|
90
|
+
|
|
91
|
+
this.isDragging = false;
|
|
92
|
+
this.dragCounter = 0;
|
|
93
|
+
|
|
94
|
+
if (ev.dataTransfer?.files.length) {
|
|
95
|
+
const file = ev.dataTransfer.files[0];
|
|
96
|
+
this.processDroppedFile(file);
|
|
97
|
+
} else if (ev.dataTransfer?.types.includes('text/plain') || ev.dataTransfer?.types.includes('text/uri-list')) {
|
|
98
|
+
const imgURL = ev.dataTransfer.getData('text');
|
|
99
|
+
this.fetchImg(imgURL);
|
|
100
|
+
console.log(`Dropped URL: ${imgURL}`);
|
|
101
|
+
} else {
|
|
102
|
+
console.error('Unsupported drop type');
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
console.error('dropHandler triggered with no event');
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
processDroppedFile(file: File) {
|
|
110
|
+
console.log(`
|
|
111
|
+
Processing dropped file:
|
|
112
|
+
|
|
113
|
+
Filename: ${file.name}
|
|
114
|
+
File size: ${file.size}
|
|
115
|
+
File type: ${file.type}
|
|
116
|
+
`);
|
|
117
|
+
this.setData({
|
|
118
|
+
content_type: file.type,
|
|
119
|
+
data: file.slice(),
|
|
120
|
+
} as PouchDB.Core.FullAttachment);
|
|
121
|
+
this.createThumbnail(file);
|
|
122
|
+
this.validate();
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async fetchImg(url: string) {
|
|
126
|
+
try {
|
|
127
|
+
const img = await fetch(url, {
|
|
128
|
+
mode: 'no-cors',
|
|
129
|
+
'content-type': 'image',
|
|
130
|
+
} as any);
|
|
131
|
+
const blob = await (img.body as any).blob();
|
|
132
|
+
|
|
133
|
+
const file = new File([blob], 'dropped_image', { type: blob.type });
|
|
134
|
+
this.setData({
|
|
135
|
+
content_type: file.type,
|
|
136
|
+
data: file.slice(),
|
|
137
|
+
});
|
|
138
|
+
this.createThumbnail(file);
|
|
139
|
+
this.validate();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Error fetching image:', error);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
dragHandler(ev: DragEvent) {
|
|
146
|
+
console.log(`Dragging... ${JSON.stringify(ev)}`);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
getValidators(): ValidatingFunction[] {
|
|
150
|
+
if (this.field.validator) {
|
|
151
|
+
return [this.field.validator.test];
|
|
152
|
+
} else {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
removeDragData(ev: DragEvent) {
|
|
158
|
+
if (ev.dataTransfer!.items) {
|
|
159
|
+
ev.dataTransfer!.items.clear();
|
|
160
|
+
} else {
|
|
161
|
+
ev.dataTransfer!.clearData();
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async processInput() {
|
|
166
|
+
if (this.blobInputElement.files) {
|
|
167
|
+
const file = this.blobInputElement.files[0];
|
|
168
|
+
console.log(`
|
|
169
|
+
Processing input file:
|
|
170
|
+
|
|
171
|
+
Filename: ${file.name}
|
|
172
|
+
File size: ${file.size}
|
|
173
|
+
File type: ${file.type}
|
|
174
|
+
`);
|
|
175
|
+
this.setData({
|
|
176
|
+
content_type: file.type,
|
|
177
|
+
data: file.slice(),
|
|
178
|
+
} as PouchDB.Core.FullAttachment);
|
|
179
|
+
this.createThumbnail(file);
|
|
180
|
+
this.validate();
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
createThumbnail(file: File) {
|
|
185
|
+
const reader = new FileReader();
|
|
186
|
+
reader.onload = (e: ProgressEvent<FileReader>) => {
|
|
187
|
+
this.thumbnailUrl = e.target?.result as string;
|
|
188
|
+
};
|
|
189
|
+
reader.readAsDataURL(file);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
clearImage() {
|
|
193
|
+
this.thumbnailUrl = null;
|
|
194
|
+
this.setData(null);
|
|
195
|
+
this.validate();
|
|
196
|
+
if (this.$refs.inputField) {
|
|
197
|
+
(this.$refs.inputField as HTMLInputElement).value = '';
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
</script>
|
|
203
|
+
|
|
204
|
+
<style scoped>
|
|
205
|
+
@import './FieldInput.css';
|
|
206
|
+
|
|
207
|
+
.drop-zone {
|
|
208
|
+
border: 2px dashed #ccc;
|
|
209
|
+
border-radius: 4px;
|
|
210
|
+
padding: 20px;
|
|
211
|
+
text-align: center;
|
|
212
|
+
transition: all 0.3s ease;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
input[type='file'] {
|
|
216
|
+
box-shadow: none !important; /* uncertain where this is coming from */
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.drop-zone--over {
|
|
220
|
+
border-color: #000;
|
|
221
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.thumbnail {
|
|
225
|
+
max-width: 100%;
|
|
226
|
+
max-height: 200px;
|
|
227
|
+
margin-bottom: 10px;
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
230
|
+
|
|
231
|
+
-->
|
|
@@ -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 { integerValidator } from './typeValidators';
|
|
18
|
+
import FieldInput from './OptionsFieldInput';
|
|
19
|
+
import { ValidatingFunction } from '@vue-skuilder/common';
|
|
20
|
+
|
|
21
|
+
export default defineComponent({
|
|
22
|
+
name: 'IntegerInput',
|
|
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 [integerValidator, ...baseValidators];
|
|
38
|
+
} else {
|
|
39
|
+
return [integerValidator];
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...parentSetup,
|
|
45
|
+
validators,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-textarea
|
|
3
|
+
ref="inputField"
|
|
4
|
+
v-model="modelValue"
|
|
5
|
+
variant="filled"
|
|
6
|
+
:name="field.name"
|
|
7
|
+
:label="field.name"
|
|
8
|
+
:rules="vuetifyRules()"
|
|
9
|
+
:hint="validationStatus.msg"
|
|
10
|
+
:autofocus="autofocus"
|
|
11
|
+
/>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { defineComponent } from 'vue';
|
|
16
|
+
import FieldInput from './OptionsFieldInput';
|
|
17
|
+
|
|
18
|
+
export default defineComponent({
|
|
19
|
+
name: 'MarkdownInput',
|
|
20
|
+
extends: FieldInput,
|
|
21
|
+
setup(props, ctx) {
|
|
22
|
+
// Get all the setup logic from parent
|
|
23
|
+
const parentSetup = FieldInput.setup?.(props, ctx);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...parentSetup,
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
/* @import url('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css'); */
|
|
34
|
+
</style>
|