@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.
Files changed (28) hide show
  1. package/dist/assets/index.css +1 -0
  2. package/dist/edit-ui.es.js +71090 -0
  3. package/dist/edit-ui.es.js.map +1 -0
  4. package/dist/edit-ui.umd.js +83 -0
  5. package/dist/edit-ui.umd.js.map +1 -0
  6. package/package.json +67 -0
  7. package/src/components/BulkImport/CardPreviewList.vue +345 -0
  8. package/src/components/BulkImportView.vue +633 -0
  9. package/src/components/CourseEditor.vue +164 -0
  10. package/src/components/ViewableDataInputForm/DataInputForm.vue +533 -0
  11. package/src/components/ViewableDataInputForm/FieldInput.types.ts +33 -0
  12. package/src/components/ViewableDataInputForm/FieldInputs/AudioInput.vue +188 -0
  13. package/src/components/ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue +79 -0
  14. package/src/components/ViewableDataInputForm/FieldInputs/FieldInput.css +12 -0
  15. package/src/components/ViewableDataInputForm/FieldInputs/ImageInput.vue +231 -0
  16. package/src/components/ViewableDataInputForm/FieldInputs/IntegerInput.vue +49 -0
  17. package/src/components/ViewableDataInputForm/FieldInputs/MarkdownInput.vue +34 -0
  18. package/src/components/ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue +246 -0
  19. package/src/components/ViewableDataInputForm/FieldInputs/MidiInput.vue +113 -0
  20. package/src/components/ViewableDataInputForm/FieldInputs/NumberInput.vue +49 -0
  21. package/src/components/ViewableDataInputForm/FieldInputs/OptionsFieldInput.ts +161 -0
  22. package/src/components/ViewableDataInputForm/FieldInputs/StringInput.vue +49 -0
  23. package/src/components/ViewableDataInputForm/FieldInputs/typeValidators.ts +49 -0
  24. package/src/components/index.ts +21 -0
  25. package/src/index.ts +6 -0
  26. package/src/stores/useDataInputFormStore.ts +49 -0
  27. package/src/stores/useFieldInputStore.ts +191 -0
  28. 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,12 @@
1
+ .ok {
2
+ box-shadow: 2px 2px 2px #383;
3
+ }
4
+ .warn {
5
+ box-shadow: 2px 2px 2px yellow;
6
+ }
7
+ .error {
8
+ box-shadow: 2px 2px 2px red;
9
+ }
10
+ input {
11
+ margin: 3px;
12
+ }
@@ -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>