npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@creaditor/cdtr-quiz-player

v0.6.0

Published

Embeddable <cdtr-quiz-player> web component. Draws a quiz authored in cdtr-course-builder, marks each answer, and grades the finished attempt against a passing mark. No networking, no storage, no identity: the host owns the gradebook and what a result mea

Readme

@creaditor/cdtr-quiz-player

The learner's half of a quiz authored in @creaditor/cdtr-course-builder. It reads the stored quiz shape, draws the questions, accepts one answer each and says whether it was right.

It does not fetch, store, or know who the learner is. Every answer leaves as a quiz-answered event, and the finished attempt leaves as quiz-completed with a grade. That is the same division the builder uses: the component owns the surface, the host owns the data.

<script type="module" src="https://unpkg.com/@creaditor/cdtr-quiz-player"></script>

<cdtr-quiz-player language="he"></cdtr-quiz-player>

<script type="module">
  const player = document.querySelector('cdtr-quiz-player');

  // The `props` of a `type: "quiz"` node, straight out of save-lesson-data.
  player.questions = quizNode.props;

  player.addEventListener('quiz-answered', (e) => {
    // { questionId, optionId, result: 'correct' | 'incorrect' | 'ungraded' }
    console.log(e.detail);
  });
</script>

What it takes

| | | |---|---| | player.questions = … | A quiz node's props, or the bare questions array. Setting it clears any answers already given. | | player.props = … | Alias, for hosts that spread a node's props. | | language="he" \| "en" | Learner-facing copy and text direction. Defaults to Hebrew, matching the builder. | | player.questions (read) | The parsed, drawable questions. | | player.dropped (read) | How many authored questions were too malformed to draw. | | passing-grade="70" | The mark, 0–100, the learner must reach. Absent means a score and no verdict. | | display-mode="all-at-once" \| "one-at-a-time" | Every question on one page, or one at a time. Defaults to all-at-once, and so does anything unrecognised. | | player.score (read) | The running QuizScore. Readable mid-attempt. | | player.passingGrade (read) | The parsed mark, or null. | | player.displayMode (read) | The parsed layout mode. | | props.intro | Instructions shown on their own screen before the questions. Optional. | | props.showAnswers | false hides the right answer after a wrong one. Defaults to showing. | | kind: "open" on a question | The learner writes an answer instead of picking one. Never graded. |

What it emits

Both events bubble and are composed, so a host can delegate from a container.

quiz-answered, once per question — answering is one-shot, and the question locks afterwards:

{ questionId: 'q_48c9574f', optionIds: ['o_178416ec'], result: 'correct' }

optionIds is always an array — one entry for a single-answer question — so a host reading the event never has to branch on the question type.

result: 'ungraded' means the authored question marked no correct answer at all; the learner could still pick, and no verdict was shown.

Choose all that apply

Mark more than one option correct: true and the question becomes a multiple-answer one on its own. No extra flag:

  • The learner gets checkboxes instead of radios, and a "choose all that apply" line.
  • It is marked right for exactly the correct set. No partial credit: half of a set is a different answer, not most of the right one, and awarding it half a mark is a grading policy the creator never set.
  • After answering, every right option is marked, not only the one picked — the learner needs to see the whole set they were aiming at.
  • It counts as one gradable question, however many boxes it has.

The input type does tell a learner whether to look for one answer or several. That is a non-issue here for the reason stated under What this is not: the correct answers are already in page memory.

quiz-completed, once, when the last question is answered — after that question's own quiz-answered, so a host sees them in the order the learner lived them:

{
  answered: 4, total: 4,      // every drawn question
  gradable: 3, correct: 3,    // of those, the ones that CAN be scored
  percent: 100,               // correct out of gradable
  complete: true,
  passed: true,               // null when there is no verdict to give
  passingGrade: 70,
  answers: [ /* every pick, in the order the questions were drawn */ ]
}

One question at a time

display-mode="one-at-a-time" shows a single question, a "Question 3 of 8" counter, and a Next button that appears once the question has been answered.

player.setAttribute('display-mode', 'one-at-a-time');

It is the creator's layout choice, not the learner's, and it changes nothing a host sees: the same quiz-answered per question, the same quiz-completed after the last one, the same score.

  • Forward only. Answering is one-shot — the inputs lock as soon as a question is checked — so a Back button would offer nothing to do but re-read, while implying an edit that is not on offer.
  • Next appears only after the question is answered, and never on the last one, where the summary is what follows.
  • Feedback still lands per question. When the verdict appears is a separate policy from how many questions are on screen; both modes check one question at a time.
  • Anything unrecognised means all-at-once. Forgiving in the direction that keeps content visible: a misspelt attribute shows the whole quiz, rather than windowing it to one question and looking like content that went missing.
  • Focus follows the learner — to the verdict after Check, to the new question after Next. Every action repaints the shadow tree, so without it the keyboard would fall to the body and Next would be reachable only by tabbing in from the top of your page.

Instructions before the questions

If the quiz node's props carry an intro string, the player opens on it — the text, and a Start button — instead of the first question.

player.questions = { questions: [...], intro: 'Read carefully — you get one attempt.' };
  • Only when the creator wrote one. No intro, whitespace-only, or a non-string, and there is no screen: the quiz opens on question 1 exactly as it did before this existed.
  • It replaces the questions, it does not sit above them. Pressing Start is what makes it an acknowledgement rather than something scrolled past.
  • Both display modes. one-at-a-time shows it before the counter.
  • Never enforced. Start records that the learner pressed a button, nothing more. "You get one attempt" is a rule only you can hold them to, because only you know who is watching.
  • Not persisted. A reload shows it again, the same way a reload already returns a one-at-a-time learner to question 1.

Setting questions again — a host swapping lessons — makes the instructions unread again, for the same reason it clears the answers.

Grading a whole attempt

Set passing-grade and the player scores the attempt, shows the learner where they landed, and reports it. It still decides nothing: whether a failed quiz blocks the lesson, allows a retry, or notifies a teacher is the host's rule.

player.setAttribute('passing-grade', '70');

player.addEventListener('quiz-completed', (e) => {
  myBackend.saveGrade(lessonId, e.detail.percent);      // YOUR gradebook
  if (e.detail.passed) markLessonComplete(lessonId);    // YOUR rule
});

Four things it is deliberate about:

  • gradable is separate from total. A question naming no single correct answer cannot be scored, so it is kept out of the denominator — otherwise a flawless learner reads 3/4 because of the creator's authoring mistake. It still draws, still takes a pick, and still has to be answered before the attempt is complete.
  • passed is null, not false, until there is a verdict to give: no pass mark set, nothing gradable, or the attempt unfinished. A quiz in progress has not been failed.
  • A pass mark is a percentage, not a question count. 70% survives the creator adding a seventh question; "5 of 6" quietly becomes wrong.
  • A quiz with nothing gradable says so in words rather than showing 0%, which a learner reads as "I got everything wrong".

The mark is read as a percentage 0–100. Anything else — blank, unparseable, out of range — means no pass mark rather than a mark of zero.

The shape it reads

Documented in full under The stored quiz shape in the builder's README. The part this package binds to:

{
  "type": "quiz",
  "props": {
    "questions": [
      {
        "id": "q_48c9574f",
        "text": "What is the capital of France?",
        "options": [
          { "id": "o_959bda38", "text": "Lyon",  "correct": false },
          { "id": "o_178416ec", "text": "Paris", "correct": true  }
        ]
      }
    ]
  }
}

That JSON is the coupling between the two packages — not a TypeScript import. This package depends on nothing at all, which is what lets it drop into a renderer without dragging an authoring tool and its editor bundle behind it.

It expects to be handed broken data

The builder enforces 2–6 options, exactly one correct, and non-blank text — but only while a creator is typing, and explicitly not on data that reached it from somewhere else. By the time a quiz arrives here it has been through a database and a network hop, so every field is read defensively:

  • A question that cannot be drawn is counted in dropped, never thrown.
  • Malformed options are dropped individually; the question survives if two usable ones remain.
  • Duplicate question or option ids collapse — answers are reported by id, and duplicates would make them unattributable.
  • correct must be exactly true. A stringified "false" from a database round-trip does not mark an option correct.
  • Zero correct answers is the only ungradable state, and the question still draws rather than hiding the creator's work from a learner. Two or more is a choose-all-that-apply question, not a broken one.

parseQuiz, gradeAnswer, scoreQuiz and readPassingGrade are exported if you want the logic without the element — scoreQuiz is what a host would call to grade server-side against answers it collected itself. They are pure, and their tests run with no DOM in scope.

What this is not

Not an assessment. The verdict is computed in the browser, so the correct answer is on the client and anyone can read it out of memory. That is fine for practice and formative feedback. A grade that counts has to be scored on a server against answers the client never sees — a different contract, deliberately not attempted here.

A score is now computed across questions, and it is still not an assessment for the reason above: the marking happens on the client.

Also absent, and belonging to a host or a later milestone: retries, saving a grade, one-question-at-a-time reveal, and anything that remembers a learner between visits.

Development

npm install
npm test          # 105 tests, node + happy-dom in one run
npm run dev       # preview.html on :5182
npm run build

preview.html carries three fixtures — a well-formed quiz, deliberately half-broken data, and an empty one — plus a live log of every event the component emits.