Render blockly blocks outside of workspace

292 views
Skip to first unread message

Rioni

unread,
May 1, 2024, 8:46:10 PM5/1/24
to Blockly
I have a series of blocks in my toolbox
blocks.png

And I want to create a help section to explain each block that would be like

"Block image"

"Text"

Of course I could just manually save block screenshots, but I would like to know if there is some way to display an specific block inside a canvas or a div for this purpose.

Also, I know blocks can have tooltips, but I prefer to write a more in depth guide

Sandell Levy

unread,
May 1, 2024, 9:33:20 PM5/1/24
to Blockly
Hello, I'm new and I would like to know if anyone can help me import my blocks into my electron projectCaptura de Tela (114).png

my code index.html:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Wisdom Big Robot Classroom</title>
  <script src="https://unpkg.com/blockly/blockly.min.js"></script>
  <!-- Load Blockly core -->
  <!-- Load the default blocks -->
  <!-- Load a generator -->
  <!-- Load a message file -->
  <script src="https://unpkg.com/blockly/msg/en.js"></script>
  <script src="blockly/blocos_up.js"></script>
  <style>
    #blocklyDiv {
      height: 80vh; /* Altura da tela */
      width: 100vw; /* Largura da tela */
      border: 1px solid #ddd;
    }
    body, html {
      margin: 0;
      padding: 0;
      height: 100%;
    }
    h1 {
      font-family: Arial, sans-serif; /* Fonte Arial */
    }

    /* Estilos para as categorias */
    .blocklyTreeRowContent {
      color: #333; /* Cor do texto das categorias */
    }
    .blocklyTreeRowContent:hover {
      background-color: #ccc; /* Cor de fundo das categorias ao passar o mouse */
    }
    .blocklyTreeRow.blocklyNonEditable {
      background-color: #ddd; /* Cor de fundo dos blocos */
    }
    .blocklyTreeRow.blocklyNonEditable:hover {
      background-color: #bbb; /* Cor de fundo dos blocos ao passar o mouse */
    }
  </style>
</head>
<body>
  <h1>Área do laboratório</h1>
  <div id="blocklyDiv"></div>
  <xml id="toolbox" style="display: none">
    <category name="Controle" colour="#32fc00">
      <!--<block type="controls_if"></block>-->
    </category>

    <category name="Lógicos" colour="#ff5733">
     <!---<block type="logic_compare"></block>
      <block type="logic_operation"></block>
      <block type="logic_boolean"></block>-->
    </category>

    <category name="Movimentos" colour="#3399ff">
      <block type="move_forward"></block>
    </category>

    <category name="Motor" colour="#ff99cc">
      <block type=""></block>
    </category>

    <category name="Sensores" colour="#66ff99">
      <block type="move_forward"></block>
    </category>

    <category name="Criados" colour="#ffcc00">
      <category name="Core">
        <block type="controls_if"></block>
        <block type="logic_compare"></block>
      </category>
      <category name="Meus Blocos">
        <block type="start"></block>
        <category name="Move">
          <block type="move_forward"></block>
        </category>
        <category name="Turn">
          <block type="turn_left"></block>
        </category>
      </category>
    </category>
  </xml>
  <script>
    var workspace = Blockly.inject('blocklyDiv', {
      toolbox: document.getElementById('toolbox')
    });

    // Adicionando blocos definidos no blocos_up.js
    for (var blockName in BlocosUp) {
      if (BlocosUp.hasOwnProperty(blockName)) {
        var block = BlocosUp[blockName];
        Blockly.Blocks[blockName] = {
          init: block
        };
        workspace.registerBlockType(blockName);
      }
    }
  </script>
</body>
</html>

Clément Contet

unread,
May 2, 2024, 5:09:30 AM5/2/24
to Blockly
Hi, there is a readonly mode that you can use for that.
See: https://developers.google.com/blockly/guides/create-custom-blocks/style-guide#4_live_block_images

You can see examples in Blockly Games: https://blockly.games/puzzle (or in my app: https://www.supercodingball.com/howto)

Regards,

Clément

Rioni

unread,
May 2, 2024, 5:13:34 AM5/2/24
to Blockly
Ohh I couldn't find that before. I'll try it, Thank you! ^^

Rioni

unread,
May 2, 2024, 10:31:01 AM5/2/24
to Blockly
I can't get it to work correctly. I have to set a fixed height because workspace won't adjust to their blocks, zoomToFit method gives me warning "Tried to move a non-movable workspace"blocks2.png

For reference, this is the code of my dialog (vue3): 

<script setup>
import { onMounted, ref } from "vue";
import { useSound } from "@vueuse/sound";
import click from "../assets/sounds/select.wav";
import { toolbox } from "../assets/blockly/toolbox";
import * as Blockly from "blockly";
import { useSimulationStore } from "../stores/simulation";


const store = useSimulationStore();
const { play } = useSound(click, { volume: 0.5, interrupt: true });

defineExpose({
    showModal,
    closeModal
});

function showModal() {
    // Check if modal is already open
    if (modal.value.open) return;

    modal.value.showModal();
    play({ playbackRate: 1 });

    // Iterate all workspaces and call zoomToFit
    for (const ws of workspaces.value) {
        ws.zoomToFit();
    }

    // dispatch an event to resize the modal
    window.dispatchEvent(new Event("resize"));
}

function closeModal() {
    play({ playbackRate: 1.5 });
}

const modal = ref(null);
const workspaces = ref([]);

onMounted(() => {

    for (const block of toolbox.contents) {
        const blocklyDiv = document.getElementById("model" + block.type);
        const ws = Blockly.inject(blocklyDiv, {
            toolbox: toolbox,
            readOnly: true,
            renderer: "Zelos",
            scrollbars: false,
            move: {
                scrollbars: false,
                drag: false,
                wheel: false,
            },
            zoom: {
                controls: false,
                wheel: false,
            },
        });

        const newBlock = ws.newBlock(block.type);
        newBlock.initSvg();
        newBlock.render();
        ws.zoomToFit();

        workspaces.value.push(ws);
    }

});

</script>

<template>
    <dialog @close="closeModal" ref="modal"
        class="backdrop:backdrop-blur-sm backdrop:backdrop-brightness-75 rounded-xl px-4 py-1">
        <div class="w-[90vmin] lg:max-h-[90vmin] max-h-[90vh]">
            <div class="w-full h-full " v-for="block in toolbox.contents" :key="block.type">
                <div class="w-full h-16 border-0" :id="'model' + block.type"></div>
                <p class="mb-8">{{ block.type }}</p>
            </div>
        </div>
    </dialog>
</template>

Clément Contet

unread,
May 2, 2024, 10:45:39 AM5/2/24
to Blockly

Rioni

unread,
May 2, 2024, 11:11:25 AM5/2/24
to Blockly
Yep, it was that, I was seeing your code and a github issue. I was able to add a scrollbar and then hide it t with CSS. Now I just need to make block default values to appear and to fix my code because for some reason, the first time the modal is opened it won't render well, it need to be closed an opened a second time so blocks are rendered in the correct size.

This is how my main workspace looks like (nothing wrong here, this is how it shuold be):

mainworks.png

This is how the modal looks the first time it is opened (bad, looks small)

blockfirst.png

And this is how it looks the second time it is opened (much better, but block fields are empty and they should have default values like in the main workspace)

blockssecond.png


My current code is below. I'm getting mad trying to get something as simple as this to work :(

<script setup>
import { onMounted, ref } from "vue";
import { useSound } from "@vueuse/sound";
import click from "../assets/sounds/select.wav";
import { toolbox } from "../assets/blockly/toolbox";
import * as Blockly from "blockly";
const { play } = useSound(click, { volume: 0.5, interrupt: true });

defineExpose({
    showModal,
    closeModal
});

function showModal() {
    // Check if modal is already open
    if (modal.value.open) return;

    modal.value.showModal();
    play({ playbackRate: 1 });

    // Iterate all workspaces and call zoomToFit
    for (const ws of workspaces.value) {
        ws.zoomToFit();
    }

    // Forcing zoomToFit to work
    window.dispatchEvent(new Event("resize"));

    // Set focus to modal to avoid focusing the first block
    modal.value.focus();
}

function closeModal() {
    play({ playbackRate: 1.5 });
}

const modal = ref(null);
const workspaces = ref([]);

onMounted(() => {

    for (const block of toolbox.contents) {
        const blocklyDiv = document.getElementById("model" + block.type);
        const ws = Blockly.inject(blocklyDiv, {
            toolbox: toolbox,
            readOnly: true,
            renderer: "Zelos",
            scrollbars: false,
            move: {
                scrollbars: { horizontal: false, vertical: true },
                drag: false,
                wheel: false,
            },
            zoom: {
                controls: false,
                wheel: false,
            },
        });

        // Hidin scrollbars
        // I cant set them to false because blockly will complain
        const rect = blocklyDiv.querySelectorAll("rect");
        rect.forEach((r) => {
            r.style.display = "none";
        });

        const newBlock = ws.newBlock(block.type);
        newBlock.initSvg();
        newBlock.render();

        workspaces.value.push(ws);
        ws.zoomToFit();
    }

});

</script>

<template>
    <dialog @close="closeModal" ref="modal"
        class="backdrop:backdrop-blur-sm backdrop:backdrop-brightness-75 rounded-xl px-4 py-1">
        <div class="w-[90vmin] lg:max-h-[90vmin] max-h-[90vh]">
            <div class="w-full h-full " v-for="block in toolbox.contents" :key="block.type">
                <div class="w-full h-24 border-0" :id="'model' + block.type"></div>
                <p class="mb-8">{{ block.type }}</p>
            </div>
        </div>
    </dialog>
</template>

Rioni

unread,
May 2, 2024, 11:18:24 AM5/2/24
to Blockly
Alright, changing my showModal function to this seemed to do the trick:
Now I just need to init blocks with default values and I'll be done

function showModal() {
    // Check if modal is already open
    if (modal.value.open) return;

    modal.value.showModal();
    play({ playbackRate: 1 });

    // Iterate all workspaces and call zoomToFit
    window.dispatchEvent(new Event("resize"));

    requestAnimationFrame(() => {
        for (const ws of workspaces.value) {
            ws.zoomToFit();
        }
    });

    // Set focus to modal to avoid focusing the first block
    modal.value.focus();
}

Clément Contet

unread,
May 2, 2024, 11:30:43 AM5/2/24
to Blockly
You are creating new blocks only with their type:

        const newBlock = ws.newBlock(block.type);
        newBlock.initSvg();
        newBlock.render();

So the inputs that you have put into your toolbox are lost.

I don't know if there is a way to automatically retrieve this from the toolbox (anyone from Blockly team? ;)), but you could instead load a complete block definition into your workspace (https://developers.google.com/blockly/guides/get-started/save-and-load#load).

Rioni

unread,
May 2, 2024, 12:41:00 PM5/2/24
to Blockly
I know, but I might change blocks in the future and my dialog is an automated one to explain block by block, having to create specialized workspaces is tiring. I'll continue trying to find a way to automate this, meanwhile I hope someone from Blockly team can answer us.

Thank you for time Clement! Your repo has helped me a lot to set this up ^^

Rioni

unread,
May 2, 2024, 9:56:33 PM5/2/24
to Blockly
Alright, I did it. This is not the most elegant solution but it works. It could probably be set up better:

<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { useSound } from "@vueuse/sound";
import click from "../assets/sounds/select.wav";
import { toolbox } from "../assets/blockly/toolbox";
import * as Blockly from "blockly";
import helpJSON from "../assets/jsons/block_help.json";
const { play } = useSound(click, { volume: 0.5, interrupt: true });

defineExpose({
    showModal
});

let blocklyMainWorkspace = null;

function showModal() {
    // Check if modal is already open
    if (modal.value.open) return;

    modal.value.showModal();
    play({ playbackRate: 1 });

    // Iterate all workspaces and call zoomToFit
    window.dispatchEvent(new Event("resize"));

    requestAnimationFrame(() => {
        for (const ws of workspaces.value) {
            ws.zoomToFit();
        }
    });

    // Set focus to modal to avoid focusing the first block
    modal.value.focus();
}

function onModalClose() {
    play({ playbackRate: 1.5 });

    // Focus is gained when tabbed on the dialog and when this component is created (mounted)
    // So we have to make absolutely sure the focus is in the main workspace or everything will break
    blocklyMainWorkspace.markFocused();
}

const modal = ref(null);
const modalInner = ref(null);
const workspaces = ref([]);

onMounted(() => {
    // We need to save the main workspace to restore it later
    // Because creating the new workspaces will change the focus and thus, the call to Blockly.getMainWorkspace()
    blocklyMainWorkspace = Blockly.getMainWorkspace();

    // Windows click eventlistener
    window.addEventListener("pointerup", pointerup);
    window.addEventListener("pointerdown", pointerdown);
        const getTypeFromConnectionCheck = (connection) => {
            switch (connection) {
                case "Particle":
                    return "particle";
                case "Vector":
                    return "direction";
                case "Number":
                    return "custom_field_slider";
                case "Boolean":
                    return "boolean";
                case "Group":
                    return "group_particle";
                default:
                    return undefined;
            }
        };
       
        if (block.inputs !== undefined) {
            // Iterate over all inputs
            for (const [key, value] of Object.entries(block.inputs)) {
                let input = newBlock.getInput(key);
                if (input.connection.check === null || input.connection.check.length === 0) continue;

                let childBlock = ws.newBlock(getTypeFromConnectionCheck(input.connection.check[0])); // Even if there are multiple checks, we only need the first one
                childBlock.initSvg();
                childBlock.render();

                input.connection.connect(childBlock.outputConnection);
            }
        }

        workspaces.value.push(ws);
        ws.zoomToFit();
    }

    // By doing this we avoid the focus being in the last created workspace.
    // Every workspace div container has class pointer-events-none, so they can't be focused by clicking
    blocklyMainWorkspace.markFocused();
});

onUnmounted(() => {
    window.removeEventListener("pointerup", pointerup);
    window.removeEventListener("pointerdown", pointerdown);
});


// If the user clicks inside the dialog and releases the mouse outside, the dialog should not close
// To achieve that I save whether the user clicked inside the dialog and check that in the pointerup event
let clickedInside = false;

function pointerdown(event) {
    // If the click is outside the modal, close it
    clickedInside = modal.value.open && !modalInner.value.contains(event.target)
}

function pointerup(event) {
    // If the click is outside the modal, close it
    if (modal.value.open && !modalInner.value.contains(event.target) && clickedInside) {
        modal.value.close();
    }
}

</script>

<template>
    <dialog ref="modal" @close="onModalClose"
        class="backdrop:backdrop-blur-sm backdrop:backdrop-brightness-75 rounded-xl px-4 py-1">
        <div ref="modalInner" class="w-[90vmin] lg:max-h-[90vmin] max-h-[90vh] flex flex-col">
            <form method="dialog">
                <button
                    class="w-full bg-slate-200 border-2 border-black  hover:bg-slate-300 hover:scale-[1.01]  font-semibold py-1 px-2 rounded sm:grow-0 grow mt-4"><i
                        class="ph-duotone ph-x-circle"></i>Close</button>
            </form>
            <!-- We need pointer events none to truly avoid these workspaces getting focus -->
            <div class="w-full h-full pointer-events-none" v-for="block in toolbox.contents" :key="block.type">
                <div class="w-full h-24 border-0" :id="'model' + block.type"></div>
                <p class="mb-8">{{ helpJSON[block.type] }}</p>
            </div>
            <form method="dialog">
                <button
                    class="w-full bg-slate-200 border-2 border-black  hover:bg-slate-300 hover:scale-[1.01]  font-semibold py-1 px-2 rounded sm:grow-0 grow mb-4"><i
                        class="ph-duotone ph-x-circle"></i>Close</button>
            </form>
        </div>
    </dialog>
</template>

Reply all
Reply to author
Forward
0 new messages