diff --git a/.gitignore b/.gitignore index 866e3f6..9edf4c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -node_modules +node_modules/ +website/ +dist/ + *.bundle.js *.bundle.js.map p5-widget.js -p5-widget.js.map -website +p5-widget.js.map \ No newline at end of file diff --git a/README.md b/README.md index 5e59b62..f2f7e03 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,10 @@ of plugins: The test suite can be run on the development server at http://localhost:8080/test/, or on the command-line with `npm test`. +## Publish to GitHub Pages + +Run `npm run publish` to build & publish a site. + [website]: https://toolness.github.io/p5.js-widget/ [TypeScript]: http://typescriptlang.org/ [React]: http://facebook.github.io/react/ diff --git a/css/p5-widget-codemirror-theme.css b/css/p5-widget-codemirror-theme.css index ea45b8f..100ae7b 100644 --- a/css/p5-widget-codemirror-theme.css +++ b/css/p5-widget-codemirror-theme.css @@ -8,32 +8,35 @@ --dark-blueish: #00A1D3; } -.cm-s-p5-widget span { color: var(--dark-gray); } +/* Commented out if ported */ -.cm-s-p5-widget span.cm-meta { color: var(--dark-gray); } -.cm-s-p5-widget span.cm-keyword { line-height: 1em; color: var(--dark-brown); } -.cm-s-p5-widget span.cm-atom { color: var(--pinkish); } -.cm-s-p5-widget span.cm-number { color: var(--pinkish); } -.cm-s-p5-widget span.cm-def { color: var(--dark-blueish); } -.cm-s-p5-widget span.cm-variable { color: var(--dark-blueish); } -.cm-s-p5-widget span.cm-variable-2 { color: var(--almost-black); } -.cm-s-p5-widget span.cm-variable-3 { color: var(--almost-black); } -.cm-s-p5-widget span.cm-property { color: var(--almost-black); } -.cm-s-p5-widget span.cm-operator { color: var(--light-brown); } -.cm-s-p5-widget span.cm-comment { color: var(--light-gray); } -.cm-s-p5-widget span.cm-string { color: var(--dark-blueish); } -.cm-s-p5-widget span.cm-string-2 { color: var(--dark-blueish); } +/* .cm-s-p5-widget span { color: var(--dark-gray); } */ -.cm-s-p5-widget span.cm-error { color: #f00; } +/* .cm-s-p5-widget span.cm-meta { color: var(--dark-gray); } */ +/* .cm-s-p5-widget span.cm-keyword { line-height: 1em; color: var(--dark-brown); } */ +.cm-s-p5-widget span.cm-atom { color: var(--pinkish); } /* Unsure what this is */ +/* .cm-s-p5-widget span.cm-number { color: var(--pinkish); } */ +/* .cm-s-p5-widget span.cm-def { color: var(--dark-blueish); } +.cm-s-p5-widget span.cm-variable { color: var(--dark-blueish); } */ +.cm-s-p5-widget span.cm-variable-2 { color: var(--almost-black); } /* Unsure what this is */ +.cm-s-p5-widget span.cm-variable-3 { color: var(--almost-black); } /* Unsure what this is */ +/* .cm-s-p5-widget span.cm-property { color: var(--almost-black); } */ /* Not supported */ +/* .cm-s-p5-widget span.cm-operator { color: var(--light-brown); } */ /* not supported */ +/* .cm-s-p5-widget span.cm-comment { color: var(--light-gray); } */ +/* .cm-s-p5-widget span.cm-string { color: var(--dark-blueish); } +.cm-s-p5-widget span.cm-string-2 { color: var(--dark-blueish); } */ -.cm-s-p5-widget .CodeMirror-activeline-background { background: #e8f2ff; } -.cm-s-p5-widget .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; } +/* .cm-s-p5-widget span.cm-error { color: #f00; } */ -/* These styles don't seem to be set by CodeMirror's javascript mode. */ +/* Adjusted slightly. */ +/* .cm-s-p5-widget .CodeMirror-activeline-background { background: #e8f2ff; } +.cm-s-p5-widget .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; } */ -.cm-s-p5-widget span.cm-qualifier { color: #555; } +/* These styles don't seem to be set by CodeMirror's javascript mode. */ +/* OK, won't port */ +/* .cm-s-p5-widget span.cm-qualifier { color: #555; } .cm-s-p5-widget span.cm-builtin { color: #30a; } .cm-s-p5-widget span.cm-bracket { color: #cc7; } .cm-s-p5-widget span.cm-tag { color: #170; } .cm-s-p5-widget span.cm-attribute { color: #00c; } -.cm-s-p5-widget span.cm-link { color: #219; } +.cm-s-p5-widget span.cm-link { color: #219; } */ diff --git a/css/style.css b/css/style.css index e4fe6a3..d488057 100644 --- a/css/style.css +++ b/css/style.css @@ -126,11 +126,6 @@ a.p5-logo img { padding-right: 1em; } -.CodeMirror-gutters { - border-right: none; - background: var(--very-light-gray); -} - .error-line { background: var(--light-pink); } diff --git a/lib/editor.tsx b/lib/editor.tsx index 99b19a3..20c9021 100644 --- a/lib/editor.tsx +++ b/lib/editor.tsx @@ -1,10 +1,15 @@ import React = require("react"); - -import CodeMirror = require("codemirror"); - -import "codemirror/mode/javascript/javascript.js"; +import * as Monaco from 'monaco-editor'; import PureComponent from "./pure-component"; +import MonacoTheme from "./monaco-theme"; +import UndoRedoHelper from "./undo-redo-helper"; + +// TODO: versions +const p5dts = require("!!raw-loader!@types/p5/global.d.ts") as string; +const p5Uri = "ts:p5.d.ts"; +// const p5domdts = require("raw-loader!@types/p5/lib/addons/p5.dom.d.ts") as string; +// const p5sounddts = require("raw-loader!@types/p5/lib/addons/p5.sound.d.ts") as string; // It seems like CodeMirror behaves oddly with a flexbox layout, so // we will manually size it. However, Chrome seems to have a bug @@ -22,43 +27,93 @@ interface State { } export default class Editor extends PureComponent { - _cm: CodeMirror.Editor + _ed: Monaco.editor.IStandaloneCodeEditor _resizeTimeout: number _errorLineHandle: any componentDidUpdate(prevProps: Props) { if (this.props.content !== prevProps.content && - this.props.content !== this._cm.getValue()) { - this._cm.setValue(this.props.content); + this.props.content !== this._ed.getValue()) { + this._ed.setValue(this.props.content); } if (this.props.errorLine !== prevProps.errorLine) { if (this._errorLineHandle) { - this._cm.removeLineClass(this._errorLineHandle, 'background', - 'error-line'); + this._ed.deltaDecorations(this._errorLineHandle, []); this._errorLineHandle = null; } if (this.props.errorLine) { - this._errorLineHandle = this._cm.addLineClass( - this.props.errorLine - 1, - 'background', - 'error-line' - ); + this._errorLineHandle = this._ed.deltaDecorations([], [ + { + range: { + startColumn: 0, + startLineNumber: this.props.errorLine, + endColumn: 0, + endLineNumber: this.props.errorLine, + }, + options: { + className: 'error-line', + isWholeLine: true, + } + } + ]); } } } componentDidMount() { - this._cm = CodeMirror(this.refs.container, { - theme: 'p5-widget', + Monaco.editor.defineTheme("p5-widget", MonacoTheme); + this._ed = Monaco.editor.create(this.refs.container, { value: this.props.content, - lineNumbers: true, - mode: 'javascript' + language: "javascript", + + // Ensure we don't have scrolling by default. + scrollBeyondLastLine: false, + + // Style: + theme: 'p5-widget', + fontSize: 16, + fontFamily: '"Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace', + lineNumbersMinChars: 2, + lineNumbers: "on", + folding: false, + minimap: { + enabled: false, + }, + guides: { + indentation: false, + }, + + // Unclear what we need to do here. + automaticLayout: true, + fixedOverflowWidgets: true, }); - this._cm.on('change', () => { + + Monaco.languages.typescript.javascriptDefaults.addExtraLib(p5dts, p5Uri); + // When resolving definitions and references, the editor will try to use created models. + // Creating a model for the library allows "peek definition/references" commands to work with the library. + Monaco.editor.createModel(p5dts, 'typescript', Monaco.Uri.parse(p5Uri)); + + // This could adapt to the browser---detect what features are supported and + // add those libraries to the `lib`s, beyond just ES2015, which + // https://www.typescriptlang.org/tsconfig#target recommends for browsers in + // general. + // + // We get errors like `Unhandled Promise Rejection: Error: Could not find + // source file: 'inmemory://model/1'.` if we don't Object.assign here. + const currentOptions = Monaco.languages.typescript.javascriptDefaults.getCompilerOptions(); + Monaco.languages.typescript.javascriptDefaults.setCompilerOptions(Object.assign({ + lib: ["dom", "es2015"], + module: Monaco.languages.typescript.ModuleKind.None, + }, currentOptions)); + + this._ed.onDidChangeModelContent(() => { if (this.props.onChange) { - let size = this._cm.getDoc().historySize(); - this.props.onChange(this._cm.getValue(), - size.undo > 0, size.redo > 0); + // TODO: extract to helper? + const helper = new UndoRedoHelper(this._ed); + this.props.onChange( + this._ed.getValue(), + helper.canUndo(), + helper.canRedo()); } }); this.resizeEditor(); @@ -70,33 +125,39 @@ export default class Editor extends PureComponent { componentWillUnmount() { // CodeMirror instances have no remove/destroy methods, so we // don't need to do anything: http://stackoverflow.com/a/18890324/2422398 - this._cm = null; + this._ed = null; clearTimeout(this._resizeTimeout); window.removeEventListener('resize', this.resizeEditor, false); } undo() { - this._cm.getDoc().undo(); + const helper = new UndoRedoHelper(this._ed); + helper.undo(); } redo() { - this._cm.getDoc().redo(); + const helper = new UndoRedoHelper(this._ed); + helper.redo(); } resizeEditor = () => { - let wrapper = this._cm.getWrapperElement(); - let oldDisplay = wrapper.style.display; + // TODO: we can use auto-layout, but it looks like the context menu gets + // clipped for small iframes. + + // let wrapper = this._cm.getContainerDomNode(); + // let oldDisplay = wrapper.style.display; - // We need to get the size of our container when it's - // "uncorrupted" by the height of our codemirror widget, so - // temporarily hide the widget. - wrapper.style.display = 'none'; + // // We need to get the size of our container when it's + // // "uncorrupted" by the height of our codemirror widget, so + // // temporarily hide the widget. + // wrapper.style.display = 'none'; - let rectHeight = this.refs.container.getBoundingClientRect().height; + // let rectHeight = this.refs.container.getBoundingClientRect().height; + // console.log(rectHeight); - wrapper.style.display = oldDisplay; + // wrapper.style.display = oldDisplay; - this._cm.setSize(null, rectHeight); + // this._cm.layout(); } // http://stackoverflow.com/a/33826399/2422398 diff --git a/lib/main.tsx b/lib/main.tsx index a468017..d25a26a 100644 --- a/lib/main.tsx +++ b/lib/main.tsx @@ -1,28 +1,24 @@ import React = require("react"); import ReactDOM = require("react-dom"); -import url = require("url"); - import * as defaults from "./defaults"; import { SessionStorageAutosaver } from "./autosaver"; import App from "./app"; -let defaultSketchJS = require("raw!./default-sketch.js") as string; +let defaultSketchJS = require("raw-loader!./default-sketch.js") as string; -require("../node_modules/codemirror/lib/codemirror.css"); require("../css/style.css"); -require("../css/p5-widget-codemirror-theme.css"); function start() { let embeddingPageURL = document.referrer; - let qs = url.parse(window.location.search, true).query; - let id = embeddingPageURL + '_' + qs['id']; - let baseSketchURL = qs['baseSketchURL'] || embeddingPageURL; - let autoplay = (qs['autoplay'] === 'on'); - let initialContent = qs['sketch'] || defaultSketchJS; - let p5version = qs['p5version'] || defaults.P5_VERSION; - let previewWidth = parseInt(qs['previewWidth']); - let maxRunTime = parseInt(qs['maxRunTime']) + let qs = new URLSearchParams(window.location.search); + let id = embeddingPageURL + '_' + qs.get('id'); + let baseSketchURL = qs.get('baseSketchURL') || embeddingPageURL; + let autoplay = (qs.get('autoplay') === 'on'); + let initialContent = qs.get('sketch') || defaultSketchJS; + let p5version = qs.get('p5version') || defaults.P5_VERSION; + let previewWidth = parseInt(qs.get('previewWidth')); + let maxRunTime = parseInt(qs.get('maxRunTime')) if (isNaN(previewWidth)) { previewWidth = defaults.PREVIEW_WIDTH; } diff --git a/lib/monaco-theme.ts b/lib/monaco-theme.ts new file mode 100644 index 0000000..419a65a --- /dev/null +++ b/lib/monaco-theme.ts @@ -0,0 +1,47 @@ +import * as Monaco from 'monaco-editor'; + +const colors = { + "very-light-gray": "#f0f0f0", + "light-gray": "#A0A0A0", + "dark-gray": "#666666", + "almost-black": "#222222", + "dark-brown": "#704F21", + "light-brown": "#a67f59", + "pinkish": "#DC3787", /* not p5 pink, but related */ + "dark-blueish": "#00A1D3", + "white": "#ffffff" +} + +// See the official themes for inspiration, but note that they use a different tokenizer: +// +// https://github.com/Microsoft/vscode/blob/main/src/vs/editor/standalone/common/themes.ts#L13 +const MonacoTheme: Monaco.editor.IStandaloneThemeData = { + base: 'vs', + inherit: true, + rules: [ + { token: "meta", foreground: colors["dark-gray"] }, + { token: "storage", foreground: colors["dark-brown"] }, + { token: "keyword", foreground: colors["dark-brown"] }, + { token: "number", foreground: colors["pinkish"] }, + { token: "identifier", foreground: colors["dark-blueish"] }, + { token: "function", foreground: colors["dark-blueish"] }, + { token: "delimiter", foreground: colors["dark-gray"] }, + { token: "operator", foreground: colors["light-brown"] }, + { token: "comment", foreground: colors["light-gray"] }, + { token: "string", foreground: colors["dark-blueish"] } + ], + colors: { + "editor.foreground": colors["dark-gray"], + "editorLineNumber.foreground": colors['light-gray'], + + // Took me a long time to find: + // https://stackoverflow.com/questions/65659354/what-is-the-name-of-configuration-to-change-the-background-of-line-number-vscode + "editorGutter.background": colors['very-light-gray'], + + "editorBracketMatch.background": colors['white'], + "editorBracketMatch.border": colors['dark-gray'], + + "errorForeground": "#ff0000", + } +}; +export default MonacoTheme; \ No newline at end of file diff --git a/lib/p5-widget.ts b/lib/p5-widget.ts index d466877..0c02dd4 100644 --- a/lib/p5-widget.ts +++ b/lib/p5-widget.ts @@ -55,8 +55,12 @@ function isElementInViewport(el: HTMLElement) { } function getDataHeight(el: HTMLScriptElement) { - let height = parseInt(el.getAttribute('data-height')); + const dataHeight = el.getAttribute('data-height') || ""; + if (dataHeight.indexOf("%") !== -1) { + return dataHeight; + } + let height = parseInt(dataHeight); if (isNaN(height)) height = defaults.HEIGHT; return height; @@ -110,7 +114,12 @@ function replaceScriptWithWidget(el: HTMLScriptElement) { function makeWidget(sketch: string) { qsArgs.push('sketch=' + encodeURIComponent(sketch)); - style.push('min-height: ' + height + 'px'); + + if (typeof height === "number") { + style.push('min-height: ' + height + 'px'); + } else { + style.push('height: ' + height); + } url = myBaseURL + IFRAME_FILENAME + '?' + qsArgs.join('&'); iframe.setAttribute('src', url); iframe.setAttribute('style', style.join('; ')); diff --git a/lib/preview-frame.ts b/lib/preview-frame.ts index 91d3699..9258f66 100644 --- a/lib/preview-frame.ts +++ b/lib/preview-frame.ts @@ -12,7 +12,7 @@ interface PreviewFrameWindow extends PreviewFrame.Runner { p5: (sketch?: Function, node?: HTMLElement, sync?: boolean) => void; } -let global = window as PreviewFrameWindow; +let global = window as unknown as PreviewFrameWindow; function loadScript(url, cb?: () => void) { let script = document.createElement('script'); diff --git a/lib/toolbar.tsx b/lib/toolbar.tsx index be240a1..930fecb 100644 --- a/lib/toolbar.tsx +++ b/lib/toolbar.tsx @@ -43,7 +43,8 @@ export default class Toolbar extends PureComponent { return (
- p5js.org + {/* TODO: include SVG in package? */} + p5js.org