@stubber/form-fields 1.0.0
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/README.md +303 -0
- package/dist/Field.svelte +36 -0
- package/dist/Field.svelte.d.ts +31 -0
- package/dist/Form.svelte +34 -0
- package/dist/Form.svelte.d.ts +29 -0
- package/dist/NullFieldWrapper.svelte +6 -0
- package/dist/NullFieldWrapper.svelte.d.ts +25 -0
- package/dist/fields/component_parts/arraybuilder/FieldWrapper.svelte +69 -0
- package/dist/fields/component_parts/arraybuilder/FieldWrapper.svelte.d.ts +29 -0
- package/dist/fields/component_parts/fieldbuilder/FieldWrapper.svelte +8 -0
- package/dist/fields/component_parts/fieldbuilder/FieldWrapper.svelte.d.ts +25 -0
- package/dist/fields/components/AgGrid.svelte +53 -0
- package/dist/fields/components/AgGrid.svelte.d.ts +18 -0
- package/dist/fields/components/Arraybuilder.svelte +101 -0
- package/dist/fields/components/Arraybuilder.svelte.d.ts +25 -0
- package/dist/fields/components/Checkbox.svelte +100 -0
- package/dist/fields/components/Checkbox.svelte.d.ts +23 -0
- package/dist/fields/components/CheckboxAutocomplete.svelte +92 -0
- package/dist/fields/components/CheckboxAutocomplete.svelte.d.ts +23 -0
- package/dist/fields/components/Contactselector.svelte +348 -0
- package/dist/fields/components/Contactselector.svelte.d.ts +25 -0
- package/dist/fields/components/Currency.svelte +258 -0
- package/dist/fields/components/Currency.svelte.d.ts +23 -0
- package/dist/fields/components/Dataindication.svelte +35 -0
- package/dist/fields/components/Dataindication.svelte.d.ts +23 -0
- package/dist/fields/components/Date.svelte +94 -0
- package/dist/fields/components/Date.svelte.d.ts +23 -0
- package/dist/fields/components/Datetime.svelte +94 -0
- package/dist/fields/components/Datetime.svelte.d.ts +23 -0
- package/dist/fields/components/Email.svelte +124 -0
- package/dist/fields/components/Email.svelte.d.ts +23 -0
- package/dist/fields/components/Fieldbuilder.svelte +340 -0
- package/dist/fields/components/Fieldbuilder.svelte.d.ts +25 -0
- package/dist/fields/components/Fieldsbuilder.svelte +122 -0
- package/dist/fields/components/Fieldsbuilder.svelte.d.ts +25 -0
- package/dist/fields/components/File.svelte +230 -0
- package/dist/fields/components/File.svelte.d.ts +25 -0
- package/dist/fields/components/Heading.svelte +17 -0
- package/dist/fields/components/Heading.svelte.d.ts +23 -0
- package/dist/fields/components/Hidden.svelte +7 -0
- package/dist/fields/components/Hidden.svelte.d.ts +23 -0
- package/dist/fields/components/Hiddenlocation.svelte +28 -0
- package/dist/fields/components/Hiddenlocation.svelte.d.ts +23 -0
- package/dist/fields/components/Html.svelte +13 -0
- package/dist/fields/components/Html.svelte.d.ts +23 -0
- package/dist/fields/components/Jsoneditor.svelte +94 -0
- package/dist/fields/components/Jsoneditor.svelte.d.ts +23 -0
- package/dist/fields/components/Map.svelte +192 -0
- package/dist/fields/components/Map.svelte.d.ts +25 -0
- package/dist/fields/components/Multicheckbox.svelte +119 -0
- package/dist/fields/components/Multicheckbox.svelte.d.ts +23 -0
- package/dist/fields/components/Multistep.svelte +86 -0
- package/dist/fields/components/Multistep.svelte.d.ts +25 -0
- package/dist/fields/components/Note.svelte +92 -0
- package/dist/fields/components/Note.svelte.d.ts +23 -0
- package/dist/fields/components/Number.svelte +118 -0
- package/dist/fields/components/Number.svelte.d.ts +23 -0
- package/dist/fields/components/Objectbuilder.svelte +152 -0
- package/dist/fields/components/Objectbuilder.svelte.d.ts +25 -0
- package/dist/fields/components/Qrcodescanner.svelte +198 -0
- package/dist/fields/components/Qrcodescanner.svelte.d.ts +23 -0
- package/dist/fields/components/Radio.svelte +116 -0
- package/dist/fields/components/Radio.svelte.d.ts +23 -0
- package/dist/fields/components/Renderfield.svelte +58 -0
- package/dist/fields/components/Renderfield.svelte.d.ts +25 -0
- package/dist/fields/components/Screenrecorder.svelte +277 -0
- package/dist/fields/components/Screenrecorder.svelte.d.ts +25 -0
- package/dist/fields/components/Screenshot.svelte +270 -0
- package/dist/fields/components/Screenshot.svelte.d.ts +25 -0
- package/dist/fields/components/Scrollandreaddisplay.svelte +122 -0
- package/dist/fields/components/Scrollandreaddisplay.svelte.d.ts +23 -0
- package/dist/fields/components/Section.svelte +64 -0
- package/dist/fields/components/Section.svelte.d.ts +25 -0
- package/dist/fields/components/Select.svelte +229 -0
- package/dist/fields/components/Select.svelte.d.ts +23 -0
- package/dist/fields/components/Selectresource.svelte +291 -0
- package/dist/fields/components/Selectresource.svelte.d.ts +25 -0
- package/dist/fields/components/Signature.svelte +153 -0
- package/dist/fields/components/Signature.svelte.d.ts +25 -0
- package/dist/fields/components/Slider.svelte +101 -0
- package/dist/fields/components/Slider.svelte.d.ts +23 -0
- package/dist/fields/components/SmartText.svelte +330 -0
- package/dist/fields/components/SmartText.svelte.d.ts +23 -0
- package/dist/fields/components/Telephone.svelte +153 -0
- package/dist/fields/components/Telephone.svelte.d.ts +23 -0
- package/dist/fields/components/Text.svelte +106 -0
- package/dist/fields/components/Text.svelte.d.ts +23 -0
- package/dist/fields/components/Voicenote.svelte +268 -0
- package/dist/fields/components/Voicenote.svelte.d.ts +25 -0
- package/dist/fields/components/index.d.ts +81 -0
- package/dist/fields/components/index.js +82 -0
- package/dist/fields/definitions/_all.json +38 -0
- package/dist/fields/definitions/_valid_fieldtype.json +220 -0
- package/dist/fields/definitions/arraybuilder.json +39 -0
- package/dist/fields/definitions/checkbox.json +44 -0
- package/dist/fields/definitions/contactselector.json +15 -0
- package/dist/fields/definitions/currency.json +42 -0
- package/dist/fields/definitions/dataindication.json +16 -0
- package/dist/fields/definitions/date.json +16 -0
- package/dist/fields/definitions/datetime.json +15 -0
- package/dist/fields/definitions/email.json +16 -0
- package/dist/fields/definitions/fieldbuilder.json +64 -0
- package/dist/fields/definitions/fieldsbuilder.json +38 -0
- package/dist/fields/definitions/file.json +42 -0
- package/dist/fields/definitions/grid.json +47 -0
- package/dist/fields/definitions/heading.json +38 -0
- package/dist/fields/definitions/hidden.json +89 -0
- package/dist/fields/definitions/hiddenlocation.json +15 -0
- package/dist/fields/definitions/html.json +34 -0
- package/dist/fields/definitions/index.d.ts +86 -0
- package/dist/fields/definitions/index.js +96 -0
- package/dist/fields/definitions/jsoneditor.json +33 -0
- package/dist/fields/definitions/map.json +36 -0
- package/dist/fields/definitions/multicheckbox.json +47 -0
- package/dist/fields/definitions/multistep.json +35 -0
- package/dist/fields/definitions/note.json +16 -0
- package/dist/fields/definitions/number.json +42 -0
- package/dist/fields/definitions/objectbuilder.json +39 -0
- package/dist/fields/definitions/qrcodescanner.json +16 -0
- package/dist/fields/definitions/radio.json +47 -0
- package/dist/fields/definitions/renderfield.json +36 -0
- package/dist/fields/definitions/richtext.json +16 -0
- package/dist/fields/definitions/screenrecorder.json +42 -0
- package/dist/fields/definitions/screenshot.json +42 -0
- package/dist/fields/definitions/scrollandreaddisplay.json +49 -0
- package/dist/fields/definitions/section.json +50 -0
- package/dist/fields/definitions/select.json +47 -0
- package/dist/fields/definitions/selectresource.json +48 -0
- package/dist/fields/definitions/signature.json +16 -0
- package/dist/fields/definitions/slider.json +78 -0
- package/dist/fields/definitions/smart_text.json +101 -0
- package/dist/fields/definitions/telephone.json +16 -0
- package/dist/fields/definitions/text.json +35 -0
- package/dist/fields/definitions/voicenote.json +43 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/thirdparty/mapbox/GeoCoder.svelte +10 -0
- package/dist/thirdparty/mapbox/GeoCoder.svelte.d.ts +25 -0
- package/dist/thirdparty/mapbox/Map.svelte +30 -0
- package/dist/thirdparty/mapbox/Map.svelte.d.ts +20 -0
- package/dist/thirdparty/mapbox/MapMarker.svelte +13 -0
- package/dist/thirdparty/mapbox/MapMarker.svelte.d.ts +31 -0
- package/dist/utils/createField.d.ts +6 -0
- package/dist/utils/createField.js +33 -0
- package/dist/utils/createForm.d.ts +1 -0
- package/dist/utils/createForm.js +501 -0
- package/dist/utils/index.d.ts +18 -0
- package/dist/utils/index.js +126 -0
- package/dist/utils/syncing.d.ts +11 -0
- package/dist/utils/syncing.js +134 -0
- package/package.json +78 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { syncStoreToStore } from "../../utils/syncing";
|
|
3
|
+
import { deepEqual } from "fast-equals";
|
|
4
|
+
import _ from "lodash-es";
|
|
5
|
+
import { onMount } from "svelte";
|
|
6
|
+
import { writable } from "svelte/store";
|
|
7
|
+
import { Form } from "../..";
|
|
8
|
+
|
|
9
|
+
export let form;
|
|
10
|
+
export let field;
|
|
11
|
+
|
|
12
|
+
let render_form = writable();
|
|
13
|
+
let dependencies = form.dependencies;
|
|
14
|
+
|
|
15
|
+
let initial_form;
|
|
16
|
+
$: updateInitForm($field?.state?.dynamic_fieldspec);
|
|
17
|
+
function updateInitForm(rf) {
|
|
18
|
+
let name = rf?.name ?? "rendered_field";
|
|
19
|
+
let f = _.cloneDeep(rf);
|
|
20
|
+
_.set(f, "details.keyname", "rendered_field");
|
|
21
|
+
let current_f = _.get(initial_form, `spec.fields.${name}`);
|
|
22
|
+
if (deepEqual(current_f, f)) return;
|
|
23
|
+
initial_form = _.cloneDeep({
|
|
24
|
+
spec: {
|
|
25
|
+
fields: {
|
|
26
|
+
[name]: f,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
data: {
|
|
30
|
+
rendered_field: $field?.data?.base,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMount(() => {
|
|
36
|
+
// set field values that aren't set yet
|
|
37
|
+
|
|
38
|
+
syncStoreToStore(
|
|
39
|
+
field,
|
|
40
|
+
render_form,
|
|
41
|
+
(a, b) => {
|
|
42
|
+
// return field internal state
|
|
43
|
+
let _clone = _.cloneDeep(b) || {};
|
|
44
|
+
_.set(_clone, "data.rendered_field", a?.data?.base);
|
|
45
|
+
return _clone;
|
|
46
|
+
},
|
|
47
|
+
(a, b) => {
|
|
48
|
+
let _clone = _.cloneDeep(a) || {};
|
|
49
|
+
_.set(_clone, "data.base", b?.data?.rendered_field ?? a?.data?.base);
|
|
50
|
+
return _clone;
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
{#if initial_form}
|
|
57
|
+
<Form bind:form={render_form} {initial_form} {dependencies} />
|
|
58
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @typedef {typeof __propDef.props} RenderfieldProps */
|
|
2
|
+
/** @typedef {typeof __propDef.events} RenderfieldEvents */
|
|
3
|
+
/** @typedef {typeof __propDef.slots} RenderfieldSlots */
|
|
4
|
+
export default class Renderfield extends SvelteComponentTyped<{
|
|
5
|
+
form: any;
|
|
6
|
+
field: any;
|
|
7
|
+
}, {
|
|
8
|
+
[evt: string]: CustomEvent<any>;
|
|
9
|
+
}, {}> {
|
|
10
|
+
}
|
|
11
|
+
export type RenderfieldProps = typeof __propDef.props;
|
|
12
|
+
export type RenderfieldEvents = typeof __propDef.events;
|
|
13
|
+
export type RenderfieldSlots = typeof __propDef.slots;
|
|
14
|
+
import { SvelteComponentTyped } from "svelte";
|
|
15
|
+
declare const __propDef: {
|
|
16
|
+
props: {
|
|
17
|
+
form: any;
|
|
18
|
+
field: any;
|
|
19
|
+
};
|
|
20
|
+
events: {
|
|
21
|
+
[evt: string]: CustomEvent<any>;
|
|
22
|
+
};
|
|
23
|
+
slots: {};
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import { Button } from "@stubber/ui";
|
|
4
|
+
import _ from "lodash-es";
|
|
5
|
+
import { deepEqual } from "fast-equals";
|
|
6
|
+
import { writable } from "svelte/store";
|
|
7
|
+
import { syncStoreToStore } from "../../utils/syncing";
|
|
8
|
+
|
|
9
|
+
export let form;
|
|
10
|
+
export let field;
|
|
11
|
+
|
|
12
|
+
const internal = writable({});
|
|
13
|
+
|
|
14
|
+
let isRecording;
|
|
15
|
+
let media = [];
|
|
16
|
+
let mediaRecorder = null;
|
|
17
|
+
|
|
18
|
+
$: state_key = $field.state?.state_key;
|
|
19
|
+
$: label = $field.spec?.title;
|
|
20
|
+
$: hide_label = $field.spec?.hide_label;
|
|
21
|
+
$: isValid = !$field.state?.validation || $field.state?.validation?.valid;
|
|
22
|
+
$: validationMessage = $field.state?.validation?.message;
|
|
23
|
+
$: max_files = isNaN(parseInt($field.spec?.params?.max_files))
|
|
24
|
+
? Infinity
|
|
25
|
+
: parseInt($field.spec?.params?.max_files);
|
|
26
|
+
$: limit_remaining =
|
|
27
|
+
max_files - ($internal?.selected_files?.length ?? 0) - ($internal?.uploaded_files?.length ?? 0);
|
|
28
|
+
|
|
29
|
+
onMount(() => {
|
|
30
|
+
// set field values that aren't set yet
|
|
31
|
+
let f = _.cloneDeep($field);
|
|
32
|
+
let initial_value = _.isArray(f?.data?.base) ? f?.data?.base : [];
|
|
33
|
+
let initial_data = {
|
|
34
|
+
...f?.data,
|
|
35
|
+
base: initial_value,
|
|
36
|
+
};
|
|
37
|
+
let initial_state_internal = {
|
|
38
|
+
selected_files: [],
|
|
39
|
+
uploaded_files: initial_value,
|
|
40
|
+
failed_files: [],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
_.set(f, "data", initial_data);
|
|
44
|
+
_.set(f, "state.internal", initial_state_internal);
|
|
45
|
+
if (!deepEqual(f, $field)) $field = f;
|
|
46
|
+
|
|
47
|
+
syncStoreToStore(
|
|
48
|
+
field,
|
|
49
|
+
internal,
|
|
50
|
+
(a, b) => {
|
|
51
|
+
let _clone = _.cloneDeep(a.state?.internal) || {};
|
|
52
|
+
|
|
53
|
+
// get parts from data
|
|
54
|
+
let files = _.isArray(a?.data?.base) ? a?.data?.base : [];
|
|
55
|
+
_clone.uploaded_files = files;
|
|
56
|
+
|
|
57
|
+
// set field state if changed
|
|
58
|
+
if (!deepEqual(a?.state?.internal, _clone)) {
|
|
59
|
+
$field.state.internal = _clone;
|
|
60
|
+
}
|
|
61
|
+
return _clone;
|
|
62
|
+
},
|
|
63
|
+
(a, b) => {
|
|
64
|
+
let _clone = _.cloneDeep(a) || {};
|
|
65
|
+
// update the state
|
|
66
|
+
_.set(_clone, "state.internal", _.cloneDeep(b));
|
|
67
|
+
// update the data
|
|
68
|
+
_.set(_clone, "data.base", b?.uploaded_files);
|
|
69
|
+
return _clone;
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
async function initializeMediaRecorder() {
|
|
75
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
76
|
+
audio: true,
|
|
77
|
+
video: { mediaSource: "screen" },
|
|
78
|
+
});
|
|
79
|
+
const audiostream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
80
|
+
const combine = new MediaStream([...stream.getVideoTracks(), ...audiostream.getAudioTracks()]);
|
|
81
|
+
mediaRecorder = new MediaRecorder(combine, { mimeType: "video/webm;codecs=vp9" });
|
|
82
|
+
mediaRecorder.ondataavailable = (e) => media.push(e.data);
|
|
83
|
+
mediaRecorder.onstop = function () {
|
|
84
|
+
let id = Math.random().toString(36).substring(7);
|
|
85
|
+
let filename = `${$field.spec?.name}_${id}.webm`;
|
|
86
|
+
let blob = new Blob(media, { type: "video/webm;codecs=vp9" });
|
|
87
|
+
let file = new File([blob], filename, { type: "video/webm;codecs=vp9" });
|
|
88
|
+
uploadFile(file, blob, filename);
|
|
89
|
+
media = [];
|
|
90
|
+
mediaRecorder = null;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function startRecording() {
|
|
95
|
+
if (limit_remaining <= 0) {
|
|
96
|
+
alert("You have reached the maximum number of files allowed");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await initializeMediaRecorder();
|
|
100
|
+
mediaRecorder.start();
|
|
101
|
+
isRecording = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stopRecording() {
|
|
105
|
+
mediaRecorder.stop();
|
|
106
|
+
isRecording = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toggleRecording() {
|
|
110
|
+
if (!isRecording) {
|
|
111
|
+
startRecording();
|
|
112
|
+
} else {
|
|
113
|
+
stopRecording();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function uploadFile(file, blob, filename) {
|
|
118
|
+
let comparison = _.cloneDeep($internal);
|
|
119
|
+
let f_id = Math.random().toString(36).substring(7);
|
|
120
|
+
comparison.selected_files = comparison.selected_files || [];
|
|
121
|
+
comparison.selected_files.push({
|
|
122
|
+
blob,
|
|
123
|
+
file,
|
|
124
|
+
filename,
|
|
125
|
+
f_id,
|
|
126
|
+
});
|
|
127
|
+
if (!deepEqual(comparison, $internal)) {
|
|
128
|
+
$internal = comparison;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let upload_res = await form.uploadFiles([file]);
|
|
132
|
+
let { uploaded_files } = upload_res || {};
|
|
133
|
+
|
|
134
|
+
let comparison2 = _.cloneDeep($internal);
|
|
135
|
+
if (_.isArray(uploaded_files)) {
|
|
136
|
+
uploaded_files.forEach((a) => {
|
|
137
|
+
a.f_id = f_id;
|
|
138
|
+
form.appendAttachment(a);
|
|
139
|
+
comparison2.uploaded_files.push(a);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!uploaded_files?.length) {
|
|
144
|
+
comparison2.failed_files.push({
|
|
145
|
+
file,
|
|
146
|
+
filename,
|
|
147
|
+
f_id,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
comparison2.selected_files = comparison2.selected_files.filter((f) => {
|
|
152
|
+
return f.f_id !== f_id;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!deepEqual(comparison2, $internal)) {
|
|
156
|
+
$internal = comparison2;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function removeFile(item) {
|
|
161
|
+
let name = item?.file?.filename ?? item?.file?.name;
|
|
162
|
+
let comparison = _.cloneDeep($internal);
|
|
163
|
+
comparison.selected_files = comparison.selected_files?.filter((f) => f.f_id !== item.f_id);
|
|
164
|
+
comparison.failed_files = comparison.failed_files?.filter((f) => f.f_id !== item.f_id);
|
|
165
|
+
comparison.uploaded_files = comparison.uploaded_files?.filter((f) => f.f_id !== item.f_id);
|
|
166
|
+
|
|
167
|
+
if (!deepEqual(comparison, $internal)) {
|
|
168
|
+
$internal = comparison;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (item?.is_uploaded) {
|
|
172
|
+
form.removeAttachment(item?.file);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function fileToBlob(file) {
|
|
177
|
+
return new Blob([new Uint8Array(await file.arrayBuffer())], { type: file.type });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let fileList = [];
|
|
181
|
+
$: formatFileList($internal);
|
|
182
|
+
async function formatFileList(i) {
|
|
183
|
+
let { uploaded_files, selected_files, failed_files } = i;
|
|
184
|
+
let r = [];
|
|
185
|
+
for (let item of uploaded_files || []) {
|
|
186
|
+
r.push({
|
|
187
|
+
file: item,
|
|
188
|
+
filename: item.filename,
|
|
189
|
+
is_uploaded: true,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
for (let item of selected_files || []) {
|
|
193
|
+
r.push({
|
|
194
|
+
...item,
|
|
195
|
+
is_selected: true,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
for (let item of failed_files || []) {
|
|
199
|
+
r.push({
|
|
200
|
+
...item,
|
|
201
|
+
is_failed: true,
|
|
202
|
+
blob: await fileToBlob(item.file),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
fileList = r;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
$: buttonLabel = isRecording ? "Stop recording" : "Start recording";
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
{#if $internal}
|
|
212
|
+
<div class="flex flex-col w-full text-surface-900">
|
|
213
|
+
<label for="input_{state_key}" class="block text-label {hide_label ? 'hidden' : ''}">
|
|
214
|
+
{label}
|
|
215
|
+
</label>
|
|
216
|
+
<div>
|
|
217
|
+
<Button
|
|
218
|
+
variant={isRecording ? "destructive" : "default"}
|
|
219
|
+
on:click={toggleRecording}
|
|
220
|
+
>
|
|
221
|
+
{#if isRecording}
|
|
222
|
+
<i class="fa-solid fa-video-slash" />
|
|
223
|
+
{:else}
|
|
224
|
+
<i class="fa-solid fa-video" />
|
|
225
|
+
{/if}
|
|
226
|
+
{buttonLabel}
|
|
227
|
+
</Button>
|
|
228
|
+
{#each fileList as item}
|
|
229
|
+
<div class="w-full flex flex-row items-center">
|
|
230
|
+
{#if item.is_uploaded}
|
|
231
|
+
<div class="px-2">
|
|
232
|
+
<i class="fa fa-check text-success-500" />
|
|
233
|
+
</div>
|
|
234
|
+
{:else if item.is_failed}
|
|
235
|
+
<div class="px-2 space-x-2 flex items-center text-danger-400">
|
|
236
|
+
<i class="fa-regular fa-triangle-exclamation" />
|
|
237
|
+
<p class="hidden md:block text-fluid-md">Failed</p>
|
|
238
|
+
</div>
|
|
239
|
+
{:else}
|
|
240
|
+
<div class="px-2 space-x-2 flex items-center text-surface-500">
|
|
241
|
+
<p class="hidden md:block text-fluid-md">uploading...</p>
|
|
242
|
+
<i class="fa fa-pulse fa-spinner" />
|
|
243
|
+
</div>
|
|
244
|
+
{/if}
|
|
245
|
+
{#if item?.blob}
|
|
246
|
+
<div class="shrink p-2">
|
|
247
|
+
<div class="overflow-hidden relative max-w-[200px]">
|
|
248
|
+
<!-- <audio controls src={window.URL.createObjectURL(item.blob)} /> -->
|
|
249
|
+
<video controls src={window.URL.createObjectURL(item.blob)} class="w-full">
|
|
250
|
+
<track kind="captions" />
|
|
251
|
+
</video>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
{:else}
|
|
255
|
+
<div class="w-full shrink py-1 pl-2 truncate border border-surface-200 rounded-sm">
|
|
256
|
+
<p class="text-surface-800 text-fluid-md">
|
|
257
|
+
{item?.filename}
|
|
258
|
+
</p>
|
|
259
|
+
</div>
|
|
260
|
+
{/if}
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
class="shrink-0 fa-solid fa-2xs fa-x bg-surface-0 rounded-full border border-surface-0 hover:border-warning-500 hover:text-warning-500"
|
|
264
|
+
on:click={() => {
|
|
265
|
+
removeFile(item);
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
{/each}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
{#if validationMessage}
|
|
273
|
+
<p class="text-label {!isValid ? `text-danger-500` : `text-success-500`}">
|
|
274
|
+
{validationMessage}
|
|
275
|
+
</p>
|
|
276
|
+
{/if}
|
|
277
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @typedef {typeof __propDef.props} ScreenrecorderProps */
|
|
2
|
+
/** @typedef {typeof __propDef.events} ScreenrecorderEvents */
|
|
3
|
+
/** @typedef {typeof __propDef.slots} ScreenrecorderSlots */
|
|
4
|
+
export default class Screenrecorder extends SvelteComponentTyped<{
|
|
5
|
+
form: any;
|
|
6
|
+
field: any;
|
|
7
|
+
}, {
|
|
8
|
+
[evt: string]: CustomEvent<any>;
|
|
9
|
+
}, {}> {
|
|
10
|
+
}
|
|
11
|
+
export type ScreenrecorderProps = typeof __propDef.props;
|
|
12
|
+
export type ScreenrecorderEvents = typeof __propDef.events;
|
|
13
|
+
export type ScreenrecorderSlots = typeof __propDef.slots;
|
|
14
|
+
import { SvelteComponentTyped } from "svelte";
|
|
15
|
+
declare const __propDef: {
|
|
16
|
+
props: {
|
|
17
|
+
form: any;
|
|
18
|
+
field: any;
|
|
19
|
+
};
|
|
20
|
+
events: {
|
|
21
|
+
[evt: string]: CustomEvent<any>;
|
|
22
|
+
};
|
|
23
|
+
slots: {};
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import { Button } from "@stubber/ui";
|
|
4
|
+
import _ from "lodash-es";
|
|
5
|
+
import { deepEqual } from "fast-equals";
|
|
6
|
+
import { writable } from "svelte/store";
|
|
7
|
+
import { syncStoreToStore } from "../../utils/syncing";
|
|
8
|
+
|
|
9
|
+
export let form;
|
|
10
|
+
export let field;
|
|
11
|
+
|
|
12
|
+
const internal = writable({});
|
|
13
|
+
|
|
14
|
+
let isRecording;
|
|
15
|
+
|
|
16
|
+
$: state_key = $field.state?.state_key;
|
|
17
|
+
$: label = $field.spec?.title;
|
|
18
|
+
$: hide_label = $field.spec?.hide_label;
|
|
19
|
+
$: isValid = !$field.state?.validation || $field.state?.validation?.valid;
|
|
20
|
+
$: validationMessage = $field.state?.validation?.message;
|
|
21
|
+
$: max_files = isNaN(parseInt($field.spec?.params?.max_files))
|
|
22
|
+
? Infinity
|
|
23
|
+
: parseInt($field.spec?.params?.max_files);
|
|
24
|
+
$: limit_remaining =
|
|
25
|
+
max_files - ($internal?.selected_files?.length ?? 0) - ($internal?.uploaded_files?.length ?? 0);
|
|
26
|
+
|
|
27
|
+
onMount(() => {
|
|
28
|
+
// set field values that aren't set yet
|
|
29
|
+
let f = _.cloneDeep($field);
|
|
30
|
+
let initial_value = _.isArray(f?.data?.base) ? f?.data?.base : [];
|
|
31
|
+
let initial_data = {
|
|
32
|
+
...f?.data,
|
|
33
|
+
base: initial_value,
|
|
34
|
+
};
|
|
35
|
+
let initial_state_internal = {
|
|
36
|
+
selected_files: [],
|
|
37
|
+
uploaded_files: initial_value,
|
|
38
|
+
failed_files: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
_.set(f, "data", initial_data);
|
|
42
|
+
_.set(f, "state.internal", initial_state_internal);
|
|
43
|
+
if (!deepEqual(f, $field)) $field = f;
|
|
44
|
+
|
|
45
|
+
syncStoreToStore(
|
|
46
|
+
field,
|
|
47
|
+
internal,
|
|
48
|
+
(a, b) => {
|
|
49
|
+
let _clone = _.cloneDeep(a.state?.internal) || {};
|
|
50
|
+
|
|
51
|
+
// get parts from data
|
|
52
|
+
let files = _.isArray(a?.data?.base) ? a?.data?.base : [];
|
|
53
|
+
_clone.uploaded_files = files;
|
|
54
|
+
|
|
55
|
+
// set field state if changed
|
|
56
|
+
if (!deepEqual(a?.state?.internal, _clone)) {
|
|
57
|
+
$field.state.internal = _clone;
|
|
58
|
+
}
|
|
59
|
+
return _clone;
|
|
60
|
+
},
|
|
61
|
+
(a, b) => {
|
|
62
|
+
let _clone = _.cloneDeep(a) || {};
|
|
63
|
+
// update the state
|
|
64
|
+
_.set(_clone, "state.internal", _.cloneDeep(b));
|
|
65
|
+
// update the data
|
|
66
|
+
_.set(_clone, "data.base", b?.uploaded_files);
|
|
67
|
+
return _clone;
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
async function startRecording() {
|
|
73
|
+
if (limit_remaining <= 0) {
|
|
74
|
+
alert("You have reached the maximum number of files allowed");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let id = Math.random().toString(36).substring(7);
|
|
78
|
+
let filename = `${$field.spec?.name}_${id}`;
|
|
79
|
+
if (!navigator.mediaDevices.getDisplayMedia) return;
|
|
80
|
+
|
|
81
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
82
|
+
video: { mediaSource: "screen" },
|
|
83
|
+
});
|
|
84
|
+
// get correct video track
|
|
85
|
+
const track = stream.getVideoTracks()[0];
|
|
86
|
+
// init Image Capture and not Video stream
|
|
87
|
+
const imageCapture = new ImageCapture(track);
|
|
88
|
+
// take first frame only
|
|
89
|
+
const bitmap = await imageCapture.grabFrame();
|
|
90
|
+
// destory video track to prevent more recording / mem leak
|
|
91
|
+
track.stop();
|
|
92
|
+
|
|
93
|
+
const canvas = document.createElement("canvas");
|
|
94
|
+
// this could be a document.createElement('canvas') if you want
|
|
95
|
+
// draw weird image type to canvas so we can get a useful image
|
|
96
|
+
canvas.width = bitmap.width;
|
|
97
|
+
canvas.height = bitmap.height;
|
|
98
|
+
const context = canvas.getContext("2d");
|
|
99
|
+
context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);
|
|
100
|
+
const image = canvas.toDataURL();
|
|
101
|
+
|
|
102
|
+
// this turns the base 64 string to a [File] object
|
|
103
|
+
const res = await fetch(image);
|
|
104
|
+
const buff = await res.arrayBuffer();
|
|
105
|
+
// clone so we can rename, and put into array for easy proccessing
|
|
106
|
+
const file = [
|
|
107
|
+
new File([buff], `${filename}.png`, {
|
|
108
|
+
type: "image/png",
|
|
109
|
+
}),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
await uploadFile(file[0], `${filename}.png`);
|
|
113
|
+
|
|
114
|
+
toggleRecording();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stopRecording() {}
|
|
118
|
+
|
|
119
|
+
function toggleRecording() {
|
|
120
|
+
isRecording = !isRecording;
|
|
121
|
+
if (isRecording) {
|
|
122
|
+
startRecording();
|
|
123
|
+
} else {
|
|
124
|
+
stopRecording();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function uploadFile(file, filename) {
|
|
129
|
+
let comparison = _.cloneDeep($internal);
|
|
130
|
+
let f_id = Math.random().toString(36).substring(7);
|
|
131
|
+
comparison.selected_files = comparison.selected_files || [];
|
|
132
|
+
comparison.selected_files.push({
|
|
133
|
+
file,
|
|
134
|
+
filename,
|
|
135
|
+
f_id,
|
|
136
|
+
});
|
|
137
|
+
if (!deepEqual(comparison, $internal)) {
|
|
138
|
+
$internal = comparison;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let upload_res = await form.uploadFiles([file]);
|
|
142
|
+
let { uploaded_files } = upload_res || {};
|
|
143
|
+
|
|
144
|
+
let comparison2 = _.cloneDeep($internal);
|
|
145
|
+
if (_.isArray(uploaded_files)) {
|
|
146
|
+
uploaded_files.forEach((a) => {
|
|
147
|
+
a.f_id = f_id;
|
|
148
|
+
form.appendAttachment(a);
|
|
149
|
+
comparison2.uploaded_files.push(a);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!uploaded_files?.length) {
|
|
154
|
+
comparison2.failed_files.push({
|
|
155
|
+
file,
|
|
156
|
+
filename,
|
|
157
|
+
f_id,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
comparison2.selected_files = comparison2.selected_files.filter((f) => {
|
|
162
|
+
return f.f_id !== f_id;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!deepEqual(comparison2, $internal)) {
|
|
166
|
+
$internal = comparison2;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function removeFile(item) {
|
|
171
|
+
let comparison = _.cloneDeep($internal);
|
|
172
|
+
comparison.selected_files = comparison.selected_files?.filter((f) => f.f_id !== item.file.f_id);
|
|
173
|
+
comparison.failed_files = comparison.failed_files?.filter((f) => f.f_id !== item.file.f_id);
|
|
174
|
+
comparison.uploaded_files = comparison.uploaded_files?.filter((f) => f.f_id !== item.file.f_id);
|
|
175
|
+
|
|
176
|
+
if (!deepEqual(comparison, $internal)) {
|
|
177
|
+
$internal = comparison;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (item?.is_uploaded) {
|
|
181
|
+
form.removeAttachment(item?.file);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
$: buttonLabel = isRecording ? "Cancel" : "Take screenshot";
|
|
186
|
+
|
|
187
|
+
$: fileList = (
|
|
188
|
+
($internal?.uploaded_files || [])?.map((f) => {
|
|
189
|
+
return {
|
|
190
|
+
file: f,
|
|
191
|
+
is_uploaded: true,
|
|
192
|
+
};
|
|
193
|
+
}) || []
|
|
194
|
+
)
|
|
195
|
+
.concat(
|
|
196
|
+
$internal?.selected_files?.map((f) => {
|
|
197
|
+
return {
|
|
198
|
+
file: f,
|
|
199
|
+
is_selected: true,
|
|
200
|
+
};
|
|
201
|
+
}) || []
|
|
202
|
+
)
|
|
203
|
+
.concat(
|
|
204
|
+
$internal?.failed_files?.map((f) => {
|
|
205
|
+
return {
|
|
206
|
+
file: f,
|
|
207
|
+
is_failed: true,
|
|
208
|
+
};
|
|
209
|
+
}) || []
|
|
210
|
+
);
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
{#if $internal}
|
|
214
|
+
<div class="flex flex-col w-full text-surface-900">
|
|
215
|
+
<label for="input_{state_key}" class="block text-label {hide_label ? 'hidden' : ''}">
|
|
216
|
+
{label}
|
|
217
|
+
</label>
|
|
218
|
+
<div>
|
|
219
|
+
<Button
|
|
220
|
+
variant={isRecording ? "destructive" : "default"}
|
|
221
|
+
on:click={toggleRecording}
|
|
222
|
+
>
|
|
223
|
+
{#if isRecording}
|
|
224
|
+
<i class="fa-solid fa-camera-slash" />
|
|
225
|
+
{:else}
|
|
226
|
+
<i class="fa-solid fa-camera" />
|
|
227
|
+
{/if}
|
|
228
|
+
{buttonLabel}
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="w-full mt-2">
|
|
232
|
+
{#each fileList as item}
|
|
233
|
+
<div class="w-full flex flex-row">
|
|
234
|
+
{#if item.is_uploaded}
|
|
235
|
+
<div class="px-2">
|
|
236
|
+
<i class="fa fa-check text-success-500" />
|
|
237
|
+
</div>
|
|
238
|
+
{:else if item.is_failed}
|
|
239
|
+
<div class="px-2 space-x-2 flex items-center text-danger-400">
|
|
240
|
+
<i class="fa-regular fa-triangle-exclamation" />
|
|
241
|
+
<p class="hidden md:block text-fluid-md">Failed</p>
|
|
242
|
+
</div>
|
|
243
|
+
{:else}
|
|
244
|
+
<div class="px-2 space-x-2 flex items-center text-surface-500">
|
|
245
|
+
<p class="hidden md:block text-fluid-md" />
|
|
246
|
+
<i class="fa fa-pulse fa-spinner" />
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
249
|
+
<div class="w-full shrink py-1 pl-2 truncate border border-surface-200 rounded-sm">
|
|
250
|
+
<p class="text-surface-800 text-fluid-md">
|
|
251
|
+
{item?.file?.filename ?? item?.file?.name ?? item?.file?.path}
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
class="shrink-0 ml-1 p-1 px-2 fa-solid fa-2xs fa-x bg-surface-0 rounded-full border border-surface-0 hover:border-warning-500 hover:text-warning-500"
|
|
257
|
+
on:click={() => {
|
|
258
|
+
removeFile(item);
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
{/each}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
{#if validationMessage}
|
|
266
|
+
<p class="text-label {!isValid ? `text-danger-500` : `text-success-500`}">
|
|
267
|
+
{validationMessage}
|
|
268
|
+
</p>
|
|
269
|
+
{/if}
|
|
270
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @typedef {typeof __propDef.props} ScreenshotProps */
|
|
2
|
+
/** @typedef {typeof __propDef.events} ScreenshotEvents */
|
|
3
|
+
/** @typedef {typeof __propDef.slots} ScreenshotSlots */
|
|
4
|
+
export default class Screenshot extends SvelteComponentTyped<{
|
|
5
|
+
form: any;
|
|
6
|
+
field: any;
|
|
7
|
+
}, {
|
|
8
|
+
[evt: string]: CustomEvent<any>;
|
|
9
|
+
}, {}> {
|
|
10
|
+
}
|
|
11
|
+
export type ScreenshotProps = typeof __propDef.props;
|
|
12
|
+
export type ScreenshotEvents = typeof __propDef.events;
|
|
13
|
+
export type ScreenshotSlots = typeof __propDef.slots;
|
|
14
|
+
import { SvelteComponentTyped } from "svelte";
|
|
15
|
+
declare const __propDef: {
|
|
16
|
+
props: {
|
|
17
|
+
form: any;
|
|
18
|
+
field: any;
|
|
19
|
+
};
|
|
20
|
+
events: {
|
|
21
|
+
[evt: string]: CustomEvent<any>;
|
|
22
|
+
};
|
|
23
|
+
slots: {};
|
|
24
|
+
};
|
|
25
|
+
export {};
|