UNPKG

blockly-field-searchable-dropdown

Version:

302 lines (270 loc) 8.96 kB
/** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview searchable dropdown field. * @author baich.info@gmail.com (Junius Cho) */ import * as Blockly from 'blockly/core'; /** * searchable dropdown field. */ export class FieldSearchable extends Blockly.FieldTextInput { /** * Array holding info needed to unbind events. * Used for disposing. * @type {!Array<!Blockly.browserEvents.Data>} * @private */ boundEvents_ = []; constructor(rules, validator) { var value = ''; super(value, validator); // Disable spellcheck. this.rules = rules; this.setSpellcheck(false); } /** * Construct a FieldSearchable from a JSON arg object. * @param {!Object} options A JSON object with options (pitch). * @returns {!FieldSearchable} The new field instance. * @package * @nocollapse */ static fromJson(options) { // `this` might be a subclass of FieldSearchable if that class doesn't // override the static fromJson method. return new this(options['pitch']); } /** * Show the inline free-text editor on top of the text and the pitch picker. * @protected */ showEditor_() { super.showEditor_(); const div = Blockly.WidgetDiv.getDiv(); if (!div.firstChild) { // Mobile interface uses Blockly.dialog.setPrompt(). return; } // Build the DOM. const editor = this.dropdownCreate_(); Blockly.DropDownDiv.getContentDiv().appendChild(editor); Blockly.DropDownDiv.setColour( this.sourceBlock_.style.colourPrimary, this.sourceBlock_.style.colourTertiary, ); Blockly.DropDownDiv.showPositionedByField( this, this.dropdownDispose_.bind(this), ); // The pitch picker is different from other fields in that it updates on // mousemove even if it's not in the middle of a drag. In future we may // change this behaviour. For now, using `bind` instead of // `conditionalBind` allows it to work without a mousedown/touchstart. this.boundEvents_.push( Blockly.browserEvents.bind(this.optionsContainer, 'click', this, this.hide_), ); this.boundEvents_.push( Blockly.browserEvents.bind( this.optionsContainer, 'mousemove', this, this.onMouseMove, ), ); this.updateGraph_(); } /** * Create the pitch picker. * @returns {!Element} The newly created pitch picker. * @private */ dropdownCreate_() { this.optionsContainer = document.createElement('div'); this.optionsContainer.className = 'blocklyMenu goog-menu blocklyNonSelectable blocklyDropdownMenu'; this.optionsContainer.id = 'blocklySearchableDropdown'; // 选项容器 this.renderOptions(); return this.optionsContainer; } // Rendering filtered options renderOptions() { this.optionsContainer.innerHTML = ''; this.filteredOptions.forEach(([text, value], index) => { const menuItemContent = document.createElement('div'); menuItemContent.className = 'blocklyMenuItemContent goog-menuitem-content'; const checkbox = document.createElement('div'); checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox'+ (this.value_ == value ? ' optionSelected' : ''); const option = document.createElement('div'); option.className = 'blocklyMenuItem blocklySearchOption'; option.textContent = text; option.dataset.value = value; option.addEventListener('click', () => this.onOptionSelected(value)); menuItemContent.appendChild(checkbox); menuItemContent.appendChild(option); this.optionsContainer.appendChild(menuItemContent); }); } onOptionSelected(value) { this.setEditorValue_(value); // background: url(https://blockly-demo.appspot.com/static/media/sprites.png) no-repeat -48px -16px this.hide_(); } /** * Dispose of events belonging to the pitch picker. * @private */ dropdownDispose_() { for (const event of this.boundEvents_) { Blockly.browserEvents.unbind(event); } this.boundEvents_.length = 0; this.optionsContainer = null; } /** * Hide the editor and picker. * @private */ hide_() { Blockly.WidgetDiv.hide(); Blockly.DropDownDiv.hideWithoutAnimation(); } /** * Set the note to match the mouse's position. * @param {!Event} e Mouse move event. */ onMouseMove(e) { // const bBox = this.optionsContainer.getBoundingClientRect(); // const dy = e.clientY - bBox.top; // const note = Blockly.utils.math.clamp(Math.round(13.5 - dy / 7.5), 0, 12); // this.optionsContainer.style.backgroundPosition = -note * 37 + 'px 0'; // this.setEditorValue_(note); } /** * Convert the machine-readable value (0-12) to human-readable text (C3-A4). * @param {number|string} value The provided value. * @returns {string|undefined} The respective pitch, or undefined if invalid. */ valueToNote(value) { // return value; if(this.rules === undefined) return value; for (const [a, b] of this.rules) { if (b === value) { return a; } } return value; } /** * Convert the human-readable text (C3-A4) to machine-readable value (0-12). * @param {string} text The provided pitch. * @returns {number|undefined} The respective value, or undefined if invalid. */ noteToValue(text) { return text } /** * Get the text to be displayed on the field node. * @returns {?string} The HTML value if we're editing, otherwise null. * Null means the super class will handle it, likely a string cast of value. * @protected */ getText_() { if (this.isBeingEdited_) { return super.getText_(); } return this.valueToNote(this.getValue()) || null; } /** * Transform the provided value into a text to show in the HTML input. * @param {*} value The value stored in this field. * @returns {string} The text to show on the HTML input. */ getEditorText_(value) { return this.valueToNote(value); } /** * Transform the text received from the HTML input (note) into a value * to store in this field. * @param {string} text Text received from the HTML input. * @returns {*} The value to store. */ getValueFromEditorText_(text) { return this.noteToValue(text); } /** * Redraw the pitch picker with the current pitch. * @private */ updateGraph_() { if (!this.optionsContainer) { return; } const i = this.getValue(); this.optionsContainer.style.backgroundPosition = -i * 37 + 'px 0'; } /** * Ensure that only a valid value may be entered. * @param {*} opt_newValue The input value. * @returns {*} A valid value, or null if invalid. */ doClassValidation_(opt_newValue) { if (opt_newValue === null || opt_newValue === undefined) { return null; } const note = this.valueToNote(opt_newValue); this.fielterOptions(note); if (note) { return opt_newValue; } return null; } fielterOptions(keyword){ const regex = new RegExp(keyword, 'i'); if(this.rules){ this.filteredOptions = this.rules.filter(([a, b]) => regex.test(a) || regex.test(b)); } if (!this.filteredOptions || !this.filteredOptions.length) { this.filteredOptions = [['No matching data','No matching data']]; } } // Rewrite the processing logic when the input box content changes onHtmlInputChange_(e) { super.onHtmlInputChange_(e); this.fielterOptions(this.htmlInput_.value); this.renderOptions(); } bindEvents_(){ super.bindEvents_(); this.inputWrapper_ = Blockly.browserEvents.conditionalBind(document, 'input', this, function(event) { if(this.htmlInput_){ this.fielterOptions(this.htmlInput_.value); this.renderOptions(); } } ); } } Blockly.fieldRegistry.register('field_grid_dropdown', FieldSearchable); /** * CSS for slider field. */ Blockly.Css.register(` /** Setup grid layout of DropDown */ .blocklyDropDownDiv { z-index: 10000 !important; background-color: white !important; } .blocklySearchOption:hover { background: #f0f0f0; } .optionSelected{ background: url(https://blockly-demo.appspot.com/static/media/sprites.png) no-repeat -48px -16px; margin-top: 6px; } `);