diff --git a/_custombuild b/_custombuild index f79b4a2..88dbafc 100755 --- a/_custombuild +++ b/_custombuild @@ -64,7 +64,8 @@ var components = gulp.src([ APP_ROOT + 'components/gaia-list/gaia-list.js', APP_ROOT + 'components/gaia-button/gaia-button.js', APP_ROOT + 'components/gaia-switch/gaia-switch.js', - APP_ROOT + 'components/jszip/dist/jszip.js' + APP_ROOT + 'components/jszip/dist/jszip.js', + APP_ROOT + 'components/esprima/esprima.js' ]) .pipe(concat('components-concat.js')) diff --git a/app/js/elements/fxos-code-editor.js b/app/js/elements/fxos-code-editor.js new file mode 100644 index 0000000..35b453d --- /dev/null +++ b/app/js/elements/fxos-code-editor.js @@ -0,0 +1,107 @@ +(function(window) { +'use strict'; + +var proto = Object.create(HTMLElement.prototype); + +var template = +` +
+
+ +
`; + +proto.createdCallback = function() { + var value = this.innerHTML; + + this.shadow = this.createShadowRoot(); + this.shadow.innerHTML = template; + + this.lineNumbers = this.shadow.querySelector('.line-numbers'); + + this.textarea = this.shadow.querySelector('textarea'); + this.textarea.value = value; + + this.textarea.addEventListener('keyup', () => { + this.dispatchEvent(new CustomEvent('change')); + updateLineNumbers(this); + }); + + this.textarea.addEventListener('scroll', () => { + this.lineNumbers.scrollTop = this.textarea.scrollTop; + }); +}; + +Object.defineProperty(proto, 'value', { + get: function() { + return this.textarea.value; + }, + + set: function(value) { + this.textarea.value = value; + this.dispatchEvent(new CustomEvent('change')); + updateLineNumbers(this); + } +}); + +function updateLineNumbers(element) { + var html = ''; + + var lines = element.value.split('\n').length; + for (var i = 1; i <= lines; i++) { + html += '
' + i + '
'; + } + + element.lineNumbers.innerHTML = html; + element.lineNumbers.scrollTop = element.textarea.scrollTop; +} + +try { + document.registerElement('fxos-code-editor', { prototype: proto }); +} catch (e) { + if (e.name !== 'NotSupportedError') { + throw e; + } +} + +})(window); diff --git a/app/js/views/edit.js b/app/js/views/edit.js index 22bb2f4..29142e0 100644 --- a/app/js/views/edit.js +++ b/app/js/views/edit.js @@ -1,5 +1,6 @@ /* global View */ +/* global esprima */ /* global html_beautify */ var editViewTemplate = @@ -30,12 +31,6 @@ var editViewTemplate = .tab-pane.active { display: block; } - textarea { - border: none; - font-family: Consolas,Monaco,"Andale Mono",monospace; - width: 100%; - height: 100%; - } textarea, input { -moz-user-select: text !important; @@ -50,6 +45,30 @@ var editViewTemplate = background: #000; color: #fff; } + fxos-code-editor { + width: 100%; + height: 100%; + } + .errors { + background: #820000; + color: #fff; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 20px; + overflow: hidden; + z-index: 2; + opacity: 0; + transition: opacity 0.2s ease; + pointer-events: none; + } + .errors.active { + opacity: 1; + } + .errors.active ~ fxos-code-editor { + height: calc(100% - 20px); + } @@ -63,10 +82,11 @@ var editViewTemplate = Properties
- +
- +
+
@@ -92,11 +112,13 @@ export default class EditView extends View { this.header = this.$('gaia-header'); this.tabs = this.$('gaia-tabs'); - this.htmlTextarea = this.$('section[data-id="html"] > textarea'); - this.scriptTextarea = this.$('section[data-id="script"] > textarea'); + this.htmlCodeEditor = this.$('section[data-id="html"] > fxos-code-editor'); + this.scriptCodeEditor = this.$('section[data-id="script"] > fxos-code-editor'); this.attributeInspector = this.$('section[data-id="attributes"] > gaia-property-inspector'); this.propertyInspector = this.$('section[data-id="properties"] > gaia-property-inspector'); + this.scriptErrors = this.$('section[data-id="script"] > .errors'); + this.tabPanes = [].slice.apply(this.$$('.tab-pane')); this.on('click', 'button[data-action="cancel"]', (evt) => { @@ -117,12 +139,15 @@ export default class EditView extends View { }); }); - this.htmlTextarea.addEventListener('keyup', (evt) => { - this.controller.changes.innerHTML = this.htmlTextarea.value; + this.htmlCodeEditor.addEventListener('change', (evt) => { + this.controller.changes.innerHTML = this.htmlCodeEditor.value; }); - this.scriptTextarea.addEventListener('keyup', (evt) => { - this.controller.changes.script = this.scriptTextarea.value; + this.scriptCodeEditor.addEventListener('change', (evt) => { + this.controller.changes.script = this.scriptCodeEditor.value; + + clearTimeout(this.validateScriptTimeout); + this.validateScriptTimeout = setTimeout(this.validateScript.bind(this), 1000); }); this.on('save', 'gaia-property-inspector', (evt) => { @@ -158,8 +183,8 @@ export default class EditView extends View { indent_size: 2 }); - this.htmlTextarea.value = html; - this.scriptTextarea.value = + this.htmlCodeEditor.value = html; + this.scriptCodeEditor.value = `/** * You can edit a script to be inserted * in the generated add-on here. @@ -178,4 +203,29 @@ export default class EditView extends View { this.attributeInspector.set(clonedTarget); this.propertyInspector.set(clonedTarget); } + + validateScript() { + var error; + + try { + var syntax = esprima.parse(this.controller.changes.script); + if (syntax.errors && syntax.errors.length > 0) { + error = syntax.errors[0]; + } + } + + catch (e) { + error = e; + } + + if (error) { + this.scriptErrors.textContent = error.message; + this.scriptErrors.classList.add('active'); + } + + else { + this.scriptErrors.textContent = ''; + this.scriptErrors.classList.remove('active'); + } + } } diff --git a/bower.json b/bower.json index 51f1b11..c2591e1 100644 --- a/bower.json +++ b/bower.json @@ -25,7 +25,8 @@ "js-beautify": "~1.5.5", "jszip": "~2.5.0", "observe-shim": "~0.4.2", - "observe-utils": "~0.3.2" + "observe-utils": "~0.3.2", + "esprima": "~2.0.0" }, "resolutions": { "font-fit": "~0.3.0"