A React-based authoring editor for Sunbird QuML QuestionSets (question banks organized into sections/hierarchy). It is distributed as:
- a native custom element —
<sb-questionset-editor>— that can be embedded in any framework or plain HTML, and - a React component (
QuestionsetEditor) for apps that are already on React.
It replaces the older Angular questionset-editor-library and is built to be embedded by a host app (e.g. the Sunbird portal), not run standalone.
npm install @project-sunbird/sunbird-questionset-editor-web-component-react react react-domreact and react-dom are peer dependencies (^18.0.0 || ^19.0.0) — they are not bundled and must be provided by the host page/app. The published bundle externalizes react, react-dom, react-dom/client, and react/jsx-runtime, so your app (or an import map, if you're not using a bundler) must resolve these.
The package ships as an ES module only:
| package.json field | value |
|---|---|
module / entry |
dist/index.js |
types |
dist/index.d.ts |
| stylesheet | dist/style.css (import path: @project-sunbird/sunbird-questionset-editor-web-component-react/dist/style.css) |
files |
dist (everything published lives under dist/) |
The component renders in the light DOM (no Shadow DOM), so it relies on the page loading dist/style.css — it does not inject its own styles.
The dist/ folder contains more than just index.js/style.css; it also includes assets that are referenced at fixed, root-relative runtime paths, not resolved via the module graph. Your app must make these reachable at the paths below (e.g. copy them into your public/static directory, or serve dist/ itself at your app root):
dist/assets/sunbird-quml-player.jsanddist/assets/sunbird-quml-player-styles.css— the QuML player used for question/questionset preview, lazy-loaded at runtime from/assets/sunbird-quml-player.jsby default (override viaconfig.playerScriptUrl).dist/assets/libs/mathEquation/**— the MathQuill/KaTeX equation-editor modal (iframe), loaded from the fixed path/assets/libs/mathEquation/plugin/mathModal/index.html. Copy the wholemathEquationfolder as-is, including its nestedfonts/(KaTeX) andcss/jssubfolders — itsindex.html,katex.min.css, andmathquill.cssall load these siblings by relative path, so they must stay nested exactly as shipped, not flattened or split across other static folders.dist/ckeditor/ckeditor.js— CKEditor 4 classic build used for rich-text question/option fields. This is not loaded automatically; the host page must load it as a global<script>tag before the editor mounts (it registerswindow.ClassicEditor, whichCKEditorFieldlooks for).
Register the custom element once, then create it like any other DOM element and set its configuration as element properties (not string attributes) so React receives real objects:
<script src="/ckeditor/ckeditor.js"></script>
<link rel="stylesheet" href="/style.css" />
<div id="root"></div>
<script type="module">
import { registerQuestionsetEditor } from '@project-sunbird/sunbird-questionset-editor-web-component-react';
registerQuestionsetEditor(); // defines <sb-questionset-editor>, idempotent
const editor = document.createElement('sb-questionset-editor');
editor.context = {
authToken: '',
userId: 'user-001',
channel: 'my-channel',
pdata: { id: 'sunbird.portal', ver: '1.0' },
env: 'questionset_editor',
contentId: 'do_123',
identifier: 'do_123',
framework: 'NCF',
};
editor.config = {
mode: 'edit',
objectType: 'QuestionSet',
primaryCategory: 'Practice Question Set',
maxDepth: 3,
};
editor.style.cssText = 'display:block;width:100%;height:100%;';
document.getElementById('root').appendChild(editor);
</script>The tag name is fixed: sb-questionset-editor. registerQuestionsetEditor() guards against double-registration, so it's safe to call more than once.
import { QuestionsetEditor } from '@project-sunbird/sunbird-questionset-editor-web-component-react';
import '@project-sunbird/sunbird-questionset-editor-web-component-react/dist/style.css';
<QuestionsetEditor
context={context}
config={config}
onQuestionSaved={(question) => console.log('saved', question)}
onHierarchySaved={(hierarchy) => console.log('hierarchy saved', hierarchy)}
onError={(err) => console.error(err)}
/>| Prop | Type | Required | Description |
|---|---|---|---|
context |
IContext |
✅ | Runtime context — user, session, channel, content identifier |
config |
IConfig |
✅ | Editor mode and behaviour |
metadata |
Record<string, unknown> |
— | Pre-loaded content metadata |
data |
unknown |
— | Reserved/advanced pass-through data |
apiBaseUrl |
string |
— | Base URL for all API calls. Omit when using a server-side proxy |
onToolbarEvent |
(e: { action: ToolbarAction; data?: unknown }) => void |
— | Fired on every toolbar action |
onQuestionSaved |
(question: unknown) => void |
— | Fired after a question is saved |
onHierarchySaved |
(hierarchy: unknown) => void |
— | Fired after a successful hierarchy save |
onError |
(error: Error) => void |
— | Fired on unrecoverable editor errors |
On the web component, set the same fields as element properties (e.g. editor.onQuestionSaved = (q) => ...), the same way context/config are set.
Identifies the current user/session and the content being edited. sid, did, channel, and pdata are required; everything else is optional.
interface IContext {
authToken?: string;
userId?: string;
user?: { id: string; fullName?: string; firstName?: string; lastName?: string; orgIds?: string[] };
sid: string;
did: string;
uid?: string;
channel: string;
pdata: { id: string; ver: string; pid?: string };
env?: string;
contentId?: string;
identifier?: string; // same value as contentId is also accepted
framework?: string;
targetFWIds?: string[];
rollup?: Record<string, string>;
contextRollup?: Record<string, string>;
objectRollup?: Record<string, string>;
cdata?: Array<Record<string, unknown>>;
tags?: string[];
host?: string; // telemetry host/endpoint
endpoint?: string;
timeDiff?: number;
defaultLicense?: string;
enableReviewEdit?: boolean; // allow reviewer edits in orgreview/sourcingreview
uiLanguage?: string; // 'en' | 'ar' | 'fr' | 'hi' | 'pt'
labels?: Record<string, string>; // overrides for editor button labels
cloudStorage?: { provider?: string; presigned_headers?: Record<string, string> };
cloudStorageUrls?: string[]; // blob-storage origins to rewrite to /assets/public/
}Controls editor mode and behaviour. mode and objectType are required.
interface IConfig {
mode: 'edit' | 'review' | 'read' | 'orgreview' | 'sourcingreview';
objectType: string; // e.g. 'QuestionSet'
hideSubmitForReviewBtn?: boolean;
playerScriptUrl?: string; // default '/assets/sunbird-quml-player.js'
apiSlug?: string; // API path prefix, default '/api'
questionSet?: { maxQuestionsLimit?: number };
showAddCollaborator?: boolean;
primaryCategory?: string;
framework?: string[];
targetFWIds?: string[];
toolbarConfig?: Record<string, unknown>;
hierarchy?: Record<string, unknown>;
children?: unknown[];
defaultFields?: Record<string, unknown>;
maxDepth?: number;
questionTypes?: string[]; // restrict which question types are offered
maxQuestions?: number;
showSolutions?: boolean;
showHints?: boolean;
showTimer?: boolean;
categoryDefinitionApiVersion?: 'v1' | 'v4'; // default 'v4'
}type ToolbarAction =
| 'back' | 'preview' | 'sendForReview' | 'onFormValueChange' | 'onFormStatusChange'
| 'saveContent' | 'publish' | 'reject';By default all API calls are made relative to the page origin (works with a server-side proxy). To point directly at a backend, either pass the prop:
<QuestionsetEditor {...config} apiBaseUrl="https://api.your-sunbird-instance.com" />or call the exported setters directly (also lets you override the /api path prefix independently):
import { setApiBaseUrl, setApiSlug } from '@project-sunbird/sunbird-questionset-editor-web-component-react';
setApiBaseUrl('https://api.your-sunbird-instance.com');
setApiSlug('/api'); // defaultThe editor exposes its internal Zustand stores for advanced integration scenarios — for example, reading the current tree selection or editor mode from outside the component:
import { useEditorStore, useTreeStore, useQuestionStore, useUiStore } from '@project-sunbird/sunbird-questionset-editor-web-component-react';
// Inside a React component
const selectedNodeId = useTreeStore((s) => s.selectedNodeId);
const editorMode = useEditorStore((s) => s.editorMode);The stylesheet must be imported once in your app — it is not auto-injected (the component renders in the light DOM, not Shadow DOM):
import '@project-sunbird/sunbird-questionset-editor-web-component-react/dist/style.css';Registered out of the box (matching the old editor's six built-in QuML types):
| Type | qType |
primaryCategory |
Description |
|---|---|---|---|
| Multiple Choice | MCQ |
Multiple Choice Question | Pick one correct option from a list |
| Subjective | SA (aliases: VSA, LA) |
Subjective Question | Free-form written or long answer |
| Fill in the Blank | FTB |
FTB Question | Hide words in a sentence with [[ ]] |
| Match the Following | MTF |
Match The Following Question | Pair items across two columns |
| Sequence | SEQ |
Sequence Question | Arrange items in the correct order |
| Reorder | REO |
Reorder Question | Rearrange shuffled words into a sentence |
| True / False | BOOL |
Boolean Question | Pick between true or false choices |
config.questionTypes can be used to restrict which of these are offered when adding a question. Host apps can also register additional/custom types before mounting the editor via the exported registry functions: registerQuestionType, resolveQuestionType, resolveByQType, resolveByCategory, resolveByInteractionType, allQuestionTypes.
# Clone the repo
git clone https://github.com/Sunbird-inQuiry/editor-v2.git
cd editor
# Install
npm install
# Dev server with mock data — no .env needed, just run:
npm run devTo point the dev server at a real backend instead of mock data, copy .env.example to .env and fill in your credentials (BASE_URL, AUTH_TOKEN, USER_TOKEN, CONTENT_ID, etc.):
cp .env.example .env
# edit .env with your credentials
npm run build # runs tsc -b && vite build
npm run serve # standalone server.js, reads .env# Run tests
npm test| Dependency | Version |
|---|---|
| React | 18.x or 19.x |
| React DOM | 18.x or 19.x |
MIT — see the repository root for the full licence text.
- CKEditor must be loaded manually. Add
<script src=".../ckeditor.js"></script>(from this package'sdist/ckeditor/ckeditor.js) to the host page before the editor mounts; rich-text fields look forwindow.ClassicEditorand log an error if it isn't present. - No Shadow DOM. The custom element renders into the light DOM so the Sunbird design system CSS (
dist/style.css) applies directly — make sure it's loaded on the page. - QuML player script for previews defaults to the root-relative path
/assets/sunbird-quml-player.js; override withconfig.playerScriptUrlif you serve it elsewhere. - Localized UI labels are available for
en,ar,fr,hi,ptviacontext.uiLanguage. - This package is ESM-only (
"type": "module") and built for evergreen browsers; there is no UMD/CJS build.