blockly-field-searchable-dropdown
Version:
302 lines (270 loc) • 8.96 kB
JavaScript
/**
* @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;
}
`);