@opensalt/ob3-definer 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/.vscode/extensions.json +3 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/favicon.svg +7 -0
- package/dist/index.html +20 -0
- package/dist/ob3-definer.css +1 -0
- package/dist/ob3-definer.js +32 -0
- package/formkit.config.js +27 -0
- package/index.html +19 -0
- package/jsconfig.json +8 -0
- package/package.json +35 -0
- package/public/favicon.svg +7 -0
- package/src/App.vue +19 -0
- package/src/assets/base.css +86 -0
- package/src/assets/main.css +24 -0
- package/src/assets/style.scss +33 -0
- package/src/components/AchievementCriteria.vue +78 -0
- package/src/components/AchievementDefiner.vue +212 -0
- package/src/components/AchievementImage.vue +116 -0
- package/src/components/AchievementType.vue +111 -0
- package/src/components/AdditionalTab.vue +32 -0
- package/src/components/AddressComponent.vue +118 -0
- package/src/components/AlignmentComponent.vue +141 -0
- package/src/components/AlignmentsComponent.vue +13 -0
- package/src/components/AlignmentsTab.vue +18 -0
- package/src/components/BasicTab.vue +55 -0
- package/src/components/CreatorProfile.vue +166 -0
- package/src/components/CriterionLevels.vue +97 -0
- package/src/components/DetailTab.vue +72 -0
- package/src/components/IndividualName.vue +63 -0
- package/src/components/MarkdownRenderer.vue +20 -0
- package/src/components/OtherIdentifiers.vue +116 -0
- package/src/components/RelatedList.vue +89 -0
- package/src/components/ResultDescription.vue +120 -0
- package/src/components/ResultType.vue +94 -0
- package/src/components/TagList.vue +121 -0
- package/src/components/ValueList.vue +144 -0
- package/src/inputs/innerLabelTextInput.js +62 -0
- package/src/inputs/innerLabelTextareaInput.js +57 -0
- package/src/inputs/selectInputGroup.js +76 -0
- package/src/main.js +50 -0
- package/src/stores/credential.js +292 -0
- package/test-index.html +17 -0
- package/trial-key +3 -0
- package/vite.config.js +39 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { generateClasses } from '@formkit/themes'
|
|
2
|
+
|
|
3
|
+
const config = {
|
|
4
|
+
config: {
|
|
5
|
+
classes: generateClasses({
|
|
6
|
+
global: { // classes
|
|
7
|
+
outer: '$reset mb-4',
|
|
8
|
+
input: 'form-control',
|
|
9
|
+
label: 'form-label',
|
|
10
|
+
help: 'form-text',
|
|
11
|
+
message: 'invalid-feedback',
|
|
12
|
+
},
|
|
13
|
+
form: {
|
|
14
|
+
form: "mt-1 mx-auto p-3 border rounded"
|
|
15
|
+
},
|
|
16
|
+
range: {
|
|
17
|
+
input: '$reset form-range',
|
|
18
|
+
},
|
|
19
|
+
submit: {
|
|
20
|
+
outer: '$reset mt-3',
|
|
21
|
+
input: '$reset btn btn-primary'
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default config
|
package/index.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<link rel="icon" href="/favicon.svg">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>OB3 Achievement Definer</title>
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
|
9
|
+
<script type="module" src="/src/main.js"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="ob3-definer"></div>
|
|
13
|
+
<script>
|
|
14
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
15
|
+
window.dispatchEvent(new CustomEvent('ob3-open'));
|
|
16
|
+
})
|
|
17
|
+
</script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
package/jsconfig.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opensalt/ob3-definer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/opensalt/OB3DefinitionWidget.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"description": "A widget to create an Open Badge achievement definition.",
|
|
11
|
+
"main": "dist/ob3-definer.js",
|
|
12
|
+
"style": "dist/ob3-definer.css",
|
|
13
|
+
"unpkg": "dist/ob3-definer.js",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "vite",
|
|
16
|
+
"build": "vite build",
|
|
17
|
+
"preview": "vite preview"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@formkit/auto-animate": "^0.8.2",
|
|
21
|
+
"@formkit/themes": "^1.6.5",
|
|
22
|
+
"@formkit/utils": "^1.6.5",
|
|
23
|
+
"@formkit/vue": "^1.6.5",
|
|
24
|
+
"@popperjs/core": "^2.11.8",
|
|
25
|
+
"bootstrap": "^5.3.3",
|
|
26
|
+
"markdown-it": "^14.1.0",
|
|
27
|
+
"uuid": "^10.0.0",
|
|
28
|
+
"vue": "^3.4.29"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@vitejs/plugin-vue": "^5.0.5",
|
|
32
|
+
"sass": "1.77.6",
|
|
33
|
+
"vite": "^5.3.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g id="Layer_1">
|
|
3
|
+
<g id="favicon" stroke-width="0"/>
|
|
4
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.586 5.062l7.876 7.878-9.063 9.156-3.39-3.387 9.063-9.157-2.977-2.977-5.502 4.086 3.993 4.007-1.414 1.415-4.007-4.008-1.472 1.374 5.502 4.086 2.836-2.838 3.888 3.89z" fill="#58B4AE"/>
|
|
5
|
+
<path d="M3.324 18.981l3.389 3.389 9.787-9.924-3.39-3.388-9.786 9.923zM7.51 22.27l-2.977-2.977-.732.866 2.836 2.838 3.112-2.256-.473-.47-2.766 2.999.232.125.284-.125 1.382-.811z" fill="#95E1D3"/>
|
|
6
|
+
</g>
|
|
7
|
+
</svg>
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import AchievementDefiner from "@/components/AchievementDefiner.vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
achievement: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: ""
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div class="container">
|
|
14
|
+
<AchievementDefiner :achievement="achievement"/>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<style scoped>
|
|
19
|
+
</style>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* color palette from <https://github.com/vuejs/theme> */
|
|
2
|
+
:root {
|
|
3
|
+
--vt-c-white: #ffffff;
|
|
4
|
+
--vt-c-white-soft: #f8f8f8;
|
|
5
|
+
--vt-c-white-mute: #f2f2f2;
|
|
6
|
+
|
|
7
|
+
--vt-c-black: #181818;
|
|
8
|
+
--vt-c-black-soft: #222222;
|
|
9
|
+
--vt-c-black-mute: #282828;
|
|
10
|
+
|
|
11
|
+
--vt-c-indigo: #2c3e50;
|
|
12
|
+
|
|
13
|
+
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
|
14
|
+
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
|
15
|
+
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
|
16
|
+
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
|
17
|
+
|
|
18
|
+
--vt-c-text-light-1: var(--vt-c-indigo);
|
|
19
|
+
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
|
20
|
+
--vt-c-text-dark-1: var(--vt-c-white);
|
|
21
|
+
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* semantic color variables for this project */
|
|
25
|
+
:root {
|
|
26
|
+
--color-background: var(--vt-c-white);
|
|
27
|
+
--color-background-soft: var(--vt-c-white-soft);
|
|
28
|
+
--color-background-mute: var(--vt-c-white-mute);
|
|
29
|
+
|
|
30
|
+
--color-border: var(--vt-c-divider-light-2);
|
|
31
|
+
--color-border-hover: var(--vt-c-divider-light-1);
|
|
32
|
+
|
|
33
|
+
--color-heading: var(--vt-c-text-light-1);
|
|
34
|
+
--color-text: var(--vt-c-text-light-1);
|
|
35
|
+
|
|
36
|
+
--section-gap: 160px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@media (prefers-color-scheme: dark) {
|
|
40
|
+
:root {
|
|
41
|
+
--color-background: var(--vt-c-black);
|
|
42
|
+
--color-background-soft: var(--vt-c-black-soft);
|
|
43
|
+
--color-background-mute: var(--vt-c-black-mute);
|
|
44
|
+
|
|
45
|
+
--color-border: var(--vt-c-divider-dark-2);
|
|
46
|
+
--color-border-hover: var(--vt-c-divider-dark-1);
|
|
47
|
+
|
|
48
|
+
--color-heading: var(--vt-c-text-dark-1);
|
|
49
|
+
--color-text: var(--vt-c-text-dark-2);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
*,
|
|
54
|
+
*::before,
|
|
55
|
+
*::after {
|
|
56
|
+
box-sizing: border-box;
|
|
57
|
+
margin: 0;
|
|
58
|
+
font-weight: normal;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
body {
|
|
62
|
+
min-height: 100vh;
|
|
63
|
+
color: var(--color-text);
|
|
64
|
+
background: var(--color-background);
|
|
65
|
+
transition:
|
|
66
|
+
color 0.5s,
|
|
67
|
+
background-color 0.5s;
|
|
68
|
+
line-height: 1.6;
|
|
69
|
+
font-family:
|
|
70
|
+
Inter,
|
|
71
|
+
-apple-system,
|
|
72
|
+
BlinkMacSystemFont,
|
|
73
|
+
'Segoe UI',
|
|
74
|
+
Roboto,
|
|
75
|
+
Oxygen,
|
|
76
|
+
Ubuntu,
|
|
77
|
+
Cantarell,
|
|
78
|
+
'Fira Sans',
|
|
79
|
+
'Droid Sans',
|
|
80
|
+
'Helvetica Neue',
|
|
81
|
+
sans-serif;
|
|
82
|
+
font-size: 15px;
|
|
83
|
+
text-rendering: optimizeLegibility;
|
|
84
|
+
-webkit-font-smoothing: antialiased;
|
|
85
|
+
-moz-osx-font-smoothing: grayscale;
|
|
86
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
@import './base.css';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
#app {
|
|
6
|
+
max-width: 1280px;
|
|
7
|
+
margin: 0 auto;
|
|
8
|
+
padding: 2rem;
|
|
9
|
+
font-weight: normal;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media (min-width: 1024px) {
|
|
13
|
+
body {
|
|
14
|
+
display: flex;
|
|
15
|
+
width: 1024px;
|
|
16
|
+
/*place-items: center;*/
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#app {
|
|
20
|
+
display: grid;
|
|
21
|
+
grid-template-columns: 1fr 1fr;
|
|
22
|
+
padding: 0 2rem;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@import 'bootstrap/scss/bootstrap';
|
|
2
|
+
|
|
3
|
+
[data-complete="true"]:not([data-empty="true"]) .form-control {
|
|
4
|
+
//@extend .form-control, .is-valid;
|
|
5
|
+
/*
|
|
6
|
+
border-color: var(--bs-form-valid-border-color);
|
|
7
|
+
padding-right: calc(1.5em + 0.75rem);
|
|
8
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
|
9
|
+
background-repeat: no-repeat;
|
|
10
|
+
background-position: right calc(0.375em + 0.1875rem) center;
|
|
11
|
+
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
|
12
|
+
*/
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
[data-invalid="true"] .form-control {
|
|
17
|
+
//@extend .form-control, .is-invalid;
|
|
18
|
+
border-color: var(--bs-form-invalid-border-color);
|
|
19
|
+
padding-right: calc(1.5em + 0.75rem);
|
|
20
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
|
|
21
|
+
background-repeat: no-repeat;
|
|
22
|
+
background-position: right calc(0.375em + 0.1875rem) center;
|
|
23
|
+
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[data-invalid="true"] .invalid-feedback {
|
|
27
|
+
display: inline;
|
|
28
|
+
width: 100%;
|
|
29
|
+
margin-top: 0.25rem;
|
|
30
|
+
font-size: 0.875em;
|
|
31
|
+
color: var(--bs-form-invalid-color);
|
|
32
|
+
}
|
|
33
|
+
*/
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {onMounted, ref, watch} from "vue";
|
|
3
|
+
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
|
|
4
|
+
|
|
5
|
+
const model = defineModel({ default: {
|
|
6
|
+
}});
|
|
7
|
+
const preview = ref('');
|
|
8
|
+
|
|
9
|
+
watch(model, () => {
|
|
10
|
+
preview.value = model.value.narrative;
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
preview.value = model.value.narrative ? model.value.narrative : '';
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const criteriaTab = ref("edit");
|
|
18
|
+
|
|
19
|
+
function selectTab(tab) {
|
|
20
|
+
criteriaTab.value = tab;
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="formkit-wrapper required">
|
|
26
|
+
<label class="form-label">Earning Criteria</label>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<ul class="nav nav-tabs" role="tablist">
|
|
30
|
+
<li class="nav-item" role="presentation">
|
|
31
|
+
<button class="nav-link" :class='{"active": criteriaTab === "edit"}' type="button" id="edit-tab" aria-controls="criteria-edit"
|
|
32
|
+
role="tab" :aria-selected="criteriaTab === 'edit'" @click="selectTab('edit')">Edit
|
|
33
|
+
</button>
|
|
34
|
+
</li>
|
|
35
|
+
<li class="nav-item" role="presentation">
|
|
36
|
+
<button class="nav-link" :class='{"active": criteriaTab === "preview"}' type="button" id="preview-tab"
|
|
37
|
+
aria-controls="criteria-preview" role="tab" :aria-selected="criteriaTab === 'preview'"
|
|
38
|
+
@click="selectTab('preview')">Preview
|
|
39
|
+
</button>
|
|
40
|
+
</li>
|
|
41
|
+
</ul>
|
|
42
|
+
|
|
43
|
+
<div class="tab-content" id="criteria-tab">
|
|
44
|
+
<div class="tab-pane fade" :class="{'show': (criteriaTab === 'edit'), 'active': (criteriaTab === 'edit')}"
|
|
45
|
+
id="criteria-edit" role="tabpanel" aria-labelledby="edit-tab" v-show="criteriaTab === 'edit'">
|
|
46
|
+
<FormKit type="group" name="criteria" v-model="model">
|
|
47
|
+
<FormKit
|
|
48
|
+
type="textarea"
|
|
49
|
+
label="Narrative"
|
|
50
|
+
name="narrative"
|
|
51
|
+
rows="5"
|
|
52
|
+
validation="require_one:url"
|
|
53
|
+
help="A narrative of what is needed to earn the credential. Markdown is allowed."
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<FormKit
|
|
57
|
+
type="text"
|
|
58
|
+
label="Criteria URL"
|
|
59
|
+
name="url"
|
|
60
|
+
validation="require_one:narrative|url"
|
|
61
|
+
help="The URL of a webpage that describes in a human-readable format the criteria for the credential."
|
|
62
|
+
/>
|
|
63
|
+
</FormKit>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="tab-pane fade" :class="{'show': (criteriaTab === 'preview'), 'active': (criteriaTab === 'preview')}"
|
|
67
|
+
id="criteria-preview" role="tabpanel" aria-labelledby="preview-tab" v-show="criteriaTab === 'preview'">
|
|
68
|
+
<label class="mt-3 mb-2">Narrative Preview</label>
|
|
69
|
+
<MarkdownRenderer :source="preview" class="border rounded p-3"/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<style scoped>
|
|
75
|
+
#criteria-preview {
|
|
76
|
+
min-height: 302px;
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {onBeforeMount, reactive, ref, watch} from "vue";
|
|
3
|
+
import BasicTab from "@/components/BasicTab.vue";
|
|
4
|
+
import DetailTab from "@/components/DetailTab.vue";
|
|
5
|
+
import AlignmentsTab from "@/components/AlignmentsTab.vue";
|
|
6
|
+
import AdditionalTab from "@/components/AdditionalTab.vue";
|
|
7
|
+
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
achievement: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: ""
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const emit = defineEmits(['saveDefinition']);
|
|
15
|
+
|
|
16
|
+
const tab = ref("basic");
|
|
17
|
+
const submitted = ref(false);
|
|
18
|
+
const form = ref(null);
|
|
19
|
+
|
|
20
|
+
const formData = reactive({
|
|
21
|
+
type: ['Achievement'],
|
|
22
|
+
basic: {},
|
|
23
|
+
detail: {},
|
|
24
|
+
alignments: {},
|
|
25
|
+
additional: {},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const achievementData = reactive({});
|
|
29
|
+
|
|
30
|
+
onBeforeMount(() => {
|
|
31
|
+
try {
|
|
32
|
+
const achievement = JSON.parse(props.achievement);
|
|
33
|
+
|
|
34
|
+
formData.basic = {
|
|
35
|
+
name: achievement.name || '',
|
|
36
|
+
achievementType: achievement.achievementType || null,
|
|
37
|
+
image: achievement.image || {},
|
|
38
|
+
description: achievement.description || '',
|
|
39
|
+
criteria: achievement.criteria || {},
|
|
40
|
+
};
|
|
41
|
+
formData.detail = {
|
|
42
|
+
humanCode: achievement.humanCode || null,
|
|
43
|
+
inLanguage: achievement.inLanguage || null,
|
|
44
|
+
version: achievement.version || null,
|
|
45
|
+
creditsAvailable: achievement.creditsAvailable || null,
|
|
46
|
+
specialization: achievement.specialization || null,
|
|
47
|
+
fieldOfStudy: achievement.fieldOfStudy || null,
|
|
48
|
+
};
|
|
49
|
+
formData.alignments = {
|
|
50
|
+
resultDescription: achievement.resultDescription || [],
|
|
51
|
+
alignment: achievement.alignment || [],
|
|
52
|
+
};
|
|
53
|
+
formData.additional = {
|
|
54
|
+
id: achievement.id || null,
|
|
55
|
+
tag: achievement.tag || [],
|
|
56
|
+
related: achievement.related || [],
|
|
57
|
+
otherIdentifier: achievement.otherIdentifier || [],
|
|
58
|
+
creator: achievement.creator || {},
|
|
59
|
+
};
|
|
60
|
+
/*
|
|
61
|
+
achievementData.basic = {...achievement.basic };
|
|
62
|
+
achievementData.detail = {...achievement.detail };
|
|
63
|
+
achievementData.alignments = {...achievement.alignments };
|
|
64
|
+
achievementData.additional = {...achievement.additional };
|
|
65
|
+
*/
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// No valid OB3 Achievement definition was passed
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
watch(formData, (value) => {
|
|
72
|
+
Object.assign(achievementData, value.basic);
|
|
73
|
+
Object.assign(achievementData, value.detail);
|
|
74
|
+
Object.assign(achievementData, value.alignments);
|
|
75
|
+
Object.assign(achievementData, value.additional);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function selectTab(selected) {
|
|
79
|
+
tab.value = selected;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeEmpty(obj) {
|
|
83
|
+
/*
|
|
84
|
+
return Object.fromEntries(
|
|
85
|
+
Object.entries(obj)
|
|
86
|
+
.filter(([_, v]) => v != null && v !== '' && (!Array.isArray(v) || v.length > 0))
|
|
87
|
+
.map(([k, v]) => [k, (v === Object(v) && !Array.isArray(v)) ? removeEmpty(v) : v])
|
|
88
|
+
);
|
|
89
|
+
*/
|
|
90
|
+
return JSON.parse(JSON.stringify(obj, (key, value) => {
|
|
91
|
+
return ((value === null || value === '' || (Array.isArray(value) && value.length === 0)) ? undefined : value);
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function save(formData) {
|
|
96
|
+
const cleaned = removeEmpty(formData);
|
|
97
|
+
//emit('saveDefinition', cleaned);
|
|
98
|
+
const formEl = document.getElementById(form.value.node.props.id);
|
|
99
|
+
formEl.dispatchEvent(new CustomEvent('saveDefinition', { bubbles: true, detail: JSON.stringify(cleaned) }));
|
|
100
|
+
//console.log('Submitting', cleaned);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sendChange(formData) {
|
|
104
|
+
try {
|
|
105
|
+
const formEl = document.getElementById(form.value.node.props.id);
|
|
106
|
+
const valid = form.value.node.context.state.valid;
|
|
107
|
+
const cleaned = removeEmpty(form.value.node.value);
|
|
108
|
+
formEl.dispatchEvent(new CustomEvent('changed', {bubbles: true, detail: JSON.stringify(cleaned)}));
|
|
109
|
+
if (valid) {
|
|
110
|
+
formEl.dispatchEvent(new CustomEvent('updated', {bubbles: true, detail: JSON.stringify(cleaned)}));
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function showErrors(node) {
|
|
116
|
+
submitted.value = true;
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<template>
|
|
121
|
+
<div>
|
|
122
|
+
<ul class="nav nav-tabs" role="tablist">
|
|
123
|
+
<li class="nav-item" role="presentation">
|
|
124
|
+
<button class="nav-link" :class='{"active": (tab === "basic")}' type="button" id="basic-tab" role="tab" aria-controls="tab-basic" :aria-selected="(tab === 'basic') ? 'true' : 'false'" @click="selectTab('basic')">Primary Details</button>
|
|
125
|
+
</li>
|
|
126
|
+
<li class="nav-item" role="presentation">
|
|
127
|
+
<button class="nav-link" :class='{"active": (tab === "detail")}' type="button" id="detail-tab" role="tab" aria-controls="tab-detail" :aria-selected="(tab === 'detail') ? 'true' : 'false'" @click="selectTab('detail')">Additional Details</button>
|
|
128
|
+
</li>
|
|
129
|
+
<li class="nav-item" role="presentation">
|
|
130
|
+
<button class="nav-link" :class='{"active": (tab === "alignments")}' type="button" id="detail-tab" role="tab" aria-controls="tab-alignments" :aria-selected="(tab === 'alignments') ? 'true' : 'false'" @click="selectTab('alignments')">Alignments</button>
|
|
131
|
+
</li>
|
|
132
|
+
<li class="nav-item" role="presentation">
|
|
133
|
+
<button class="nav-link" :class='{"active": (tab === "additional")}' type="button" id="additional-tab" role="tab" aria-controls="tab-additional" :aria-selected="(tab === 'additional') ? 'true' : 'false'" @click="selectTab('additional')">Additional Information</button>
|
|
134
|
+
</li>
|
|
135
|
+
</ul>
|
|
136
|
+
|
|
137
|
+
<div class="tab-content mt-3" id="tab-content">
|
|
138
|
+
<FormKit
|
|
139
|
+
type="form"
|
|
140
|
+
:actions="false"
|
|
141
|
+
ref="form"
|
|
142
|
+
@submit="save"
|
|
143
|
+
@submit-invalid="showErrors"
|
|
144
|
+
@change="sendChange"
|
|
145
|
+
#default="{ state: { valid } }"
|
|
146
|
+
validation-visibility="live"
|
|
147
|
+
>
|
|
148
|
+
<div class="alert alert-warning" role="alert" v-if="!valid && submitted">
|
|
149
|
+
There are some errors in the form submission. Please correct the errors and then resubmit the form.
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<FormKit
|
|
153
|
+
type="hidden"
|
|
154
|
+
name="@context"
|
|
155
|
+
:value="[ 'https://www.w3.org/2018/credentials/v2', 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json' ]"
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<FormKit
|
|
159
|
+
type="hidden"
|
|
160
|
+
name="type"
|
|
161
|
+
v-model="formData.type"
|
|
162
|
+
:value="[ 'Achievement' ]"
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
<BasicTab
|
|
166
|
+
v-model="formData.basic"
|
|
167
|
+
v-show="tab === 'basic'"
|
|
168
|
+
/>
|
|
169
|
+
<DetailTab
|
|
170
|
+
v-model="formData.detail"
|
|
171
|
+
v-show="tab === 'detail'"
|
|
172
|
+
/>
|
|
173
|
+
<AlignmentsTab
|
|
174
|
+
v-model="formData.alignments"
|
|
175
|
+
v-show="tab === 'alignments'"
|
|
176
|
+
/>
|
|
177
|
+
<AdditionalTab
|
|
178
|
+
v-model="formData.additional"
|
|
179
|
+
v-show="tab === 'additional'"
|
|
180
|
+
/>
|
|
181
|
+
|
|
182
|
+
<button class="btn btn-primary float-end mt-5" type="submit" :disabled="false">Save</button>
|
|
183
|
+
</FormKit>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</template>
|
|
187
|
+
|
|
188
|
+
<style>
|
|
189
|
+
.formkit-wrapper.required .form-label:before{
|
|
190
|
+
color: red;
|
|
191
|
+
content: "*";
|
|
192
|
+
position: absolute;
|
|
193
|
+
margin-left: -10px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
[data-invalid="true"] .form-control {
|
|
197
|
+
border-color: #dc3545;
|
|
198
|
+
padding-right: calc(1.5em + 0.75rem);
|
|
199
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
|
|
200
|
+
background-repeat: no-repeat;
|
|
201
|
+
background-position: right calc(0.375em + 0.1875rem) center;
|
|
202
|
+
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
[data-invalid="true"] .invalid-feedback {
|
|
206
|
+
display: inline;
|
|
207
|
+
width: 100%;
|
|
208
|
+
margin-top: 0.25rem;
|
|
209
|
+
font-size: 0.875em;
|
|
210
|
+
color: #dc3545;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {onBeforeMount, onMounted, ref, watch} from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
help: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: 'An image that represents the credential. Must be a PNG or SVG image.'
|
|
8
|
+
}
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const model = defineModel({ default: { id: '', type: 'Image', caption: ''} });
|
|
12
|
+
const emit = defineEmits(['update:modelValue']);
|
|
13
|
+
const image = ref('');
|
|
14
|
+
const caption = ref('');
|
|
15
|
+
|
|
16
|
+
onBeforeMount(() => {
|
|
17
|
+
// console.log('Mounted', model.value);
|
|
18
|
+
image.value = model.value.id || null;
|
|
19
|
+
caption.value = model.value.caption || null;
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
watch(caption, (newCaption) => {
|
|
23
|
+
// console.log('watching caption', newCaption);
|
|
24
|
+
model.value.caption = newCaption;
|
|
25
|
+
emit('update:modelValue', model.value);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
watch(image, (newImage) => {
|
|
29
|
+
// console.log('watching', newImage);
|
|
30
|
+
model.value.id = newImage;
|
|
31
|
+
model.value.type = newImage ? 'Image' : null;
|
|
32
|
+
emit('update:modelValue', model.value);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function handleImage(e) {
|
|
36
|
+
// console.log('handle image');
|
|
37
|
+
|
|
38
|
+
if (0 === Object.keys(e).length) {
|
|
39
|
+
// console.log('no keys');
|
|
40
|
+
image.value = null;
|
|
41
|
+
caption.value = null;
|
|
42
|
+
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const file = e[0].file;
|
|
47
|
+
const reader = new FileReader();
|
|
48
|
+
|
|
49
|
+
reader.onerror = (e) => {
|
|
50
|
+
// console.log('image error');
|
|
51
|
+
image.value = null;
|
|
52
|
+
};
|
|
53
|
+
reader.onload = (e) => {
|
|
54
|
+
// Use the loaded image data to set the badgeImage
|
|
55
|
+
image.value = reader.result;
|
|
56
|
+
// console.log('onload');
|
|
57
|
+
|
|
58
|
+
if (image.value.substring(0, 11) !== 'data:image/') {
|
|
59
|
+
// console.log(image.value);
|
|
60
|
+
image.value = null;
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
reader.readAsDataURL(file);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// console.log('error', e);
|
|
70
|
+
image.value = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<FormKit
|
|
77
|
+
type="group"
|
|
78
|
+
ignore="true"
|
|
79
|
+
>
|
|
80
|
+
<FormKit
|
|
81
|
+
label="Image"
|
|
82
|
+
type="file"
|
|
83
|
+
accept="image/png, image/svg+xml"
|
|
84
|
+
:help="props.help"
|
|
85
|
+
inner-class=""
|
|
86
|
+
@input="handleImage"
|
|
87
|
+
ignore="true"
|
|
88
|
+
>
|
|
89
|
+
<template #fileList></template>
|
|
90
|
+
<template #suffix>
|
|
91
|
+
<figure class="figure ms-3 mt-2" v-show="image">
|
|
92
|
+
<img :src="image" alt="Image preview" class="img-thumbnail figure-img img-fluid rounded" id="cm-image-thumbnail" v-show="image">
|
|
93
|
+
<figcaption class="figure-caption text-center">Image preview</figcaption>
|
|
94
|
+
</figure>
|
|
95
|
+
</template>
|
|
96
|
+
</FormKit>
|
|
97
|
+
|
|
98
|
+
<FormKit
|
|
99
|
+
label="Image Caption"
|
|
100
|
+
type="text"
|
|
101
|
+
v-model="caption"
|
|
102
|
+
help="A caption for the image."
|
|
103
|
+
v-show="image"
|
|
104
|
+
ignore="true"
|
|
105
|
+
/>
|
|
106
|
+
</FormKit>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<style scoped>
|
|
110
|
+
#cm-image-thumbnail {
|
|
111
|
+
max-width: 200px;
|
|
112
|
+
max-height: 200px;
|
|
113
|
+
object-fit: contain;
|
|
114
|
+
margin-bottom: 10px;
|
|
115
|
+
}
|
|
116
|
+
</style>
|