feat: create action row and style Multi-file editor (#41579)
* feat: add action row & style editor * fix: separate conditional for splittter and reflex element * fix: move jaws whenever a line is deleted or added * feat: keep line indicator inside editable region * feat: add submit button and restyle decorator Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -1,13 +1,58 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import EditorTabs from './EditorTabs';
|
import EditorTabs from './EditorTabs';
|
||||||
|
|
||||||
const ActionRow = () => (
|
const propTypes = {
|
||||||
<div className='action-row'>
|
block: PropTypes.string,
|
||||||
<EditorTabs />
|
showConsole: PropTypes.bool,
|
||||||
</div>
|
showNotes: PropTypes.bool,
|
||||||
);
|
showPreview: PropTypes.bool,
|
||||||
|
superBlock: PropTypes.string,
|
||||||
|
switchDisplayTab: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
const ActionRow = ({ switchDisplayTab, showPreview, showConsole }) => {
|
||||||
|
const restartStep = () => {
|
||||||
|
console.log('restart');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className='action-row'>
|
||||||
|
<div>
|
||||||
|
<h5 className='breadcrumbs-demo'>
|
||||||
|
Responsive Web Design > Basic HTML Cat Photo App >{' '}
|
||||||
|
<span>Step 23 of 213</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className='tabs-row'>
|
||||||
|
<EditorTabs />
|
||||||
|
<button
|
||||||
|
className='restart-step-tab'
|
||||||
|
onClick={() => restartStep()}
|
||||||
|
role='tab'
|
||||||
|
>
|
||||||
|
Restart Step
|
||||||
|
</button>
|
||||||
|
<div className='panel-display-tabs'>
|
||||||
|
<button
|
||||||
|
className={showConsole ? 'active-tab' : ''}
|
||||||
|
onClick={() => switchDisplayTab('showConsole')}
|
||||||
|
role='tab'
|
||||||
|
>
|
||||||
|
JS Console
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={showPreview ? 'active-tab' : ''}
|
||||||
|
onClick={() => switchDisplayTab('showPreview')}
|
||||||
|
role='tab'
|
||||||
|
>
|
||||||
|
Show Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ActionRow.propTypes = propTypes;
|
||||||
ActionRow.displayName = 'ActionRow';
|
ActionRow.displayName = 'ActionRow';
|
||||||
|
|
||||||
export default ActionRow;
|
export default ActionRow;
|
||||||
|
@ -29,6 +29,20 @@ const reflexProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class DesktopLayout extends Component {
|
class DesktopLayout extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { showNotes: false, showPreview: true, showConsole: false };
|
||||||
|
this.switchDisplayTab = this.switchDisplayTab.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
switchDisplayTab(displayTab) {
|
||||||
|
this.setState(state => {
|
||||||
|
return {
|
||||||
|
[displayTab]: !state[displayTab]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getChallengeFile() {
|
getChallengeFile() {
|
||||||
const { challengeFiles } = this.props;
|
const { challengeFiles } = this.props;
|
||||||
return first(Object.keys(challengeFiles).map(key => challengeFiles[key]));
|
return first(Object.keys(challengeFiles).map(key => challengeFiles[key]));
|
||||||
@ -45,16 +59,30 @@ class DesktopLayout extends Component {
|
|||||||
hasEditableBoundries
|
hasEditableBoundries
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const { showPreview, showConsole } = this.state;
|
||||||
|
|
||||||
const challengeFile = this.getChallengeFile();
|
const challengeFile = this.getChallengeFile();
|
||||||
|
const projectBasedChallenge = showUpcomingChanges && hasEditableBoundries;
|
||||||
|
const isPreviewDisplayable = projectBasedChallenge
|
||||||
|
? showPreview && hasPreview
|
||||||
|
: hasPreview;
|
||||||
|
const isConsoleDisplayable = projectBasedChallenge ? showConsole : true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{showUpcomingChanges && hasEditableBoundries && <ActionRow />}
|
{projectBasedChallenge && (
|
||||||
|
<ActionRow switchDisplayTab={this.switchDisplayTab} {...this.state} />
|
||||||
|
)}
|
||||||
<ReflexContainer className='desktop-layout' orientation='vertical'>
|
<ReflexContainer className='desktop-layout' orientation='vertical'>
|
||||||
<ReflexElement flex={1} {...resizeProps}>
|
{!projectBasedChallenge && (
|
||||||
{instructions}
|
<ReflexElement flex={1} {...resizeProps}>
|
||||||
</ReflexElement>
|
{instructions}
|
||||||
<ReflexSplitter propagate={true} {...resizeProps} />
|
</ReflexElement>
|
||||||
|
)}
|
||||||
|
{!projectBasedChallenge && (
|
||||||
|
<ReflexSplitter propagate={true} {...resizeProps} />
|
||||||
|
)}
|
||||||
|
|
||||||
<ReflexElement flex={1} {...resizeProps}>
|
<ReflexElement flex={1} {...resizeProps}>
|
||||||
{challengeFile && (
|
{challengeFile && (
|
||||||
<ReflexContainer key={challengeFile.key} orientation='horizontal'>
|
<ReflexContainer key={challengeFile.key} orientation='horizontal'>
|
||||||
@ -68,15 +96,21 @@ class DesktopLayout extends Component {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
</ReflexElement>
|
</ReflexElement>
|
||||||
<ReflexSplitter propagate={true} {...resizeProps} />
|
{isConsoleDisplayable && (
|
||||||
<ReflexElement flex={0.25} {...reflexProps} {...resizeProps}>
|
<ReflexSplitter propagate={true} {...resizeProps} />
|
||||||
{testOutput}
|
)}
|
||||||
</ReflexElement>
|
{isConsoleDisplayable && (
|
||||||
|
<ReflexElement flex={0.25} {...reflexProps} {...resizeProps}>
|
||||||
|
{testOutput}
|
||||||
|
</ReflexElement>
|
||||||
|
)}
|
||||||
</ReflexContainer>
|
</ReflexContainer>
|
||||||
)}
|
)}
|
||||||
</ReflexElement>
|
</ReflexElement>
|
||||||
{hasPreview && <ReflexSplitter propagate={true} {...resizeProps} />}
|
{isPreviewDisplayable && (
|
||||||
{hasPreview && (
|
<ReflexSplitter propagate={true} {...resizeProps} />
|
||||||
|
)}
|
||||||
|
{isPreviewDisplayable && (
|
||||||
<ReflexElement flex={0.7} {...resizeProps}>
|
<ReflexElement flex={0.7} {...resizeProps}>
|
||||||
{preview}
|
{preview}
|
||||||
</ReflexElement>
|
</ReflexElement>
|
||||||
|
@ -12,7 +12,9 @@ import {
|
|||||||
saveEditorContent,
|
saveEditorContent,
|
||||||
setEditorFocusability,
|
setEditorFocusability,
|
||||||
setAccessibilityMode,
|
setAccessibilityMode,
|
||||||
updateFile
|
updateFile,
|
||||||
|
challengeTestsSelector,
|
||||||
|
submitChallenge
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
|
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
|
||||||
import { Loader } from '../../../components/helpers';
|
import { Loader } from '../../../components/helpers';
|
||||||
@ -43,6 +45,8 @@ const propTypes = {
|
|||||||
saveEditorContent: PropTypes.func.isRequired,
|
saveEditorContent: PropTypes.func.isRequired,
|
||||||
setAccessibilityMode: PropTypes.func.isRequired,
|
setAccessibilityMode: PropTypes.func.isRequired,
|
||||||
setEditorFocusability: PropTypes.func,
|
setEditorFocusability: PropTypes.func,
|
||||||
|
submitChallenge: PropTypes.func,
|
||||||
|
tests: PropTypes.arrayOf(PropTypes.object),
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
updateFile: PropTypes.func.isRequired
|
updateFile: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
@ -53,11 +57,20 @@ const mapStateToProps = createSelector(
|
|||||||
inAccessibilityModeSelector,
|
inAccessibilityModeSelector,
|
||||||
isDonationModalOpenSelector,
|
isDonationModalOpenSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
(canFocus, output, accessibilityMode, open, { theme = 'default' }) => ({
|
challengeTestsSelector,
|
||||||
|
(
|
||||||
|
canFocus,
|
||||||
|
output,
|
||||||
|
accessibilityMode,
|
||||||
|
open,
|
||||||
|
{ theme = 'default' },
|
||||||
|
tests
|
||||||
|
) => ({
|
||||||
canFocus: open ? false : canFocus,
|
canFocus: open ? false : canFocus,
|
||||||
output,
|
output,
|
||||||
inAccessibilityMode: accessibilityMode,
|
inAccessibilityMode: accessibilityMode,
|
||||||
theme
|
theme,
|
||||||
|
tests
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,7 +79,8 @@ const mapDispatchToProps = {
|
|||||||
saveEditorContent,
|
saveEditorContent,
|
||||||
setAccessibilityMode,
|
setAccessibilityMode,
|
||||||
setEditorFocusability,
|
setEditorFocusability,
|
||||||
updateFile
|
updateFile,
|
||||||
|
submitChallenge
|
||||||
};
|
};
|
||||||
|
|
||||||
const modeMap = {
|
const modeMap = {
|
||||||
@ -140,6 +154,7 @@ class Editor extends Component {
|
|||||||
viewZoneId: null,
|
viewZoneId: null,
|
||||||
startEditDecId: null,
|
startEditDecId: null,
|
||||||
endEditDecId: null,
|
endEditDecId: null,
|
||||||
|
insideEditDecId: null,
|
||||||
viewZoneHeight: null
|
viewZoneHeight: null
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -182,6 +197,13 @@ class Editor extends Component {
|
|||||||
this.focusOnEditor = this.focusOnEditor.bind(this);
|
this.focusOnEditor = this.focusOnEditor.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEditableRegion = () => {
|
||||||
|
const { challengeFiles, fileKey } = this.props;
|
||||||
|
return challengeFiles[fileKey].editableRegionBoundaries
|
||||||
|
? [...challengeFiles[fileKey].editableRegionBoundaries]
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
editorWillMount = monaco => {
|
editorWillMount = monaco => {
|
||||||
this._monaco = monaco;
|
this._monaco = monaco;
|
||||||
const { challengeFiles, fileKey } = this.props;
|
const { challengeFiles, fileKey } = this.props;
|
||||||
@ -201,9 +223,7 @@ class Editor extends Component {
|
|||||||
);
|
);
|
||||||
this.data.model = model;
|
this.data.model = model;
|
||||||
|
|
||||||
const editableRegion = challengeFiles[fileKey].editableRegionBoundaries
|
const editableRegion = this.getEditableRegion();
|
||||||
? [...challengeFiles[fileKey].editableRegionBoundaries]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (editableRegion.length === 2)
|
if (editableRegion.length === 2)
|
||||||
this.decorateForbiddenRanges(editableRegion);
|
this.decorateForbiddenRanges(editableRegion);
|
||||||
@ -224,7 +244,6 @@ class Editor extends Component {
|
|||||||
|
|
||||||
editorDidMount = (editor, monaco) => {
|
editorDidMount = (editor, monaco) => {
|
||||||
this._editor = editor;
|
this._editor = editor;
|
||||||
const { challengeFiles, fileKey } = this.props;
|
|
||||||
editor.updateOptions({
|
editor.updateOptions({
|
||||||
accessibilitySupport: this.props.inAccessibilityMode ? 'on' : 'auto'
|
accessibilitySupport: this.props.inAccessibilityMode ? 'on' : 'auto'
|
||||||
});
|
});
|
||||||
@ -286,9 +305,7 @@ class Editor extends Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const editableBoundaries = challengeFiles[fileKey].editableRegionBoundaries
|
const editableBoundaries = this.getEditableRegion();
|
||||||
? [...challengeFiles[fileKey].editableRegionBoundaries]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (editableBoundaries.length === 2) {
|
if (editableBoundaries.length === 2) {
|
||||||
// TODO: is there a nicer approach/way of organising everything that
|
// TODO: is there a nicer approach/way of organising everything that
|
||||||
@ -363,7 +380,7 @@ class Editor extends Component {
|
|||||||
this.data.viewZoneHeight = domNode.offsetHeight;
|
this.data.viewZoneHeight = domNode.offsetHeight;
|
||||||
|
|
||||||
var background = document.createElement('div');
|
var background = document.createElement('div');
|
||||||
background.style.background = 'lightgreen';
|
// background.style.background = 'lightgreen';
|
||||||
|
|
||||||
// We have to wait for the viewZone to finish rendering before adjusting the
|
// We have to wait for the viewZone to finish rendering before adjusting the
|
||||||
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
|
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
|
||||||
@ -392,7 +409,7 @@ class Editor extends Component {
|
|||||||
this.data.outputZoneHeight = outputNode.offsetHeight;
|
this.data.outputZoneHeight = outputNode.offsetHeight;
|
||||||
|
|
||||||
var background = document.createElement('div');
|
var background = document.createElement('div');
|
||||||
background.style.background = 'lightpink';
|
// background.style.background = 'lightpink';
|
||||||
|
|
||||||
// We have to wait for the viewZone to finish rendering before adjusting the
|
// We have to wait for the viewZone to finish rendering before adjusting the
|
||||||
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
|
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
|
||||||
@ -413,19 +430,14 @@ class Editor extends Component {
|
|||||||
const { description } = this.props;
|
const { description } = this.props;
|
||||||
var domNode = document.createElement('div');
|
var domNode = document.createElement('div');
|
||||||
var desc = document.createElement('div');
|
var desc = document.createElement('div');
|
||||||
var button = document.createElement('button');
|
var descContainer = document.createElement('div');
|
||||||
button.innerHTML = 'Run the Tests (Ctrl + Enter)';
|
descContainer.classList.add('description-container');
|
||||||
button.onclick = () => {
|
domNode.classList.add('editor-upper-jaw');
|
||||||
const { executeChallenge } = this.props;
|
domNode.appendChild(descContainer);
|
||||||
executeChallenge();
|
descContainer.appendChild(desc);
|
||||||
};
|
|
||||||
|
|
||||||
domNode.appendChild(desc);
|
|
||||||
domNode.appendChild(button);
|
|
||||||
desc.innerHTML = description;
|
desc.innerHTML = description;
|
||||||
|
// desc.style.background = 'white';
|
||||||
desc.style.background = 'white';
|
// domNode.style.background = 'lightgreen';
|
||||||
domNode.style.background = 'lightgreen';
|
|
||||||
// TODO: the solution is probably just to use an overlay that's forced to
|
// TODO: the solution is probably just to use an overlay that's forced to
|
||||||
// follow the decorations.
|
// follow the decorations.
|
||||||
// TODO: this is enough for Firefox, but Chrome needs more before the
|
// TODO: this is enough for Firefox, but Chrome needs more before the
|
||||||
@ -436,7 +448,7 @@ class Editor extends Component {
|
|||||||
|
|
||||||
domNode.setAttribute('aria-hidden', true);
|
domNode.setAttribute('aria-hidden', true);
|
||||||
|
|
||||||
domNode.style.background = 'lightYellow';
|
// domNode.style.background = 'lightYellow';
|
||||||
domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
|
domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
|
||||||
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
|
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
|
||||||
domNode.style.top = this.getViewZoneTop();
|
domNode.style.top = this.getViewZoneTop();
|
||||||
@ -449,11 +461,23 @@ class Editor extends Component {
|
|||||||
const outputNode = document.createElement('div');
|
const outputNode = document.createElement('div');
|
||||||
const statusNode = document.createElement('div');
|
const statusNode = document.createElement('div');
|
||||||
const hintNode = document.createElement('div');
|
const hintNode = document.createElement('div');
|
||||||
outputNode.appendChild(statusNode);
|
const editorActionRow = document.createElement('div');
|
||||||
outputNode.appendChild(hintNode);
|
editorActionRow.classList.add('action-row-container');
|
||||||
|
outputNode.classList.add('editor-lower-jaw');
|
||||||
|
outputNode.appendChild(editorActionRow);
|
||||||
hintNode.setAttribute('id', 'test-output');
|
hintNode.setAttribute('id', 'test-output');
|
||||||
statusNode.setAttribute('id', 'test-status');
|
statusNode.setAttribute('id', 'test-status');
|
||||||
statusNode.innerHTML = '// tests';
|
var button = document.createElement('button');
|
||||||
|
button.setAttribute('id', 'test-button');
|
||||||
|
button.classList.add('btn-block');
|
||||||
|
button.innerHTML = 'Check Your Code (Ctrl + Enter)';
|
||||||
|
editorActionRow.appendChild(button);
|
||||||
|
editorActionRow.appendChild(statusNode);
|
||||||
|
editorActionRow.appendChild(hintNode);
|
||||||
|
button.onclick = () => {
|
||||||
|
const { executeChallenge } = this.props;
|
||||||
|
executeChallenge();
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: does it?
|
// TODO: does it?
|
||||||
// The z-index needs increasing as ViewZones default to below the lines.
|
// The z-index needs increasing as ViewZones default to below the lines.
|
||||||
@ -528,6 +552,19 @@ class Editor extends Component {
|
|||||||
return target.deltaDecorations(oldIds, [lineDecoration]);
|
return target.deltaDecorations(oldIds, [lineDecoration]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
highlightEditableLines(stickiness, target, range, oldIds = []) {
|
||||||
|
const lineDecoration = {
|
||||||
|
range,
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
linesDecorationsClassName: 'myEditableLineDecoration',
|
||||||
|
className: 'do-not-edit',
|
||||||
|
stickiness
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return target.deltaDecorations(oldIds, [lineDecoration]);
|
||||||
|
}
|
||||||
|
|
||||||
highlightText(stickiness, target, range, oldIds = []) {
|
highlightText(stickiness, target, range, oldIds = []) {
|
||||||
const inlineDecoration = {
|
const inlineDecoration = {
|
||||||
range,
|
range,
|
||||||
@ -655,6 +692,17 @@ class Editor extends Component {
|
|||||||
return this.positionsToRange(model, positions);
|
return this.positionsToRange(model, positions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editableRange = this.positionsToRange(model, [
|
||||||
|
editableRegion[0] + 1,
|
||||||
|
editableRegion[1] - 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.data.insideEditDecId = this.highlightEditableLines(
|
||||||
|
this._monaco.editor.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
|
||||||
|
model,
|
||||||
|
editableRange
|
||||||
|
);
|
||||||
|
|
||||||
// if the forbidden range includes the top of the editor
|
// if the forbidden range includes the top of the editor
|
||||||
// we simply don't add those decorations
|
// we simply don't add those decorations
|
||||||
if (forbiddenRanges[0][1] > 0) {
|
if (forbiddenRanges[0][1] > 0) {
|
||||||
@ -743,53 +791,25 @@ class Editor extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make sure the zone tracks the decoration (i.e. the region)
|
// Make sure the zone tracks the decoration (i.e. the region), which might
|
||||||
const handleHintsZoneChange = id => {
|
// have changed if a line has been added or removed
|
||||||
const startOfZone = toStartOfLine(
|
const handleHintsZoneChange = () => {
|
||||||
model.getDecorationRange(id)
|
if (newLineRanges.length > 0 || deletedLine > 0) {
|
||||||
).collapseToStart();
|
this.updateOutputZone();
|
||||||
// the decoration needs adjusting if the user creates a line immediately
|
|
||||||
// before the greyed out region...
|
|
||||||
const lineOneRange = this.translateRange(startOfZone, -2);
|
|
||||||
// or immediately after it
|
|
||||||
const lineTwoRange = this.translateRange(startOfZone, -1);
|
|
||||||
|
|
||||||
for (const lineRange of newLineRanges) {
|
|
||||||
const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
|
|
||||||
lineRange,
|
|
||||||
lineOneRange.plusRange(lineTwoRange)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldMoveZone) {
|
|
||||||
this.updateOutputZone();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make sure the zone tracks the decoration (i.e. the region)
|
// Make sure the zone tracks the decoration (i.e. the region), which might
|
||||||
const handleDescriptionZoneChange = id => {
|
// have changed if a line has been added or removed
|
||||||
const endOfZone = toLastLine(
|
const handleDescriptionZoneChange = () => {
|
||||||
model.getDecorationRange(id)
|
if (newLineRanges.length > 0 || deletedLine > 0) {
|
||||||
).collapseToStart();
|
this.updateViewZone();
|
||||||
// the decoration needs adjusting if the user creates a line immediately
|
|
||||||
// before the editable region.
|
|
||||||
const lineOneRange = this.translateRange(endOfZone, -1);
|
|
||||||
|
|
||||||
for (const lineRange of newLineRanges) {
|
|
||||||
const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
|
|
||||||
lineRange,
|
|
||||||
lineOneRange
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldMoveZone) {
|
|
||||||
this.updateViewZone();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stops the greyed out region from covering the editable region. Does not
|
// Stops the greyed out region from covering the editable region. Does not
|
||||||
// change the font decoration.
|
// change the font decoration.
|
||||||
const preventOverlap = id => {
|
const preventOverlap = (id, stickiness, highlightFunction) => {
|
||||||
// Even though the decoration covers the whole line, it has a
|
// Even though the decoration covers the whole line, it has a
|
||||||
// startColumn that moves. toStartOfLine ensures that the
|
// startColumn that moves. toStartOfLine ensures that the
|
||||||
// comparison detects if any change has occurred on that line
|
// comparison detects if any change has occurred on that line
|
||||||
@ -821,17 +841,15 @@ class Editor extends Component {
|
|||||||
|
|
||||||
if (touchingDeleted) {
|
if (touchingDeleted) {
|
||||||
// TODO: if they undo this should be reversed
|
// TODO: if they undo this should be reversed
|
||||||
const decorations = this.highlightLines(
|
const decorations = highlightFunction(
|
||||||
this._monaco.editor.TrackedRangeStickiness
|
stickiness,
|
||||||
.NeverGrowsWhenTypingAtEdges,
|
|
||||||
model,
|
model,
|
||||||
newCoveringRange,
|
newCoveringRange,
|
||||||
[id]
|
id
|
||||||
);
|
);
|
||||||
|
|
||||||
this.updateOutputZone();
|
this.updateOutputZone();
|
||||||
// when there's a change, decorations will be [oldId, newId]
|
return decorations;
|
||||||
return decorations.slice(-1)[0];
|
|
||||||
} else {
|
} else {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@ -839,7 +857,17 @@ class Editor extends Component {
|
|||||||
|
|
||||||
// we only need to handle the special case of the second region being
|
// we only need to handle the special case of the second region being
|
||||||
// pulled up, the first region already behaves correctly.
|
// pulled up, the first region already behaves correctly.
|
||||||
this.data.endEditDecId = preventOverlap(this.data.endEditDecId);
|
this.data.endEditDecId = preventOverlap(
|
||||||
|
this.data.endEditDecId,
|
||||||
|
this._monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore,
|
||||||
|
this.highlightLines
|
||||||
|
);
|
||||||
|
|
||||||
|
this.data.insideEditDecId = preventOverlap(
|
||||||
|
this.data.insideEditDecId,
|
||||||
|
this._monaco.editor.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
|
||||||
|
this.highlightEditableLines
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: do the same for the description widget
|
// TODO: do the same for the description widget
|
||||||
// this has to be handle differently, because we care about the END
|
// this has to be handle differently, because we care about the END
|
||||||
@ -847,10 +875,10 @@ class Editor extends Component {
|
|||||||
// if the editable region includes the first line, the first decoration
|
// if the editable region includes the first line, the first decoration
|
||||||
// will be missing.
|
// will be missing.
|
||||||
if (this.data.startEditDecId) {
|
if (this.data.startEditDecId) {
|
||||||
handleDescriptionZoneChange(this.data.startEditDecId);
|
handleDescriptionZoneChange();
|
||||||
warnUser(this.data.startEditDecId);
|
warnUser(this.data.startEditDecId);
|
||||||
}
|
}
|
||||||
handleHintsZoneChange(this.data.endEditDecId);
|
handleHintsZoneChange();
|
||||||
warnUser(this.data.endEditDecId);
|
warnUser(this.data.endEditDecId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -889,7 +917,47 @@ class Editor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._editor) {
|
if (this._editor) {
|
||||||
const { output } = this.props;
|
const { output, tests } = this.props;
|
||||||
|
const editableRegion = this.getEditableRegion();
|
||||||
|
if (this.props.tests !== prevProps.tests && editableRegion.length === 2) {
|
||||||
|
const challengeComplete = tests.every(test => test.pass && !test.err);
|
||||||
|
const chellengeHasErrors = tests.some(test => test.err);
|
||||||
|
|
||||||
|
if (challengeComplete) {
|
||||||
|
let testButton = document.getElementById('test-button');
|
||||||
|
testButton.innerHTML =
|
||||||
|
'Submit your code and go to next challenge (Ctrl + Enter)';
|
||||||
|
testButton.onclick = () => {
|
||||||
|
const { submitChallenge } = this.props;
|
||||||
|
submitChallenge();
|
||||||
|
};
|
||||||
|
|
||||||
|
let editableRegionDecorators = document.getElementsByClassName(
|
||||||
|
'myEditableLineDecoration'
|
||||||
|
);
|
||||||
|
if (editableRegionDecorators.length > 0) {
|
||||||
|
for (var i of editableRegionDecorators) {
|
||||||
|
i.classList.add('tests-passed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('test-output').innerHTML = '';
|
||||||
|
document.getElementById('test-status').innerHTML =
|
||||||
|
'✅ Step completed.';
|
||||||
|
} else if (chellengeHasErrors) {
|
||||||
|
const wordsArray = [
|
||||||
|
"Not quite. Here's a hint:",
|
||||||
|
'Try again. This might help:',
|
||||||
|
'Keep trying. A quick hint for you:',
|
||||||
|
"You're getting there. This may help:",
|
||||||
|
"Hang in there. You'll get there. A hint:",
|
||||||
|
"Don't give up. Here's a hint to get you thinking:"
|
||||||
|
];
|
||||||
|
document.getElementById('test-status').innerHTML = `✖️ ${
|
||||||
|
wordsArray[Math.floor(Math.random() * wordsArray.length)]
|
||||||
|
}`;
|
||||||
|
document.getElementById('test-output').innerHTML = `${output[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (this.props.output !== prevProps.output && this._outputNode) {
|
if (this.props.output !== prevProps.output && this._outputNode) {
|
||||||
// TODO: output gets wiped when the preview gets updated, keeping the
|
// TODO: output gets wiped when the preview gets updated, keeping the
|
||||||
// display is an anti-pattern (the render should not ignore props!).
|
// display is an anti-pattern (the render should not ignore props!).
|
||||||
@ -897,14 +965,6 @@ class Editor extends Component {
|
|||||||
// (shownHint,maybe) and have that persist through previews. But, for
|
// (shownHint,maybe) and have that persist through previews. But, for
|
||||||
// now:
|
// now:
|
||||||
if (output) {
|
if (output) {
|
||||||
if (output[0]) {
|
|
||||||
document.getElementById('test-status').innerHTML = output[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output[1]) {
|
|
||||||
document.getElementById('test-output').innerHTML = output[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// if either id exists, the editable region exists
|
// if either id exists, the editable region exists
|
||||||
// TODO: add a layer of abstraction: we should be interacting with
|
// TODO: add a layer of abstraction: we should be interacting with
|
||||||
// the editable region, not the ids
|
// the editable region, not the ids
|
||||||
|
@ -6,10 +6,6 @@
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
[widgetid='my.overlay.widget'] {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vs .monaco-scrollable-element > .scrollbar > .slider {
|
.vs .monaco-scrollable-element > .scrollbar > .slider {
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
@ -17,3 +13,64 @@
|
|||||||
.editor-container {
|
.editor-container {
|
||||||
background: var(--editor-background);
|
background: var(--editor-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrumbs-demo {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs-demo span {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row button {
|
||||||
|
padding: 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-tab {
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: var(--secondary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* [widgetid='my.overlay.widget'] {
|
||||||
|
padding: 10px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.editor-upper-jaw,
|
||||||
|
.editor-lower-jaw {
|
||||||
|
padding: 15px 15px 15px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row-container,
|
||||||
|
.description-container {
|
||||||
|
background-color: var(--secondary-background);
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid var(--tertiary-background);
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#description p:last-child {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#test-status {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.myEditableLineDecoration {
|
||||||
|
background-color: var(--gray-45);
|
||||||
|
width: 15px !important;
|
||||||
|
margin-left: 5px !important;
|
||||||
|
margin-right: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.myEditableLineDecoration.tests-passed {
|
||||||
|
background-color: #4caf50;
|
||||||
|
}
|
||||||
|
40
client/src/templates/Challenges/components/BreadCrumb.js
Normal file
40
client/src/templates/Challenges/components/BreadCrumb.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from '../../../components/helpers/index';
|
||||||
|
|
||||||
|
import './challenge-title.css';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
block: PropTypes.string,
|
||||||
|
superBlock: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function BreadCrumb({ block, superBlock }) {
|
||||||
|
return (
|
||||||
|
<div className='challenge-title-breadcrumbs'>
|
||||||
|
<Link
|
||||||
|
className='breadcrumb-left'
|
||||||
|
state={{ breadcrumbBlockClick: block }}
|
||||||
|
to={`/learn/${superBlock}`}
|
||||||
|
>
|
||||||
|
<span className='ellipsis'>
|
||||||
|
{i18next.t(`intro:${superBlock}.title`)}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<div className='breadcrumb-center' />
|
||||||
|
<Link
|
||||||
|
className='breadcrumb-right'
|
||||||
|
state={{ breadcrumbBlockClick: block }}
|
||||||
|
to={`/learn/${superBlock}/#${block}`}
|
||||||
|
>
|
||||||
|
{i18next.t(`intro:${superBlock}.blocks.${block}.title`)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BreadCrumb.displayName = 'BreadCrumb';
|
||||||
|
BreadCrumb.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default BreadCrumb;
|
@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from '../../../components/helpers/index';
|
import { Link } from '../../../components/helpers/index';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
import './challenge-title.css';
|
import './challenge-title.css';
|
||||||
import GreenPass from '../../../assets/icons/GreenPass';
|
import GreenPass from '../../../assets/icons/GreenPass';
|
||||||
import i18next from 'i18next';
|
import BreadCrumb from './BreadCrumb';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
block: PropTypes.string,
|
block: PropTypes.string,
|
||||||
@ -31,25 +32,7 @@ function ChallengeTitle({
|
|||||||
{i18next.t('misc.translation-pending')}
|
{i18next.t('misc.translation-pending')}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<div className='challenge-title-breadcrumbs'>
|
<BreadCrumb block={block} superBlock={superBlock} />
|
||||||
<Link
|
|
||||||
className='breadcrumb-left'
|
|
||||||
state={{ breadcrumbBlockClick: block }}
|
|
||||||
to={`/learn/${superBlock}`}
|
|
||||||
>
|
|
||||||
<span className='ellipsis'>
|
|
||||||
{i18next.t(`intro:${superBlock}.title`)}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<div className='breadcrumb-center' />
|
|
||||||
<Link
|
|
||||||
className='breadcrumb-right'
|
|
||||||
state={{ breadcrumbBlockClick: block }}
|
|
||||||
to={`/learn/${superBlock}/#${block}`}
|
|
||||||
>
|
|
||||||
{i18next.t(`intro:${superBlock}.blocks.${block}.title`)}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className='challenge-title'>
|
<div className='challenge-title'>
|
||||||
<div className='title-text'>
|
<div className='title-text'>
|
||||||
<b>{children}</b>
|
<b>{children}</b>
|
||||||
|
Reference in New Issue
Block a user