Skip to content

Commit

Permalink
feat: Support dock re-positioning and fix some editor quirks (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeltaranto authored Dec 9, 2019
1 parent 4a99eec commit b7b884a
Show file tree
Hide file tree
Showing 27 changed files with 1,477 additions and 927 deletions.
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"babel-plugin-transform-class-properties": "^6.24.1",
"base64-url": "^2.2.0",
"buble": "^0.19.3",
"classnames": "^2.2.6",
"codemirror": "^5.48.2",
"command-line-args": "^5.0.2",
"command-line-usage": "^5.0.5",
Expand All @@ -86,10 +87,8 @@
"prop-types": "^15.6.2",
"query-string": "^6.1.0",
"re-resizable": "^4.9.3",
"react": "^16.8.6",
"react-codemirror2": "^6.0.0",
"react-docgen-typescript": "^1.12.2",
"react-dom": "^16.8.6",
"read-pkg-up": "^5.0.0",
"scope-eval": "^1.0.0",
"style-loader": "^0.23.0",
Expand All @@ -106,15 +105,20 @@
"commitlint-config-seek": "^1.0.0",
"cypress": "^3.4.0",
"cz-conventional-changelog": "^2.1.0",
"eslint": "^5.8.0",
"eslint-config-seek": "^3.2.1",
"eslint-plugin-cypress": "^2.6.0",
"eslint": "^6.7.2",
"eslint-config-seek": "^4.3.2",
"extract-text-webpack-plugin": "^3.0.2",
"husky": "^1.1.3",
"jest": "^23.6.0",
"lint-staged": "^8.0.4",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"semantic-release": "^15.10.8",
"serve": "^11.1.0",
"start-server-and-test": "^1.7.11"
},
"peerDependencies": {
"react": "^16.8",
"react-dom": "^16.8"
}
}
156 changes: 156 additions & 0 deletions src/Playroom/CodeEditor/CodeEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, { useRef, useEffect } from 'react';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/neo.css';

import { formatCode as format } from '../../utils/formatting';

import styles from './CodeEditor.less';

import { Controlled as ReactCodeMirror } from 'react-codemirror2';
import 'codemirror/mode/jsx/jsx';
import 'codemirror/addon/edit/closetag';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/xml-hint';
import compileJsx from '../../utils/compileJsx';

const completeAfter = (cm, predicate) => {
const CodeMirror = cm.constructor;
if (!predicate || predicate()) {
setTimeout(() => {
if (!cm.state.completionActive) {
cm.showHint({ completeSingle: false });
}
}, 100);
}

return CodeMirror.Pass;
};

const completeIfAfterLt = cm => {
const CodeMirror = cm.constructor;

return completeAfter(cm, () => {
const cur = cm.getCursor();
// eslint-disable-next-line new-cap
return cm.getRange(CodeMirror.Pos(cur.line, cur.ch - 1), cur) === '<';
});
};

const completeIfInTag = cm => {
const CodeMirror = cm.constructor;

return completeAfter(cm, () => {
const tok = cm.getTokenAt(cm.getCursor());
if (
tok.type === 'string' &&
(!/['"]/.test(tok.string.charAt(tok.string.length - 1)) ||
tok.string.length === 1)
) {
return false;
}
const inner = CodeMirror.innerMode(cm.getMode(), tok.state).state;
return inner.tagName;
});
};

const validateCode = (editorInstance, code) => {
editorInstance.clearGutter(styles.gutter);

try {
compileJsx(code);
} catch (err) {
const errorMessage = err && (err.message || '');
const matches = errorMessage.match(/\(([0-9]+):/);
const lineNumber =
matches && matches.length >= 2 && matches[1] && parseInt(matches[1], 10);

if (lineNumber) {
const marker = document.createElement('div');
marker.classList.add(styles.marker);
marker.setAttribute('title', err.message);
editorInstance.setGutterMarker(lineNumber - 1, styles.gutter, marker);
}
}
};

export const CodeEditor = ({ code, onChange, hints }) => {
const editorInstanceRef = useRef(null);

useEffect(
() => {
const handleKeyDown = e => {
if (
editorInstanceRef &&
editorInstanceRef.current &&
e.keyCode === 83 &&
(navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey)
) {
e.preventDefault();

const { formattedCode, line, ch } = format({
code,
cursor: editorInstanceRef.current.getCursor()
});

onChange(formattedCode);

setTimeout(() => {
editorInstanceRef.current.focus();
editorInstanceRef.current.setCursor({
line,
ch
});
});
}
};

window.addEventListener('keydown', handleKeyDown);

return () => {
window.removeEventListener('keydown', handleKeyDown);
};
},
[code, onChange]
);

return (
<ReactCodeMirror
editorDidMount={editorInstance => {
editorInstanceRef.current = editorInstance;
validateCode(editorInstance, code);
editorInstance.focus();
editorInstance.setCursor(0, 0);
}}
value={code}
onBeforeChange={(editor, data, newCode) => {
onChange(newCode);
validateCode(editorInstanceRef.current, newCode);
}}
options={{
mode: 'jsx',
autoCloseTags: true,
autoCloseBrackets: true,
theme: 'neo',
gutters: [styles.gutter],
hintOptions: { schemaInfo: hints },
viewportMargin: 50,
extraKeys: {
Tab: cm => {
if (cm.somethingSelected()) {
cm.indentSelection('add');
} else {
const indent = cm.getOption('indentUnit');
const spaces = Array(indent + 1).join(' ');
cm.replaceSelection(spaces);
}
},
"'<'": completeAfter,
"'/'": completeIfAfterLt,
"' '": completeIfInTag,
"'='": completeIfInTag
}
}}
/>
);
};
122 changes: 122 additions & 0 deletions src/Playroom/CodeEditor/CodeEditor.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
@import (reference) '../variables.less';

@editor-font-family: Source Code Pro, Firacode, Hasklig, Menlo, monospace;
@sandbox-editor-height: 30vh;
@gutter-size: 40px;

.gutter {
@box-fade-size: 10px;
width: @gutter-size - @box-fade-size;
background: white;
position: absolute;
right: @box-fade-size;
box-shadow: 0 0 @box-fade-size 5px white;
}

.marker {
width: @sandbox-marker-size;
height: @sandbox-marker-size;
border-radius: (@sandbox-marker-size / 2);
background-color: @sandbox-marker-color;
position: relative;
top: (@sandbox-marker-size / 2);
left: 10px;
}

:global {
.react-codemirror2 {
height: 100%;
background-color: #fff;
}

.CodeMirror {
height: 100%;
width: 100%;
font-family: @editor-font-family;
}

.CodeMirror-gutters {
width: @gutter-size;
}

.CodeMirror-gutter-elt {
position: relative;
}

.CodeMirror pre,
.CodeMirror-linenumber {
font-size: 16px;
}

.CodeMirror-line > span[role='presentation'] {
padding-right: (@dock-size + @dock-fade-distance + @gutter-size) !important;
}

.CodeMirror-lines {
padding: 16px 0;
}

.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;
margin: 0;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
border-radius: 3px;
background: white;
font-size: 90%;
line-height: 150%;
font-family: @editor-font-family;
max-height: 20em;
overflow-y: auto;
}

.CodeMirror-hint {
margin: 0;
padding: 4px 8px;
border-radius: 2px;
white-space: pre;
color: black;
cursor: pointer;
}

li.CodeMirror-hint-active {
background: #08f;
color: white;
}

.cm-s-neo {
&.CodeMirror {
background-color: #fff;
}
.CodeMirror-cursor {
background: black;
width: 2px;
}
.CodeMirror-gutters {
background: none;
border: none;
pointer-events: none;
}
.CodeMirror-linenumber {
color: white;
}
.cm-tag {
color: #040080;
}
.cm-attribute {
color: #005ad2;
}
.cm-string {
color: #00439c;
}
.cm-atom {
color: #00439c;
}
.cm-variable {
color: #827eff;
}
}
}
64 changes: 64 additions & 0 deletions src/Playroom/DockPosition/DockPosition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import EditorUndockedSvg from './icons/EditorUndockedSvg';
import EditorLeftSvg from './icons/EditorLeftSvg';
import EditorBottomSvg from './icons/EditorBottomSvg';
import EditorRightSvg from './icons/EditorRightSvg';

import styles from './DockPosition.less';

export default ({ position, setPosition }) => {
return (
<div className={styles.root}>
<div className={styles.container}>
<div className={styles.currentPosition}>
{
{
undocked: <EditorUndockedSvg />,
left: <EditorLeftSvg />,
right: <EditorRightSvg />,
bottom: <EditorBottomSvg />
}[position]
}
</div>
<div className={styles.buttons}>
{position !== 'undocked' && (
<button
title="Undock editor"
className={styles.button}
onClick={() => setPosition('undocked')}
>
<EditorUndockedSvg />
</button>
)}
{position !== 'left' && (
<button
title="Dock editor to the left"
className={styles.button}
onClick={() => setPosition('left')}
>
<EditorLeftSvg />
</button>
)}
{position !== 'right' && (
<button
title="Dock editor to the right"
className={styles.button}
onClick={() => setPosition('right')}
>
<EditorRightSvg />
</button>
)}
{position !== 'bottom' && (
<button
title="Dock editor to the bottom"
className={styles.button}
onClick={() => setPosition('bottom')}
>
<EditorBottomSvg />
</button>
)}
</div>
</div>
</div>
);
};
Loading

0 comments on commit b7b884a

Please sign in to comment.