diff --git a/content.js b/content.js index 0b8bdf43e94beb7871e63d0ac83ed65ac2fb122a..df1c082573311d8eba7b745f31ec6751fd5e0bd1 100644 --- a/content.js +++ b/content.js @@ -277,28 +277,17 @@ let Content = { note_array: { instructions: "Define <code>notes</code> to be an array that contains 7 \"note arrays\" that are pairs of note pitch and it's length, e.g. <code>[\"C5\",\"8n\"]</code>. The array must first have four sixteenth (16n) notes A4, B4, C#4, A4. Then, TWICE the eighth (8n) note E5. And finally, the half (2n) note C#4.", initialJs: 'let notes;', - postExecuteJs: ';\n' - + 'let notesTimed = [];' - + 'display.cmd([0,1,2,3,4,5,6].map(i => { return "notes[" + i + "]"; }).join(", "));\n' - + 'for (var i = 0; i < 7; i++) {\n' - + ' display.res(JSON.stringify((notes || [])[i]), (notes || [])[i]);\n' - + ' if (notes && notes[i] && notes[i].length > 1) {\n' - + ' notesTimed.push(notes[i]);\n' - + ' }\n' - + '}\n' - + 'document.write("<script src=\\"https://cdnjs.cloudflare.com/ajax/libs/tone/14.5.41/Tone.js\\"></"+"script>");\n' - + 'function music() {\n' - + ' var synth = new Tone.Synth().toDestination();\n' - + ' var pattern = new Tone.Pattern(function(time, note) {\n' - + ' synth.triggerAttackRelease(note[0], note[1]);\n' - + ' }, notesTimed);\n' - + ' pattern.start(0);\n' - + ' Tone.Transport.bpm.value = 160;\n' - + ' Tone.Transport.start();\n' - + '}\n' - + 'if (notesTimed.length > 0) {\n' - + ' document.write("<button onclick=\\"music();\\">Play music</button>");\n' - + '}\n', + postExecuteJs: ';' + + 'window["sequence"] = [];\n' + + 'display.cmd([0,1,2,3,4,5,6].map(i => { return "notes[" + i + "]"; }).join(", "));\n' + + 'for (var i = 0; i < 7; i++) {\n' + + ' display.res(JSON.stringify((notes || [])[i]), (notes || [])[i]);\n' + + ' if (notes && notes[i] && notes[i].length > 1) {\n' + + ' window["sequence"].push(notes[i]);\n' + + ' }\n' + + '}\n', + postExecuteHtml: '<button class="tone-play-sequence">Play music</button>\n', + postExecuteScript: '/static/webdev/augment-tone.js', executeAtStart: false, points: function ($element, config, accessor) { var correct = [["A4","16n"],["B4","16n"],["C#4","16n"],["A4","16n"],["E5","8n"],["E5","8n"],["C#4","2n"]]; @@ -579,6 +568,46 @@ let Content = { order: 14 }, + adjust_css_classes: { + instructions: 'The function named <code>onPlay</code> below is called with the id of a button that was just clicked. ' + + 'Your task is to set a CSS class <code>last-played</code> to this button that was just clicked. ' + + 'In addition, you must make it the only button that has this CSS class.', + preExecuteHtml: '<div id="keyboard" class="tone-keyboard">\n' + + ' <button id="C4">C4</button>\n' + + ' <button id="C#4" class="black">C#4</button>\n' + + ' <button id="D4">D4</button>\n' + + ' <button id="D#4" class="black">D#4</button>\n' + + ' <button id="E4">E4</button>\n' + + '</div>\n', + initialJs: 'function onPlay(id) {\n console.log(id);\n}\n', + postExecuteJs: ';' + + 'document.querySelectorAll("#keyboard button").forEach(function (button) {\n' + + ' button.addEventListener("click", function (event) {\n' + + ' onPlay(event.target.getAttribute("id"));\n' + + ' display.showCode("keyboard");\n' + + ' });\n' + + '});\n' + + 'display.showCode("keyboard");\n', + postExecuteScript: '/static/webdev/augment-tone.js', + executeAtStart: true, + points: function ($element, config, accessor) { + let doc = accessor.doc(); + let p = 0; + ['C4', 'D4', 'D#4', 'C#4', 'E4'].forEach(function (id) { + let b = doc.getElementById(id); + b.click(); + p += b.classList.contains('last-played') ? 1 : 0; + p += doc.querySelectorAll('.last-played').length == 1 ? 1 : 0; + }); + return { points: p }; + }, + maxPoints: 10, + title: "Adjust CSS classes", + description: "Set CSS classes for elements based on a given element id.", + concepts: ["JavaScript", "getElementById", "childNodes", "classes", "modify"], + order: 15 + }, + events: { instructions: `Now we want to upgrade our keyboard. We would like to show the note names on the buttons only when the mouse pointer enters the button, and remove them @@ -636,12 +665,11 @@ for(let i = 0; i < buttonsIds.length; i++) { return { points: p }; }, maxPoints: 10, - title: "modify the elements on mouseenter and mouseleave events", + title: "Modify the elements on mouseenter and mouseleave events", description: "", concepts: ["JavaScript", "mouseenter", "mouseleave"], - order: 15 + order: 16 } - }; module.exports = Content; diff --git a/static/webdev-editor.css b/static/webdev-editor.css index cac9d6bfdc4f1144a975861b142571d2bd8f7912..de856ce9b93522b8b903b4fe972ae5061a6f7549 100644 --- a/static/webdev-editor.css +++ b/static/webdev-editor.css @@ -40,6 +40,12 @@ body.execute ul#display > li.error { body.execute ul#display > li.error::before { content: "тип "; } +body.execute textarea.show-code { + display: block; + width: 98%; + margin: 0 auto; +} + body.execute button { border: 1px solid silver; border-radius: 0.5em; @@ -51,3 +57,25 @@ body.execute button { font-size: 90%; font-weight: bold; } + +body.execute .tone-keyboard { + text-align: center; + padding-top: 20px; +} +body.execute .tone-keyboard button { + width: 50px; + height: 120px; + padding: 2px; + background-color: #F0F0F0; +} +body.execute .tone-keyboard button.black { + height: 100px; + transform: translate(0, -20px); + background-color: #333; + color: #FFF; + border-color: #000; +} +body.execute .tone-keyboard button.last-played { + border-left-width: 10px; + border-top-width: 3px; +} diff --git a/static/webdev-editor.js b/static/webdev-editor.js index ca9ba49120bcc1af7a3a8ef18b48ec169b22ac64..ed1fdbae0b05e2cd20249cea4753212440325ec8 100644 --- a/static/webdev-editor.js +++ b/static/webdev-editor.js @@ -71,10 +71,9 @@ ACOSWebdev.prototype.extendGrade = function (eventOrMutations, cb) { }, 0); } }; - this.editorExecute(); - setTimeout(function () { + this.editorExecute(function () { cb(self.config.points(self.$element, self.config, accessor)); - }, 1000); + }); }; ACOSWebdev.prototype.extendProtocolFeedback = function (feedback) { @@ -83,7 +82,7 @@ ACOSWebdev.prototype.extendProtocolFeedback = function (feedback) { return '<pre><code>' + this.editor.getValue() + '</code></pre><div>' + $out.html() + '</div>'; }; -ACOSWebdev.prototype.editorExecute = function () { +ACOSWebdev.prototype.editorExecute = function (cb) { var $iframe = $('<iframe src="about:blank"></iframe>'); this.$editorOutput.empty().append($iframe); $iframe.get(0).contentWindow.contents = '<!DOCTYPE html>\n' @@ -92,10 +91,31 @@ ACOSWebdev.prototype.editorExecute = function () { + '<link href="/static/webdev-editor/webdev-editor.css" rel="stylesheet">\n' + '<script src="/static/webdev-editor/webdev-execute.js"></script>\n' + '</head>\n<body class="execute">\n' + + (this.config.preExecuteScript ? ('<script src="' + this.config.preExecuteScript + '"></script>\n') : '') + (this.config.preExecuteHtml || '') - + '<script>' + (this.config.preExecuteJs || '') + + '<script>' + + 'try {\n' + + (this.config.preExecuteJs || '') + this.editor.getValue() - + (this.config.postExecuteJs || '') + '</script>\n' + + (this.config.postExecuteJs || '') + + '} catch (error) {\n' + + ' display.err(error.message);\n' + + ' throw error;\n' + + '}\n' + + 'window.postMessage({state: "done"}, "*");\n' + + 'window.parent.postMessage({state: "done"}, "*");\n' + + '</script>\n' + + (this.config.postExecuteHtml || '') + + (this.config.postExecuteScript ? ('<script src="' + this.config.postExecuteScript + '"></script>\n') : '') + '</body>\n</html>\n'; + function onDone(event) { + if (event.data.state == 'done') { + window.removeEventListener('message', onDone); + var h = $iframe.get(0).contentWindow.document.body.scrollHeight; + $iframe.css('height', h + 10 + 'px'); + cb(); + } + } + window.addEventListener('message', onDone); $iframe.attr('src', 'javascript:window["contents"]'); }; diff --git a/static/webdev-execute.js b/static/webdev-execute.js index 51307aa1e2d44b1a115b3f02006fd7295a8a9c81..03f5a6cce4225751ba659853a1da82a676735e1a 100644 --- a/static/webdev-execute.js +++ b/static/webdev-execute.js @@ -1,6 +1,7 @@ var display = (function () { var element = undefined; + var code = undefined; function addLine(html, cls, args) { if (element === undefined) { @@ -28,6 +29,21 @@ var display = (function () { }, res: function (html, args) { addLine(html, 'res', args); + }, + err: function (html) { + addLine(html, 'error text-danger'); + }, + showCode: function (id) { + let element = document.getElementById(id); + if (code === undefined) { + code = document.createElement('textarea'); + code.classList.add('show-code'); + code.disabled = true; + document.body.insertBefore(code, element); + } + code.style.height = null; + code.textContent = element.outerHTML; + code.style.height = code.scrollHeight + 3 + 'px'; } }; })();