From e2df0fc2885cb994886c0af863035f50bb849948 Mon Sep 17 00:00:00 2001 From: Mike Harvey <43474485+mikeharv@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:56:36 -0400 Subject: [PATCH 1/8] fix: correct dropdown arrow placement for Thrasos on Safari (#8017) --- core/field_dropdown.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 6fb779c4f..d92d02ec2 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -229,6 +229,9 @@ export class FieldDropdown extends Field { : ' ' + FieldDropdown.ARROW_CHAR, ), ); + if (this.getConstants()!.FIELD_TEXT_BASELINE_CENTER) { + this.arrow.setAttribute('dominant-baseline', 'central'); + } if (this.getSourceBlock()?.RTL) { this.getTextElement().insertBefore(this.arrow, this.textContent_); } else { From 5a5184ab4fa911b7826d5b424716b972215b1740 Mon Sep 17 00:00:00 2001 From: Neil Fraser Date: Fri, 26 Apr 2024 20:23:43 +0200 Subject: [PATCH 2/8] fix: Correct list's "find" and "get" blocks help. (#8041) * Correct list's "find" and "get" blocks help. Both blocks were using the same help URL. Two unrelated results of recompiling messages: * Allow unwanted 'qqq' messages to be reverted. * 'constants.json' is unchanged, but sorted. Also improve python json dumping. --- msg/json/constants.json | 13 ++++++++++++- msg/json/en.json | 5 +++-- msg/json/qqq.json | 15 ++++++++------- msg/json/synonyms.json | 23 ++++++++++++++++++++++- msg/messages.js | 10 ++++++---- scripts/i18n/create_messages.py | 7 +++++-- scripts/i18n/js_to_json.py | 19 ++++++------------- 7 files changed, 62 insertions(+), 30 deletions(-) diff --git a/msg/json/constants.json b/msg/json/constants.json index f2801d355..2677e406f 100644 --- a/msg/json/constants.json +++ b/msg/json/constants.json @@ -1 +1,12 @@ -{"MATH_HUE": "230", "LOOPS_HUE": "120", "LISTS_HUE": "260", "LOGIC_HUE": "210", "VARIABLES_HUE": "330", "TEXTS_HUE": "160", "PROCEDURES_HUE": "290", "COLOUR_HUE": "20", "VARIABLES_DYNAMIC_HUE": "310"} \ No newline at end of file +{ + "#": "Automatically generated, do not edit this file!", + "COLOUR_HUE": "20", + "LISTS_HUE": "260", + "LOGIC_HUE": "210", + "LOOPS_HUE": "120", + "MATH_HUE": "230", + "PROCEDURES_HUE": "290", + "TEXTS_HUE": "160", + "VARIABLES_DYNAMIC_HUE": "310", + "VARIABLES_HUE": "330" +} \ No newline at end of file diff --git a/msg/json/en.json b/msg/json/en.json index d9bddec31..38e53c8ad 100644 --- a/msg/json/en.json +++ b/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2023-12-08 18:42:04.679586", + "lastupdated": "2024-04-16 23:19:53.668551", "locale": "en", "messagedocumentation" : "qqq" }, @@ -291,10 +291,11 @@ "LISTS_ISEMPTY_TITLE": "%1 is empty", "LISTS_ISEMPTY_TOOLTIP": "Returns true if the list is empty.", "LISTS_INLIST": "in list", - "LISTS_INDEX_OF_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", + "LISTS_INDEX_OF_HELPURL": "https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list", "LISTS_INDEX_OF_FIRST": "find first occurrence of item", "LISTS_INDEX_OF_LAST": "find last occurrence of item", "LISTS_INDEX_OF_TOOLTIP": "Returns the index of the first/last occurrence of the item in the list. Returns %1 if item is not found.", + "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "get", "LISTS_GET_INDEX_GET_REMOVE": "get and remove", "LISTS_GET_INDEX_REMOVE": "remove", diff --git a/msg/json/qqq.json b/msg/json/qqq.json index 60509d798..e11c65f5d 100644 --- a/msg/json/qqq.json +++ b/msg/json/qqq.json @@ -53,7 +53,7 @@ "COLOUR_RANDOM_HELPURL": "{{Optional}} url - A link that displays a random colour each time you visit it.", "COLOUR_RANDOM_TITLE": "block text - Title of block that generates a colour at random.", "COLOUR_RANDOM_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Colour#generating-a-random-colour https://github.com/google/blockly/wiki/Colour#generating-a-random-colour].", - "COLOUR_RGB_HELPURL": "{{Ignored}} url - A link for colour codes with percentages (0-100%) for each component, instead of the more common 0-255, which may be more difficult for beginners.", + "COLOUR_RGB_HELPURL": "{{Optional}} url - A link for colour codes with percentages (0-100%) for each component, instead of the more common 0-255, which may be more difficult for beginners.", "COLOUR_RGB_TITLE": "block text - Title of block for [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", "COLOUR_RGB_RED": "block input text - The amount of red (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Red}}", "COLOUR_RGB_GREEN": "block input text - The amount of green (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", @@ -250,7 +250,7 @@ "TEXT_GET_SUBSTRING_END_FROM_START": "dropdown - Indicates that the following number specifies the position (relative to the start position) of the end of the region of text that should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", "TEXT_GET_SUBSTRING_END_FROM_END": "dropdown - Indicates that the following number specifies the position (relative to the end position) of the end of the region of text that should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", "TEXT_GET_SUBSTRING_END_LAST": "block text - Indicates that a region ending with the last letter of the preceding piece of text should be extracted. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}}\nblock text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text extracting a region of text]. In most languages, this will be the empty string. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text extracting a region of text]. In most languages, this will be the empty string. [[File:Blockly-get-substring.png]]", "TEXT_CHANGECASE_HELPURL": "{{Optional}} url - Information about the case of letters (upper-case and lower-case).", "TEXT_CHANGECASE_TOOLTIP": "tooltip - Describes a block to adjust the case of letters. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case].", "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "block text - Indicates that all of the letters in the following piece of text should be capitalized. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case].", @@ -297,10 +297,11 @@ "LISTS_ISEMPTY_TITLE": "block text - See [https://github.com/google/blockly/wiki/Lists#is-empty https://github.com/google/blockly/wiki/Lists#is-empty]. \n\nParameters:\n* %1 - the list to test", "LISTS_ISEMPTY_TOOLTIP": "block tooltip - See [https://github.com/google/blockly/wiki/Lists#is-empty https://github.com/google/blockly/wiki/Lists#is-empty].", "LISTS_INLIST": "block text - Title of blocks operating on [https://github.com/google/blockly/wiki/Lists lists].", - "LISTS_INDEX_OF_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list].", + "LISTS_INDEX_OF_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list].", "LISTS_INDEX_OF_FIRST": "dropdown - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", "LISTS_INDEX_OF_LAST": "dropdown - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", "LISTS_INDEX_OF_TOOLTIP": "tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", + "LISTS_GET_INDEX_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list].", "LISTS_GET_INDEX_GET": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item get an item from a list] without removing it from the list.", "LISTS_GET_INDEX_GET_REMOVE": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item get and remove an item from a list], as opposed to merely getting it without modifying the list.", "LISTS_GET_INDEX_REMOVE": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#removing-an-item remove an item from a list].\n{{Identical|Remove}}", @@ -309,7 +310,7 @@ "LISTS_GET_INDEX_FIRST": "dropdown - Indicates that the '''first''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", "LISTS_GET_INDEX_LAST": "dropdown - Indicates that the '''last''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", "LISTS_GET_INDEX_RANDOM": "dropdown - Indicates that a '''random''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", - "LISTS_GET_INDEX_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}}\n\nblock text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessing an item from a list]. In most languages, this will be the empty string. [[File:Blockly-list-get-item.png]]", + "LISTS_GET_INDEX_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessing an item from a list]. In most languages, this will be the empty string. [[File:Blockly-list-get-item.png]]", "LISTS_INDEX_FROM_START_TOOLTIP": "tooltip - Indicates the ordinal number that the first item in a list is referenced by. %1 will be replaced by either '#0' or '#1' depending on the indexing mode.", "LISTS_INDEX_FROM_END_TOOLTIP": "tooltip - Indicates the ordinal number that the last item in a list is referenced by. %1 will be replaced by either '#0' or '#1' depending on the indexing mode.", "LISTS_GET_INDEX_TOOLTIP_GET_FROM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information.", @@ -343,7 +344,7 @@ "LISTS_GET_SUBLIST_END_FROM_START": "dropdown - Indicates that an index relative to the front of the list should be used to specify the end of the range from which to [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]]", "LISTS_GET_SUBLIST_END_FROM_END": "dropdown - Indicates that an index relative to the end of the list should be used to specify the end of the range from which to [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]]", "LISTS_GET_SUBLIST_END_LAST": "dropdown - Indicates that the '''last''' item in the given list should be [https://github.com/google/blockly/wiki/Lists#getting-a-sublist the end of the selected sublist]. [[File:Blockly-get-sublist.png]]", - "LISTS_GET_SUBLIST_TAIL": "{{Optional}}\nblock text - This appears in the rightmost position ('tail') of the sublist block, as described at [https://github.com/google/blockly/wiki/Lists#getting-a-sublist https://github.com/google/blockly/wiki/Lists#getting-a-sublist]. In English and most other languages, this is the empty string. [[File:Blockly-get-sublist.png]]", + "LISTS_GET_SUBLIST_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - This appears in the rightmost position ('tail') of the sublist block, as described at [https://github.com/google/blockly/wiki/Lists#getting-a-sublist https://github.com/google/blockly/wiki/Lists#getting-a-sublist]. In English and most other languages, this is the empty string. [[File:Blockly-get-sublist.png]]", "LISTS_GET_SUBLIST_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-sublist https://github.com/google/blockly/wiki/Lists#getting-a-sublist] for more information. [[File:Blockly-get-sublist.png]]", "LISTS_SORT_HELPURL": "{{Optional}} url - Information describing sorting a list.", "LISTS_SORT_TITLE": "Sort as type %1 (numeric or alphabetic) in order %2 (ascending or descending) a list of items %3.\n{{Identical|Sort}}", @@ -362,7 +363,7 @@ "LISTS_REVERSE_HELPURL": "{{Optional}} url - Information describing reversing a list.", "LISTS_REVERSE_MESSAGE0": "block text - Title of block that returns a copy of a list (%1) with the order of items reversed.", "LISTS_REVERSE_TOOLTIP": "tooltip - Short description for a block that reverses a copy of a list.", - "ORDINAL_NUMBER_SUFFIX": "{{Optional}}\ngrammar - Text that follows an ordinal number (a number that indicates position relative to other numbers). In most languages, such text appears before the number, so this should be blank. An exception is Hungarian. See [[Translating:Blockly#Ordinal_numbers]] for more information.", + "ORDINAL_NUMBER_SUFFIX": "{{Optional|Supply translation only if your language requires it. Most do not.}} grammar - Text that follows an ordinal number (a number that indicates position relative to other numbers). In most languages, such text appears before the number, so this should be blank. An exception is Hungarian. See [[Translating:Blockly#Ordinal_numbers]] for more information.", "VARIABLES_GET_HELPURL": "{{Optional}} url - Information about ''variables'' in computer programming. Consider using your language's translation of [https://en.wikipedia.org/wiki/Variable_(computer_science) https://en.wikipedia.org/wiki/Variable_(computer_science)], if it exists.", "VARIABLES_GET_TOOLTIP": "tooltip - This gets the value of the named variable without modifying it.", "VARIABLES_GET_CREATE_SET": "context menu - Selecting this creates a block to set (change) the value of this variable. \n\nParameters:\n* %1 - the name of the variable.", @@ -375,7 +376,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "default name - This acts as a placeholder for the name of a function on a function definition block, as shown on [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#w7cfju this block]. The user will replace it with the function's name.", "PROCEDURES_BEFORE_PARAMS": "block text - This precedes the list of parameters on a function's definition block. See [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function with parameters].", "PROCEDURES_CALL_BEFORE_PARAMS": "block text - This precedes the list of parameters on a function's caller block. See [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function with parameters].", - "PROCEDURES_DEFNORETURN_DO": "{{Optional}}\nblock text - This appears next to the function's 'body', the blocks that should be run when the function is called, as shown in [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function definition].", + "PROCEDURES_DEFNORETURN_DO": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - This appears next to the function's 'body', the blocks that should be run when the function is called, as shown in [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function definition].", "PROCEDURES_DEFNORETURN_TOOLTIP": "tooltip", "PROCEDURES_DEFNORETURN_COMMENT": "Placeholder text that the user is encouraged to replace with a description of what their function does.", "PROCEDURES_DEFRETURN_HELPURL": "{{Optional}} url - Information about defining [https://en.wikipedia.org/wiki/Subroutine functions] that have return values.", diff --git a/msg/json/synonyms.json b/msg/json/synonyms.json index d7a05c3ab..9fc089ebe 100644 --- a/msg/json/synonyms.json +++ b/msg/json/synonyms.json @@ -1 +1,22 @@ -{"CONTROLS_FOREACH_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_FOR_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_IF_ELSEIF_TITLE_ELSEIF": "CONTROLS_IF_MSG_ELSEIF", "CONTROLS_IF_ELSE_TITLE_ELSE": "CONTROLS_IF_MSG_ELSE", "CONTROLS_IF_IF_TITLE_IF": "CONTROLS_IF_MSG_IF", "CONTROLS_IF_MSG_THEN": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_WHILEUNTIL_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "LISTS_CREATE_WITH_ITEM_TITLE": "VARIABLES_DEFAULT_NAME", "LISTS_GET_INDEX_HELPURL": "LISTS_INDEX_OF_HELPURL", "LISTS_GET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_GET_SUBLIST_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_INDEX_OF_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_SET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", "MATH_CHANGE_TITLE_ITEM": "VARIABLES_DEFAULT_NAME", "PROCEDURES_DEFRETURN_COMMENT": "PROCEDURES_DEFNORETURN_COMMENT", "PROCEDURES_DEFRETURN_DO": "PROCEDURES_DEFNORETURN_DO", "PROCEDURES_DEFRETURN_PROCEDURE": "PROCEDURES_DEFNORETURN_PROCEDURE", "PROCEDURES_DEFRETURN_TITLE": "PROCEDURES_DEFNORETURN_TITLE", "TEXT_APPEND_VARIABLE": "VARIABLES_DEFAULT_NAME", "TEXT_CREATE_JOIN_ITEM_TITLE_ITEM": "VARIABLES_DEFAULT_NAME"} \ No newline at end of file +{ + "#": "Automatically generated, do not edit this file!", + "CONTROLS_FOREACH_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", + "CONTROLS_FOR_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", + "CONTROLS_IF_ELSEIF_TITLE_ELSEIF": "CONTROLS_IF_MSG_ELSEIF", + "CONTROLS_IF_ELSE_TITLE_ELSE": "CONTROLS_IF_MSG_ELSE", + "CONTROLS_IF_IF_TITLE_IF": "CONTROLS_IF_MSG_IF", + "CONTROLS_IF_MSG_THEN": "CONTROLS_REPEAT_INPUT_DO", + "CONTROLS_WHILEUNTIL_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", + "LISTS_CREATE_WITH_ITEM_TITLE": "VARIABLES_DEFAULT_NAME", + "LISTS_GET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", + "LISTS_GET_SUBLIST_INPUT_IN_LIST": "LISTS_INLIST", + "LISTS_INDEX_OF_INPUT_IN_LIST": "LISTS_INLIST", + "LISTS_SET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", + "MATH_CHANGE_TITLE_ITEM": "VARIABLES_DEFAULT_NAME", + "PROCEDURES_DEFRETURN_COMMENT": "PROCEDURES_DEFNORETURN_COMMENT", + "PROCEDURES_DEFRETURN_DO": "PROCEDURES_DEFNORETURN_DO", + "PROCEDURES_DEFRETURN_PROCEDURE": "PROCEDURES_DEFNORETURN_PROCEDURE", + "PROCEDURES_DEFRETURN_TITLE": "PROCEDURES_DEFNORETURN_TITLE", + "TEXT_APPEND_VARIABLE": "VARIABLES_DEFAULT_NAME", + "TEXT_CREATE_JOIN_ITEM_TITLE_ITEM": "VARIABLES_DEFAULT_NAME" +} \ No newline at end of file diff --git a/msg/messages.js b/msg/messages.js index a313b269a..153df1565 100644 --- a/msg/messages.js +++ b/msg/messages.js @@ -1154,9 +1154,9 @@ Blockly.Msg.LISTS_ISEMPTY_TOOLTIP = 'Returns true if the list is empty.'; Blockly.Msg.LISTS_INLIST = 'in list'; /** @type {string} */ -/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list -/// https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list]. -Blockly.Msg.LISTS_INDEX_OF_HELPURL = 'https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list'; +/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list +/// https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. +Blockly.Msg.LISTS_INDEX_OF_HELPURL = 'https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list'; /** @type {string} */ Blockly.Msg.LISTS_INDEX_OF_INPUT_IN_LIST = Blockly.Msg.LISTS_INLIST; /** @type {string} */ @@ -1176,7 +1176,9 @@ Blockly.Msg.LISTS_INDEX_OF_LAST = 'find last occurrence of item'; Blockly.Msg.LISTS_INDEX_OF_TOOLTIP = 'Returns the index of the first/last occurrence of the item in the list. Returns %1 if item is not found.'; /** @type {string} */ -Blockly.Msg.LISTS_GET_INDEX_HELPURL = Blockly.Msg.LISTS_INDEX_OF_HELPURL; +/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list +/// https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list]. +Blockly.Msg.LISTS_GET_INDEX_HELPURL = 'https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list'; /** @type {string} */ /// dropdown - Indicates that the user wishes to /// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item diff --git a/scripts/i18n/create_messages.py b/scripts/i18n/create_messages.py index d0d739734..3d22a2342 100755 --- a/scripts/i18n/create_messages.py +++ b/scripts/i18n/create_messages.py @@ -37,12 +37,13 @@ def string_is_ascii(s): def load_constants(filename): """Read in constants file, which must be output in every language.""" constant_defs = read_json_file(filename) + if '#' in constant_defs: # Delete any comment. + del constant_defs['#'] constants_text = '\n' for key in constant_defs: value = constant_defs[key] value = value.replace('"', '\\"') - constants_text += u'\nBlockly.Msg["{0}"] = \"{1}\";'.format( - key, value) + constants_text += u'\nBlockly.Msg["{0}"] = \"{1}\";'.format(key, value) return constants_text def main(): @@ -86,6 +87,8 @@ def main(): # Read in synonyms file, which must be output in every language. synonym_defs = read_json_file(os.path.join( os.curdir, args.source_synonym_file)) + if '#' in synonym_defs: # Delete any comment. + del synonym_defs['#'] # synonym_defs is also being sorted to ensure the same order is kept synonym_text = '\n'.join([u'Blockly.Msg["{0}"] = Blockly.Msg["{1}"];' diff --git a/scripts/i18n/js_to_json.py b/scripts/i18n/js_to_json.py index 53651935e..18daf1853 100755 --- a/scripts/i18n/js_to_json.py +++ b/scripts/i18n/js_to_json.py @@ -114,29 +114,22 @@ def main(): write_files(args.author, args.lang, args.output_dir, results, False) # Create synonyms.json. - synonyms_sorted = sort_dict(synonyms) synonym_file_name = os.path.join(os.curdir, args.output_dir, 'synonyms.json') + synonyms['#'] = 'Automatically generated, do not edit this file!' with open(synonym_file_name, 'w') as outfile: - json.dump(synonyms_sorted, outfile) + json.dump(synonyms, outfile, indent=2, sort_keys=True) if not args.quiet: print("Wrote {0} synonym pairs to {1}.".format( - len(synonyms_sorted), synonym_file_name)) + len(synonyms) - 1, synonym_file_name)) # Create constants.json - constants_sorted = sort_dict(constants) constants_file_name = os.path.join(os.curdir, args.output_dir, 'constants.json') + constants['#'] = 'Automatically generated, do not edit this file!' with open(constants_file_name, 'w') as outfile: - json.dump(constants_sorted, outfile) + json.dump(constants, outfile, indent=2, sort_keys=True) if not args.quiet: print("Wrote {0} constant pairs to {1}.".format( - len(constants_sorted), synonym_file_name)) - -def sort_dict(unsorted_dict): - # Sort the dictionary (thereby enabling better diffing of changes). - myKeys = list(unsorted_dict.keys()) - myKeys.sort() - sorted_dict = {i: unsorted_dict[i] for i in myKeys} - return sorted_dict + len(constants) - 1, synonym_file_name)) if __name__ == '__main__': main() From da97e782c40d52c86d50d7d67120e637e2469c11 Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Mon, 29 Apr 2024 16:41:09 -0700 Subject: [PATCH 3/8] feat: add block factory export in json (#8051) --- demos/blockfactory/app_controller.js | 14 +++++++++++ .../blockfactory/block_library_controller.js | 23 +++++++++++++++++++ demos/blockfactory/index.html | 3 +++ 3 files changed, 40 insertions(+) diff --git a/demos/blockfactory/app_controller.js b/demos/blockfactory/app_controller.js index 5698033fe..fcfa2296d 100644 --- a/demos/blockfactory/app_controller.js +++ b/demos/blockfactory/app_controller.js @@ -126,6 +126,16 @@ AppController.prototype.exportBlockLibraryToFile = function() { } }; +AppController.prototype.exportBlockLibraryAsJson = function() { + const blockJson = this.blockLibraryController.getBlockLibraryAsJson(); + if (blockJson.length === 0) { + alert('No blocks in library to export'); + return; + } + const filename = 'legacy_block_factory_export.txt'; + FactoryUtils.createAndDownloadFile(JSON.stringify(blockJson), filename, 'plain'); +}; + /** * Converts an object mapping block type to XML to text file for output. * @param {!Object} blockXmlMap Object mapping block type to XML. @@ -491,6 +501,10 @@ AppController.prototype.assignBlockFactoryClickHandlers = function() { self.exportBlockLibraryToFile(); }); + document.getElementById('exportAsJson').addEventListener('click', function() { + self.exportBlockLibraryAsJson(); + }); + document.getElementById('helpButton').addEventListener('click', function() { open('https://developers.google.com/blockly/custom-blocks/block-factory', diff --git a/demos/blockfactory/block_library_controller.js b/demos/blockfactory/block_library_controller.js index 2192a7bdd..7bb34e8d6 100644 --- a/demos/blockfactory/block_library_controller.js +++ b/demos/blockfactory/block_library_controller.js @@ -173,6 +173,29 @@ BlockLibraryController.prototype.getBlockLibrary = function() { return this.storage.getBlockXmlTextMap(); }; +/** + * @return {Object[]} Array of JSON data, where each item is the data for one block type. + */ +BlockLibraryController.prototype.getBlockLibraryAsJson = function() { + const xmlBlocks = this.storage.getBlockXmlMap(this.storage.getBlockTypes()); + const jsonBlocks = []; + const headlessWorkspace = new Blockly.Workspace(); + + for (const blockName in xmlBlocks) { + // Load the block XML into a workspace so we can save it as JSON + headlessWorkspace.clear(); + const blockXml = xmlBlocks[blockName]; + Blockly.Xml.domToWorkspace(blockXml, headlessWorkspace); + const block = headlessWorkspace.getBlocksByType('factory_base', false)[0]; + + if (!block) continue; + + const json = Blockly.serialization.blocks.save(block, {addCoordinates: false, saveIds: false}); + jsonBlocks.push(json); + } + return jsonBlocks; +} + /** * Return stored XML of a given block type. * @param {string} blockType The type of block. diff --git a/demos/blockfactory/index.html b/demos/blockfactory/index.html index 9f33aa4fb..c72efb7fa 100644 --- a/demos/blockfactory/index.html +++ b/demos/blockfactory/index.html @@ -339,6 +339,9 @@ + From 185c82a991484e68b1a76825f2c4b082b0168677 Mon Sep 17 00:00:00 2001 From: Neil Fraser Date: Tue, 30 Apr 2024 23:51:20 +0200 Subject: [PATCH 4/8] fix: Don't record undo events for enable/disable (#8049) There's no need to record and replay these events since the change will happen automatically anyway. Related to #7951 and #7950. --- blocks/loops.ts | 15 ++++++++++----- blocks/procedures.ts | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/blocks/loops.ts b/blocks/loops.ts index 02d9d34be..1b8e68f7c 100644 --- a/blocks/loops.ts +++ b/blocks/loops.ts @@ -20,6 +20,7 @@ import { createBlockDefinitionsFromJsonArray, defineBlocks, } from '../core/common.js'; +import * as eventUtils from '../core/events/utils.js'; import '../core/field_dropdown.js'; import '../core/field_label.js'; import '../core/field_number.js'; @@ -372,12 +373,16 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = { this.setWarningText( enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING'], ); + if (!this.isInFlyout) { - const group = Events.getGroup(); - // Makes it so the move and the disable event get undone together. - Events.setGroup(e.group); - this.setEnabled(enabled); - Events.setGroup(group); + try { + // There is no need to record the enable/disable change on the undo/redo + // list since the change will be automatically recreated when replayed. + eventUtils.setRecordUndo(false); + this.setEnabled(enabled); + } finally { + eventUtils.setRecordUndo(true); + } } }, }; diff --git a/blocks/procedures.ts b/blocks/procedures.ts index f7e3bd62d..77efecb84 100644 --- a/blocks/procedures.ts +++ b/blocks/procedures.ts @@ -25,6 +25,7 @@ import type { ContextMenuOption, LegacyContextMenuOption, } from '../core/contextmenu_registry.js'; +import * as eventUtils from '../core/events/utils.js'; import {FieldCheckbox} from '../core/field_checkbox.js'; import {FieldLabel} from '../core/field_label.js'; import {FieldTextInput} from '../core/field_textinput.js'; @@ -1316,12 +1317,16 @@ const PROCEDURES_IFRETURN = { } else { this.setWarningText(Msg['PROCEDURES_IFRETURN_WARNING']); } + if (!this.isInFlyout) { - const group = Events.getGroup(); - // Makes it so the move and the disable event get undone together. - Events.setGroup(e.group); - this.setEnabled(legal); - Events.setGroup(group); + try { + // There is no need to record the enable/disable change on the undo/redo + // list since the change will be automatically recreated when replayed. + eventUtils.setRecordUndo(false); + this.setEnabled(legal); + } finally { + eventUtils.setRecordUndo(true); + } } }, /** From 171befa7462b4a5288ce0fa775814c267f424e2c Mon Sep 17 00:00:00 2001 From: John Nesky Date: Thu, 2 May 2024 18:57:57 -0700 Subject: [PATCH 5/8] fix!: Only fire intermediate events when editing input with invalid text. (#8054) * fix: Fire intermediate events only when editing text input. * Prefix unused arg with underscore. * Fix tests. --- core/field.ts | 22 ++++++++++++++++++---- core/field_angle.ts | 2 +- core/field_input.ts | 24 ++++++++++++++++++------ tests/mocha/field_angle_test.js | 3 ++- tests/mocha/field_number_test.js | 3 ++- tests/mocha/field_textinput_test.js | 1 + 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/core/field.ts b/core/field.ts index 7522cde93..fb6c078e9 100644 --- a/core/field.ts +++ b/core/field.ts @@ -1086,14 +1086,22 @@ export abstract class Field } const classValidation = this.doClassValidation_(newValue); - const classValue = this.processValidation_(newValue, classValidation); + const classValue = this.processValidation_( + newValue, + classValidation, + fireChangeEvent, + ); if (classValue instanceof Error) { doLogging && console.log('invalid class validation, return'); return; } const localValidation = this.getValidator()?.call(this, classValue); - const localValue = this.processValidation_(classValue, localValidation); + const localValue = this.processValidation_( + classValue, + localValidation, + fireChangeEvent, + ); if (localValue instanceof Error) { doLogging && console.log('invalid local validation, return'); return; @@ -1135,14 +1143,16 @@ export abstract class Field * * @param newValue New value. * @param validatedValue Validated value. + * @param fireChangeEvent Whether to fire a change event if the value changes. * @returns New value, or an Error object. */ private processValidation_( newValue: AnyDuringMigration, validatedValue: T | null | undefined, + fireChangeEvent: boolean, ): T | Error { if (validatedValue === null) { - this.doValueInvalid_(newValue); + this.doValueInvalid_(newValue, fireChangeEvent); if (this.isDirty_) { this.forceRerender(); } @@ -1209,8 +1219,12 @@ export abstract class Field * No-op by default. * * @param _invalidValue The input value that was determined to be invalid. + * @param _fireChangeEvent Whether to fire a change event if the value changes. */ - protected doValueInvalid_(_invalidValue: AnyDuringMigration) {} + protected doValueInvalid_( + _invalidValue: AnyDuringMigration, + _fireChangeEvent: boolean = true, + ) {} // NOP /** diff --git a/core/field_angle.ts b/core/field_angle.ts index e65ef16a9..9e94ea4d8 100644 --- a/core/field_angle.ts +++ b/core/field_angle.ts @@ -366,7 +366,7 @@ export class FieldAngle extends FieldInput { // normal block change events, and instead report them as special // intermediate changes that do not get recorded in undo history. const oldValue = this.value_; - this.setEditorValue_(angle, false); + this.setEditorValue_(angle, /* fireChangeEvent= */ false); if ( this.sourceBlock_ && eventUtils.isEnabled() && diff --git a/core/field_input.ts b/core/field_input.ts index 513047054..5c26f42b3 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -166,17 +166,26 @@ export abstract class FieldInput extends Field< * value while allowing the display text to be handled by the htmlInput_. * * @param _invalidValue The input value that was determined to be invalid. - * This is not used by the text input because its display value is stored - * on the htmlInput_. + * This is not used by the text input because its display value is stored + * on the htmlInput_. + * @param fireChangeEvent Whether to fire a change event if the value changes. */ - protected override doValueInvalid_(_invalidValue: AnyDuringMigration) { + protected override doValueInvalid_( + _invalidValue: AnyDuringMigration, + fireChangeEvent: boolean = true, + ) { if (this.isBeingEdited_) { this.isDirty_ = true; this.isTextValid_ = false; const oldValue = this.value_; // Revert value when the text becomes invalid. - this.value_ = this.htmlInput_!.getAttribute('data-untyped-default-value'); - if (this.sourceBlock_ && eventUtils.isEnabled()) { + this.value_ = this.valueWhenEditorWasOpened_; + if ( + this.sourceBlock_ && + eventUtils.isEnabled() && + this.value_ !== oldValue && + fireChangeEvent + ) { eventUtils.fire( new (eventUtils.get(eventUtils.BLOCK_CHANGE))( this.sourceBlock_, @@ -566,7 +575,10 @@ export abstract class FieldInput extends Field< // intermediate changes that do not get recorded in undo history. const oldValue = this.value_; // Change the field's value without firing the normal change event. - this.setValue(this.getValueFromEditorText_(this.htmlInput_!.value), false); + this.setValue( + this.getValueFromEditorText_(this.htmlInput_!.value), + /* fireChangeEvent= */ false, + ); if ( this.sourceBlock_ && eventUtils.isEnabled() && diff --git a/tests/mocha/field_angle_test.js b/tests/mocha/field_angle_test.js index 62afd8d27..d2630581f 100644 --- a/tests/mocha/field_angle_test.js +++ b/tests/mocha/field_angle_test.js @@ -138,6 +138,7 @@ suite('Angle Fields', function () { suite('Validators', function () { setup(function () { this.field = new Blockly.FieldAngle(1); + this.field.valueWhenEditorWasOpened_ = this.field.getValue(); this.field.htmlInput_ = document.createElement('input'); this.field.htmlInput_.setAttribute('data-old-value', '1'); this.field.htmlInput_.setAttribute('data-untyped-default-value', '1'); @@ -153,7 +154,7 @@ suite('Angle Fields', function () { return null; }, value: 2, - expectedValue: '1', + expectedValue: 1, }, { title: 'Force Mult of 30 Validator', diff --git a/tests/mocha/field_number_test.js b/tests/mocha/field_number_test.js index 4dcc930da..c6737668d 100644 --- a/tests/mocha/field_number_test.js +++ b/tests/mocha/field_number_test.js @@ -261,6 +261,7 @@ suite('Number Fields', function () { suite('Validators', function () { setup(function () { this.field = new Blockly.FieldNumber(1); + this.field.valueWhenEditorWasOpened_ = this.field.getValue(); this.field.htmlInput_ = document.createElement('input'); this.field.htmlInput_.setAttribute('data-old-value', '1'); this.field.htmlInput_.setAttribute('data-untyped-default-value', '1'); @@ -276,7 +277,7 @@ suite('Number Fields', function () { return null; }, value: 2, - expectedValue: '1', + expectedValue: 1, }, { title: 'Force End with 6 Validator', diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js index 5a8a82479..f68006e87 100644 --- a/tests/mocha/field_textinput_test.js +++ b/tests/mocha/field_textinput_test.js @@ -129,6 +129,7 @@ suite('Text Input Fields', function () { suite('Validators', function () { setup(function () { this.field = new Blockly.FieldTextInput('value'); + this.field.valueWhenEditorWasOpened_ = this.field.getValue(); this.field.htmlInput_ = document.createElement('input'); this.field.htmlInput_.setAttribute('data-old-value', 'value'); this.field.htmlInput_.setAttribute('data-untyped-default-value', 'value'); From 76e179d65513998ac0e405f3ae0b9772a401c083 Mon Sep 17 00:00:00 2001 From: Neil Fraser Date: Tue, 7 May 2024 20:34:15 +0200 Subject: [PATCH 6/8] Revert "fix: Don't record undo events for enable/disable" (#8069) This reverts commit 1a8e4d9e691fd7ed8fb00295918f67c4c61937e6. --- blocks/loops.ts | 15 +++++---------- blocks/procedures.ts | 15 +++++---------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/blocks/loops.ts b/blocks/loops.ts index 1b8e68f7c..02d9d34be 100644 --- a/blocks/loops.ts +++ b/blocks/loops.ts @@ -20,7 +20,6 @@ import { createBlockDefinitionsFromJsonArray, defineBlocks, } from '../core/common.js'; -import * as eventUtils from '../core/events/utils.js'; import '../core/field_dropdown.js'; import '../core/field_label.js'; import '../core/field_number.js'; @@ -373,16 +372,12 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = { this.setWarningText( enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING'], ); - if (!this.isInFlyout) { - try { - // There is no need to record the enable/disable change on the undo/redo - // list since the change will be automatically recreated when replayed. - eventUtils.setRecordUndo(false); - this.setEnabled(enabled); - } finally { - eventUtils.setRecordUndo(true); - } + const group = Events.getGroup(); + // Makes it so the move and the disable event get undone together. + Events.setGroup(e.group); + this.setEnabled(enabled); + Events.setGroup(group); } }, }; diff --git a/blocks/procedures.ts b/blocks/procedures.ts index 77efecb84..f7e3bd62d 100644 --- a/blocks/procedures.ts +++ b/blocks/procedures.ts @@ -25,7 +25,6 @@ import type { ContextMenuOption, LegacyContextMenuOption, } from '../core/contextmenu_registry.js'; -import * as eventUtils from '../core/events/utils.js'; import {FieldCheckbox} from '../core/field_checkbox.js'; import {FieldLabel} from '../core/field_label.js'; import {FieldTextInput} from '../core/field_textinput.js'; @@ -1317,16 +1316,12 @@ const PROCEDURES_IFRETURN = { } else { this.setWarningText(Msg['PROCEDURES_IFRETURN_WARNING']); } - if (!this.isInFlyout) { - try { - // There is no need to record the enable/disable change on the undo/redo - // list since the change will be automatically recreated when replayed. - eventUtils.setRecordUndo(false); - this.setEnabled(legal); - } finally { - eventUtils.setRecordUndo(true); - } + const group = Events.getGroup(); + // Makes it so the move and the disable event get undone together. + Events.setGroup(e.group); + this.setEnabled(legal); + Events.setGroup(group); } }, /** From 54eeb85d8947d5dfe487d2b1ce810eb34c2c4a4f Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Wed, 8 May 2024 11:44:00 -0700 Subject: [PATCH 7/8] fix!: add getContents to IFlyout (#8064) --- core/flyout_base.ts | 11 +------ core/interfaces/i_flyout.ts | 11 +++++++ core/keyboard_nav/ast_node.ts | 60 ++++++++++++++++++++++++++++------- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 58b748433..6d8760b7f 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -566,16 +566,7 @@ export abstract class Flyout * @param contents - The array of items for the flyout. */ setContents(contents: FlyoutItem[]): void { - const blocksAndButtons = contents.map((item) => { - if (item.type === 'block' && item.block) { - return item.block as BlockSvg; - } - if (item.type === 'button' && item.button) { - return item.button as FlyoutButton; - } - }); - - this.contents = blocksAndButtons as FlyoutItem[]; + this.contents = contents; } /** * Update the display property of the flyout based whether it thinks it should diff --git a/core/interfaces/i_flyout.ts b/core/interfaces/i_flyout.ts index e73845dc7..84067f755 100644 --- a/core/interfaces/i_flyout.ts +++ b/core/interfaces/i_flyout.ts @@ -12,6 +12,7 @@ import type {Coordinate} from '../utils/coordinate.js'; import type {FlyoutDefinition} from '../utils/toolbox.js'; import type {Svg} from '../utils/svg.js'; import type {IRegistrable} from './i_registrable.js'; +import {FlyoutItem} from '../flyout_base.js'; /** * Interface for a flyout. @@ -117,6 +118,16 @@ export interface IFlyout extends IRegistrable { */ show(flyoutDef: FlyoutDefinition | string): void; + /** + * Returns the list of flyout items currently present in the flyout. + * The `show` method parses the flyout definition into a list of actual + * flyout items. This method should return those concrete items, which + * may be used for e.g. keyboard navigation. + * + * @returns List of flyout items. + */ + getContents(): FlyoutItem[]; + /** * Create a copy of this block on the workspace. * diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index fb20539be..7985ac6dc 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -12,7 +12,7 @@ */ // Former goog.module ID: Blockly.ASTNode -import type {Block} from '../block.js'; +import {Block} from '../block.js'; import type {Connection} from '../connection.js'; import {ConnectionType} from '../connection_type.js'; import type {Field} from '../field.js'; @@ -23,7 +23,7 @@ import {Coordinate} from '../utils/coordinate.js'; import type {Workspace} from '../workspace.js'; import {FlyoutButton} from '../flyout_button.js'; import {WorkspaceSvg} from '../workspace_svg.js'; -import {Flyout} from '../flyout_base.js'; +import {FlyoutItem} from '../flyout_base.js'; /** * Class for an AST node. @@ -337,21 +337,59 @@ export class ASTNode { return null; } - const flyout = targetWorkspace.getFlyout() as Flyout; - const flyoutContents = flyout.getContents() as (Block | FlyoutButton)[]; + const flyout = targetWorkspace.getFlyout(); + if (!flyout) return null; + + const nextItem = this.findNextLocationInFlyout( + flyout.getContents(), + location, + forward, + ); + if (!nextItem) return null; + + if (nextItem.type === 'button' && nextItem.button) { + return ASTNode.createButtonNode(nextItem.button); + } else if (nextItem.type === 'block' && nextItem.block) { + return ASTNode.createStackNode(nextItem.block); + } + + return null; + } + + /** + * Finds the next (or previous if navigating backward) item in the flyout that should be navigated to. + * + * @param flyoutContents Contents of the current flyout. + * @param currentLocation Current ASTNode location. + * @param forward True if we're navigating forward, else false. + * @returns The next (or previous) FlyoutItem, or null if there is none. + */ + private findNextLocationInFlyout( + flyoutContents: FlyoutItem[], + currentLocation: IASTNodeLocation, + forward: boolean, + ): FlyoutItem | null { + const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => { + if (currentLocation instanceof Block && item.block === currentLocation) { + return true; + } + if ( + currentLocation instanceof FlyoutButton && + item.button === currentLocation + ) { + return true; + } + return false; + }); + + if (currentIndex < 0) return null; - const currentIndex = flyoutContents.indexOf(location); const resultIndex = forward ? currentIndex + 1 : currentIndex - 1; if (resultIndex === -1 || resultIndex === flyoutContents.length) { return null; } - const newLocation = flyoutContents[resultIndex]; - if (newLocation instanceof FlyoutButton) { - return ASTNode.createButtonNode(newLocation); - } else { - return ASTNode.createStackNode(newLocation); - } + return flyoutContents[resultIndex]; } /** From 28ac0c44732d45114761392528fe706ff76c2f4a Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Fri, 10 May 2024 14:14:50 -0700 Subject: [PATCH 8/8] fix: improve types in FieldRegistry (#8062) * fix: improve types in FieldRegistry * chore: tsdoc --- core/field.ts | 24 ++++++++++++-- core/field_angle.ts | 2 +- core/field_checkbox.ts | 4 ++- core/field_colour.ts | 2 +- core/field_dropdown.ts | 4 ++- core/field_image.ts | 2 +- core/field_label.ts | 2 +- core/field_number.ts | 2 +- core/field_registry.ts | 51 ++++++++++++++++++++++++------ core/field_textinput.ts | 4 ++- tests/mocha/field_registry_test.js | 22 ++++++++++--- 11 files changed, 95 insertions(+), 24 deletions(-) diff --git a/core/field.ts b/core/field.ts index fb6c078e9..58f120cb8 100644 --- a/core/field.ts +++ b/core/field.ts @@ -1433,6 +1433,22 @@ export abstract class Field workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw(); } } + + /** + * Subclasses should reimplement this method to construct their Field + * subclass from a JSON arg object. + * + * It is an error to attempt to register a field subclass in the + * FieldRegistry if that subclass has not overridden this method. + * + * @param _options JSON configuration object with properties needed + * to configure a specific field. + */ + static fromJson(_options: FieldConfig): Field { + throw new Error( + `Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`, + ); + } } /** @@ -1443,12 +1459,14 @@ export interface FieldConfig { } /** - * For use by Field and descendants of Field. Constructors can change + * Represents an object that has all the prototype properties of the `Field` + * class. This is necessary because constructors can change * in descendants, though they should contain all of Field's prototype methods. * - * @internal + * This type should only be used in places where we directly access the prototype + * of a Field class or subclass. */ -export type FieldProto = Pick; +type FieldProto = Pick; /** * Represents an error where the field is trying to access its block or diff --git a/core/field_angle.ts b/core/field_angle.ts index 9e94ea4d8..7eef3099f 100644 --- a/core/field_angle.ts +++ b/core/field_angle.ts @@ -516,7 +516,7 @@ export class FieldAngle extends FieldInput { * @nocollapse * @internal */ - static fromJson(options: FieldAngleFromJsonConfig): FieldAngle { + static override fromJson(options: FieldAngleFromJsonConfig): FieldAngle { // `this` might be a subclass of FieldAngle if that class doesn't override // the static fromJson method. return new this(options.angle, undefined, options); diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts index 7bda3a58f..83f460bb9 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -226,7 +226,9 @@ export class FieldCheckbox extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldCheckboxFromJsonConfig): FieldCheckbox { + static override fromJson( + options: FieldCheckboxFromJsonConfig, + ): FieldCheckbox { // `this` might be a subclass of FieldCheckbox if that class doesn't // 'override' the static fromJson method. return new this(options.checked, undefined, options); diff --git a/core/field_colour.ts b/core/field_colour.ts index 6b78a2e50..46bf3f0e2 100644 --- a/core/field_colour.ts +++ b/core/field_colour.ts @@ -641,7 +641,7 @@ export class FieldColour extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldColourFromJsonConfig): FieldColour { + static override fromJson(options: FieldColourFromJsonConfig): FieldColour { // `this` might be a subclass of FieldColour if that class doesn't override // the static fromJson method. return new this(options.colour, undefined, options); diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index d92d02ec2..58a4b0732 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -647,7 +647,9 @@ export class FieldDropdown extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldDropdownFromJsonConfig): FieldDropdown { + static override fromJson( + options: FieldDropdownFromJsonConfig, + ): FieldDropdown { if (!options.options) { throw new Error( 'options are required for the dropdown field. The ' + diff --git a/core/field_image.ts b/core/field_image.ts index 6d31a9772..09461a790 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -250,7 +250,7 @@ export class FieldImage extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldImageFromJsonConfig): FieldImage { + static override fromJson(options: FieldImageFromJsonConfig): FieldImage { if (!options.src || !options.width || !options.height) { throw new Error( 'src, width, and height values for an image field are' + diff --git a/core/field_label.ts b/core/field_label.ts index 7409ca594..2b77b0d25 100644 --- a/core/field_label.ts +++ b/core/field_label.ts @@ -117,7 +117,7 @@ export class FieldLabel extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldLabelFromJsonConfig): FieldLabel { + static override fromJson(options: FieldLabelFromJsonConfig): FieldLabel { const text = parsing.replaceMessageReferences(options.text); // `this` might be a subclass of FieldLabel if that class doesn't override // the static fromJson method. diff --git a/core/field_number.ts b/core/field_number.ts index ee04eab49..e8e51d060 100644 --- a/core/field_number.ts +++ b/core/field_number.ts @@ -315,7 +315,7 @@ export class FieldNumber extends FieldInput { * @nocollapse * @internal */ - static fromJson(options: FieldNumberFromJsonConfig): FieldNumber { + static override fromJson(options: FieldNumberFromJsonConfig): FieldNumber { // `this` might be a subclass of FieldNumber if that class doesn't override // the static fromJson method. return new this( diff --git a/core/field_registry.ts b/core/field_registry.ts index 0bd9e5626..06bb9acd0 100644 --- a/core/field_registry.ts +++ b/core/field_registry.ts @@ -6,12 +6,46 @@ // Former goog.module ID: Blockly.fieldRegistry -import type {Field, FieldProto} from './field.js'; +import type {Field, FieldConfig} from './field.js'; import * as registry from './registry.js'; -interface RegistryOptions { +/** + * When constructing a field from JSON using the registry, the + * `fromJson` method in this file is called with an options parameter + * object consisting of the "type" which is the name of the field, and + * other options that are part of the field's config object. + * + * These options are then passed to the field's static `fromJson` + * method. That method accepts an options parameter with a type that usually + * extends from FieldConfig, and may or may not have a "type" attribute (in + * fact, it shouldn't, because we'd overwrite it as described above!) + * + * Unfortunately the registry has no way of knowing the actual Field subclass + * that will be returned from passing in the name of the field. Therefore it + * also has no way of knowing that the options object not only implements + * `FieldConfig`, but it also should satisfy the Config that belongs to that + * specific class's `fromJson` method. + * + * Because of this uncertainty, we just give up on type checking the properties + * passed to the `fromJson` method, and allow arbitrary string keys with + * unknown types. + */ +type RegistryOptions = FieldConfig & { + // The name of the field, e.g. field_dropdown type: string; [key: string]: unknown; +}; + +/** + * Represents the static methods that must be defined on any + * field that is registered, i.e. the constructor and fromJson methods. + * + * Because we don't know which Field subclass will be registered, we + * are unable to typecheck the parameters of the constructor. + */ +export interface RegistrableField { + new (...args: any[]): Field; + fromJson(options: FieldConfig): Field; } /** @@ -25,7 +59,7 @@ interface RegistryOptions { * @throws {Error} if the type name is empty, the field is already registered, * or the fieldClass is not an object containing a fromJson function. */ -export function register(type: string, fieldClass: FieldProto) { +export function register(type: string, fieldClass: RegistrableField) { registry.register(registry.Type.FIELD, type, fieldClass); } @@ -59,7 +93,10 @@ export function fromJson(options: RegistryOptions): Field | null { * @param options */ function fromJsonInternal(options: RegistryOptions): Field | null { - const fieldObject = registry.getObject(registry.Type.FIELD, options.type); + const fieldObject = registry.getObject( + registry.Type.FIELD, + options.type, + ) as unknown as RegistrableField; if (!fieldObject) { console.warn( 'Blockly could not create a field of type ' + @@ -69,12 +106,8 @@ function fromJsonInternal(options: RegistryOptions): Field | null { ' #1584), or the registration is not being reached.', ); return null; - } else if (typeof (fieldObject as any).fromJson !== 'function') { - throw new TypeError('returned Field was not a IRegistrableField'); - } else { - type fromJson = (options: {}) => Field; - return (fieldObject as unknown as {fromJson: fromJson}).fromJson(options); } + return fieldObject.fromJson(options); } export const TEST_ONLY = { diff --git a/core/field_textinput.ts b/core/field_textinput.ts index 39e82b5a4..39bdca970 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -73,7 +73,9 @@ export class FieldTextInput extends FieldInput { * @nocollapse * @internal */ - static fromJson(options: FieldTextInputFromJsonConfig): FieldTextInput { + static override fromJson( + options: FieldTextInputFromJsonConfig, + ): FieldTextInput { const text = parsing.replaceMessageReferences(options.text); // `this` might be a subclass of FieldTextInput if that class doesn't // override the static fromJson method. diff --git a/tests/mocha/field_registry_test.js b/tests/mocha/field_registry_test.js index f8e4bb9b4..aca548746 100644 --- a/tests/mocha/field_registry_test.js +++ b/tests/mocha/field_registry_test.js @@ -42,12 +42,10 @@ suite('Field Registry', function () { }, 'Invalid name'); }); test('No fromJson', function () { - const fromJson = CustomFieldType.fromJson; - delete CustomFieldType.fromJson; + class IncorrectField {} chai.assert.throws(function () { - Blockly.fieldRegistry.register('field_custom_test', CustomFieldType); + Blockly.fieldRegistry.register('field_custom_test', IncorrectField); }, 'must have a fromJson function'); - CustomFieldType.fromJson = fromJson; }); test('fromJson not a function', function () { const fromJson = CustomFieldType.fromJson; @@ -97,5 +95,21 @@ suite('Field Registry', function () { chai.assert.isNotNull(field); chai.assert.equal(field.getValue(), 'ok'); }); + test('Did not override fromJson', function () { + // This class will have a fromJson method, so it can be registered + // but it doesn't override the abstract class's method so it throws + class IncorrectField extends Blockly.Field {} + + Blockly.fieldRegistry.register('field_custom_test', IncorrectField); + + const json = { + type: 'field_custom_test', + value: 'ok', + }; + + chai.assert.throws(function () { + Blockly.fieldRegistry.fromJson(json); + }, 'Attempted to instantiate a field from the registry'); + }); }); });