diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1bac6456b88..ae282897763 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,27 @@ } } }, + "@babel/polyfill": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.4.4.tgz", + "integrity": "sha512-WlthFLfhQQhh+A2Gn5NSFl0Huxz36x86Jn+E9OW7ibK8edKPq+KLy4apM1yDpQ8kJOVi1OVjpP4vSDLdrI04dg==", + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + } + } + }, "@babel/runtime": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", @@ -2255,6 +2276,11 @@ } } }, + "brace": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz", + "integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4105,6 +4131,11 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" }, + "diff-match-patch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", + "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==" + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -5319,7 +5350,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5684,7 +5716,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5732,6 +5765,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5770,11 +5804,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -8418,6 +8454,11 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -8433,8 +8474,7 @@ "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "lodash.isfunction": { "version": "3.0.9", @@ -11194,6 +11234,36 @@ "scheduler": "^0.12.0" } }, + "react-ace": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-7.0.2.tgz", + "integrity": "sha512-+TFuO1nO6dme/q+qEHjb7iOuWI8jRDzeALs9JyH8HoyHb9+A2bC8WHuJyNU3pmPo8623bytgAgzEJAzDMkzjlw==", + "requires": { + "@babel/polyfill": "^7.4.4", + "brace": "^0.11.1", + "diff-match-patch": "^1.0.4", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "prop-types": "^15.7.2" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react-is": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", + "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==" + } + } + }, "react-codemirror2": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-5.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ee01ec5ea47..168405881c7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "@material-ui/core": "3.7.1", "@material-ui/icons": "^3.0.1", "@types/js-yaml": "^3.11.2", + "brace": "^0.11.1", "codemirror": "^5.40.2", "d3": "^5.7.0", "d3-dsv": "^1.0.10", @@ -18,6 +19,7 @@ "portable-fetch": "^3.0.0", "re-resizable": "^4.9.0", "react": "^16.7.0", + "react-ace": "^7.0.2", "react-codemirror2": "^5.1.0", "react-dom": "^16.5.2", "react-dropzone": "^5.1.0", diff --git a/frontend/src/components/Editor.test.tsx b/frontend/src/components/Editor.test.tsx new file mode 100644 index 00000000000..2245fb6ba66 --- /dev/null +++ b/frontend/src/components/Editor.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from 'react'; +import { mount } from 'enzyme'; +import Editor from './Editor'; + +/* + These tests mimic https://github.com/securingsincity/react-ace/blob/master/tests/src/ace.spec.js + to ensure that editor properties (placeholder and value) can be properly + tested. +*/ + +describe('Editor', () => { + it('renders without a placeholder and value', () => { + const tree = mount(); + expect(tree.html()).toMatchSnapshot(); + }); + + it('renders with a placeholder', () => { + const placeholder = 'I am a placeholder.'; + const tree = mount(); + expect(tree.html()).toMatchSnapshot(); + }); + + it ('renders a placeholder that contains HTML', () => { + const placeholder = 'I am a placeholder with HTML.'; + const tree = mount(); + expect(tree.html()).toMatchSnapshot(); + }); + + it('has its value set to the provided value', () => { + const value = 'I am a value.'; + const tree = mount(); + expect(tree).not.toBeNull(); + const editor = (tree.instance() as any).editor; + expect(editor.getValue()).toBe(value); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/Editor.tsx b/frontend/src/components/Editor.tsx new file mode 100644 index 00000000000..11dec1c990d --- /dev/null +++ b/frontend/src/components/Editor.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import AceEditor from 'react-ace'; + +// Modified AceEditor that supports HTML within provided placeholder. This is +// important because it allows for the usage of multi-line placeholders. +class Editor extends AceEditor { + public updatePlaceholder(): void { + const editor = this.editor; + const { placeholder } = this.props; + + const showPlaceholder = !editor.session.getValue().length; + let node = editor.renderer.placeholderNode; + if (!showPlaceholder && node) { + editor.renderer.scroller.removeChild(editor.renderer.placeholderNode); + editor.renderer.placeholderNode = null; + } else if (showPlaceholder && !node) { + node = editor.renderer.placeholderNode = document.createElement('div'); + node.innerHTML = placeholder || ''; + node.className = 'ace_comment ace_placeholder'; + node.style.padding = '0 9px'; + node.style.position = 'absolute'; + node.style.zIndex = '3'; + editor.renderer.scroller.appendChild(node); + } else if (showPlaceholder && node) { + node.innerHTML = placeholder; + } + } +} + +export default Editor; \ No newline at end of file diff --git a/frontend/src/components/__snapshots__/Editor.test.tsx.snap b/frontend/src/components/__snapshots__/Editor.test.tsx.snap new file mode 100644 index 00000000000..10762a86460 --- /dev/null +++ b/frontend/src/components/__snapshots__/Editor.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Editor renders a placeholder that contains HTML 1`] = `"
I am a placeholder with HTML.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
"`; + +exports[`Editor renders with a placeholder 1`] = `"
I am a placeholder.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
"`; + +exports[`Editor renders without a placeholder and value 1`] = `"
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
"`;