Merge pull request #2389 from BeksOmega/feature/SerializableFields

Added isSerializable and SERIALIZABLE to Field
This commit is contained in:
Rachel Fenichel
2019-04-22 11:05:50 -07:00
committed by GitHub
21 changed files with 583 additions and 132 deletions

File diff suppressed because one or more lines are too long

View File

@@ -172,10 +172,24 @@ Blockly.Field.prototype.clickTarget_ = null;
Blockly.Field.NBSP = '\u00A0';
/**
* Editable fields are saved by the XML renderer, non-editable fields are not.
* Editable fields usually show some sort of UI indicating they are editable.
* They will also be saved by the XML renderer.
* @type {boolean}
* @const
* @default
*/
Blockly.Field.prototype.EDITABLE = true;
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable. This is not the
* case by default so that SERIALIZABLE is backwards compatible.
* @type {boolean}
* @const
* @default
*/
Blockly.Field.prototype.SERIALIZABLE = false;
/**
* Attach this field to a block.
* @param {!Blockly.Block} block The block containing this field.
@@ -268,8 +282,7 @@ Blockly.Field.prototype.updateEditable = function() {
/**
* Check whether this field is currently editable. Some fields are never
* editable (e.g. text labels). Those fields are not serialized to XML. Other
* fields may be editable, and therefore serialized, but may exist on
* EDITABLE (e.g. text labels). Other fields may be EDITABLE but may exist on
* non-editable blocks.
* @return {boolean} Whether this field is editable and on an editable block
*/
@@ -277,6 +290,26 @@ Blockly.Field.prototype.isCurrentlyEditable = function() {
return this.EDITABLE && !!this.sourceBlock_ && this.sourceBlock_.isEditable();
};
/**
* Check whether this field should be serialized by the XML renderer.
* Handles the logic for backwards compatibility and incongruous states.
* @return {boolean} Whether this field should be serialized or not.
*/
Blockly.Field.prototype.isSerializable = function() {
var isSerializable = false;
if (this.name) {
if (this.SERIALIZABLE) {
isSerializable = true;
} else if (this.EDITABLE) {
console.warn('Detected an editable field that was not serializable.' +
' Please define SERIALIZABLE property as true on all editable custom' +
' fields. Proceeding with serialization.');
isSerializable = true;
}
}
return isSerializable;
};
/**
* Gets whether this editable field is visible or not.
* @return {boolean} True if visible.

View File

@@ -66,6 +66,14 @@ Blockly.FieldAngle.fromJson = function(options) {
return new Blockly.FieldAngle(options['angle']);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
* @const
*/
Blockly.FieldAngle.prototype.SERIALIZABLE = true;
/**
* Round angles to the nearest 15 degrees when using mouse.
* Set to 0 to disable rounding.

View File

@@ -58,6 +58,14 @@ Blockly.FieldCheckbox.fromJson = function(options) {
return new Blockly.FieldCheckbox(options['checked'] ? 'TRUE' : 'FALSE');
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
* @const
*/
Blockly.FieldCheckbox.prototype.SERIALIZABLE = true;
/**
* Character for the checkmark.
*/

View File

@@ -61,6 +61,14 @@ Blockly.FieldColour.fromJson = function(options) {
return new Blockly.FieldColour(options['colour']);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
* @const
*/
Blockly.FieldColour.prototype.SERIALIZABLE = true;
/**
* Default width of a colour field.
* @type {number}

View File

@@ -69,6 +69,14 @@ Blockly.FieldDate.fromJson = function(options) {
return new Blockly.FieldDate(options['date']);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
* @const
*/
Blockly.FieldDate.prototype.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates the editor.
*/

View File

@@ -76,6 +76,14 @@ Blockly.FieldDropdown.fromJson = function(options) {
return new Blockly.FieldDropdown(options['options']);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
* @const
*/
Blockly.FieldDropdown.prototype.SERIALIZABLE = true;
/**
* Horizontal distance that a checkmark overhangs the dropdown.
*/

View File

@@ -84,7 +84,10 @@ Blockly.FieldImage.fromJson = function(options) {
};
/**
* Editable fields are saved by the XML renderer, non-editable fields are not.
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
* @const
*/
Blockly.FieldImage.prototype.EDITABLE = false;

View File

@@ -19,7 +19,8 @@
*/
/**
* @fileoverview Non-editable text field. Used for titles, labels, etc.
* @fileoverview Non-editable, non-serializable text field. Used for titles,
* labels, etc.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
@@ -34,7 +35,7 @@ goog.require('goog.math.Size');
/**
* Class for a non-editable field.
* Class for a non-editable, non-serializable text field.
* @param {string} text The initial content of the field.
* @param {string=} opt_class Optional CSS class for the field's text.
* @extends {Blockly.Field}
@@ -62,7 +63,10 @@ Blockly.FieldLabel.fromJson = function(options) {
};
/**
* Editable fields are saved by the XML renderer, non-editable fields are not.
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
* @const
*/
Blockly.FieldLabel.prototype.EDITABLE = false;

View File

@@ -65,6 +65,14 @@ Blockly.FieldNumber.fromJson = function(options) {
options['min'], options['max'], options['precision']);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
* @const
*/
Blockly.FieldNumber.prototype.SERIALIZABLE = true;
/**
* Set the maximum, minimum and precision constraints on this field.
* Any of these properties may be undefiend or NaN to be disabled.

View File

@@ -69,6 +69,14 @@ Blockly.FieldTextInput.fromJson = function(options) {
return field;
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
* @const
*/
Blockly.FieldTextInput.prototype.SERIALIZABLE = true;
/**
* Point size of text. Should match blocklyText's font-size in CSS.
*/

View File

@@ -78,6 +78,14 @@ Blockly.FieldVariable.fromJson = function(options) {
return new Blockly.FieldVariable(varname, null, variableTypes, defaultType);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
* @const
*/
Blockly.FieldVariable.prototype.SERIALIZABLE = true;
/**
* Initialize everything needed to render this field. This includes making sure
* that the field's value is valid.

View File

@@ -140,7 +140,7 @@ Blockly.Xml.fieldToDomVariable_ = function(field) {
* @private
*/
Blockly.Xml.fieldToDom_ = function(field) {
if (field.name && field.EDITABLE) {
if (field.isSerializable()) {
if (field.referencesVariables()) {
return Blockly.Xml.fieldToDomVariable_(field);
} else {

View File

@@ -63,7 +63,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"style": "math_blocks",
},
{
"type": "test_fields_dropdown_long",
"type": "test_dropdowns_long",
"message0": "long: %1",
"args0": [
{
@@ -107,7 +107,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
]
},
{
"type": "test_fields_dropdown_images",
"type": "test_dropdowns_images",
"message0": "%1",
"args0": [
{
@@ -133,7 +133,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
]
},
{
"type": "test_fields_dropdown_images_and_text",
"type": "test_dropdowns_images_and_text",
"message0": "%1",
"args0": [
{
@@ -250,7 +250,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"helpUrl": ""
},
{
"type": "test_fields_number",
"type": "test_numbers_float",
"message0": "float %1",
"args0": [
{
@@ -264,7 +264,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"tooltip": "A number."
},
{
"type": "test_fields_number_whole",
"type": "test_numbers_whole",
"message0": "precision 1 %1",
"args0": [
{
@@ -279,7 +279,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"tooltip": "The number should be rounded to multiples of 1"
},
{
"type": "test_fields_number_hundredths",
"type": "test_numbers_hundredths",
"message0": "precision 0.01 %1",
"args0": [
{
@@ -294,7 +294,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"tooltip": "The number should be rounded to multiples of 0.01"
},
{
"type": "test_fields_number_halves",
"type": "test_numbers_halves",
"message0": "precision 0.5 %1",
"args0": [
{
@@ -309,7 +309,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"tooltip": "The number should be rounded to multiples of 0.5"
},
{
"type": "test_fields_number_three_halves",
"type": "test_numbers_three_halves",
"message0": "precision 1.5 %1",
"args0": [
{
@@ -324,7 +324,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"tooltip": "The number should be rounded to multiples of 1.5"
},
{
"type": "test_fields_integer_bounded",
"type": "test_numbers_whole_bounded",
"message0": "midi note %1",
"args0": [
{
@@ -662,7 +662,7 @@ Blockly.Blocks['test_basic_empty_with_mutator'] = {
}
};
Blockly.Blocks['test_fields_dropdown_dynamic'] = {
Blockly.Blocks['test_dropdowns_dynamic'] = {
init: function() {
var dropdown = new Blockly.FieldDropdown(this.dynamicOptions);
this.appendDummyInput()

View File

@@ -16,5 +16,5 @@
"assertNotUndefined": true
},
"extends": "eslint:recommended"
"extends": "../../.eslintrc.json"
}

View File

@@ -3,18 +3,12 @@
suite('Connections', function() {
suite('Rendered', function() {
function assertAllConnectionsHidden(block) {
assertAllConnectionsHiddenState(block, true);
}
function assertAllConnectionsVisible(block) {
assertAllConnectionsHiddenState(block, false);
}
function assertAllConnectionsHiddenState(block, hidden) {
var connections = block.getConnections_(true);
for (var i = 0; i < connections.length; i++) {
var connection = connections[i];
if (connection.type == Blockly.PREVIOUS_STATEMENT
|| connection.type == Blockly.OUTPUT_VALUE) {
|| connection.type == Blockly.OUTPUT_VALUE) {
// Only superior connections on inputs get hidden
continue;
}
@@ -22,9 +16,15 @@ suite('Connections', function() {
// The next connection is not hidden when collapsed
continue;
}
assertEquals('Connection ' + i + ' failed', hidden, connections[i].hidden_)
assertEquals('Connection ' + i + ' failed', hidden, connections[i].hidden_);
}
}
function assertAllConnectionsHidden(block) {
assertAllConnectionsHiddenState(block, true);
}
function assertAllConnectionsVisible(block) {
assertAllConnectionsHiddenState(block, false);
}
setup(function() {
Blockly.defineBlocksWithJsonArray([{
@@ -83,7 +83,7 @@ suite('Connections', function() {
blockA.setCollapsed(true);
assertEquals(blockA, blockB.getParent());
assertNull(blockC.getParent())
assertNull(blockC.getParent());
assertTrue(blockA.isCollapsed());
assertAllConnectionsHidden(blockA);
assertAllConnectionsHidden(blockB);
@@ -180,7 +180,7 @@ suite('Connections', function() {
blockA.setCollapsed(true);
assertEquals(blockA, blockB.getParent());
assertNull(blockC.getParent())
assertNull(blockC.getParent());
assertTrue(blockA.isCollapsed());
assertAllConnectionsHidden(blockA);
assertAllConnectionsHidden(blockB);
@@ -286,7 +286,7 @@ suite('Connections', function() {
blockA.setCollapsed(true);
assertEquals(blockA, blockB.getParent());
assertNull(blockC.getParent())
assertNull(blockC.getParent());
assertTrue(blockA.isCollapsed());
assertAllConnectionsHidden(blockA);
assertAllConnectionsHidden(blockB);
@@ -307,7 +307,7 @@ suite('Connections', function() {
var shadowBlock = connection.targetBlock();
assertTrue(shadowBlock.isShadow());
assertAllConnectionsHidden(shadowBlock);
})
});
test('Reveal shadow value', function() {
var blocks = this.blocks;
@@ -316,7 +316,7 @@ suite('Connections', function() {
var shadowBlock = connection.targetBlock();
assertTrue(shadowBlock.isShadow());
assertAllConnectionsHidden(shadowBlock);
})
});
});
});
});

View File

@@ -32,7 +32,7 @@ suite('Events', function() {
function checkExactEventValues(event, values) {
var keys = Object.keys(values);
for (var i = 0; i < keys.length; i++) {
var field = keys[i]
var field = keys[i];
assertEquals(values[field], event[field]);
}
}

73
tests/mocha/field_test.js Normal file
View File

@@ -0,0 +1,73 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2019 Google Inc.
* https://developers.google.com/blockly/
*
* 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.
*/
suite ('Abstract Fields', function() {
suite ('Is Serializable', function() {
// Both EDITABLE and SERIALIZABLE are default.
function FieldDefault() {
this.name = 'NAME';
}
FieldDefault.prototype = Object.create(Blockly.Field.prototype);
// EDITABLE is false and SERIALIZABLE is default.
function FieldFalseDefault() {
this.name = 'NAME';
}
FieldFalseDefault.prototype = Object.create(Blockly.Field.prototype);
FieldFalseDefault.prototype.EDITABLE = false;
// EDITABLE is default and SERIALIZABLE is true.
function FieldDefaultTrue() {
this.name = 'NAME';
}
FieldDefaultTrue.prototype = Object.create(Blockly.Field.prototype);
FieldDefaultTrue.prototype.SERIALIZABLE = true;
// EDITABLE is false and SERIALIZABLE is true.
function FieldFalseTrue() {
this.name = 'NAME';
}
FieldFalseTrue.prototype = Object.create(Blockly.Field.prototype);
FieldFalseTrue.prototype.EDITABLE = false;
FieldFalseTrue.prototype.SERIALIZABLE = true;
/* Test Backwards Compatibility*/
test('Editable Default(true), Serializable Default(false)', function() {
// An old default field should be serialized.
var field = new FieldDefault();
console.log('You should receive a warning after this message');
assertEquals(true, field.isSerializable());
});
test('Editable False, Serializable Default(false)', function() {
// An old non-editable field should not be serialized.
var field = new FieldFalseDefault();
assertEquals(false, field.isSerializable());
});
/* Test Other Cases */
test('Editable Default(true), Serializable True', function() {
// A field that is both editable and serializable should be serialized.
var field = new FieldDefaultTrue();
assertEquals(true, field.isSerializable());
});
test('Editable False, Serializable True', function() {
// A field that is not editable, but overrides serializable to true
// should be serialized (e.g. field_label_serializable)
var field = new FieldFalseTrue();
assertEquals(true, field.isSerializable());
});
});
});

View File

@@ -22,7 +22,9 @@
<script src="block_test.js"></script>
<script src="event_test.js"></script>
<script src="connection_test.js"></script>
<script src="field_test.js"></script>
<script src="field_variable_test.js"></script>
<script src="xml_test.js"></script>
<script src="utils_test.js"></script>
<div id="blocklyDiv"></div>

273
tests/mocha/xml_test.js Normal file
View File

@@ -0,0 +1,273 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2019 Google Inc.
* https://developers.google.com/blockly/
*
* 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.
*/
suite('XML', function() {
setup(function() {
this.workspace = new Blockly.Workspace();
});
teardown(function() {
this.workspace.dispose();
});
var assertSimpleField = function(fieldDom, name, text) {
assertEquals(text, fieldDom.textContent);
assertEquals(name, fieldDom.getAttribute('name'));
};
var assertNonSerializingField = function(fieldDom) {
assertEquals(undefined, fieldDom.childNodes[0]);
};
var assertVariableField = function(fieldDom, name, type, id, text) {
assertEquals(name, fieldDom.getAttribute('name'));
assertEquals(type, fieldDom.getAttribute('variabletype'));
assertEquals(id, fieldDom.getAttribute('id'));
assertEquals(text, fieldDom.textContent);
};
suite('Serialization', function() {
suite('Fields', function() {
test('Angle', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_angle_test_block",
"message0": "%1",
"args0": [
{
"type": "field_angle",
"name": "ANGLE",
"angle": 90
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_angle_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
assertSimpleField(resultFieldDom, 'ANGLE', '90');
delete Blockly.Blocks['field_angle_test_block'];
});
test('Checkbox', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_checkbox_test_block",
"message0": "%1",
"args0": [
{
"type": "field_checkbox",
"name": "CHECKBOX",
"checked": true
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_checkbox_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
assertSimpleField(resultFieldDom, 'CHECKBOX', 'TRUE');
delete Blockly.Blocks['field_checkbox_test_block'];
});
test('Colour', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_colour_test_block",
"message0": "%1",
"args0": [
{
"type": "field_colour",
"name": "COLOUR",
"colour": '#000099'
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_colour_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
assertSimpleField(resultFieldDom, 'COLOUR', '#000099');
delete Blockly.Blocks['field_colour_test_block'];
});
test('Date', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_date_test_block",
"message0": "%1",
"args0": [
{
"type": "field_date",
"name": "DATE",
"date": "2020-02-20"
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_date_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
assertSimpleField(resultFieldDom, 'DATE', '2020-02-20');
delete Blockly.Blocks['field_date_test_block'];
});
test('Dropdown', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_dropdown_test_block",
"message0": "%1",
"args0": [
{
"type": "field_dropdown",
"name": "DROPDOWN",
"options": [
[
"a",
"A"
],
[
"b",
"B"
],
[
"c",
"C"
]
]
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_dropdown_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
assertSimpleField(resultFieldDom, 'DROPDOWN', 'A');
delete Blockly.Blocks['field_dropdown_test_block'];
});
test('Image', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_image_test_block",
"message0": "%1",
"args0": [
{
"type": "field_image",
"name": "IMAGE",
"src": "https://blockly-demo.appspot.com/static/tests/media/a.png",
"width": 32,
"height": 32,
"alt": "A"
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_image_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block);
assertNonSerializingField(resultFieldDom);
delete Blockly.Blocks['field_image_test_block'];
});
test('Label', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_label_test_block",
"message0": "%1",
"args0": [
{
"type": "field_label",
"name": "LABEL",
"text": "default"
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_label_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block);
assertNonSerializingField(resultFieldDom);
delete Blockly.Blocks['field_label_test_block'];
});
test('Number', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_number_test_block",
"message0": "%1",
"args0": [
{
"type": "field_number",
"name": "NUMBER",
"value": 97
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_number_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
assertSimpleField(resultFieldDom, 'NUMBER', '97');
delete Blockly.Blocks['field_number_test_block'];
});
test('Text Input', function() {
Blockly.defineBlocksWithJsonArray([{
"type": "field_text_input_test_block",
"message0": "%1",
"args0": [
{
"type": "field_input",
"name": "TEXT",
"text": "default"
}
],
}]);
var block = new Blockly.Block(this.workspace,
'field_text_input_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
assertSimpleField(resultFieldDom, 'TEXT', 'default');
});
suite('Variable Fields', function() {
setup(function() {
Blockly.defineBlocksWithJsonArray([{
'type': 'field_variable_test_block',
'message0': '%1',
'args0': [
{
'type': 'field_variable',
'name': 'VAR',
'variable': 'item'
}
]
}]);
});
teardown(function() {
delete Blockly.Blocks['field_variable_test_block'];
});
test('Variable Trivial', function() {
this.workspace.createVariable('name1', '', 'id1');
var block = new Blockly.Block(this.workspace,
'field_variable_test_block');
block.inputList[0].fieldRow[0].setValue('id1');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
assertVariableField(resultFieldDom, 'VAR', '', 'id1', 'name1');
});
test('Variable Default Case', function() {
var cacheGenUid = Blockly.utils.genUid;
Blockly.utils.genUid = function() {
return '1';
};
try {
this.workspace.createVariable('name1');
Blockly.Events.disable();
var block = new Blockly.Block(this.workspace,
'field_variable_test_block');
block.inputList[0].fieldRow[0].setValue('1');
Blockly.Events.enable();
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
// Expect type is '' and id is '1' since we don't specify type and id.
assertVariableField(resultFieldDom, 'VAR', '', '1', 'name1');
} finally {
Blockly.utils.genUid = cacheGenUid;
}
});
});
});
});
suite('Deserialization', function() {
});
});

View File

@@ -1192,41 +1192,63 @@ h1 {
</statement>
</block>
</category>
<category name="Fields">
<label text="Numbers"></label>
<block type="test_fields_number">
<field name="NUM">123.456</field>
</block>
<block type="test_fields_number_hundredths">
<field name="NUM">123.456</field>
</block>
<block type="test_fields_number_halves">
<field name="NUM">123.456</field>
</block>
<block type="test_fields_number_whole">
<field name="NUM">123.456</field>
</block>
<block type="test_fields_number_three_halves">
<field name="NUM">123.456</field>
</block>
<block type="test_fields_integer_bounded">
<field name="NOTE">60</field>
</block>
<label text="Drop-downs"></label>
<block type="test_fields_dropdown_long"></block>
<block type="test_fields_dropdown_images"></block>
<block type="test_fields_dropdown_images_and_text"></block>
<label text="Dynamic Drop-downs"></label>
<block type="test_fields_dropdown_dynamic"></block>
<button text="Add option" callbackKey="addDynamicOption"></button>
<button text="Remove option" callbackKey="removeDynamicOption"></button>
<label text="Others"></label>
<block type="test_fields_angle"></block>
<block type="test_fields_date"></block>
<block type="test_fields_text_input"></block>
<block type="test_fields_checkbox"></block>
<block type="test_fields_colour"></block>
<block type="test_fields_variable"></block>
<category name="Fields" expanded="true">
<category name="Defaults">
<block type="test_fields_angle"></block>
<block type="test_fields_date"></block>
<block type="test_fields_checkbox"></block>
<block type="test_fields_colour"></block>
<block type="test_fields_text_input"></block>
<block type="test_fields_variable"></block>
</category>
<category name="Numbers">
<block type="test_numbers_float">
<field name="NUM">123.456</field>
</block>
<block type="test_numbers_hundredths">
<field name="NUM">123.456</field>
</block>
<block type="test_numbers_halves">
<field name="NUM">123.456</field>
</block>
<block type="test_numbers_whole">
<field name="NUM">123.456</field>
</block>
<block type="test_numbers_three_halves">
<field name="NUM">123.456</field>
</block>
<block type="test_numbers_whole_bounded">
<field name="NOTE">60</field>
</block>
</category>
<category name="Drop-downs">
<label text="Dynamic"></label>
<block type="test_dropdowns_dynamic"></block>
<button text="Add option" callbackKey="addDynamicOption"></button>
<button text="Remove option" callbackKey="removeDynamicOption"></button>
<label text="Other"></label>
<block type="test_dropdowns_long"></block>
<block type="test_dropdowns_images"></block>
<block type="test_dropdowns_images_and_text"></block>
</category>
<category name="Images">
<block type="test_images_datauri"></block>
<block type="test_images_small"></block>
<block type="test_images_large"></block>
<block type="test_images_fliprtl"></block>
<block type="test_images_missing"></block>
<block type="test_images_many_icons"></block>
</category>
<category name="Emoji! o((*>ᴗ<*))o">
<label text="Unicode & Emojis"></label>
<block type="test_style_emoji"></block>
<block type="text">
<field name="TEXT">Robot face in text field: &#x1f916;</field>
</block>
<block type="text">
<field name="TEXT">Zalgo in text field: B&#776;&#788;&#862;&#795;&#842;&#827;&#806;&#837;&#812;&#792;&#816;&#846;&#805;l&#771;&#832;&#833;&#864;&#849;&#849;&#789;&#861;&#801;&#854;&#863;&#811;&#826;&#812;&#790;&#803;&#819;o&#843;&#777;&#778;&#785;&#831;&#829;&#794;&#825;&#857;&#814;&#802;&#811;&#852;c&#843;&#786;&#849;&#778;&#861;&#775;&#825;&#825;&#796;&#857;&#825;&#800;&#824;k&#778;&#850;&#833;&#774;&#772;&#782;&#862;&#770;&#789;&#788;&#841;&#801;&#811;&#860;&#839;&#790;&#819;&#854;l&#832;&#774;&#836;&#831;&#776;&#787;&#855;&#816;&#793;&#798;&#819;&#809;&#800;&#854;&#815;y&#864;&#783;&#856;&#773;&#832;&#808;&#799;&#839;&#814;&#840;&#812;&#793;&#818;&#801;</field>
</block>
</category>
</category>
<category name="Mutators">
<label text="logic_compare"></label>
@@ -1266,22 +1288,6 @@ h1 {
<block type="test_style_hex4"></block>
<block type="test_style_hex5"></block>
</category>
<category name="Images">
<block type="test_images_datauri"></block>
<block type="test_images_small"></block>
<block type="test_images_large"></block>
<block type="test_images_fliprtl"></block>
<block type="test_images_missing"></block>
<block type="test_images_many_icons"></block>
<label text="Unicode & Emojis"></label>
<block type="test_style_emoji"></block>
<block type="text">
<field name="TEXT">Robot face in text field: &#x1f916;</field>
</block>
<block type="text">
<field name="TEXT">Zalgo in text field: B&#776;&#788;&#862;&#795;&#842;&#827;&#806;&#837;&#812;&#792;&#816;&#846;&#805;l&#771;&#832;&#833;&#864;&#849;&#849;&#789;&#861;&#801;&#854;&#863;&#811;&#826;&#812;&#790;&#803;&#819;o&#843;&#777;&#778;&#785;&#831;&#829;&#794;&#825;&#857;&#814;&#802;&#811;&#852;c&#843;&#786;&#849;&#778;&#861;&#775;&#825;&#825;&#796;&#857;&#825;&#800;&#824;k&#778;&#850;&#833;&#774;&#772;&#782;&#862;&#770;&#789;&#788;&#841;&#801;&#811;&#860;&#839;&#790;&#819;&#854;l&#832;&#774;&#836;&#831;&#776;&#787;&#855;&#816;&#793;&#798;&#819;&#809;&#800;&#854;&#815;y&#864;&#783;&#856;&#773;&#832;&#808;&#799;&#839;&#814;&#840;&#812;&#793;&#818;&#801;</field>
</block>
</category>
</xml>
</body>
</html>