diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index 64936a1e998..9a69e69c50d 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -523,7 +523,8 @@
"instructions": "Instructions",
"notes": "Notes",
"preview": "Preview",
- "editor": "Editor"
+ "editor": "Editor",
+ "interactive-editor": "Interactive Editor"
},
"editor-alerts": {
"tab-trapped": "Pressing tab will now insert the tab character",
@@ -952,7 +953,8 @@
"editor-a11y-on-macos": "{{editorName}} editor content. Accessibility mode set to 'on'. Press Command+E to disable or press Option+F1 for more options.",
"editor-a11y-on-non-macos": "{{editorName}} editor content. Accessibility mode set to 'on'. Press Ctrl+E to disable or press Alt+F1 for more options.",
"terminal-output": "Terminal output",
- "not-available": "Not available"
+ "not-available": "Not available",
+ "interactive-editor-desc": "Turn static code examples into interactive editors. This allows you to edit and run the code directly on the page."
},
"flash": {
"no-email-in-userinfo": "We could not retrieve an email from your chosen provider. Please try another provider or use the 'Continue with Email' option.",
diff --git a/client/src/components/Flash/flash.css b/client/src/components/Flash/flash.css
index a95786d08f8..88fccd1ad2a 100644
--- a/client/src/components/Flash/flash.css
+++ b/client/src/components/Flash/flash.css
@@ -10,5 +10,5 @@ div.flash-message {
padding-bottom: 3px;
position: fixed;
width: 100%;
- z-index: 150;
+ z-index: var(--z-index-flash);
}
diff --git a/client/src/components/Header/components/universal-nav.css b/client/src/components/Header/components/universal-nav.css
index 1a19453a19c..85ed12e0759 100644
--- a/client/src/components/Header/components/universal-nav.css
+++ b/client/src/components/Header/components/universal-nav.css
@@ -73,6 +73,7 @@
position: absolute;
right: 0;
width: 100%;
+ z-index: var(--z-index-site-header);
}
@media (min-width: 980px) {
diff --git a/client/src/components/layouts/global.css b/client/src/components/layouts/global.css
index e776537c708..a12de135c44 100644
--- a/client/src/components/layouts/global.css
+++ b/client/src/components/layouts/global.css
@@ -139,6 +139,14 @@ hr {
min-height: 0;
}
+.default-layout:has(.breadcrumbs-demo) #learn-app-wrapper {
+ padding-top: var(--breadcrumbs-height);
+}
+
+.default-layout:has(.action-row) #learn-app-wrapper {
+ padding-top: calc(var(--breadcrumbs-height) + var(--action-row-height));
+}
+
h1 {
color: var(--secondary-color);
font-weight: 700;
diff --git a/client/src/components/layouts/variables.css b/client/src/components/layouts/variables.css
index 76b5a316968..cef80095521 100644
--- a/client/src/components/layouts/variables.css
+++ b/client/src/components/layouts/variables.css
@@ -37,6 +37,9 @@
--header-sub-element-size: 45px;
--header-height: 38px;
--breadcrumbs-height: 44px;
+ --action-row-height: 64px;
+ --z-index-breadcrumbs: 100;
+ --z-index-flash: 150;
--z-index-site-header: 200;
}
diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts
index e14270a4650..39c1fb56881 100644
--- a/client/src/redux/prop-types.ts
+++ b/client/src/redux/prop-types.ts
@@ -182,7 +182,12 @@ type ParagraphNodule = {
type InteractiveEditorNodule = {
type: 'interactiveEditor';
- data: { ext: Ext; name: string; contents: string }[];
+ data: {
+ ext: Ext;
+ name: string;
+ contents: string;
+ contentsHtml: string;
+ }[];
};
export type ChallengeNode = {
diff --git a/client/src/templates/Challenges/classic/action-row.tsx b/client/src/templates/Challenges/classic/action-row.tsx
index bea0000d277..c14fe5d4aef 100644
--- a/client/src/templates/Challenges/classic/action-row.tsx
+++ b/client/src/templates/Challenges/classic/action-row.tsx
@@ -6,7 +6,7 @@ import store from 'store';
import { DailyCodingChallengeLanguages } from '../../../redux/prop-types';
import EditorTabs from './editor-tabs';
-interface ActionRowProps {
+interface ClassicLayoutProps {
dailyCodingChallengeLanguage: DailyCodingChallengeLanguages;
hasNotes: boolean;
hasPreview: boolean;
@@ -21,24 +21,58 @@ interface ActionRowProps {
showPreviewPane: boolean;
showPreviewPortal: boolean;
togglePane: (pane: string) => void;
+ hasInteractiveEditor?: never;
}
-const ActionRow = ({
- hasPreview,
- hasNotes,
- togglePane,
- showNotes,
- showPreviewPane,
- showPreviewPortal,
- showConsole,
- showInstructions,
- areInstructionsDisplayable,
- isDailyCodingChallenge,
- dailyCodingChallengeLanguage,
- setDailyCodingChallengeLanguage
-}: ActionRowProps): JSX.Element => {
+interface InteractiveEditorProps {
+ hasInteractiveEditor: true;
+ showInteractiveEditor: boolean;
+ toggleInteractiveEditor: () => void;
+}
+
+type ActionRowProps = ClassicLayoutProps | InteractiveEditorProps;
+
+const ActionRow = (props: ActionRowProps): JSX.Element => {
const { t } = useTranslation();
+ if (props.hasInteractiveEditor) {
+ const { toggleInteractiveEditor, showInteractiveEditor } = props;
+
+ return (
+
+
+
+
+
+ {t('aria.interactive-editor-desc')}
+
+
+
+
+ );
+ }
+
+ const {
+ togglePane,
+ hasPreview,
+ hasNotes,
+ areInstructionsDisplayable,
+ showConsole,
+ showNotes,
+ showInstructions,
+ showPreviewPane,
+ showPreviewPortal,
+ isDailyCodingChallenge,
+ dailyCodingChallengeLanguage,
+ setDailyCodingChallengeLanguage
+ } = props;
+
// sets screen reader text for the two preview buttons
function getPreviewBtnsSrText() {
// no preview open
diff --git a/client/src/templates/Challenges/classic/classic.css b/client/src/templates/Challenges/classic/classic.css
index 7b8da74da0a..df482e7f517 100644
--- a/client/src/templates/Challenges/classic/classic.css
+++ b/client/src/templates/Challenges/classic/classic.css
@@ -18,8 +18,15 @@
}
.action-row {
+ height: var(--action-row-height);
+ position: fixed;
+ top: calc(var(--header-height) + var(--breadcrumbs-height));
+ left: 0;
+ right: 0;
+ z-index: 100;
padding: 10px;
border-bottom: 1px solid var(--quaternary-background);
+ background-color: var(--secondary-background);
}
.monaco-editor-tabs button[aria-expanded='true'],
@@ -79,6 +86,7 @@
width: 30%;
display: flex;
justify-content: flex-end;
+ margin-inline-start: auto;
}
.monaco-editor-tabs button + button {
diff --git a/client/src/templates/Challenges/classic/editor.css b/client/src/templates/Challenges/classic/editor.css
index bbea71a0d3c..9480a784f72 100644
--- a/client/src/templates/Challenges/classic/editor.css
+++ b/client/src/templates/Challenges/classic/editor.css
@@ -32,10 +32,16 @@ textarea.inputarea {
}
.breadcrumbs-demo {
+ position: fixed;
+ top: var(--header-height);
+ left: 0;
+ right: 0;
+ z-index: var(--z-index-breadcrumbs);
font-size: 16px;
margin: 0;
padding: 10px;
height: var(--breadcrumbs-height);
+ background: var(--secondary-background);
}
@media screen and (max-height: 300px) {
diff --git a/client/src/templates/Challenges/components/interactive-editor.css b/client/src/templates/Challenges/components/interactive-editor.css
index 853cce90680..8a9c80705ae 100644
--- a/client/src/templates/Challenges/components/interactive-editor.css
+++ b/client/src/templates/Challenges/components/interactive-editor.css
@@ -23,7 +23,7 @@
}
.sp-preview-actions .sp-button:hover {
- background-color: var(--gray-10);
+ background-color: var(--gray-10) !important;
color: var(--gray-90) !important;
- border-color: var(--gray-90);
+ border-color: var(--gray-90) !important;
}
diff --git a/client/src/templates/Challenges/components/interactive-editor.tsx b/client/src/templates/Challenges/components/interactive-editor.tsx
index 2955faf8d10..12c836ddba4 100644
--- a/client/src/templates/Challenges/components/interactive-editor.tsx
+++ b/client/src/templates/Challenges/components/interactive-editor.tsx
@@ -7,6 +7,7 @@ export interface InteractiveFile {
ext: string;
name: string;
contents: string;
+ contentsHtml: string;
fileKey?: string;
}
@@ -52,7 +53,7 @@ const InteractiveEditor = ({ files }: Props) => {
};
return (
-
+
void;
}
-function renderNodule(nodule: ChallengeNode['challenge']['nodules'][number]) {
+function renderNodule(
+ nodule: ChallengeNode['challenge']['nodules'][number],
+ showInteractiveEditor: boolean
+) {
switch (nodule.type) {
case 'paragraph':
return ;
case 'interactiveEditor':
- return ;
+ if (showInteractiveEditor) {
+ return ;
+ } else {
+ const files = nodule.data;
+ return files.map((file, index) => (
+
+ ));
+ }
default:
return null;
}
@@ -207,6 +218,20 @@ const ShowGeneric = ({
const sceneSubject = new SceneSubject();
+ // interactive editor
+ const hasInteractiveEditor = nodules?.some(
+ nodule => nodule.type === 'interactiveEditor'
+ );
+
+ const [showInteractiveEditor, setShowInteractiveEditor] = useState(
+ () => !!store.get('showInteractiveEditor')
+ );
+
+ const toggleInteractiveEditor = () => {
+ store.set('showInteractiveEditor', !showInteractiveEditor);
+ setShowInteractiveEditor(!showInteractiveEditor);
+ };
+
return (
-
-
-
-
- {title}
-
+
+ {hasInteractiveEditor && (
+
+ )}
-
+
+
+
+
+ {title}
+
- {description && (
-
-
-
-
- )}
+
- {nodules?.map((nodule, i) => {
- return (
- {renderNodule(nodule)}
- );
- })}
-
-
- {videoId && (
- <>
-
-
- >
- )}
-
-
- {scene && }
-
-
- {transcript && }
-
- {instructions && (
- <>
+ {description && (
+
- >
+
)}
- {assignments.length > 0 && (
-
-
-
- )}
+ {nodules?.map((nodule, i) => {
+ return (
+
+ {renderNodule(nodule, showInteractiveEditor)}
+
+ );
+ })}
- {questions.length > 0 && (
-
-
-
- )}
+
+ {videoId && (
+ <>
+
+
+ >
+ )}
+
- {explanation ? (
-
- ) : null}
+ {scene && }
- {!hasAnsweredMcqCorrectly && (
- {t('learn.answered-mcq')}
- )}
+
+ {transcript && }
-
-
-
+ {instructions && (
+ <>
+
+
+ >
+ )}
-
-
-
-
-
+ {assignments.length > 0 && (
+
+
+
+ )}
+
+ {questions.length > 0 && (
+
+
+
+ )}
+
+ {explanation ? (
+
+ ) : null}
+
+ {!hasAnsweredMcqCorrectly && (
+ {t('learn.answered-mcq')}
+ )}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/curriculum/schema/challenge-schema.js b/curriculum/schema/challenge-schema.js
index eb12c4ada1a..affad7cc2da 100644
--- a/curriculum/schema/challenge-schema.js
+++ b/curriculum/schema/challenge-schema.js
@@ -192,7 +192,8 @@ const schema = Joi.object().keys({
Joi.object().keys({
ext: Joi.string().required(),
name: Joi.string().required(),
- contents: Joi.string().required()
+ contents: Joi.string().required(),
+ contentsHtml: Joi.string().required()
})
),
otherwise: Joi.string().required()
diff --git a/tools/challenge-parser/parser/plugins/add-interactive-elements.js b/tools/challenge-parser/parser/plugins/add-interactive-elements.js
index d6940d69c8d..21ab46d4bbb 100644
--- a/tools/challenge-parser/parser/plugins/add-interactive-elements.js
+++ b/tools/challenge-parser/parser/plugins/add-interactive-elements.js
@@ -54,12 +54,16 @@ function getFiles(filesNodes) {
return filesNodes.map(node => {
counts[node.lang] = counts[node.lang] ? counts[node.lang] + 1 : 1;
+
+ const contentsHtml = mdastToHTML([node]);
+
const out = {
contents: node.value,
ext: node.lang,
name:
getFilenames(node.lang) +
- (counts[node.lang] ? `-${counts[node.lang]}` : '')
+ (counts[node.lang] ? `-${counts[node.lang]}` : ''),
+ contentsHtml
};
return out;
diff --git a/tools/challenge-parser/parser/plugins/add-interactive-elements.test.js b/tools/challenge-parser/parser/plugins/add-interactive-elements.test.js
index db6e9a185af..1bdc7167a1d 100644
--- a/tools/challenge-parser/parser/plugins/add-interactive-elements.test.js
+++ b/tools/challenge-parser/parser/plugins/add-interactive-elements.test.js
@@ -28,51 +28,51 @@ describe('add-interactive-editor plugin', () => {
element => element.type === 'interactiveEditor'
);
- expect(editorElements).toEqual(
- expect.arrayContaining([
- {
- data: [
- {
- ext: expect.any(String),
- name: expect.any(String),
- contents: expect.stringContaining(
- 'This is an interactive element
'
- )
- }
- ],
- type: 'interactiveEditor'
- }
- ])
- );
-
- expect(editorElements).toEqual(
- expect.arrayContaining([
- {
- data: [
- {
- ext: expect.any(String),
- name: expect.any(String),
- contents: expect.stringContaining(
- 'This is an interactive element'
- )
- }
- ],
- type: 'interactiveEditor'
- },
- {
- data: [
- {
- ext: expect.any(String),
- name: expect.any(String),
- contents: expect.stringContaining(
- "console.log('Interactive JS');"
- )
- }
- ],
- type: 'interactiveEditor'
- }
- ])
- );
+ expect(editorElements).toEqual([
+ {
+ type: 'interactiveEditor',
+ data: [
+ {
+ contents: "console.log('Interactive JS');",
+ ext: 'js',
+ name: 'script-1',
+ contentsHtml:
+ 'console.log(\'Interactive JS\');\n
'
+ }
+ ]
+ },
+ {
+ type: 'interactiveEditor',
+ data: [
+ {
+ contents: 'This is an interactive element
',
+ ext: 'html',
+ name: 'index-1',
+ contentsHtml:
+ '<div>This is an interactive element</div>\n
'
+ }
+ ]
+ },
+ {
+ type: 'interactiveEditor',
+ data: [
+ {
+ contents: 'This is an interactive element
',
+ ext: 'html',
+ name: 'index-1',
+ contentsHtml:
+ '<div>This is an interactive element</div>\n
'
+ },
+ {
+ contents: "console.log('Interactive JS');",
+ ext: 'js',
+ name: 'script-1',
+ contentsHtml:
+ 'console.log(\'Interactive JS\');\n
'
+ }
+ ]
+ }
+ ]);
});
it('provides unique names for each file with the same extension', async () => {
@@ -97,6 +97,9 @@ describe('add-interactive-editor plugin', () => {
// Contents should match
expect(files[0].contents).toBe("console.log('First JavaScript file');");
expect(files[1].contents).toBe("console.log('Second JavaScript file');");
+
+ expect(files[0].contentsHtml).toContain('');
+ expect(files[1].contentsHtml).toContain('');
});
it('respects the order of elements in the original markdown', async () => {