@erudit-js/prose 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/default/Inliners.vue +11 -11
- package/dist/app/default/Mix.vue +11 -11
- package/dist/app/default/Text.vue +25 -25
- package/dist/app/shared/Prose.vue +40 -40
- package/dist/app/shared/Render.vue +51 -51
- package/dist/app/shared/assets/block.svg +2 -2
- package/dist/app/shared/assets/check.svg +2 -2
- package/dist/app/shared/assets/inliner.svg +2 -2
- package/dist/app/shared/assets/plus.svg +2 -2
- package/dist/app/shared/block/AsideMenu.vue +44 -44
- package/dist/app/shared/block/AsideMenuButton.vue +51 -51
- package/dist/app/shared/block/AsideMenuCopyLink.vue +40 -40
- package/dist/app/shared/block/AsideMenuSeparator.vue +3 -3
- package/dist/app/shared/block/Block.vue +270 -270
- package/dist/app/shared/inliner/Inliner.vue +11 -11
- package/dist/app/shared/photoswipe/style.css +22 -22
- package/dist/elements/accent/Accent.vue +88 -88
- package/dist/elements/accent/AccentColumnSection.vue +61 -61
- package/dist/elements/accent/AccentRowSections.vue +64 -64
- package/dist/elements/callout/Callout.vue +81 -81
- package/dist/elements/callout/_global.d.ts +15 -15
- package/dist/elements/caption/Caption.vue +44 -44
- package/dist/elements/caption/_global.d.ts +26 -26
- package/dist/elements/details/Details.vue +49 -49
- package/dist/elements/details/_global.d.ts +27 -27
- package/dist/elements/details/icon.svg +2 -2
- package/dist/elements/diagram/Diagram.vue +360 -360
- package/dist/elements/diagram/_global.d.ts +19 -19
- package/dist/elements/emphasis/Emphasis.vue +25 -25
- package/dist/elements/emphasis/_global.d.ts +18 -18
- package/dist/elements/flex/Flex.vue +36 -36
- package/dist/elements/flex/_global.d.ts +23 -23
- package/dist/elements/gallery/Gallery.vue +56 -56
- package/dist/elements/gallery/_global.d.ts +18 -18
- package/dist/elements/heading/Heading.vue +44 -44
- package/dist/elements/heading/_global.d.ts +42 -42
- package/dist/elements/heading/icon.svg +2 -2
- package/dist/elements/horizontalLine/HorizontalLine.vue +6 -6
- package/dist/elements/horizontalLine/_global.d.ts +17 -17
- package/dist/elements/image/Image.vue +15 -15
- package/dist/elements/image/ImageElement.vue +80 -80
- package/dist/elements/image/_global.d.ts +18 -18
- package/dist/elements/lineBreak/LineBreak.vue +3 -3
- package/dist/elements/lineBreak/_global.d.ts +18 -18
- package/dist/elements/link/BlockLink.vue +108 -108
- package/dist/elements/link/Link.vue +92 -92
- package/dist/elements/link/dependency/_global.d.ts +47 -47
- package/dist/elements/link/reference/_global.d.ts +49 -49
- package/dist/elements/list/List.vue +58 -58
- package/dist/elements/list/_global.d.ts +50 -50
- package/dist/elements/math/_global.d.ts +72 -72
- package/dist/elements/math/_global.ts +3 -3
- package/dist/elements/math/components/BlockMath.vue +30 -30
- package/dist/elements/math/components/InlinerMath.vue +65 -65
- package/dist/elements/math/components/Katex.vue +88 -88
- package/dist/elements/math/components/MathGroup.vue +41 -41
- package/dist/elements/paragraph/Paragraph.vue +25 -25
- package/dist/elements/paragraph/_global.d.ts +27 -27
- package/dist/elements/paragraph/icon.svg +3 -3
- package/dist/elements/problem/_global.d.ts +112 -112
- package/dist/elements/problem/assets/actions/answer.svg +2 -2
- package/dist/elements/problem/assets/actions/check.svg +2 -2
- package/dist/elements/problem/assets/actions/generate.svg +2 -2
- package/dist/elements/problem/assets/actions/hint.svg +2 -2
- package/dist/elements/problem/assets/actions/note.svg +2 -2
- package/dist/elements/problem/assets/actions/solution.svg +2 -2
- package/dist/elements/problem/assets/icon.svg +2 -2
- package/dist/elements/problem/components/Problem.vue +22 -22
- package/dist/elements/problem/components/ProblemButton.vue +20 -20
- package/dist/elements/problem/components/ProblemContainer.vue +8 -8
- package/dist/elements/problem/components/ProblemContent.vue +356 -356
- package/dist/elements/problem/components/ProblemExpander.vue +7 -7
- package/dist/elements/problem/components/ProblemExpanderSection.vue +57 -57
- package/dist/elements/problem/components/ProblemHeader.vue +100 -100
- package/dist/elements/problem/components/Problems.vue +83 -83
- package/dist/elements/problem/components/SubProblem.vue +14 -14
- package/dist/elements/problem/components/expanders/Check.vue +153 -153
- package/dist/elements/problem/components/expanders/Checks.vue +146 -146
- package/dist/elements/problem/components/expanders/DefaultPlusSections.vue +38 -38
- package/dist/elements/problem/components/expanders/Hint.vue +26 -26
- package/dist/elements/table/Table.vue +100 -100
- package/dist/elements/table/_global.d.ts +36 -36
- package/dist/elements/video/Video.vue +110 -110
- package/dist/elements/video/_global.d.ts +18 -18
- package/package.json +4 -4
- package/types.d.ts +4 -4
|
@@ -1,356 +1,356 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
3
|
-
computed,
|
|
4
|
-
onMounted,
|
|
5
|
-
ref,
|
|
6
|
-
shallowRef,
|
|
7
|
-
useTemplateRef,
|
|
8
|
-
watchEffect,
|
|
9
|
-
type Component,
|
|
10
|
-
} from 'vue';
|
|
11
|
-
import { autoUpdate, offset, useFloating } from '@floating-ui/vue';
|
|
12
|
-
import {
|
|
13
|
-
isProseElement,
|
|
14
|
-
resolveRawElement,
|
|
15
|
-
type ProseElement,
|
|
16
|
-
} from '@jsprose/core';
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
problemAnswer,
|
|
20
|
-
problemCheckSchema,
|
|
21
|
-
problemDescriptionSchema,
|
|
22
|
-
problemHintSchema,
|
|
23
|
-
problemNote,
|
|
24
|
-
problemSolution,
|
|
25
|
-
type CheckFunction,
|
|
26
|
-
type ProblemContentChild,
|
|
27
|
-
} from '../problemContent.js';
|
|
28
|
-
import { useProseContext } from '../../../app/composables/context.js';
|
|
29
|
-
import { useProblemPhrase } from '../composables/phrase.js';
|
|
30
|
-
import type { ProblemAction } from '../shared.js';
|
|
31
|
-
import { useArrayContainsAnchor } from '../../../app/composables/anchor.js';
|
|
32
|
-
import type { ProblemScriptInstance } from '../problemScript.js';
|
|
33
|
-
import type { problemSchema } from '../problem.js';
|
|
34
|
-
import type { subProblemSchema } from '../problems.js';
|
|
35
|
-
import { useElementStorage } from '../../../app/composables/storage.js';
|
|
36
|
-
import type { ProblemScriptStorage } from '../storage.js';
|
|
37
|
-
import { createProblemScriptInstance } from '../composables/problemScript.js';
|
|
38
|
-
import { DEFAULT_SEED, type ProblemSeed } from '../rng.js';
|
|
39
|
-
import plusIcon from '../../../app/shared/assets/plus.svg?raw';
|
|
40
|
-
import Render from '../../../app/shared/Render.vue';
|
|
41
|
-
import Hint from './expanders/Hint.vue';
|
|
42
|
-
import DefaultPlusSections from './expanders/DefaultPlusSections.vue';
|
|
43
|
-
import ProblemButton from './ProblemButton.vue';
|
|
44
|
-
import Checks from './expanders/Checks.vue';
|
|
45
|
-
|
|
46
|
-
const { element, initialElements } = defineProps<{
|
|
47
|
-
element:
|
|
48
|
-
| ProseElement<typeof problemSchema>
|
|
49
|
-
| ProseElement<typeof subProblemSchema>;
|
|
50
|
-
initialElements: ProseElement<ProblemContentChild>[];
|
|
51
|
-
}>();
|
|
52
|
-
|
|
53
|
-
const { EruditIcon, EruditTransition } = useProseContext();
|
|
54
|
-
const phrase = await useProblemPhrase();
|
|
55
|
-
|
|
56
|
-
const actionIcons: Record<ProblemAction, string> = Object.fromEntries(
|
|
57
|
-
Object.entries(
|
|
58
|
-
// @ts-ignore
|
|
59
|
-
import.meta.glob('../assets/actions/*.svg', {
|
|
60
|
-
query: 'raw',
|
|
61
|
-
eager: true,
|
|
62
|
-
import: 'default',
|
|
63
|
-
}),
|
|
64
|
-
).map(([key, value]) => {
|
|
65
|
-
const name = key.split('/').pop()?.replace('.svg', '') ?? key;
|
|
66
|
-
return [name as any, value as string];
|
|
67
|
-
}),
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const key = ref(0);
|
|
71
|
-
const elements =
|
|
72
|
-
shallowRef<ProseElement<ProblemContentChild>[]>(initialElements);
|
|
73
|
-
|
|
74
|
-
const description = computed(() => {
|
|
75
|
-
return elements.value.find((element) =>
|
|
76
|
-
isProseElement(element, problemDescriptionSchema),
|
|
77
|
-
)!;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const expanderComponents: Record<
|
|
81
|
-
Exclude<ProblemAction, 'generate'>,
|
|
82
|
-
Component
|
|
83
|
-
> = {
|
|
84
|
-
check: Checks,
|
|
85
|
-
hint: Hint,
|
|
86
|
-
answer: DefaultPlusSections,
|
|
87
|
-
solution: DefaultPlusSections,
|
|
88
|
-
note: DefaultPlusSections,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const expandableActions = computed(() => {
|
|
92
|
-
type ActionMap<T> =
|
|
93
|
-
T extends Partial<Exclude<Record<ProblemAction, any>, 'generate'>>
|
|
94
|
-
? T
|
|
95
|
-
: never;
|
|
96
|
-
|
|
97
|
-
const actionMap: ActionMap<{
|
|
98
|
-
hint?: ProseElement<typeof problemHintSchema>[];
|
|
99
|
-
answer?: ProseElement<typeof problemAnswer.schema>;
|
|
100
|
-
solution?: ProseElement<typeof problemSolution.schema>;
|
|
101
|
-
note?: ProseElement<typeof problemNote.schema>;
|
|
102
|
-
check?: {
|
|
103
|
-
checkElements: ProseElement<typeof problemCheckSchema>[];
|
|
104
|
-
checkFunction?: CheckFunction;
|
|
105
|
-
};
|
|
106
|
-
}> = {};
|
|
107
|
-
|
|
108
|
-
const checks = elements.value.filter((element) =>
|
|
109
|
-
isProseElement(element, problemCheckSchema),
|
|
110
|
-
);
|
|
111
|
-
if (checks.length > 0) {
|
|
112
|
-
actionMap.check = {
|
|
113
|
-
checkElements: checks,
|
|
114
|
-
checkFunction: scriptCheck.value,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const hints = elements.value.filter((element) =>
|
|
119
|
-
isProseElement(element, problemHintSchema),
|
|
120
|
-
);
|
|
121
|
-
if (hints.length > 0) {
|
|
122
|
-
actionMap.hint = hints;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
(['answer', 'solution', 'note'] as const).forEach((action) => {
|
|
126
|
-
const actionElements = elements.value.filter((element) => {
|
|
127
|
-
switch (action) {
|
|
128
|
-
case 'answer':
|
|
129
|
-
return isProseElement(element, problemAnswer.schema);
|
|
130
|
-
case 'solution':
|
|
131
|
-
return isProseElement(element, problemSolution.schema);
|
|
132
|
-
case 'note':
|
|
133
|
-
return isProseElement(element, problemNote.schema);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
if (actionElements.length > 0) {
|
|
138
|
-
actionMap[action] = actionElements[0]! as any;
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
return actionMap;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const currentAction = ref<Exclude<ProblemAction, 'generate'>>();
|
|
146
|
-
|
|
147
|
-
const containsAnchorArray = useArrayContainsAnchor(elements.value);
|
|
148
|
-
|
|
149
|
-
watchEffect(() => {
|
|
150
|
-
const anchorIndex = containsAnchorArray.value;
|
|
151
|
-
|
|
152
|
-
if (anchorIndex === undefined) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const anchorElement = elements.value.at(anchorIndex);
|
|
157
|
-
|
|
158
|
-
switch (true) {
|
|
159
|
-
case isProseElement(anchorElement, problemHintSchema):
|
|
160
|
-
currentAction.value = 'hint';
|
|
161
|
-
break;
|
|
162
|
-
case isProseElement(anchorElement, problemAnswer.schema):
|
|
163
|
-
currentAction.value = 'answer';
|
|
164
|
-
break;
|
|
165
|
-
case isProseElement(anchorElement, problemSolution.schema):
|
|
166
|
-
currentAction.value = 'solution';
|
|
167
|
-
break;
|
|
168
|
-
case isProseElement(anchorElement, problemNote.schema):
|
|
169
|
-
currentAction.value = 'note';
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
// Problem Script
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
const scriptInstance = shallowRef<ProblemScriptInstance>();
|
|
179
|
-
const scriptStorage = (await useElementStorage(
|
|
180
|
-
element as any,
|
|
181
|
-
)) as ProblemScriptStorage;
|
|
182
|
-
const isGenerator = computed(() => Boolean(scriptInstance.value?.isGenerator));
|
|
183
|
-
const scriptCheck = shallowRef<CheckFunction>();
|
|
184
|
-
|
|
185
|
-
onMounted(async () => {
|
|
186
|
-
scriptInstance.value = await createProblemScriptInstance(
|
|
187
|
-
scriptStorage?.resolvedScriptSrc,
|
|
188
|
-
element.data.scriptUniques,
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
if (scriptInstance.value) {
|
|
192
|
-
const initialGenerateResult = scriptInstance.value!.generate(DEFAULT_SEED);
|
|
193
|
-
scriptCheck.value = initialGenerateResult.check;
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const generateRotation = ref(0);
|
|
198
|
-
const generating = ref(false);
|
|
199
|
-
const seed = ref<ProblemSeed>(DEFAULT_SEED);
|
|
200
|
-
const usingCustomSeed = ref(false);
|
|
201
|
-
|
|
202
|
-
async function doGenerate() {
|
|
203
|
-
if (!scriptInstance.value) {
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
generateRotation.value += 180;
|
|
208
|
-
|
|
209
|
-
if (generating.value) {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
generating.value = true;
|
|
214
|
-
|
|
215
|
-
if (usingCustomSeed.value) {
|
|
216
|
-
usingCustomSeed.value = false;
|
|
217
|
-
} else {
|
|
218
|
-
seed.value = Math.floor(Math.random() * 1000000000) + 1;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const generateResult = scriptInstance.value.generate(seed.value);
|
|
222
|
-
|
|
223
|
-
if (generateResult.check) {
|
|
224
|
-
scriptCheck.value = generateResult.check;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const rawElements = generateResult.problemContent;
|
|
228
|
-
const proseElements: ProseElement<ProblemContentChild>[] = [];
|
|
229
|
-
for (const rawElement of rawElements) {
|
|
230
|
-
const resolveResult = await resolveRawElement({
|
|
231
|
-
rawElement,
|
|
232
|
-
linkable: false,
|
|
233
|
-
});
|
|
234
|
-
proseElements.push(resolveResult.proseElement as any);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
elements.value = proseElements;
|
|
238
|
-
|
|
239
|
-
key.value++;
|
|
240
|
-
generating.value = false;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
// Seed Popup
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
const seedPopupVisible = ref(false);
|
|
248
|
-
const seedReferenceElement = useTemplateRef('seedReference');
|
|
249
|
-
const seedPopupElement = useTemplateRef('seedPopup');
|
|
250
|
-
|
|
251
|
-
const { floatingStyles: seedFloatingStyles } = useFloating(
|
|
252
|
-
seedReferenceElement,
|
|
253
|
-
seedPopupElement,
|
|
254
|
-
{
|
|
255
|
-
whileElementsMounted: autoUpdate,
|
|
256
|
-
placement: 'top',
|
|
257
|
-
},
|
|
258
|
-
);
|
|
259
|
-
</script>
|
|
260
|
-
|
|
261
|
-
<template>
|
|
262
|
-
<div>
|
|
263
|
-
<Suspense suspensible>
|
|
264
|
-
<div class="py-(--proseAsideWidth)" :key>
|
|
265
|
-
<Render v-for="child of description.children" :element="child" />
|
|
266
|
-
</div>
|
|
267
|
-
</Suspense>
|
|
268
|
-
|
|
269
|
-
<div
|
|
270
|
-
v-if="Object.values(expandableActions).some(Boolean) || isGenerator"
|
|
271
|
-
class="gap-small micro:gap-normal flex flex-wrap p-(--proseAsideWidth)
|
|
272
|
-
pt-0"
|
|
273
|
-
>
|
|
274
|
-
<ProblemButton
|
|
275
|
-
v-for="(_, actionKey) in expandableActions"
|
|
276
|
-
:key="actionKey"
|
|
277
|
-
@click="
|
|
278
|
-
currentAction = actionKey === currentAction ? undefined : actionKey
|
|
279
|
-
"
|
|
280
|
-
:active="actionKey === currentAction"
|
|
281
|
-
class="flex items-center gap-[7px]"
|
|
282
|
-
>
|
|
283
|
-
<EruditIcon :name="actionIcons[actionKey]" class="text-[1.3em]" />
|
|
284
|
-
<span>{{ phrase[`action_${actionKey}`] }}</span>
|
|
285
|
-
</ProblemButton>
|
|
286
|
-
<div
|
|
287
|
-
v-if="isGenerator"
|
|
288
|
-
ref="seedReference"
|
|
289
|
-
@mouseenter="seedPopupVisible = true"
|
|
290
|
-
@mouseleave="seedPopupVisible = false"
|
|
291
|
-
>
|
|
292
|
-
<ProblemButton
|
|
293
|
-
@touchstart="seedPopupVisible = seedPopupVisible ? false : true"
|
|
294
|
-
@click="doGenerate"
|
|
295
|
-
class="flex items-center gap-[7px]"
|
|
296
|
-
>
|
|
297
|
-
<EruditIcon
|
|
298
|
-
:name="actionIcons.generate"
|
|
299
|
-
:style="{ transform: `rotate(${generateRotation}deg)` }"
|
|
300
|
-
class="text-[1.3em] transition-[transform] backface-hidden"
|
|
301
|
-
/>
|
|
302
|
-
<span>{{ phrase.action_generate }}</span>
|
|
303
|
-
</ProblemButton>
|
|
304
|
-
<EruditTransition>
|
|
305
|
-
<div
|
|
306
|
-
v-if="seedPopupVisible"
|
|
307
|
-
ref="seedPopup"
|
|
308
|
-
:style="seedFloatingStyles"
|
|
309
|
-
class="pb-2.5"
|
|
310
|
-
>
|
|
311
|
-
<form
|
|
312
|
-
class="shadow-border text-main-xs flex rounded bg-neutral-900
|
|
313
|
-
text-white shadow-lg dark:bg-neutral-200 dark:text-black"
|
|
314
|
-
@submit.prevent="doGenerate"
|
|
315
|
-
>
|
|
316
|
-
<input
|
|
317
|
-
type="text"
|
|
318
|
-
v-model="seed"
|
|
319
|
-
@input="usingCustomSeed = true"
|
|
320
|
-
:title="phrase.seed_explain"
|
|
321
|
-
@focus="($event as any).target.select()"
|
|
322
|
-
class="max-w-[100px] flex-1 p-[5px] text-center outline-none"
|
|
323
|
-
/>
|
|
324
|
-
<button
|
|
325
|
-
v-if="seed !== DEFAULT_SEED"
|
|
326
|
-
type="button"
|
|
327
|
-
@click="
|
|
328
|
-
seed = DEFAULT_SEED;
|
|
329
|
-
usingCustomSeed = true;
|
|
330
|
-
doGenerate();
|
|
331
|
-
"
|
|
332
|
-
class="cursor-pointer pr-[3px]"
|
|
333
|
-
>
|
|
334
|
-
<EruditIcon
|
|
335
|
-
:name="plusIcon"
|
|
336
|
-
class="hocus:text-white dark:hocus:text-black rotate-45
|
|
337
|
-
text-[1.3em] text-neutral-400 transition-[color]
|
|
338
|
-
dark:text-neutral-600"
|
|
339
|
-
/>
|
|
340
|
-
</button>
|
|
341
|
-
</form>
|
|
342
|
-
</div>
|
|
343
|
-
</EruditTransition>
|
|
344
|
-
</div>
|
|
345
|
-
</div>
|
|
346
|
-
|
|
347
|
-
<Suspense suspensible>
|
|
348
|
-
<component
|
|
349
|
-
v-if="currentAction"
|
|
350
|
-
:key="`${key}-${currentAction}`"
|
|
351
|
-
:is="expanderComponents[currentAction]"
|
|
352
|
-
:value="expandableActions[currentAction]"
|
|
353
|
-
/>
|
|
354
|
-
</Suspense>
|
|
355
|
-
</div>
|
|
356
|
-
</template>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
onMounted,
|
|
5
|
+
ref,
|
|
6
|
+
shallowRef,
|
|
7
|
+
useTemplateRef,
|
|
8
|
+
watchEffect,
|
|
9
|
+
type Component,
|
|
10
|
+
} from 'vue';
|
|
11
|
+
import { autoUpdate, offset, useFloating } from '@floating-ui/vue';
|
|
12
|
+
import {
|
|
13
|
+
isProseElement,
|
|
14
|
+
resolveRawElement,
|
|
15
|
+
type ProseElement,
|
|
16
|
+
} from '@jsprose/core';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
problemAnswer,
|
|
20
|
+
problemCheckSchema,
|
|
21
|
+
problemDescriptionSchema,
|
|
22
|
+
problemHintSchema,
|
|
23
|
+
problemNote,
|
|
24
|
+
problemSolution,
|
|
25
|
+
type CheckFunction,
|
|
26
|
+
type ProblemContentChild,
|
|
27
|
+
} from '../problemContent.js';
|
|
28
|
+
import { useProseContext } from '../../../app/composables/context.js';
|
|
29
|
+
import { useProblemPhrase } from '../composables/phrase.js';
|
|
30
|
+
import type { ProblemAction } from '../shared.js';
|
|
31
|
+
import { useArrayContainsAnchor } from '../../../app/composables/anchor.js';
|
|
32
|
+
import type { ProblemScriptInstance } from '../problemScript.js';
|
|
33
|
+
import type { problemSchema } from '../problem.js';
|
|
34
|
+
import type { subProblemSchema } from '../problems.js';
|
|
35
|
+
import { useElementStorage } from '../../../app/composables/storage.js';
|
|
36
|
+
import type { ProblemScriptStorage } from '../storage.js';
|
|
37
|
+
import { createProblemScriptInstance } from '../composables/problemScript.js';
|
|
38
|
+
import { DEFAULT_SEED, type ProblemSeed } from '../rng.js';
|
|
39
|
+
import plusIcon from '../../../app/shared/assets/plus.svg?raw';
|
|
40
|
+
import Render from '../../../app/shared/Render.vue';
|
|
41
|
+
import Hint from './expanders/Hint.vue';
|
|
42
|
+
import DefaultPlusSections from './expanders/DefaultPlusSections.vue';
|
|
43
|
+
import ProblemButton from './ProblemButton.vue';
|
|
44
|
+
import Checks from './expanders/Checks.vue';
|
|
45
|
+
|
|
46
|
+
const { element, initialElements } = defineProps<{
|
|
47
|
+
element:
|
|
48
|
+
| ProseElement<typeof problemSchema>
|
|
49
|
+
| ProseElement<typeof subProblemSchema>;
|
|
50
|
+
initialElements: ProseElement<ProblemContentChild>[];
|
|
51
|
+
}>();
|
|
52
|
+
|
|
53
|
+
const { EruditIcon, EruditTransition } = useProseContext();
|
|
54
|
+
const phrase = await useProblemPhrase();
|
|
55
|
+
|
|
56
|
+
const actionIcons: Record<ProblemAction, string> = Object.fromEntries(
|
|
57
|
+
Object.entries(
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
import.meta.glob('../assets/actions/*.svg', {
|
|
60
|
+
query: 'raw',
|
|
61
|
+
eager: true,
|
|
62
|
+
import: 'default',
|
|
63
|
+
}),
|
|
64
|
+
).map(([key, value]) => {
|
|
65
|
+
const name = key.split('/').pop()?.replace('.svg', '') ?? key;
|
|
66
|
+
return [name as any, value as string];
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const key = ref(0);
|
|
71
|
+
const elements =
|
|
72
|
+
shallowRef<ProseElement<ProblemContentChild>[]>(initialElements);
|
|
73
|
+
|
|
74
|
+
const description = computed(() => {
|
|
75
|
+
return elements.value.find((element) =>
|
|
76
|
+
isProseElement(element, problemDescriptionSchema),
|
|
77
|
+
)!;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const expanderComponents: Record<
|
|
81
|
+
Exclude<ProblemAction, 'generate'>,
|
|
82
|
+
Component
|
|
83
|
+
> = {
|
|
84
|
+
check: Checks,
|
|
85
|
+
hint: Hint,
|
|
86
|
+
answer: DefaultPlusSections,
|
|
87
|
+
solution: DefaultPlusSections,
|
|
88
|
+
note: DefaultPlusSections,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const expandableActions = computed(() => {
|
|
92
|
+
type ActionMap<T> =
|
|
93
|
+
T extends Partial<Exclude<Record<ProblemAction, any>, 'generate'>>
|
|
94
|
+
? T
|
|
95
|
+
: never;
|
|
96
|
+
|
|
97
|
+
const actionMap: ActionMap<{
|
|
98
|
+
hint?: ProseElement<typeof problemHintSchema>[];
|
|
99
|
+
answer?: ProseElement<typeof problemAnswer.schema>;
|
|
100
|
+
solution?: ProseElement<typeof problemSolution.schema>;
|
|
101
|
+
note?: ProseElement<typeof problemNote.schema>;
|
|
102
|
+
check?: {
|
|
103
|
+
checkElements: ProseElement<typeof problemCheckSchema>[];
|
|
104
|
+
checkFunction?: CheckFunction;
|
|
105
|
+
};
|
|
106
|
+
}> = {};
|
|
107
|
+
|
|
108
|
+
const checks = elements.value.filter((element) =>
|
|
109
|
+
isProseElement(element, problemCheckSchema),
|
|
110
|
+
);
|
|
111
|
+
if (checks.length > 0) {
|
|
112
|
+
actionMap.check = {
|
|
113
|
+
checkElements: checks,
|
|
114
|
+
checkFunction: scriptCheck.value,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const hints = elements.value.filter((element) =>
|
|
119
|
+
isProseElement(element, problemHintSchema),
|
|
120
|
+
);
|
|
121
|
+
if (hints.length > 0) {
|
|
122
|
+
actionMap.hint = hints;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
(['answer', 'solution', 'note'] as const).forEach((action) => {
|
|
126
|
+
const actionElements = elements.value.filter((element) => {
|
|
127
|
+
switch (action) {
|
|
128
|
+
case 'answer':
|
|
129
|
+
return isProseElement(element, problemAnswer.schema);
|
|
130
|
+
case 'solution':
|
|
131
|
+
return isProseElement(element, problemSolution.schema);
|
|
132
|
+
case 'note':
|
|
133
|
+
return isProseElement(element, problemNote.schema);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (actionElements.length > 0) {
|
|
138
|
+
actionMap[action] = actionElements[0]! as any;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return actionMap;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const currentAction = ref<Exclude<ProblemAction, 'generate'>>();
|
|
146
|
+
|
|
147
|
+
const containsAnchorArray = useArrayContainsAnchor(elements.value);
|
|
148
|
+
|
|
149
|
+
watchEffect(() => {
|
|
150
|
+
const anchorIndex = containsAnchorArray.value;
|
|
151
|
+
|
|
152
|
+
if (anchorIndex === undefined) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const anchorElement = elements.value.at(anchorIndex);
|
|
157
|
+
|
|
158
|
+
switch (true) {
|
|
159
|
+
case isProseElement(anchorElement, problemHintSchema):
|
|
160
|
+
currentAction.value = 'hint';
|
|
161
|
+
break;
|
|
162
|
+
case isProseElement(anchorElement, problemAnswer.schema):
|
|
163
|
+
currentAction.value = 'answer';
|
|
164
|
+
break;
|
|
165
|
+
case isProseElement(anchorElement, problemSolution.schema):
|
|
166
|
+
currentAction.value = 'solution';
|
|
167
|
+
break;
|
|
168
|
+
case isProseElement(anchorElement, problemNote.schema):
|
|
169
|
+
currentAction.value = 'note';
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
//
|
|
175
|
+
// Problem Script
|
|
176
|
+
//
|
|
177
|
+
|
|
178
|
+
const scriptInstance = shallowRef<ProblemScriptInstance>();
|
|
179
|
+
const scriptStorage = (await useElementStorage(
|
|
180
|
+
element as any,
|
|
181
|
+
)) as ProblemScriptStorage;
|
|
182
|
+
const isGenerator = computed(() => Boolean(scriptInstance.value?.isGenerator));
|
|
183
|
+
const scriptCheck = shallowRef<CheckFunction>();
|
|
184
|
+
|
|
185
|
+
onMounted(async () => {
|
|
186
|
+
scriptInstance.value = await createProblemScriptInstance(
|
|
187
|
+
scriptStorage?.resolvedScriptSrc,
|
|
188
|
+
element.data.scriptUniques,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (scriptInstance.value) {
|
|
192
|
+
const initialGenerateResult = scriptInstance.value!.generate(DEFAULT_SEED);
|
|
193
|
+
scriptCheck.value = initialGenerateResult.check;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const generateRotation = ref(0);
|
|
198
|
+
const generating = ref(false);
|
|
199
|
+
const seed = ref<ProblemSeed>(DEFAULT_SEED);
|
|
200
|
+
const usingCustomSeed = ref(false);
|
|
201
|
+
|
|
202
|
+
async function doGenerate() {
|
|
203
|
+
if (!scriptInstance.value) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
generateRotation.value += 180;
|
|
208
|
+
|
|
209
|
+
if (generating.value) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
generating.value = true;
|
|
214
|
+
|
|
215
|
+
if (usingCustomSeed.value) {
|
|
216
|
+
usingCustomSeed.value = false;
|
|
217
|
+
} else {
|
|
218
|
+
seed.value = Math.floor(Math.random() * 1000000000) + 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const generateResult = scriptInstance.value.generate(seed.value);
|
|
222
|
+
|
|
223
|
+
if (generateResult.check) {
|
|
224
|
+
scriptCheck.value = generateResult.check;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const rawElements = generateResult.problemContent;
|
|
228
|
+
const proseElements: ProseElement<ProblemContentChild>[] = [];
|
|
229
|
+
for (const rawElement of rawElements) {
|
|
230
|
+
const resolveResult = await resolveRawElement({
|
|
231
|
+
rawElement,
|
|
232
|
+
linkable: false,
|
|
233
|
+
});
|
|
234
|
+
proseElements.push(resolveResult.proseElement as any);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
elements.value = proseElements;
|
|
238
|
+
|
|
239
|
+
key.value++;
|
|
240
|
+
generating.value = false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
//
|
|
244
|
+
// Seed Popup
|
|
245
|
+
//
|
|
246
|
+
|
|
247
|
+
const seedPopupVisible = ref(false);
|
|
248
|
+
const seedReferenceElement = useTemplateRef('seedReference');
|
|
249
|
+
const seedPopupElement = useTemplateRef('seedPopup');
|
|
250
|
+
|
|
251
|
+
const { floatingStyles: seedFloatingStyles } = useFloating(
|
|
252
|
+
seedReferenceElement,
|
|
253
|
+
seedPopupElement,
|
|
254
|
+
{
|
|
255
|
+
whileElementsMounted: autoUpdate,
|
|
256
|
+
placement: 'top',
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
</script>
|
|
260
|
+
|
|
261
|
+
<template>
|
|
262
|
+
<div>
|
|
263
|
+
<Suspense suspensible>
|
|
264
|
+
<div class="py-(--proseAsideWidth)" :key>
|
|
265
|
+
<Render v-for="child of description.children" :element="child" />
|
|
266
|
+
</div>
|
|
267
|
+
</Suspense>
|
|
268
|
+
|
|
269
|
+
<div
|
|
270
|
+
v-if="Object.values(expandableActions).some(Boolean) || isGenerator"
|
|
271
|
+
class="gap-small micro:gap-normal flex flex-wrap p-(--proseAsideWidth)
|
|
272
|
+
pt-0"
|
|
273
|
+
>
|
|
274
|
+
<ProblemButton
|
|
275
|
+
v-for="(_, actionKey) in expandableActions"
|
|
276
|
+
:key="actionKey"
|
|
277
|
+
@click="
|
|
278
|
+
currentAction = actionKey === currentAction ? undefined : actionKey
|
|
279
|
+
"
|
|
280
|
+
:active="actionKey === currentAction"
|
|
281
|
+
class="flex items-center gap-[7px]"
|
|
282
|
+
>
|
|
283
|
+
<EruditIcon :name="actionIcons[actionKey]" class="text-[1.3em]" />
|
|
284
|
+
<span>{{ phrase[`action_${actionKey}`] }}</span>
|
|
285
|
+
</ProblemButton>
|
|
286
|
+
<div
|
|
287
|
+
v-if="isGenerator"
|
|
288
|
+
ref="seedReference"
|
|
289
|
+
@mouseenter="seedPopupVisible = true"
|
|
290
|
+
@mouseleave="seedPopupVisible = false"
|
|
291
|
+
>
|
|
292
|
+
<ProblemButton
|
|
293
|
+
@touchstart="seedPopupVisible = seedPopupVisible ? false : true"
|
|
294
|
+
@click="doGenerate"
|
|
295
|
+
class="flex items-center gap-[7px]"
|
|
296
|
+
>
|
|
297
|
+
<EruditIcon
|
|
298
|
+
:name="actionIcons.generate"
|
|
299
|
+
:style="{ transform: `rotate(${generateRotation}deg)` }"
|
|
300
|
+
class="text-[1.3em] transition-[transform] backface-hidden"
|
|
301
|
+
/>
|
|
302
|
+
<span>{{ phrase.action_generate }}</span>
|
|
303
|
+
</ProblemButton>
|
|
304
|
+
<EruditTransition>
|
|
305
|
+
<div
|
|
306
|
+
v-if="seedPopupVisible"
|
|
307
|
+
ref="seedPopup"
|
|
308
|
+
:style="seedFloatingStyles"
|
|
309
|
+
class="pb-2.5"
|
|
310
|
+
>
|
|
311
|
+
<form
|
|
312
|
+
class="shadow-border text-main-xs flex rounded bg-neutral-900
|
|
313
|
+
text-white shadow-lg dark:bg-neutral-200 dark:text-black"
|
|
314
|
+
@submit.prevent="doGenerate"
|
|
315
|
+
>
|
|
316
|
+
<input
|
|
317
|
+
type="text"
|
|
318
|
+
v-model="seed"
|
|
319
|
+
@input="usingCustomSeed = true"
|
|
320
|
+
:title="phrase.seed_explain"
|
|
321
|
+
@focus="($event as any).target.select()"
|
|
322
|
+
class="max-w-[100px] flex-1 p-[5px] text-center outline-none"
|
|
323
|
+
/>
|
|
324
|
+
<button
|
|
325
|
+
v-if="seed !== DEFAULT_SEED"
|
|
326
|
+
type="button"
|
|
327
|
+
@click="
|
|
328
|
+
seed = DEFAULT_SEED;
|
|
329
|
+
usingCustomSeed = true;
|
|
330
|
+
doGenerate();
|
|
331
|
+
"
|
|
332
|
+
class="cursor-pointer pr-[3px]"
|
|
333
|
+
>
|
|
334
|
+
<EruditIcon
|
|
335
|
+
:name="plusIcon"
|
|
336
|
+
class="hocus:text-white dark:hocus:text-black rotate-45
|
|
337
|
+
text-[1.3em] text-neutral-400 transition-[color]
|
|
338
|
+
dark:text-neutral-600"
|
|
339
|
+
/>
|
|
340
|
+
</button>
|
|
341
|
+
</form>
|
|
342
|
+
</div>
|
|
343
|
+
</EruditTransition>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<Suspense suspensible>
|
|
348
|
+
<component
|
|
349
|
+
v-if="currentAction"
|
|
350
|
+
:key="`${key}-${currentAction}`"
|
|
351
|
+
:is="expanderComponents[currentAction]"
|
|
352
|
+
:value="expandableActions[currentAction]"
|
|
353
|
+
/>
|
|
354
|
+
</Suspense>
|
|
355
|
+
</div>
|
|
356
|
+
</template>
|