diff --git a/docs/defining-blocks.md b/docs/defining-blocks.md
index 7f717fe75130..c3bde459992a 100644
--- a/docs/defining-blocks.md
+++ b/docs/defining-blocks.md
@@ -455,6 +455,14 @@ parameter like so:
**Playground example**: [Enumerations](https://makecode.com/playground#basic-enums)
+### Color picker shadows
+
+Use `color.shadow=makecode_color_picker` with a numeric color parameter. Set
+`color.fieldOptions.format` to `rgb` (the default), `hsv`, `hsl`, `cmyk`, or `hex`.
+`color.defl=0x7f3fbf` supplies the RGB color regardless of the initial display mode.
+For example, `color.fieldOptions.format=hex` displays that default as `#7F3FBF`.
+The picker always outputs a number through `colorHelpers`.
+
### Creating enumerations with blocks
You can have blocks themselves define an enumeration dynamically. The block will specify some initial members but additional ones are added by selecting the "Add a new <enum_name>..." option in the parameter dropdown.
diff --git a/pxtblocks/loader.ts b/pxtblocks/loader.ts
index 4b040a501c8f..216496aee8bc 100644
--- a/pxtblocks/loader.ts
+++ b/pxtblocks/loader.ts
@@ -23,6 +23,7 @@ import { initOnStart } from "./builtins/misc";
import { initContextMenu } from "./contextMenu";
import { renderCodeCard } from "./codecardRenderer";
import { FieldDropdown } from "./fields/field_dropdown";
+import { COLOR_PICKER_BLOCK_TYPE } from "./plugins/colorpicker";
import { setDraggableShadowBlocks, setDuplicateOnDrag, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag";
import { initCopyPaste } from "./copyPaste";
export { initCopyPaste } from "./copyPaste";
@@ -110,7 +111,10 @@ export function blockSymbol(type: string): pxtc.SymbolInfo {
export function injectBlocks(blockInfo: pxtc.BlocksInfo): pxtc.SymbolInfo[] {
cachedBlockInfo = blockInfo;
- setDraggableShadowBlocks(blockInfo.blocks.filter(fn => fn.attributes.duplicateShadowOnDrag).map(fn => fn.attributes.blockId));
+ setDraggableShadowBlocks([
+ COLOR_PICKER_BLOCK_TYPE,
+ ...blockInfo.blocks.filter(fn => fn.attributes.duplicateShadowOnDrag).map(fn => fn.attributes.blockId)
+ ]);
setArgumentReporterLocalizeFunction((arg, block) => {
return localizeArgumentReporter(blockInfo, arg, block);
diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts
index 1937c9bf3256..1f246cdc6ba1 100644
--- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts
+++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts
@@ -3,6 +3,7 @@ import { ColorDropdownField } from "./colorPickerDropdown";
import { ColorPickerNumberBlock, COLOR_NUMBER_BLOCK_TYPE, generateColorPickerNumberShadowDom } from "./colorPickerNumberBlock";
import { COLOR_STRING_BLOCK_TYPE, generateColorPickerStringShadowDom } from "./colorPickerStringBlock";
import { FieldColorPickerNumberType, fromFormatToHex, fromFormatToHSV, fromHexToFormat, fromHSVToFormat, getFieldTypesForFormat } from "./util";
+import { setDuplicateOnDragStrategy, updateDuplicateOnDragState } from "../duplicateOnDrag";
export interface ColorPickerBlock extends Blockly.Block {
colorHSVLoaded: boolean;
@@ -11,6 +12,8 @@ export interface ColorPickerBlock extends Blockly.Block {
setColorHSV: (hsv: number[]) => void;
setFormat: (format: string, prevFormat?: string) => void;
readColorFromInputs: () => void;
+ updateBeforeRender: () => void;
+ updateColorPreview: () => void;
}
const HEX_INPUT_NAME = "HEX_INPUT";
@@ -31,10 +34,56 @@ export function initColorPickerBlock() {
this.setInputsInline(true);
this.appendDummyInput()
+ .appendField(new Blockly.FieldImage(previewImage("#000000"), 24, 24, lf("Choose color"), () => {
+ const inputs = [HEX_INPUT_NAME, "INPUT0", "INPUT1", "INPUT2", "INPUT3"];
+ for (const input of inputs) {
+ const child = this.getInputTargetBlock(input);
+ if (child?.type === COLOR_STRING_BLOCK_TYPE || child?.type === COLOR_NUMBER_BLOCK_TYPE) {
+ this.readColorFromInputs();
+ child.getField(input === HEX_INPUT_NAME ? "TEXT" : "NUM").showEditor();
+ return;
+ }
+ }
+ }), "PREVIEW")
.appendField(new ColorDropdownField("rgb"), "FORMAT");
this.updateShape(this.getFieldValue("FORMAT"));
this.setColorHSV(this.colorHSV);
+ setDuplicateOnDragStrategy(this);
+ if (this instanceof Blockly.BlockSvg) updateDuplicateOnDragState(this);
+ this.setOnChange((event: Blockly.Events.BlockBase) => {
+ if (event.type !== Blockly.Events.BLOCK_CHANGE && event.type !== Blockly.Events.BLOCK_MOVE
+ && event.type !== Blockly.Events.BLOCK_CREATE) return;
+ const moved = event as Blockly.Events.BlockMove;
+ if (event.blockId === this.id || moved.oldParentId === this.id || moved.newParentId === this.id
+ || this.getChildren(false).some(child => child.id === event.blockId)) {
+ this.updateColorPreview();
+ }
+ });
+ },
+
+ updateBeforeRender: function (this: ColorPickerBlock) {
+ this.updateColorPreview();
+ },
+
+ updateColorPreview: function (this: ColorPickerBlock) {
+ const preview = this.getField("PREVIEW") as Blockly.FieldImage;
+ const color = getColorPickerColor(this);
+ // This is derived UI, not an edit: don't add history or invalidate redo.
+ Blockly.Events.disable();
+ try {
+ if (color) {
+ const image = previewImage(color);
+ if (preview.getValue() !== image) {
+ preview.setValue(image);
+ }
+ preview.setAlt(lf("Color {0}. Choose color", color));
+ }
+ preview.setVisible(!!color);
+ }
+ finally {
+ Blockly.Events.enable();
+ }
},
domToMutation: function (this: ColorPickerBlock, xmlElement: Element) {
@@ -134,6 +183,7 @@ export function initColorPickerBlock() {
},
setColorHSV: function (this: ColorPickerBlock, hsv: number[]) {
+ this.colorHSV = hsv;
if (this.getFieldValue("FORMAT") === "hex") {
const color = fromFormatToHex("hsv", hsv);
@@ -144,6 +194,7 @@ export function initColorPickerBlock() {
const field = target.getField("TEXT");
field.setValue(color);
}
+ this.updateColorPreview();
return;
}
@@ -159,7 +210,7 @@ export function initColorPickerBlock() {
}
}
- this.colorHSV = hsv;
+ this.updateColorPreview();
},
setFormat: function (this: ColorPickerBlock, format: string, prevFormat = format) {
@@ -210,4 +261,30 @@ export function initColorPickerBlock() {
this.colorHSV = fromFormatToHSV(this.getFieldValue("FORMAT"), newValues);
}
}
+}
+
+/** Evaluate literal inputs only; never guess a runtime value for variables or expressions. */
+export function getColorPickerColor(block: Blockly.Block): string | undefined {
+ const format = block.getFieldValue("FORMAT");
+ if (format === "hex") {
+ const child = block.getInputTargetBlock(HEX_INPUT_NAME);
+ if (child?.type !== COLOR_STRING_BLOCK_TYPE && child?.type !== "text") return undefined;
+ const hex = child.getFieldValue("TEXT");
+ return /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex)
+ ? fromFormatToHex("hsv", fromHexToFormat("hsv", hex)) : undefined;
+ }
+ const values: number[] = [];
+ for (let i = 0; i < getFieldTypesForFormat(format).length; i++) {
+ const child = block.getInputTargetBlock("INPUT" + i);
+ if (child?.type !== COLOR_NUMBER_BLOCK_TYPE && child?.type !== "math_number") return undefined;
+ const value = Number(child.getFieldValue("NUM"));
+ if (!Number.isFinite(value)) return undefined;
+ values.push(value);
+ }
+ return fromFormatToHex(format, values);
+}
+
+function previewImage(color: string): string {
+ const content = ``;
+ return "data:image/svg+xml," + encodeURIComponent(``);
}
\ No newline at end of file
diff --git a/pxtblocks/plugins/colorpicker/colorPickerDropdown.ts b/pxtblocks/plugins/colorpicker/colorPickerDropdown.ts
index 12f5da5a9543..905f21d6a341 100644
--- a/pxtblocks/plugins/colorpicker/colorPickerDropdown.ts
+++ b/pxtblocks/plugins/colorpicker/colorPickerDropdown.ts
@@ -1,6 +1,7 @@
import * as Blockly from "blockly";
import { FieldDropdown } from "../../fields/field_dropdown";
-import { ColorPickerBlock, COLOR_PICKER_BLOCK_TYPE } from "./colorPickerBlock";
+import { ColorPickerBlock, COLOR_PICKER_BLOCK_TYPE, getColorPickerColor } from "./colorPickerBlock";
+import { fromFormatToHex } from "./util";
const COLOR_FORMATS: [string, string][] = [
["RGB", "rgb"],
@@ -20,7 +21,7 @@ export class ColorDropdownField extends FieldDropdown {
override onItemSelected_(menu: Blockly.Menu, menuItem: Blockly.MenuItem) {
if (this.sourceBlock_?.type === COLOR_PICKER_BLOCK_TYPE) {
const colorPicker = this.sourceBlock_ as ColorPickerBlock;
- if (!colorPicker.colorHSVLoaded) {
+ if (!colorPicker.colorHSVLoaded || getColorPickerColor(colorPicker) !== fromFormatToHex("hsv", colorPicker.colorHSV)) {
colorPicker.readColorFromInputs();
}
}
diff --git a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts
index 8038ff605273..f8645d42f26b 100644
--- a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts
+++ b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts
@@ -40,12 +40,13 @@ export class DuplicateOnDragStrategy extends Blockly.dragging.BlockDragStrategy
let target: Blockly.Connection;
let xml: Element;
const isShadow = self.block.isShadow();
+ const duplicate = shouldDuplicateOnDrag(self.block);
if (isShadow) {
self.block.setShadow(false);
}
- if (shouldDuplicateOnDrag(self.block)) {
+ if (duplicate) {
const output = self.block.outputConnection;
if (!output?.targetConnection) return;
diff --git a/pxtblocks/toolbox.ts b/pxtblocks/toolbox.ts
index eb3c3f61fe1e..f1eb54f21dea 100644
--- a/pxtblocks/toolbox.ts
+++ b/pxtblocks/toolbox.ts
@@ -3,6 +3,10 @@
import * as Blockly from "blockly";
import { flyoutCategory, getAllFunctionDefinitionBlocks, LOCALIZATION_NAME_MUTATION_KEY } from "./plugins/functions";
import { DRAGGABLE_PARAM_INPUT_PREFIX } from "./loader";
+import { COLOR_PICKER_BLOCK_TYPE } from "./plugins/colorpicker/colorPickerBlock";
+import { generateColorPickerStringShadowDom } from "./plugins/colorpicker/colorPickerStringBlock";
+import { generateColorPickerNumberShadowDom } from "./plugins/colorpicker/colorPickerNumberBlock";
+import { fromFormatToHex, fromHexToFormat, fromHSVToFormat, getFieldTypesForFormat } from "./plugins/colorpicker/util";
const primitiveTypeRegex = /^(string|number|boolean)$/;
@@ -111,6 +115,19 @@ export function createShadowValue(info: pxtc.BlocksInfo, p: pxt.blocks.BlockPara
shadow.setAttribute("type", shadowId || (isArray ? 'lists_create_with' : typeInfo && typeInfo.block || p.type));
shadow.setAttribute("colour", "#fff");
+ const shadowSymbol = info.blocksById[shadowId];
+ if (shadowId === COLOR_PICKER_BLOCK_TYPE || shadowSymbol?.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE) {
+ const contributor = shadowSymbol || info.blocks.find(block => block.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE);
+ const defaults = contributor && pxt.blocks.compileInfo(contributor).parameters[0];
+ value.replaceChild(createColorPickerBlock(
+ defaultV !== undefined ? defaultV : defaults?.defaultValue,
+ p.fieldOptions?.format || defaults?.fieldOptions?.format,
+ contributor?.attributes.color,
+ true
+ ), shadow);
+ return value;
+ }
+
if (isArray) {
// if an array of booleans, numbers, or strings
if (typeInfo && !shadowId) {
@@ -314,7 +331,7 @@ export function createToolboxBlock(info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, c
let parentInput: HTMLElement;
if (fn.attributes.builtinBlockId) {
- return createBuiltinBlock(fn);
+ return createBuiltinBlock(fn, comp, isShadow);
}
if (fn.attributes.toolboxParent) {
@@ -587,60 +604,50 @@ export function createFunctionsFlyoutCategory(workspace: Blockly.WorkspaceSvg) {
return res;
};
-function createBuiltinBlock(fn: pxtc.SymbolInfo) {
+function createBuiltinBlock(fn: pxtc.SymbolInfo, comp: pxt.blocks.BlockCompileInfo, isShadow: boolean) {
const id = fn.attributes.builtinBlockId;
-
- const blockColor = fn.attributes.color;
-
- if (id === "makecode_color_picker") {
- //
- // rgb
- //
- //
- // 255
- //
- //
- //
- //
- // 255
- //
- //
- //
- //
- // 0
- //
- //
- //
- const block = document.createElement("block");
- block.setAttribute("type", "makecode_color_picker");
-
- const field = document.createElement("field");
- field.setAttribute("name", "FORMAT");
- field.textContent = "rgb";
- block.appendChild(field);
-
- for (let i = 0; i < 3; i++) {
- const value = document.createElement("value");
- value.setAttribute("name", `INPUT${i}`);
- const shadow = document.createElement("shadow");
- shadow.setAttribute("type", "makecode_color_picker_number");
- const numField = document.createElement("field");
- numField.setAttribute("name", "NUM");
- numField.textContent = "0";
- shadow.appendChild(numField);
- value.appendChild(shadow);
- block.appendChild(value);
- }
-
- if (blockColor) {
- const mutation = document.createElement("mutation");
- mutation.setAttribute("color", blockColor);
- block.appendChild(mutation);
- }
-
- return block;
+ if (id === COLOR_PICKER_BLOCK_TYPE) {
+ const defaults = comp.parameters[0];
+ return createColorPickerBlock(defaults?.defaultValue, defaults?.fieldOptions?.format, fn.attributes.color, isShadow);
}
pxt.warn(`Unsupported builtin block id: ${id}`);
return undefined;
+}
+
+function createColorPickerBlock(defaultValue: string, requestedFormat: string, blockColor: string, isShadow: boolean): HTMLElement {
+ if (defaultValue?.charAt(0) === '"') defaultValue = JSON.parse(defaultValue);
+ const formatName = ["rgb", "hsv", "hsl", "cmyk", "hex"].indexOf(requestedFormat) >= 0 ? requestedFormat : "rgb";
+ const rgb = Number(defaultValue) || 0;
+ const hex = typeof defaultValue === "string" && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(defaultValue)
+ ? defaultValue : fromFormatToHex("rgb", [rgb >> 16 & 255, rgb >> 8 & 255, rgb & 255]);
+ const block = document.createElement(isShadow ? "shadow" : "block");
+ block.setAttribute("type", COLOR_PICKER_BLOCK_TYPE);
+ const mutation = document.createElement("mutation");
+ if (blockColor) mutation.setAttribute("color", blockColor);
+ const hsv = fromHexToFormat("hsv", hex);
+ ["hue", "saturation", "value"].forEach((name, index) => mutation.setAttribute(name, hsv[index].toString()));
+ block.appendChild(mutation);
+
+ const format = document.createElement("field");
+ format.setAttribute("name", "FORMAT");
+ format.textContent = formatName;
+ block.appendChild(format);
+
+ if (formatName === "hex") {
+ const input = document.createElement("value");
+ input.setAttribute("name", "HEX_INPUT");
+ input.appendChild(generateColorPickerStringShadowDom(hex));
+ block.appendChild(input);
+ }
+ else {
+ const types = getFieldTypesForFormat(formatName);
+ fromHSVToFormat(formatName, hsv).forEach((channel, index) => {
+ const input = document.createElement("value");
+ input.setAttribute("name", "INPUT" + index);
+ input.appendChild(generateColorPickerNumberShadowDom(types[index], channel));
+ block.appendChild(input);
+ });
+ }
+ return block;
}
\ No newline at end of file
diff --git a/pxtcompiler/emitter/decompiler.ts b/pxtcompiler/emitter/decompiler.ts
index ecb86c56cba0..bef8aec96cfb 100644
--- a/pxtcompiler/emitter/decompiler.ts
+++ b/pxtcompiler/emitter/decompiler.ts
@@ -686,6 +686,13 @@ ${output}`;
}
function mkValue(name: string, value: ExpressionNode | TextNode, shadowType?: string, shadowMutation?: pxt.Map): ValueNode {
+ const contributor = blocksInfo.blocksById[shadowType];
+ const builtinBlockId = contributor?.attributes.builtinBlockId;
+ // Keep the contributor ID so a fallback shadow can be rebuilt with its configured defaults.
+ if (builtinBlockId === "makecode_color_picker" && value.kind === "expr" && value.type === builtinBlockId) {
+ value.mutation = value.mutation || {};
+ if (contributor?.attributes.color) value.mutation.color = contributor.attributes.color;
+ }
if ((!shadowType || shadowType === numberType) && shadowMutation && shadowMutation['min'] && shadowMutation['max']) {
// Convert a number to a number with a slider (math_number_minmax) if min and max shadow options are defined
shadowType = minmaxNumberType;
@@ -4004,8 +4011,10 @@ ${output}`;
let emitShadowOnly = false;
if (n.value.kind === "expr") {
- if (n.value.type !== n.shadowType) {
- const shadowBlockInfo = blocksInfo.blocksById[n.shadowType];
+ const shadowBlockInfo = blocksInfo.blocksById[n.shadowType];
+ const normalizedShadowType = shadowBlockInfo?.attributes?.builtinBlockId || n.shadowType;
+
+ if (n.value.type !== normalizedShadowType) {
let shadowBlockShimType: string;
let shadowFieldName: string;
@@ -4043,7 +4052,7 @@ ${output}`;
value.type = colorPickerString;
}
- emitShadowOnly = value.type === n.shadowType;
+ emitShadowOnly = value.type === normalizedShadowType;
if (!emitShadowOnly) {
if (isNumberBlockType(value.type) || isBooleanBlockType(value.type) || isStringBlockType(value.type)) {
emitShadowOnly = !n.shadowType
diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts
index 63755d02d6c2..2c7ed6fbaa75 100644
--- a/tests/blocklycompiler-test/test.spec.ts
+++ b/tests/blocklycompiler-test/test.spec.ts
@@ -3,7 +3,7 @@
import * as Blockly from "blockly";
import * as pxtblockly from "../../pxtblocks";
-import { DuplicateOnDragConnectionChecker } from "../../pxtblocks/plugins/duplicateOnDrag";
+import { DuplicateOnDragConnectionChecker, shouldDuplicateOnDrag } from "../../pxtblocks/plugins/duplicateOnDrag";
import "./commentparsing.spec";
import "./fieldUserEnum.spec";
@@ -391,6 +391,107 @@ describe("blockly compiler", function () {
});
describe("compiling text", () => {
+ it("should initialize color picker shadows in the parameter's requested mode", async () => {
+ const info = await getBlocksInfoAsync();
+ const coloredInfo = {
+ ...info,
+ blocks: info.blocks.concat({
+ attributes: { builtinBlockId: "makecode_color_picker", color: "#6554C0" }
+ } as pxtc.SymbolInfo)
+ };
+ const workspace = new Blockly.Workspace();
+ try {
+ for (const format of ["hex", "rgb", "hsv", "hsl", "cmyk", "invalid"]) {
+ const value = pxtblockly.createShadowValue(coloredInfo, {
+ definitionName: "color", actualName: "color", type: "number",
+ shadowBlockId: "makecode_color_picker", defaultValue: "0x7f3fbf",
+ fieldOptions: { format }
+ });
+ const picker = Blockly.Xml.domToBlock(value.firstElementChild, workspace) as pxtblockly.ColorPickerBlock;
+ const preview = picker.getField("PREVIEW");
+ picker.updateColorPreview();
+ chai.assert.isTrue(picker.isShadow());
+ chai.assert.isTrue(preview.isVisible());
+ chai.assert.equal(picker.getColour().toLowerCase(), "#6554c0");
+ chai.assert.equal(picker.getFieldValue("FORMAT"), format === "invalid" ? "rgb" : format);
+ chai.assert.equal(pxtblockly.getColorPickerColor(picker), "#7F3FBF");
+ const mutation = value.querySelector("mutation");
+ chai.assert.equal(mutation.getAttribute("color"), "#6554C0");
+ chai.assert.isTrue(shouldDuplicateOnDrag(picker));
+ mutation.removeAttribute("hue");
+ mutation.removeAttribute("saturation");
+ mutation.removeAttribute("value");
+ const restored = Blockly.Xml.domToBlock(value.firstElementChild, workspace) as pxtblockly.ColorPickerBlock;
+ restored.updateBeforeRender();
+ chai.assert.include(restored.getField("PREVIEW").getText(), "#7F3FBF");
+ restored.dispose();
+ picker.setFieldValue("rgb", "FORMAT");
+ chai.assert.deepEqual([0, 1, 2].map(i => Math.round(Number(picker.getInputTargetBlock("INPUT" + i).getFieldValue("NUM")))), [127, 63, 191]);
+ picker.getInputTargetBlock("INPUT0").setFieldValue("255", "NUM");
+ picker.updateColorPreview();
+ chai.assert.include(picker.getField("PREVIEW").getText(), "#FF3FBF");
+ // An expression has no statically known preview, even if another channel is literal.
+ const input = picker.getInput("INPUT0");
+ input.connection.setShadowDom(null);
+ chai.assert.isUndefined(pxtblockly.getColorPickerColor(picker));
+ picker.updateColorPreview();
+ chai.assert.isFalse(preview.isVisible());
+ picker.dispose();
+ }
+ }
+ finally {
+ workspace.dispose();
+ }
+ });
+
+ it("should share named picker defaults without taking another contributor's settings", async () => {
+ const info = await getBlocksInfoAsync();
+ const contributor = (id: string, color: string, format: string, value: string): pxtc.SymbolInfo => ({
+ name: id, namespace: "test", fileName: "test.ts", kind: pxtc.SymbolKind.Function, retType: "number",
+ attributes: {
+ blockId: id, builtinBlockId: "makecode_color_picker", color,
+ paramFieldEditorOptions: { value: { format } },
+ paramDefl: { value }, callingConvention: pxtc.ir.CallingConvention.Plain
+ },
+ parameters: [{ name: "value", type: "number", description: "", default: value }]
+ });
+ const first = contributor("other_picker", "#008800", "rgb", "0xff0000");
+ const selected = contributor("test_color_picker", "#6554C0", "hex", "0x7f3fbf");
+ const namedInfo = {
+ ...info, blocks: info.blocks.concat(first, selected),
+ blocksById: {
+ ...info.blocksById, other_picker: first, test_color_picker: selected
+ }
+ };
+ const shadow = pxtblockly.createShadowValue(namedInfo, {
+ definitionName: "color", actualName: "color", type: "number", shadowBlockId: "test_color_picker"
+ }).firstElementChild;
+ const otherShadow = pxtblockly.createShadowValue(namedInfo, {
+ definitionName: "color", actualName: "color", type: "number", shadowBlockId: "other_picker"
+ }).firstElementChild;
+ const standalone = pxtblockly.createToolboxBlock(namedInfo, selected, pxt.blocks.compileInfo(selected));
+ chai.assert.equal(shadow.getAttribute("type"), "makecode_color_picker");
+ chai.assert.equal(shadow.innerHTML, standalone.innerHTML);
+ chai.assert.equal(shadow.querySelector("mutation").getAttribute("color"), "#6554C0");
+ chai.assert.equal(otherShadow.querySelector("mutation").getAttribute("color"), "#008800");
+ chai.assert.equal(shadow.querySelector('field[name="FORMAT"]').textContent, "hex");
+ chai.assert.equal(shadow.querySelector('field[name="TEXT"]').textContent, "#7F3FBF");
+ const workspace = new Blockly.Workspace();
+ try {
+ const picker = Blockly.Xml.domToBlock(shadow, workspace);
+ const unmarked = Blockly.Xml.domToBlock(otherShadow, workspace);
+ const standalonePicker = Blockly.Xml.domToBlock(pxtblockly.createToolboxBlock(namedInfo, selected, pxt.blocks.compileInfo(selected)), workspace);
+ chai.assert.isTrue(shouldDuplicateOnDrag(picker));
+ chai.assert.isTrue(shouldDuplicateOnDrag(unmarked));
+ chai.assert.isFalse(shouldDuplicateOnDrag(standalonePicker));
+ const restored = Blockly.Xml.domToBlock(Blockly.Xml.blockToDom(picker, true) as Element, workspace);
+ chai.assert.isTrue(shouldDuplicateOnDrag(restored));
+ }
+ finally {
+ workspace.dispose();
+ }
+ });
+
it("should handle the text blocks", (done: () => void) => {
blockTestAsync("text").then(done, done);
});
diff --git a/tests/decompile-test/baselines/contributed_builtin_shadows.blocks b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks
new file mode 100644
index 000000000000..fad148bb4dbc
--- /dev/null
+++ b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks
@@ -0,0 +1,46 @@
+
+
+
+
+selectedColor
+
+0
+
+
+hex
+
+
+#123456
+
+
+
+
+
+
+
+
+
+selectedColor
+
+
+
+
+
+
+
+hex
+
+
+#abcdef
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/decompile-test/cases/contributed_builtin_shadows.ts b/tests/decompile-test/cases/contributed_builtin_shadows.ts
new file mode 100644
index 000000000000..4dedb9020882
--- /dev/null
+++ b/tests/decompile-test/cases/contributed_builtin_shadows.ts
@@ -0,0 +1,3 @@
+let selectedColor = colorHelpers.hex("#123456")
+testNamespace.colorArgument(selectedColor)
+testNamespace.colorArgument(colorHelpers.hex("#abcdef"))
diff --git a/tests/decompile-test/cases/testBlocks/basic.ts b/tests/decompile-test/cases/testBlocks/basic.ts
index bcb69657837f..d01dffbdab5d 100644
--- a/tests/decompile-test/cases/testBlocks/basic.ts
+++ b/tests/decompile-test/cases/testBlocks/basic.ts
@@ -15,6 +15,19 @@ namespace testNamespace {
//% block="Number arg %arg"
export function numberArgument(arg: number): void {}
+ //% blockId=test_color_argument
+ //% block="Color arg %color"
+ //% color.shadow=test_color_picker
+ export function colorArgument(color: number): void {}
+
+ //% block blockId=test_color_picker builtinBlockId=makecode_color_picker
+ //% blockHidden=true
+ //% value.fieldOptions.format=hex value.defl=0x7f3fbf
+ //% color="#6554C0"
+ export function __colorPicker(value: number): number {
+ return value;
+ }
+
//% blockId=test_string_argument
//% block="String arg %arg"
export function stringArgument(arg: string): void {}
diff --git a/webapp/src/blocks.tsx b/webapp/src/blocks.tsx
index 1a30d88a0aef..42da81e8c74f 100644
--- a/webapp/src/blocks.tsx
+++ b/webapp/src/blocks.tsx
@@ -955,13 +955,16 @@ export class Editor extends toolboxeditor.ToolboxEditor {
})
this.editor.addChangeListener((e) => {
- if (e.type === Blockly.Events.BLOCK_MOVE) {
+ if (e.type === Blockly.Events.BLOCK_MOVE && e.recordUndo) {
const parent = this.editor.getBlockById((e as Blockly.Events.BlockMove).newParentId);
- if (parent && parent.isShadow()) {
+ const child = this.editor.getBlockById((e as Blockly.Events.BlockMove).blockId);
+ // Nested shadows change during picker format switches; only promote real inputs.
+ if (parent?.isShadow() && parent.outputConnection?.targetConnection
+ && child && !child.isShadow() && child.getParent() === parent) {
Blockly.Events.setGroup(e.group)
const json = Blockly.serialization.blocks.save(parent);
// Dispose of the original block so it doesn't get saved in the parent connection's shadow state.
- this.editor.getBlockById((e as Blockly.Events.BlockMove).blockId).dispose();
+ child.dispose();
const dupe = Blockly.serialization.blocks.append(json, this.editor, { recordUndo: true });
parent.outputConnection.targetConnection.connect(dupe.outputConnection);
Blockly.Events.setGroup(false);
@@ -2497,7 +2500,7 @@ export class Editor extends toolboxeditor.ToolboxEditor {
const info = this.blockInfo.blocksById[type];
const shadowType = info?.attributes?.builtinBlockId || type;
- let b = this.getBlockXml(builtin ? builtin : { name: type, type: type, attributes: { blockId: type } }, ignoregap, true);
+ let b = this.getBlockXml(builtin || info || { name: type, type: type, attributes: { blockId: type } }, ignoregap, true);
// Note: we're setting one innerHTML to another
if (b && b.length > 0 && b[0] && b[0].getAttribute("type") === shadowType) {
// eslint-disable-next-line @microsoft/sdl/no-inner-html
diff --git a/webapp/src/monaco.tsx b/webapp/src/monaco.tsx
index 5a6cb1ddd7e4..8ef592422d14 100644
--- a/webapp/src/monaco.tsx
+++ b/webapp/src/monaco.tsx
@@ -1932,7 +1932,9 @@ export class Editor extends toolboxeditor.ToolboxEditor {
}
if (fn.attributes.builtinBlockId) {
- res[ns].push(...snippets.getExtensionContributedBuiltinBlock(fn.attributes.builtinBlockId, fn.attributes.weight || 50));
+ if (!fn.attributes.blockHidden) {
+ res[ns].push(...snippets.getExtensionContributedBuiltinBlock(fn.attributes.builtinBlockId, fn.attributes.weight || 50));
+ }
return;
}