@vue-skuilder/common-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 (68) hide show
  1. package/dist/assets/index.css +10 -0
  2. package/dist/common-ui.es.js +16404 -0
  3. package/dist/common-ui.es.js.map +1 -0
  4. package/dist/common-ui.umd.js +9 -0
  5. package/dist/common-ui.umd.js.map +1 -0
  6. package/dist/components/HeatMap.types.d.ts +13 -0
  7. package/dist/components/PaginatingToolbar.types.d.ts +40 -0
  8. package/dist/components/SkMouseTrap.types.d.ts +3 -0
  9. package/dist/components/SkMouseTrapToolTip.types.d.ts +35 -0
  10. package/dist/components/SnackbarService.d.ts +11 -0
  11. package/dist/components/StudySession.types.d.ts +6 -0
  12. package/dist/components/auth/index.d.ts +4 -0
  13. package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts +22 -0
  14. package/dist/components/studentInputs/BaseUserInput.d.ts +16 -0
  15. package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +5 -0
  16. package/dist/composables/CompositionViewable.d.ts +33 -0
  17. package/dist/composables/Displayable.d.ts +47 -0
  18. package/dist/composables/index.d.ts +2 -0
  19. package/dist/index.d.ts +36 -0
  20. package/dist/plugins/pinia.d.ts +5 -0
  21. package/dist/stores/useAuthStore.d.ts +225 -0
  22. package/dist/stores/useCardPreviewModeStore.d.ts +8 -0
  23. package/dist/stores/useConfigStore.d.ts +11 -0
  24. package/dist/utils/SkldrMouseTrap.d.ts +32 -0
  25. package/package.json +67 -0
  26. package/src/components/HeatMap.types.ts +15 -0
  27. package/src/components/HeatMap.vue +354 -0
  28. package/src/components/PaginatingToolbar.types.ts +48 -0
  29. package/src/components/PaginatingToolbar.vue +75 -0
  30. package/src/components/SkMouseTrap.types.ts +3 -0
  31. package/src/components/SkMouseTrap.vue +70 -0
  32. package/src/components/SkMouseTrapToolTip.types.ts +41 -0
  33. package/src/components/SkMouseTrapToolTip.vue +316 -0
  34. package/src/components/SnackbarService.ts +39 -0
  35. package/src/components/SnackbarService.vue +71 -0
  36. package/src/components/StudySession.types.ts +6 -0
  37. package/src/components/StudySession.vue +670 -0
  38. package/src/components/StudySessionTimer.vue +121 -0
  39. package/src/components/auth/UserChip.vue +106 -0
  40. package/src/components/auth/UserLogin.vue +141 -0
  41. package/src/components/auth/UserLoginAndRegistrationContainer.vue +85 -0
  42. package/src/components/auth/UserRegistration.vue +181 -0
  43. package/src/components/auth/index.ts +4 -0
  44. package/src/components/cardRendering/AudioAutoPlayer.vue +131 -0
  45. package/src/components/cardRendering/CardLoader.vue +123 -0
  46. package/src/components/cardRendering/CardViewer.vue +101 -0
  47. package/src/components/cardRendering/CodeBlockRenderer.vue +81 -0
  48. package/src/components/cardRendering/MarkdownRenderer.vue +46 -0
  49. package/src/components/cardRendering/MarkdownRendererHelpers.ts +114 -0
  50. package/src/components/cardRendering/MdTokenRenderer.vue +244 -0
  51. package/src/components/studentInputs/BaseUserInput.ts +71 -0
  52. package/src/components/studentInputs/MultipleChoiceOption.vue +127 -0
  53. package/src/components/studentInputs/RadioMultipleChoice.types.ts +6 -0
  54. package/src/components/studentInputs/RadioMultipleChoice.vue +168 -0
  55. package/src/components/studentInputs/TrueFalse.vue +27 -0
  56. package/src/components/studentInputs/UserInputNumber.vue +63 -0
  57. package/src/components/studentInputs/UserInputString.vue +89 -0
  58. package/src/components/studentInputs/fillInInput.vue +71 -0
  59. package/src/composables/CompositionViewable.ts +180 -0
  60. package/src/composables/Displayable.ts +133 -0
  61. package/src/composables/index.ts +2 -0
  62. package/src/index.ts +79 -0
  63. package/src/plugins/pinia.ts +24 -0
  64. package/src/stores/useAuthStore.ts +92 -0
  65. package/src/stores/useCardPreviewModeStore.ts +32 -0
  66. package/src/stores/useConfigStore.ts +60 -0
  67. package/src/utils/SkldrMouseTrap.ts +141 -0
  68. package/src/vue-shims.d.ts +5 -0
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <v-btn size="large" icon :color="playing ? 'primary lighten-3' : 'primary'" :class="{ playing }" @click="play">
3
+ <v-icon>mdi-volume-high</v-icon>
4
+ </v-btn>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { ref, onBeforeUnmount, onMounted, getCurrentInstance } from 'vue';
9
+ import { SkldrMouseTrap } from '../../utils/SkldrMouseTrap';
10
+
11
+ const props = defineProps<{
12
+ src: string | string[];
13
+ }>();
14
+
15
+ const audioElems = ref<HTMLAudioElement[]>([]);
16
+ const playTimeouts = ref<NodeJS.Timer[]>([]);
17
+ const playing = ref(false);
18
+
19
+ let staticLOCK: any = null;
20
+ const playbackGap = 500;
21
+
22
+ const stop = () => {
23
+ playing.value = false;
24
+ setTimeout(() => {
25
+ staticLOCK = null;
26
+ }, playbackGap);
27
+
28
+ playTimeouts.value.forEach(clearTimeout);
29
+
30
+ console.log(`Audio stopping...`);
31
+ audioElems.value.forEach((audio) => {
32
+ if (!audio.paused) {
33
+ audio.pause();
34
+ audio.currentTime = 0;
35
+ }
36
+ });
37
+ };
38
+
39
+ const downloadFinished = (i: number): boolean => {
40
+ try {
41
+ return !isNaN(audioElems.value[i].duration);
42
+ } catch (e) {
43
+ throw new Error('AudioPlayer does not have an element at this index:', e);
44
+ }
45
+ };
46
+
47
+ const playByIndex = (n: number) => {
48
+ if (downloadFinished(n)) {
49
+ audioElems.value[n].play();
50
+
51
+ if (n + 1 < audioElems.value.length) {
52
+ const delay = (audioElems.value[n].duration + 0.7) * 1000;
53
+ playTimeouts.value.push(
54
+ setTimeout(() => {
55
+ if (playing.value) {
56
+ playByIndex(n + 1);
57
+ }
58
+ }, delay)
59
+ );
60
+ } else {
61
+ setTimeout(() => {
62
+ playing.value = false;
63
+ }, audioElems.value[n].duration * 1000);
64
+ setTimeout(() => {
65
+ staticLOCK = null;
66
+ }, audioElems.value[n].duration * 1000 + playbackGap);
67
+ }
68
+ } else {
69
+ setTimeout(playByIndex, 100, n);
70
+ }
71
+ };
72
+
73
+ const play = () => {
74
+ if (staticLOCK === null || staticLOCK === getCurrentInstance()) {
75
+ staticLOCK = getCurrentInstance();
76
+ playing.value = true;
77
+ playByIndex(0);
78
+ } else {
79
+ setTimeout(play, 100);
80
+ }
81
+ };
82
+
83
+ onMounted(() => {
84
+ staticLOCK = null;
85
+
86
+ if (typeof props.src === 'string') {
87
+ audioElems.value.push(new Audio(props.src));
88
+ } else {
89
+ props.src.forEach((url) => {
90
+ audioElems.value.push(new Audio(url));
91
+ });
92
+ }
93
+
94
+ // Define hotkeys
95
+ const hotkeys = [
96
+ {
97
+ hotkey: 'up',
98
+ callback: play,
99
+ command: 'Replay Audio',
100
+ },
101
+ ];
102
+
103
+ // Add bindings
104
+ SkldrMouseTrap.addBinding(hotkeys);
105
+
106
+ play();
107
+ });
108
+
109
+ onBeforeUnmount(() => {
110
+ // Clean up hotkey bindings
111
+ SkldrMouseTrap.removeBinding('up');
112
+ stop();
113
+ });
114
+ </script>
115
+
116
+ <style scoped>
117
+ .playing {
118
+ /* transform: rotate(3deg) scale(1.15); */
119
+ animation: 0.85s ease-in-out infinite alternate pulse;
120
+ z-index: 1;
121
+ }
122
+
123
+ @keyframes pulse {
124
+ 0% {
125
+ transform: scale(1);
126
+ }
127
+ 100% {
128
+ transform: scale(1.15);
129
+ }
130
+ }
131
+ </style>
@@ -0,0 +1,123 @@
1
+ <template>
2
+ <card-viewer
3
+ v-if="!loading"
4
+ class="ma-2"
5
+ :class="loading ? 'muted' : ''"
6
+ :view="view"
7
+ :data="data"
8
+ :card_id="cardID"
9
+ :course_id="courseID"
10
+ :session-order="sessionOrder"
11
+ @emit-response="processResponse($event)"
12
+ />
13
+ </template>
14
+
15
+ <script lang="ts">
16
+ import { defineComponent, PropType } from 'vue';
17
+ import { getDataLayer, CardData, CardRecord, DisplayableData } from '@vue-skuilder/db';
18
+ import { log, displayableDataToViewData, ViewData, ViewDescriptor } from '@vue-skuilder/common';
19
+ import { ViewComponent } from '../../composables';
20
+ import CardViewer from './CardViewer.vue';
21
+
22
+ export default defineComponent({
23
+ name: 'CardLoader',
24
+
25
+ components: {
26
+ CardViewer,
27
+ },
28
+
29
+ props: {
30
+ sessionOrder: {
31
+ type: Number,
32
+ required: false,
33
+ default: 0,
34
+ },
35
+ qualified_id: {
36
+ type: String,
37
+ required: true,
38
+ },
39
+ viewLookup: {
40
+ type: Function as PropType<(viewDescription: ViewDescriptor | string) => ViewComponent>,
41
+ required: true,
42
+ },
43
+ },
44
+
45
+ data() {
46
+ return {
47
+ loading: true,
48
+ view: null as ViewComponent | null,
49
+ data: [] as ViewData[],
50
+ courseID: '',
51
+ cardID: '',
52
+ };
53
+ },
54
+
55
+ created() {
56
+ this.loadCard();
57
+ },
58
+
59
+ methods: {
60
+ processResponse(r: CardRecord) {
61
+ log(`
62
+ Card was displayed at ${r.timeStamp}
63
+ User spent ${r.timeSpent} milliseconds with the card.
64
+ `);
65
+ this.$emit('emitResponse', r);
66
+ },
67
+
68
+ async loadCard() {
69
+ const qualified_id = this.qualified_id;
70
+ console.log(`Card Loader displaying: ${qualified_id}`);
71
+
72
+ this.loading = true;
73
+ const _courseID = qualified_id.split('-')[0];
74
+ const _cardID = qualified_id.split('-')[1];
75
+ const courseDB = getDataLayer().getCourseDB(_courseID);
76
+
77
+ try {
78
+ const tmpCardData = await courseDB.getCourseDoc<CardData>(_cardID);
79
+ const tmpView = this.viewLookup(tmpCardData.id_view);
80
+ const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {
81
+ return courseDB.getCourseDoc<DisplayableData>(id, {
82
+ attachments: true,
83
+ binary: true,
84
+ });
85
+ });
86
+
87
+ const tmpData = [];
88
+
89
+ for (const docPromise of tmpDataDocs) {
90
+ const doc = await docPromise;
91
+ tmpData.unshift(displayableDataToViewData(doc));
92
+ }
93
+
94
+ this.data = tmpData;
95
+ this.view = tmpView as ViewComponent;
96
+ this.cardID = _cardID;
97
+ this.courseID = _courseID;
98
+ } catch (e) {
99
+ throw new Error(`[CardLoader] Error loading card: ${JSON.stringify(e)}, ${e}`);
100
+ } finally {
101
+ this.loading = false;
102
+ this.$emit('card-loaded');
103
+ }
104
+ },
105
+ },
106
+ });
107
+ </script>
108
+
109
+ <style scoped>
110
+ .cardView {
111
+ padding: 15px;
112
+ border-radius: 8px;
113
+ }
114
+
115
+ .component-fade-enter-active,
116
+ .component-fade-leave-active {
117
+ transition: opacity 0.3s ease;
118
+ }
119
+ .component-fade-enter, .component-fade-leave-to
120
+ /* .component-fade-leave-active below version 2.1.8 */ {
121
+ opacity: 0;
122
+ }
123
+ </style>
@@ -0,0 +1,101 @@
1
+ <template>
2
+ <v-card elevation="12">
3
+ <transition name="component-fade" mode="out-in">
4
+ <component
5
+ :is="view"
6
+ ref="activeView"
7
+ :key="course_id + '-' + card_id + '-' + sessionOrder"
8
+ :data="data"
9
+ :modify-difficulty="user_elo.global.score - card_elo"
10
+ class="cardView ma-2 pa-2"
11
+ @emit-response="processResponse($event)"
12
+ />
13
+ </transition>
14
+ </v-card>
15
+ </template>
16
+
17
+ <script lang="ts">
18
+ import { defineComponent, PropType } from 'vue';
19
+ import { CardRecord } from '@vue-skuilder/db';
20
+ import { CourseElo, ViewData } from '@vue-skuilder/common';
21
+ import { ViewComponent } from '../../composables';
22
+
23
+ interface CardViewerRefs {
24
+ activeView: ViewComponent;
25
+ }
26
+
27
+ export default defineComponent({
28
+ name: 'CardViewer',
29
+
30
+ ref: {} as CardViewerRefs,
31
+
32
+ props: {
33
+ sessionOrder: {
34
+ type: Number,
35
+ required: false,
36
+ default: 0,
37
+ },
38
+ card_id: {
39
+ type: String as () => PouchDB.Core.DocumentId,
40
+ required: true,
41
+ default: '',
42
+ },
43
+ course_id: {
44
+ type: String,
45
+ required: true,
46
+ default: '',
47
+ },
48
+ view: {
49
+ type: [Function, Object] as PropType<ViewComponent>,
50
+ required: true,
51
+ },
52
+ data: {
53
+ type: Array as () => ViewData[],
54
+ required: true,
55
+ },
56
+ user_elo: {
57
+ type: Object as () => CourseElo,
58
+ default: () => ({
59
+ global: {
60
+ score: 1000,
61
+ count: 0,
62
+ },
63
+ tags: {},
64
+ misc: {},
65
+ }),
66
+ },
67
+ card_elo: {
68
+ type: Number,
69
+ default: 1000,
70
+ },
71
+ },
72
+
73
+ emits: ['emitResponse'],
74
+
75
+ methods: {
76
+ processResponse(r: CardRecord): void {
77
+ console.log(`
78
+ Card was displayed at ${r.timeStamp}
79
+ User spent ${r.timeSpent} milliseconds with the card.
80
+ `);
81
+ this.$emit('emitResponse', r);
82
+ },
83
+ },
84
+ });
85
+ </script>
86
+
87
+ <style scoped>
88
+ .cardView {
89
+ padding: 15px;
90
+ border-radius: 8px;
91
+ }
92
+
93
+ .component-fade-enter-active,
94
+ .component-fade-leave-active {
95
+ transition: opacity 0.3s ease;
96
+ }
97
+ .component-fade-enter, .component-fade-leave-to
98
+ /* .component-fade-leave-active below version 2.1.8 */ {
99
+ opacity: 0;
100
+ }
101
+ </style>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div class="code-block-wrapper pa-2">
3
+ <div class="language-indicator" v-if="language">{{ language }}</div>
4
+ <highlightjs :language="language" :code="code" />
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import hljs from 'highlight.js/lib/core';
10
+ import hljsVuePlugin from '@highlightjs/vue-plugin';
11
+ import { getCurrentInstance } from 'vue';
12
+
13
+ // Import only the languages you need
14
+ import javascript from 'highlight.js/lib/languages/javascript';
15
+ import typescript from 'highlight.js/lib/languages/typescript';
16
+ import css from 'highlight.js/lib/languages/css';
17
+ import html from 'highlight.js/lib/languages/xml';
18
+ import bash from 'highlight.js/lib/languages/bash';
19
+ import json from 'highlight.js/lib/languages/json';
20
+ import go from 'highlight.js/lib/languages/go';
21
+ import python from 'highlight.js/lib/languages/python';
22
+
23
+ // Register languages
24
+ hljs.registerLanguage('js', javascript);
25
+ hljs.registerLanguage('javascript', javascript);
26
+ hljs.registerLanguage('ts', typescript);
27
+ hljs.registerLanguage('typescript', typescript);
28
+ hljs.registerLanguage('css', css);
29
+ hljs.registerLanguage('html', html);
30
+ hljs.registerLanguage('bash', bash);
31
+ hljs.registerLanguage('sh', bash);
32
+ hljs.registerLanguage('json', json);
33
+ hljs.registerLanguage('go', go);
34
+ hljs.registerLanguage('golang', go);
35
+ hljs.registerLanguage('python', python);
36
+
37
+ // Get access to the current component instance
38
+ const instance = getCurrentInstance();
39
+ if (instance) {
40
+ // Register the component locally
41
+ instance.appContext.app.component('highlightjs', hljsVuePlugin.component);
42
+ }
43
+
44
+ defineProps({
45
+ code: {
46
+ type: String,
47
+ required: true,
48
+ },
49
+ language: {
50
+ type: String,
51
+ default: '',
52
+ },
53
+ });
54
+ </script>
55
+
56
+ <style>
57
+ /* Import a highlight.js theme */
58
+ @import 'highlight.js/styles/github.css';
59
+
60
+ .code-block-wrapper {
61
+ margin: 1rem 0;
62
+ border-radius: 4px;
63
+ background-color: #f6f8fa;
64
+ overflow: auto;
65
+ position: relative;
66
+ }
67
+
68
+ .language-indicator {
69
+ position: absolute;
70
+ top: 8px;
71
+ right: 10px;
72
+ padding: 2px 8px;
73
+ font-size: 0.75rem;
74
+ color: #666;
75
+ background-color: rgba(255, 255, 255, 0.7);
76
+ border-radius: 4px;
77
+ z-index: 1;
78
+ font-family: monospace;
79
+ text-transform: lowercase;
80
+ }
81
+ </style>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <div>
3
+ <!-- <v-textarea v-if="testRoute" v-model="md" variant="outlined" label="Type Here" name="name" /> -->
4
+ <span v-for="(token, i) in tokens" :key="i">
5
+ <md-token-renderer v-if="token.type" :token="token" :last="i === tokens.length - 1" />
6
+ <audio-auto-player v-else-if="token.audio" :src="token.audio" />
7
+ <!-- // [ ] insert img display here. "if token.image ? (also see if the audio aboke is functional)" -->
8
+ </span>
9
+ </div>
10
+ </template>
11
+
12
+ <script lang="ts">
13
+ import { defineComponent, PropType } from 'vue';
14
+ import MdTokenRenderer from './MdTokenRenderer.vue';
15
+ import AudioAutoPlayer from './AudioAutoPlayer.vue';
16
+ import * as marked from 'marked';
17
+
18
+ type SkldrToken =
19
+ | marked.Token
20
+ | {
21
+ type: false;
22
+ audio: string;
23
+ };
24
+
25
+ // Using Options API with mixin
26
+ export default defineComponent({
27
+ name: 'MarkdownRenderer',
28
+ components: {
29
+ MdTokenRenderer,
30
+ AudioAutoPlayer,
31
+ },
32
+ props: {
33
+ md: {
34
+ type: String as PropType<string>,
35
+ required: true,
36
+ },
37
+ },
38
+ computed: {
39
+ tokens(): SkldrToken[] {
40
+ return marked.lexer(this.md);
41
+ },
42
+ },
43
+ });
44
+ </script>
45
+
46
+ <style lang="css" scoped></style>
@@ -0,0 +1,114 @@
1
+ import { Token, MarkedToken, Tokens } from 'marked';
2
+ import * as marked from 'marked';
3
+
4
+ /**
5
+ * recursively splits text according to the passed delimiters.
6
+ *
7
+ * eg: ("abcde", "b", "d") => ["a", "bcd", "e"]
8
+ * ("a[b][c]", "[", "]") => ["a", "[b]", "[c]"]
9
+ *
10
+ * it does not check that the delimiters are well formed in the text
11
+ * @param text the text to be split
12
+ * @param l the left delimiter
13
+ * @param r the right delimiter
14
+ * @returns the split result
15
+ */
16
+ export function splitByDelimiters(text: string, l: string, r: string): string[] {
17
+ if (text.length === 0) return [];
18
+
19
+ let ret: string[] = [];
20
+
21
+ const left = text.indexOf(l);
22
+ const right = text.indexOf(r, left);
23
+
24
+ if (left >= 0 && right > left) {
25
+ // pre-delimited characters
26
+ ret.push(text.substring(0, left));
27
+ // delimited section
28
+ ret.push(text.substring(left, right + r.length));
29
+ // recurse on remaining text
30
+ ret = ret.concat(splitByDelimiters(text.substring(right + r.length), l, r));
31
+ } else {
32
+ return [text];
33
+ }
34
+
35
+ return ret;
36
+ }
37
+
38
+ export function splitTextToken(token: Tokens.Text): Tokens.Text[] {
39
+ if (containsComponent(token)) {
40
+ const textChunks = splitByDelimiters(token.text, '{{', '}}');
41
+ const rawChunks = splitByDelimiters(token.raw, '{{', '}}');
42
+
43
+ if (textChunks.length === rawChunks.length) {
44
+ return textChunks.map((c, i) => {
45
+ return {
46
+ type: 'text',
47
+ text: textChunks[i],
48
+ raw: rawChunks[i],
49
+ };
50
+ });
51
+ } else {
52
+ throw new Error(`Error parsing markdown`);
53
+ }
54
+ } else {
55
+ return [token];
56
+ }
57
+ }
58
+
59
+ export type TokenOrComponent = MarkedToken | { type: 'component'; raw: string };
60
+
61
+ export function splitParagraphToken(token: Tokens.Paragraph): TokenOrComponent[] {
62
+ let ret: MarkedToken[] = [];
63
+
64
+ if (containsComponent(token)) {
65
+ const textChunks = splitByDelimiters(token.text, '{{', '}}');
66
+ const rawChunks = splitByDelimiters(token.raw, '{{', '}}');
67
+ if (textChunks.length === rawChunks.length) {
68
+ for (let i = 0; i < textChunks.length; i++) {
69
+ const textToken = {
70
+ type: 'text',
71
+ text: textChunks[i],
72
+ raw: rawChunks[i],
73
+ } as Tokens.Text;
74
+
75
+ if (isComponent(textToken)) {
76
+ ret.push(textToken);
77
+ } else {
78
+ marked.lexer(rawChunks[i]).forEach((t) => {
79
+ if (t.type === 'paragraph') {
80
+ ret = ret.concat(t.tokens as MarkedToken[]);
81
+ } else {
82
+ ret.push(t as MarkedToken);
83
+ }
84
+ });
85
+ }
86
+ }
87
+ return ret;
88
+ } else {
89
+ throw new Error(`Error parsing Markdown`);
90
+ }
91
+ } else {
92
+ ret.push(token);
93
+ }
94
+ return ret;
95
+ }
96
+
97
+ export function containsComponent(token: MarkedToken) {
98
+ if (token.type === 'text' || token.type === 'paragraph') {
99
+ const opening = token.raw.indexOf('{{');
100
+ const closing = token.raw.indexOf('}}');
101
+
102
+ if (opening !== -1 && closing !== -1 && closing > opening) {
103
+ return true;
104
+ } else {
105
+ return false;
106
+ }
107
+ } else {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ export function isComponent(token: MarkedToken) {
113
+ return token.type === 'text' && token.text.startsWith('{{') && token.text.endsWith('}}');
114
+ }