/** * @license * Visual Blocks Editor * * Copyright 2018 Massachusetts Institute of Technology * All rights reserved. * * 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 * * http://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. */ /** * @fileoverview Note input field, for selecting a musical note on a piano. * @author ericr@media.mit.edu (Eric Rosenbaum) */ 'use strict'; goog.provide('Blockly.FieldNote'); goog.require('Blockly.DropDownDiv'); goog.require('Blockly.FieldTextInput'); goog.require('goog.math'); goog.require('goog.userAgent'); /** * Class for a note input field, for selecting a musical note on a piano. * @param {(string|number)=} opt_value The initial content of the field. The * value should cast to a number, and if it does not, '0' will be used. * @param {Function=} opt_validator An optional function that is called * to validate any constraints on what the user entered. Takes the new * text as an argument and returns the accepted text or null to abort * the change. * @extends {Blockly.FieldTextInput} * @constructor */ Blockly.FieldNote = function(opt_value, opt_validator) { opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0'; Blockly.FieldNote.superClass_.constructor.call( this, opt_value, opt_validator); this.addArgType('note'); /** * Width of the field. Computed when drawing it, and used for animation. * @type {number} * @private */ this.fieldEditorWidth_ = 0; /** * Height of the field. Computed when drawing it. * @type {number} * @private */ this.fieldEditorHeight_ = 0; /** * The piano SVG. * @type {SVGElement} * @private */ this.pianoSVG_ = null; /** * Array of SVG elements representing the clickable piano keys. * @type {!Array<SVGElement>} * @private */ this.keySVGs_ = []; /** * Note name indicator at the top of the field. * @type {SVGElement} * @private */ this.noteNameText_ = null; /** * Note name indicator on the low C key. * @type {SVGElement} * @private */ this.lowCText_ = null; /** * Note name indicator on the low C key. * @type {SVGElement} * @private */ this.highCText_ = null; /** * Octave number of the currently displayed range of keys. * @type {number} * @private */ this.displayedOctave_ = null; /** * Current animation position of the piano SVG, as it shifts left or right to * change octaves. * @type {number} * @private */ this.animationPos_ = 0; /** * Target position for the animation as the piano SVG shifts left or right. * @type {number} * @private */ this.animationTarget_ = 0; /** * A flag indicating that the mouse is currently down. Used in combination with * mouse enter events to update the key selection while dragging. * @type {boolean} * @private */ this.mouseIsDown_ = false; /** * An array of wrappers for mouse down events on piano keys. * @type {!Array.<!Array>} * @private */ this.mouseDownWrappers_ = []; /** * A wrapper for the mouse up event. * @type {!Array.<!Array>} * @private */ this.mouseUpWrapper_ = null; /** * An array of wrappers for mouse enter events on piano keys. * @type {!Array.<!Array>} * @private */ this.mouseEnterWrappers_ = []; /** * A wrapper for the mouse down event on the octave down button. * @type {!Array.<!Array>} * @private */ this.octaveDownMouseDownWrapper_ = null; /** * A wrapper for the mouse down event on the octave up button. * @type {!Array.<!Array>} * @private */ this.octaveUpMouseDownWrapper_ = null; }; goog.inherits(Blockly.FieldNote, Blockly.FieldTextInput); /** * Inset in pixels of content displayed in the field, caused by parent properties. * The inset is actually determined by the CSS property blocklyDropDownDiv- it is * the sum of the padding and border thickness. */ Blockly.FieldNote.INSET = 5; /** * Height of the top area of the field, in px. * @type {number} * @const */ Blockly.FieldNote.TOP_MENU_HEIGHT = 32 - Blockly.FieldNote.INSET; /** * Padding on the top and sides of the field, in px. * @type {number} * @const */ Blockly.FieldNote.EDGE_PADDING = 1; /** * Height of the drop shadow on the piano, in px. * @type {number} * @const */ Blockly.FieldNote.SHADOW_HEIGHT = 4; /** * Color for the shadow on the piano. * @type {string} * @const */ Blockly.FieldNote.SHADOW_COLOR = '#000'; /** * Opacity for the shadow on the piano. * @type {string} * @const */ Blockly.FieldNote.SHADOW_OPACITY = .2; /** * A color for the white piano keys. * @type {string} * @const */ Blockly.FieldNote.WHITE_KEY_COLOR = '#FFFFFF'; /** * A color for the black piano keys. * @type {string} * @const */ Blockly.FieldNote.BLACK_KEY_COLOR = '#323133'; /** * A color for stroke around black piano keys. * @type {string} * @const */ Blockly.FieldNote.BLACK_KEY_STROKE = '#555555'; /** * A color for the selected state of a piano key. * @type {string} * @const */ Blockly.FieldNote.KEY_SELECTED_COLOR = '#b0d6ff'; /** * The number of white keys in one octave on the piano. * @type {number} * @const */ Blockly.FieldNote.NUM_WHITE_KEYS = 8; /** * Height of a white piano key, in px. * @type {string} * @const */ Blockly.FieldNote.WHITE_KEY_HEIGHT = 72; /** * Width of a white piano key, in px. * @type {string} * @const */ Blockly.FieldNote.WHITE_KEY_WIDTH = 40; /** * Height of a black piano key, in px. * @type {string} * @const */ Blockly.FieldNote.BLACK_KEY_HEIGHT = 40; /** * Width of a black piano key, in px. * @type {string} * @const */ Blockly.FieldNote.BLACK_KEY_WIDTH = 32; /** * Radius of the curved bottom corner of a piano key, in px. * @type {string} * @const */ Blockly.FieldNote.KEY_RADIUS = 6; /** * Bottom padding for the labels on C keys. * @type {string} * @const */ Blockly.FieldNote.KEY_LABEL_PADDING = 8; /** * An array of objects with data describing the keys on the piano. * @type {Array.<{name: String, pitch: Number, isBlack: boolean}>} * @const */ Blockly.FieldNote.KEY_INFO = [ {name: 'C', pitch: 0}, {name: 'C♯', pitch: 1, isBlack: true}, {name: 'D', pitch: 2}, {name: 'E♭', pitch: 3, isBlack: true}, {name: 'E', pitch: 4}, {name: 'F', pitch: 5}, {name: 'F♯', pitch: 6, isBlack: true}, {name: 'G', pitch: 7}, {name: 'G♯', pitch: 8, isBlack: true}, {name: 'A', pitch: 9}, {name: 'B♭', pitch: 10, isBlack: true}, {name: 'B', pitch: 11}, {name: 'C', pitch: 12} ]; /** * The MIDI note number of the highest note selectable on the piano. * @type {number} * @const */ Blockly.FieldNote.MAX_NOTE = 130; /** * The fraction of the distance to the target location to move the piano at each * step of the animation. * @type {number} * @const */ Blockly.FieldNote.ANIMATION_FRACTION = 0.2; /** * Path to the arrow svg icon, used on the octave buttons. * @type {string} * @const */ Blockly.FieldNote.ARROW_SVG_PATH = 'icons/arrow_button.svg'; /** * The size of the square octave buttons. * @type {number} * @const */ Blockly.FieldNote.OCTAVE_BUTTON_SIZE = 32; /** * Construct a FieldNote from a JSON arg object. * @param {!Object} options A JSON object with options. * @returns {!Blockly.FieldNote} The new field instance. * @package * @nocollapse */ Blockly.FieldNote.fromJson = function(options) { return new Blockly.FieldNote(options['note']); }; /** * Clean up this FieldNote, as well as the inherited FieldTextInput. * @return {!Function} Closure to call on destruction of the WidgetDiv. * @private */ Blockly.FieldNote.prototype.dispose_ = function() { var thisField = this; return function() { Blockly.FieldNote.superClass_.dispose_.call(thisField)(); thisField.mouseDownWrappers_.forEach(function(wrapper) { Blockly.unbindEvent_(wrapper); }); thisField.mouseEnterWrappers_.forEach(function(wrapper) { Blockly.unbindEvent_(wrapper); }); if (thisField.mouseUpWrapper_) { Blockly.unbindEvent_(thisField.mouseUpWrapper_); } if (thisField.octaveDownMouseDownWrapper_) { Blockly.unbindEvent_(thisField.octaveDownMouseDownWrapper_); } if (thisField.octaveUpMouseDownWrapper_) { Blockly.unbindEvent_(thisField.octaveUpMouseDownWrapper_); } this.pianoSVG_ = null; this.keySVGs_.length = 0; this.noteNameText_ = null; this.lowCText_ = null; this.highCText_ = null; }; }; /** * Show a field with piano keys. * @private */ Blockly.FieldNote.prototype.showEditor_ = function() { // Mobile browsers have issues with in-line textareas (focus & keyboards). Blockly.FieldNote.superClass_.showEditor_.call(this, this.useTouchInteraction_); // If there is an existing drop-down someone else owns, hide it immediately and clear it. Blockly.DropDownDiv.hideWithoutAnimation(); Blockly.DropDownDiv.clearContent(); // Build the SVG DOM. var div = Blockly.DropDownDiv.getContentDiv(); this.fieldEditorWidth_ = Blockly.FieldNote.NUM_WHITE_KEYS * Blockly.FieldNote.WHITE_KEY_WIDTH + Blockly.FieldNote.EDGE_PADDING; this.fieldEditorHeight_ = Blockly.FieldNote.TOP_MENU_HEIGHT + Blockly.FieldNote.WHITE_KEY_HEIGHT + Blockly.FieldNote.EDGE_PADDING; var svg = Blockly.utils.createSvgElement('svg', { 'xmlns': 'http://www.w3.org/2000/svg', 'xmlns:html': 'http://www.w3.org/1999/xhtml', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'version': '1.1', 'height': this.fieldEditorHeight_ + 'px', 'width': this.fieldEditorWidth_ + 'px' }, div); // Add the white and black keys // Since we are adding the keys from left to right in order, they need // to be in two groups in order to layer correctly. this.pianoSVG_ = Blockly.utils.createSvgElement('g', {}, svg); var whiteKeyGroup = Blockly.utils.createSvgElement('g', {}, this.pianoSVG_); var blackKeyGroup = Blockly.utils.createSvgElement('g', {}, this.pianoSVG_); // Add three piano octaves, so we can animate moving up or down an octave. // Only the middle octave gets bound to events. this.keySVGs_ = []; this.addPianoOctave_(-this.fieldEditorWidth_ + Blockly.FieldNote.EDGE_PADDING, whiteKeyGroup, blackKeyGroup, null); this.addPianoOctave_(0, whiteKeyGroup, blackKeyGroup, this.keySVGs_); this.addPianoOctave_(this.fieldEditorWidth_ - Blockly.FieldNote.EDGE_PADDING, whiteKeyGroup, blackKeyGroup, null); // Note name indicator at the top of the field this.noteNameText_ = Blockly.utils.createSvgElement('text', { 'x': this.fieldEditorWidth_ / 2, 'y': Blockly.FieldNote.TOP_MENU_HEIGHT / 2, 'class': 'blocklyText', 'text-anchor': 'middle', 'dominant-baseline': 'middle', }, svg); // Note names on the low and high C keys var lowCX = Blockly.FieldNote.WHITE_KEY_WIDTH / 2; this.lowCText_ = this.addCKeyLabel_(lowCX, svg); var highCX = lowCX + (Blockly.FieldNote.WHITE_KEY_WIDTH * (Blockly.FieldNote.NUM_WHITE_KEYS - 1)); this.highCText_ = this.addCKeyLabel_(highCX, svg); // Horizontal line at the top of the keys Blockly.utils.createSvgElement('line', { 'stroke': this.sourceBlock_.getColourTertiary(), 'x1': 0, 'y1': Blockly.FieldNote.TOP_MENU_HEIGHT, 'x2': this.fieldEditorWidth_, 'y2': Blockly.FieldNote.TOP_MENU_HEIGHT }, svg); // Drop shadow at the top of the keys Blockly.utils.createSvgElement('rect', { 'x': 0, 'y': Blockly.FieldNote.TOP_MENU_HEIGHT, 'width': this.fieldEditorWidth_, 'height': Blockly.FieldNote.SHADOW_HEIGHT, 'fill': Blockly.FieldNote.SHADOW_COLOR, 'fill-opacity': Blockly.FieldNote.SHADOW_OPACITY }, svg); // Octave buttons this.octaveDownButton = this.addOctaveButton_(0, true, svg); this.octaveUpButton = this.addOctaveButton_( (this.fieldEditorWidth_ + Blockly.FieldNote.INSET * 2) - Blockly.FieldNote.OCTAVE_BUTTON_SIZE, false, svg); this.octaveDownMouseDownWrapper_ = Blockly.bindEvent_(this.octaveDownButton, 'mousedown', this, function() { this.changeOctaveBy_(-1); }); this.octaveUpMouseDownWrapper_ = Blockly.bindEvent_(this.octaveUpButton, 'mousedown', this,function() { this.changeOctaveBy_(1); }); Blockly.DropDownDiv.setColour(this.sourceBlock_.parentBlock_.getColour(), this.sourceBlock_.getColourTertiary()); Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory()); Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_); this.updateSelection_(); }; /** * Add one octave of piano keys drawn using SVG. * @param {number} x The x position of the left edge of this octave of keys. * @param {SVGElement} whiteKeyGroup The group for all white piano keys. * @param {SvgElement} blackKeyGroup The group for all black piano keys. * @param {!Array.<SvgElement>} keySVGarray An array containing all the key SVGs. * @private */ Blockly.FieldNote.prototype.addPianoOctave_ = function(x, whiteKeyGroup, blackKeyGroup, keySVGarray) { var xIncrement, width, height, fill, stroke, group; x += Blockly.FieldNote.EDGE_PADDING / 2; var y = Blockly.FieldNote.TOP_MENU_HEIGHT; for (var i = 0; i < Blockly.FieldNote.KEY_INFO.length; i++) { // Draw a black or white key if (Blockly.FieldNote.KEY_INFO[i].isBlack) { // Black keys are shifted back half a key x -= Blockly.FieldNote.BLACK_KEY_WIDTH / 2; xIncrement = Blockly.FieldNote.BLACK_KEY_WIDTH / 2; width = Blockly.FieldNote.BLACK_KEY_WIDTH; height = Blockly.FieldNote.BLACK_KEY_HEIGHT; fill = Blockly.FieldNote.BLACK_KEY_COLOR; stroke = Blockly.FieldNote.BLACK_KEY_STROKE; group = blackKeyGroup; } else { xIncrement = Blockly.FieldNote.WHITE_KEY_WIDTH; width = Blockly.FieldNote.WHITE_KEY_WIDTH; height = Blockly.FieldNote.WHITE_KEY_HEIGHT; fill = Blockly.FieldNote.WHITE_KEY_COLOR; stroke = this.sourceBlock_.getColourTertiary(); group = whiteKeyGroup; } var attr = { 'd': this.getPianoKeyPath_(x, y, width, height), 'fill': fill, 'stroke': stroke }; x += xIncrement; var keySVG = Blockly.utils.createSvgElement('path', attr, group); if (keySVGarray) { keySVGarray[i] = keySVG; keySVG.setAttribute('data-pitch', Blockly.FieldNote.KEY_INFO[i].pitch); keySVG.setAttribute('data-name', Blockly.FieldNote.KEY_INFO[i].name); keySVG.setAttribute('data-isBlack', Blockly.FieldNote.KEY_INFO[i].isBlack); this.mouseDownWrappers_[i] = Blockly.bindEvent_(keySVG, 'mousedown', this, this.onMouseDownOnKey_); this.mouseEnterWrappers_[i] = Blockly.bindEvent_(keySVG, 'mouseenter', this, this.onMouseEnter_); } } }; /** * Construct the SVG path string for a piano key shape: a rectangle with rounded * corners at the bottom. * @param {number} x the x position for the key. * @param {number} y the y position for the key. * @param {number} width the width of the key. * @param {number} height the height of the key. * @returns {string} the SVG path as a string. * @private */ Blockly.FieldNote.prototype.getPianoKeyPath_ = function(x, y, width, height) { return 'M' + x + ' ' + y + ' ' + 'L' + x + ' ' + (y + height - Blockly.FieldNote.KEY_RADIUS) + ' ' + 'Q' + x + ' ' + (y + height) + ' ' + (x + Blockly.FieldNote.KEY_RADIUS) + ' ' + (y + height) + ' ' + 'L' + (x + width - Blockly.FieldNote.KEY_RADIUS) + ' ' + (y + height) + ' ' + 'Q' + (x + width) + ' ' + (y + height) + ' ' + (x + width) + ' ' + (y + height - Blockly.FieldNote.KEY_RADIUS) + ' ' + 'L' + (x + width) + ' ' + y + ' ' + 'L' + x + ' ' + y; }; /** * Add a button for switching the displayed octave of the piano up or down. * @param {number} x The x position of the button. * @param {boolean} flipped If true, the icon should be flipped. * @param {SvgElement} svg The svg element to add the buttons to. * @returns {SvgElement} A group containing the button SVG elements. * @private */ Blockly.FieldNote.prototype.addOctaveButton_ = function(x, flipped, svg) { var group = Blockly.utils.createSvgElement('g', {}, svg); var imageSize = Blockly.FieldNote.OCTAVE_BUTTON_SIZE; var arrow = Blockly.utils.createSvgElement('image', { 'width': imageSize, 'height': imageSize, 'x': x - Blockly.FieldNote.INSET, 'y': -1 * Blockly.FieldNote.INSET }, group); arrow.setAttributeNS( 'http://www.w3.org/1999/xlink', 'xlink:href', Blockly.mainWorkspace.options.pathToMedia + Blockly.FieldNote.ARROW_SVG_PATH ); Blockly.utils.createSvgElement('line', { 'stroke': this.sourceBlock_.getColourTertiary(), 'x1': x - Blockly.FieldNote.INSET, 'y1': 0, 'x2': x - Blockly.FieldNote.INSET, 'y2': Blockly.FieldNote.TOP_MENU_HEIGHT - Blockly.FieldNote.INSET }, group); if (flipped) { var translateX = -1 * Blockly.FieldNote.OCTAVE_BUTTON_SIZE + (Blockly.FieldNote.INSET * 2); group.setAttribute('transform', 'scale(-1, 1) ' + 'translate(' + translateX + ', 0)'); } return group; }; /** * Add an SVG text label for display on the C keys of the piano. * @param {number} x The x position for the label. * @param {SvgElement} svg The SVG element to add the label to. * @returns {SvgElement} The SVG element containing the label. * @private */ Blockly.FieldNote.prototype.addCKeyLabel_ = function(x, svg) { return Blockly.utils.createSvgElement('text', { 'x': x, 'y': Blockly.FieldNote.TOP_MENU_HEIGHT + Blockly.FieldNote.WHITE_KEY_HEIGHT - Blockly.FieldNote.KEY_LABEL_PADDING, 'class': 'scratchNotePickerKeyLabel', 'text-anchor': 'middle' }, svg); }; /** * Set the visibility of the C key labels. * @param {boolean} visible If true, set labels to be visible. * @private */ Blockly.FieldNote.prototype.setCKeyLabelsVisible_ = function(visible) { if (visible) { this.fadeSvgToOpacity_(this.lowCText_, 1); this.fadeSvgToOpacity_(this.highCText_, 1); } else { this.fadeSvgToOpacity_(this.lowCText_, 0); this.fadeSvgToOpacity_(this.highCText_, 0); } }; /** * Animate an SVG to fade it in or out to a target opacity. * @param {SvgElement} svg The SVG element to apply the fade to. * @param {number} opacity The target opacity. * @private */ Blockly.FieldNote.prototype.fadeSvgToOpacity_ = function(svg, opacity) { svg.setAttribute('style', 'opacity: ' + opacity + '; transition: opacity 0.1s;'); }; /** * Handle the mouse down event on a piano key. * @param {!Event} e Mouse down event. * @private */ Blockly.FieldNote.prototype.onMouseDownOnKey_ = function(e) { this.mouseIsDown_ = true; this.mouseUpWrapper_ = Blockly.bindEvent_(document.body, 'mouseup', this, this.onMouseUp_); this.selectNoteWithMouseEvent_(e); }; /** * Handle the mouse up event following a mouse down on a piano key. * @private */ Blockly.FieldNote.prototype.onMouseUp_ = function() { this.mouseIsDown_ = false; Blockly.unbindEvent_(this.mouseUpWrapper_); }; /** * Handle the event when the mouse enters a piano key. * @param {!Event} e Mouse enter event. * @private */ Blockly.FieldNote.prototype.onMouseEnter_ = function(e) { if (this.mouseIsDown_) { this.selectNoteWithMouseEvent_(e); } }; /** * Use the data in a mouse event to select a new note, and play it. * @param {!Event} e Mouse event. * @private */ Blockly.FieldNote.prototype.selectNoteWithMouseEvent_ = function(e) { var newNoteNum = Number(e.target.getAttribute('data-pitch')) + this.displayedOctave_ * 12; this.setNoteNum_(newNoteNum); this.playNoteInternal_(); }; /** * Play a note, by calling the externally overriden play note function. * @private */ Blockly.FieldNote.prototype.playNoteInternal_ = function() { if (Blockly.FieldNote.playNote_) { Blockly.FieldNote.playNote_( this.getValue(), this.sourceBlock_.parentBlock_.getCategory() ); } }; /** * Function to play a musical note corresponding to the key selected. * Overridden externally. * @param {number} noteNum the MIDI note number to play. * @param {string} id An id to select a scratch extension to play the note. * @private */ Blockly.FieldNote.playNote_ = function(/* noteNum, id*/) { return; }; /** * Change the selected note by a number of octaves, and start the animation. * @param {number} octaves The number of octaves to change by. * @private */ Blockly.FieldNote.prototype.changeOctaveBy_ = function(octaves) { this.displayedOctave_ += octaves; if (this.displayedOctave_ < 0) { this.displayedOctave_ = 0; return; } var maxOctave = Math.floor(Blockly.FieldNote.MAX_NOTE / 12); if (this.displayedOctave_ > maxOctave) { this.displayedOctave_ = maxOctave; return; } var newNote = Number(this.getText()) + (octaves * 12); this.setNoteNum_(newNote); this.animationTarget_ = this.fieldEditorWidth_ * octaves * -1; this.animationPos_ = 0; this.stepOctaveAnimation_(); this.setCKeyLabelsVisible_(false); }; /** * Animate the piano up or down an octave by sliding it to the left or right. * @private */ Blockly.FieldNote.prototype.stepOctaveAnimation_ = function() { var absDiff = Math.abs(this.animationPos_ - this.animationTarget_); if (absDiff < 1) { this.pianoSVG_.setAttribute('transform', 'translate(0, 0)'); this.setCKeyLabelsVisible_(true); this.playNoteInternal_(); return; } this.animationPos_ += (this.animationTarget_ - this.animationPos_) * Blockly.FieldNote.ANIMATION_FRACTION; this.pianoSVG_.setAttribute('transform', 'translate(' + this.animationPos_ + ',0)'); requestAnimationFrame(this.stepOctaveAnimation_.bind(this)); }; /** * Set the selected note number, and update the piano display and the input field. * @param {number} noteNum The MIDI note number to select. * @private */ Blockly.FieldNote.prototype.setNoteNum_ = function(noteNum) { noteNum = this.callValidator(noteNum); this.setValue(noteNum); Blockly.FieldTextInput.htmlInput_.value = noteNum; }; /** * Sets the text in this field. Triggers a rerender of the source block, and * updates the selection on the field. * @param {?string} text New text. */ Blockly.FieldNote.prototype.setText = function(text) { Blockly.FieldNote.superClass_.setText.call(this, text); if (!this.textElement_) { // Not rendered yet. return; } this.updateSelection_(); // Cached width is obsolete. Clear it. this.size_.width = 0; }; /** * For a MIDI note number, find the index of the corresponding piano key. * @param {number} noteNum The note number. * @returns {number} The index of the piano key. * @private */ Blockly.FieldNote.prototype.noteNumToKeyIndex_ = function(noteNum) { return Math.floor(noteNum) - (this.displayedOctave_ * 12); }; /** * Update the selected note and labels on the field. * @private */ Blockly.FieldNote.prototype.updateSelection_ = function() { var noteNum = Number(this.getText()); // If the note is outside the currently displayed octave, update it if (this.displayedOctave_ == null || noteNum > ((this.displayedOctave_ * 12) + 12) || noteNum < (this.displayedOctave_ * 12)) { this.displayedOctave_ = Math.floor(noteNum / 12); } var index = this.noteNumToKeyIndex_(noteNum); // Clear the highlight on all keys this.keySVGs_.forEach(function(svg) { var isBlack = svg.getAttribute('data-isBlack'); if (isBlack === 'true') { svg.setAttribute('fill', Blockly.FieldNote.BLACK_KEY_COLOR); } else { svg.setAttribute('fill', Blockly.FieldNote.WHITE_KEY_COLOR); } }); // Set the highlight on the selected key if (this.keySVGs_[index]) { this.keySVGs_[index].setAttribute('fill', Blockly.FieldNote.KEY_SELECTED_COLOR); // Update the note name text var noteName = Blockly.FieldNote.KEY_INFO[index].name; this.noteNameText_.textContent = noteName + ' (' + Math.floor(noteNum) + ')'; // Update the low and high C note names var lowCNum = this.displayedOctave_ * 12; this.lowCText_.textContent = 'C(' + lowCNum + ')'; this.highCText_.textContent = 'C(' + (lowCNum + 12) + ')'; } }; /** * Ensure that only a valid MIDI note number may be entered. * @param {string} text The user's text. * @return {?string} A string representing a valid note number, or null if invalid. */ Blockly.FieldNote.prototype.classValidator = function(text) { if (text === null) { return null; } var n = parseFloat(text || 0); if (isNaN(n)) { return null; } if (n < 0) { n = 0; } if (n > Blockly.FieldNote.MAX_NOTE) { n = Blockly.FieldNote.MAX_NOTE; } return String(n); }; Blockly.Field.register('field_note', Blockly.FieldNote);