@0m0g1/griot 0.1.16 → 0.1.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0m0g1/griot",
3
- "version": "0.1.16",
3
+ "version": "0.1.20",
4
4
  "description": "A self-contained block-based rich text editor and renderer built for historical document authoring.",
5
5
  "type": "module",
6
6
  "main": "./src/Griot.js",
@@ -432,184 +432,67 @@ function _renderQuiz(block, opts) {
432
432
  submitBtn.textContent = 'Check answers';
433
433
  submitBtn.addEventListener('click', () => {
434
434
  let score = 0;
435
+ let answered = 0;
435
436
  const answers = {};
437
+
436
438
  questions.forEach((q, idx) => {
437
439
  const qid = q.id || `q${idx}`;
438
440
  const radios = form.querySelectorAll(`input[name="quiz_${block.id}_${qid}"]`);
439
441
  let selected = null;
440
442
  radios.forEach((r, i) => { if (r.checked) selected = i; });
441
- answers[qid] = selected;
442
- const isCorrect = (selected !== null && selected === q.correctOption);
443
- if (isCorrect) score++;
444
-
445
- const feedbackDiv = form.querySelector(`fieldset[data-index="${idx}"] .griot-quiz__feedback`);
446
- if (feedbackDiv) {
447
- if (selected === null) {
448
- feedbackDiv.textContent = '❓ No answer selected.';
443
+
444
+ // Only count attempted questions
445
+ if (selected !== null) {
446
+ answered++;
447
+ answers[qid] = selected;
448
+ const isCorrect = selected === q.correctOption;
449
+ if (isCorrect) score++;
450
+
451
+ const feedbackDiv = form.querySelector(`fieldset[data-index="${idx}"] .griot-quiz__feedback`);
452
+ if (feedbackDiv) {
453
+ if (isCorrect) {
454
+ feedbackDiv.textContent = '✓ Correct!';
455
+ feedbackDiv.className = 'griot-quiz__feedback griot-quiz__feedback--correct';
456
+ } else {
457
+ const correctAnswerText = q.options[q.correctOption];
458
+ feedbackDiv.innerHTML = `✗ Incorrect. Correct answer: ${escapeHtml(correctAnswerText)}. ${escapeHtml(q.explanation || '')}`;
459
+ feedbackDiv.className = 'griot-quiz__feedback griot-quiz__feedback--wrong';
460
+ }
461
+ }
462
+ } else {
463
+ // Unanswered — show skipped, don't penalise
464
+ const feedbackDiv = form.querySelector(`fieldset[data-index="${idx}"] .griot-quiz__feedback`);
465
+ if (feedbackDiv) {
466
+ feedbackDiv.textContent = '— Skipped';
449
467
  feedbackDiv.className = 'griot-quiz__feedback griot-quiz__feedback--missing';
450
- } else if (isCorrect) {
451
- feedbackDiv.textContent = '✓ Correct!';
452
- feedbackDiv.className = 'griot-quiz__feedback griot-quiz__feedback--correct';
453
- } else {
454
- const correctAnswerText = q.options[q.correctOption];
455
- feedbackDiv.innerHTML = `✗ Incorrect. Correct answer: ${escapeHtml(correctAnswerText)}. ${escapeHtml(q.explanation || '')}`;
456
- feedbackDiv.className = 'griot-quiz__feedback griot-quiz__feedback--wrong';
457
468
  }
458
469
  }
459
470
  });
460
- const totalScore = questions.length;
461
471
 
462
- const existingScore = container.querySelector('.griot-quiz__score');
463
- if (existingScore) existingScore.remove();
464
- const scoreDiv = document.createElement('div');
465
- scoreDiv.className = 'griot-quiz__score';
466
- scoreDiv.textContent = `You scored ${score} out of ${totalScore}.`;
467
- container.appendChild(scoreDiv);
468
-
469
- if (typeof opts.onQuizSubmit === 'function') {
470
- opts.onQuizSubmit(block.id, score, totalScore, answers);
472
+ if (answered === 0) {
473
+ alert('Please answer at least one question.');
474
+ return;
471
475
  }
472
- });
473
-
474
- form.appendChild(submitBtn);
475
- container.appendChild(form);
476
- return container;
477
- }
478
-
479
- function _renderQuiz(block, opts) {
480
- const { meta = {} } = block;
481
- const title = meta.title || '';
482
- const questions = Array.isArray(meta.questions) ? meta.questions : [];
483
- const answers = meta._submittedAnswers || {}; // optional: store last answers
484
-
485
- const container = document.createElement('div');
486
- container.className = 'griot-block griot-quiz';
487
-
488
- if (title) {
489
- const titleEl = document.createElement('h4');
490
- titleEl.className = 'griot-quiz__title';
491
- titleEl.textContent = title;
492
- container.appendChild(titleEl);
493
- }
494
-
495
- if (questions.length === 0) {
496
- const empty = document.createElement('p');
497
- empty.className = 'griot-quiz__empty';
498
- empty.textContent = 'No questions yet.';
499
- container.appendChild(empty);
500
- return container;
501
- }
502
476
 
503
- const form = document.createElement('form');
504
- form.className = 'griot-quiz__form';
505
-
506
- let totalScore = 0;
507
- let userScore = 0;
508
-
509
- questions.forEach((q, idx) => {
510
- const qid = q.id || `q${idx}`;
511
- const text = q.text || `Question ${idx + 1}`;
512
- const options = Array.isArray(q.options) ? q.options : [];
513
- const correctIdx = q.correctOption; // 0‑based index
514
- const explanation = q.explanation || '';
515
- const userChoice = answers[qid] !== undefined ? answers[qid] : null;
516
-
517
- const fieldset = document.createElement('fieldset');
518
- fieldset.className = 'griot-quiz__question';
519
- fieldset.dataset.index = idx;
520
-
521
- const legend = document.createElement('legend');
522
- legend.className = 'griot-quiz__question-text';
523
- legend.innerHTML = `<span class="griot-quiz__q-num">${idx + 1}.</span> ${escapeHtml(text)}`;
524
- fieldset.appendChild(legend);
525
-
526
- const optionsContainer = document.createElement('div');
527
- optionsContainer.className = 'griot-quiz__options';
528
-
529
- options.forEach((opt, optIdx) => {
530
- const label = document.createElement('label');
531
- label.className = 'griot-quiz__option';
532
-
533
- const radio = document.createElement('input');
534
- radio.type = 'radio';
535
- radio.name = `quiz_${block.id}_${qid}`;
536
- radio.value = optIdx;
537
- if (userChoice === optIdx) radio.checked = true;
538
- radio.addEventListener('change', () => {
539
- // Update stored answers in meta (optional – allows preserving after submit)
540
- const newAnswers = { ...(block.meta._submittedAnswers || {}), [qid]: optIdx };
541
- // We'll trigger an external callback later – for now just store in meta
542
- // But meta updates must go through the editor, not viewer. So we only calculate on‑the‑fly.
543
- // Instead, we call a user callback when the quiz is submitted.
544
- });
545
-
546
- const span = document.createElement('span');
547
- span.textContent = `${String.fromCharCode(65 + optIdx)}. ${escapeHtml(opt)}`;
548
-
549
- label.appendChild(radio);
550
- label.appendChild(span);
551
- optionsContainer.appendChild(label);
552
- });
553
-
554
- fieldset.appendChild(optionsContainer);
555
-
556
- // Show correct / wrong feedback after evaluation
557
- const feedback = document.createElement('div');
558
- feedback.className = 'griot-quiz__feedback';
559
- fieldset.appendChild(feedback);
560
-
561
- form.appendChild(fieldset);
562
- });
563
-
564
- const submitBtn = document.createElement('button');
565
- submitBtn.type = 'button';
566
- submitBtn.className = 'griot-quiz__submit';
567
- submitBtn.textContent = 'Check answers';
568
- submitBtn.addEventListener('click', () => {
569
- let score = 0;
570
- const newAnswers = {};
571
- questions.forEach((q, idx) => {
572
- const qid = q.id || `q${idx}`;
573
- const radios = form.querySelectorAll(`input[name="quiz_${block.id}_${qid}"]`);
574
- let selected = null;
575
- radios.forEach((r, i) => { if (r.checked) selected = i; });
576
- newAnswers[qid] = selected;
577
- const isCorrect = (selected !== null && selected === q.correctOption);
578
- if (isCorrect) score++;
579
-
580
- // Show feedback per question
581
- const feedbackDiv = form.querySelector(`fieldset[data-index="${idx}"] .griot-quiz__feedback`);
582
- if (feedbackDiv) {
583
- if (selected === null) {
584
- feedbackDiv.textContent = '❓ No answer selected.';
585
- feedbackDiv.className = 'griot-quiz__feedback griot-quiz__feedback--missing';
586
- } else if (isCorrect) {
587
- feedbackDiv.textContent = '✓ Correct!';
588
- feedbackDiv.className = 'griot-quiz__feedback griot-quiz__feedback--correct';
589
- } else {
590
- const correctAnswerText = q.options[q.correctOption];
591
- feedbackDiv.innerHTML = `✗ Incorrect. Correct answer: ${escapeHtml(correctAnswerText)}. ${escapeHtml(q.explanation || '')}`;
592
- feedbackDiv.className = 'griot-quiz__feedback griot-quiz__feedback--wrong';
593
- }
594
- if (q.explanation && selected !== null && !isCorrect) {
595
- // Already added above
596
- }
597
- }
598
- });
599
- totalScore = questions.length;
600
- userScore = score;
477
+ // Lock the form prevent resubmit
478
+ form.querySelectorAll('input[type="radio"]').forEach(r => r.disabled = true);
479
+ submitBtn.disabled = true;
480
+ submitBtn.textContent = 'Submitted';
481
+ submitBtn.style.opacity = '0.5';
482
+ submitBtn.style.cursor = 'default';
601
483
 
602
- // Display overall score
484
+ // Show score
603
485
  const existingScore = container.querySelector('.griot-quiz__score');
604
486
  if (existingScore) existingScore.remove();
605
487
  const scoreDiv = document.createElement('div');
606
488
  scoreDiv.className = 'griot-quiz__score';
607
- scoreDiv.textContent = `You scored ${score} out of ${totalScore}.`;
489
+ scoreDiv.textContent = answered < questions.length
490
+ ? `You scored ${score} out of ${answered} answered (${questions.length - answered} skipped).`
491
+ : `You scored ${score} out of ${answered}.`;
608
492
  container.appendChild(scoreDiv);
609
493
 
610
- // Fire user callback if provided
611
494
  if (typeof opts.onQuizSubmit === 'function') {
612
- opts.onQuizSubmit(block.id, score, totalScore, newAnswers);
495
+ opts.onQuizSubmit(block.id, score, answered, answers);
613
496
  }
614
497
  });
615
498